Refactor and enhance day-night cycle handling in runtime-project-time.ts

This commit is contained in:
2026-04-12 14:09:26 +02:00
parent 237588f369
commit 0641e41d88

View File

@@ -3,6 +3,7 @@ import {
formatTimeOfDayHours,
HOURS_PER_DAY,
normalizeTimeOfDayHours,
type ProjectTimePhaseProfile,
type ProjectTimeSettings
} from "../document/project-time-settings";
import {
@@ -13,11 +14,6 @@ import {
type WorldSunLightSettings
} from "../document/world-settings";
const NIGHT_AMBIENT_COLOR = "#162033";
const NIGHT_SUN_COLOR = "#6f7fb5";
const NIGHT_SOLID_BACKGROUND_COLOR = "#09111f";
const NIGHT_GRADIENT_TOP_COLOR = "#0b1730";
const NIGHT_GRADIENT_BOTTOM_COLOR = "#182134";
const DEFAULT_NOON_DIRECTION: Vec3 = {
x: 0.45,
y: 0.88,
@@ -38,10 +34,18 @@ export interface RuntimeClockState {
export interface RuntimeDayNightWorldState {
ambientLight: WorldAmbientLightSettings;
sunLight: WorldSunLightSettings;
moonLight: WorldSunLightSettings | null;
background: WorldBackgroundSettings;
daylightFactor: number;
}
interface RuntimeDayNightPhaseWeights {
day: number;
dawn: number;
dusk: number;
night: number;
}
function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
}
@@ -144,6 +148,69 @@ function blendHexColors(leftHex: string, rightHex: string, amount: number): stri
});
}
function blendHexColorsByWeights(
dayHex: string,
dawnHex: string,
duskHex: string,
nightHex: string,
weights: RuntimeDayNightPhaseWeights
): string {
const totalWeight =
weights.day + weights.dawn + weights.dusk + weights.night;
if (totalWeight <= 1e-6) {
return dayHex;
}
const day = parseHexColor(dayHex);
const dawn = parseHexColor(dawnHex);
const dusk = parseHexColor(duskHex);
const night = parseHexColor(nightHex);
return formatHexColor({
r:
(day.r * weights.day +
dawn.r * weights.dawn +
dusk.r * weights.dusk +
night.r * weights.night) /
totalWeight,
g:
(day.g * weights.day +
dawn.g * weights.dawn +
dusk.g * weights.dusk +
night.g * weights.night) /
totalWeight,
b:
(day.b * weights.day +
dawn.b * weights.dawn +
dusk.b * weights.dusk +
night.b * weights.night) /
totalWeight
});
}
function blendScalarByWeights(
dayValue: number,
dawnValue: number,
duskValue: number,
nightValue: number,
weights: RuntimeDayNightPhaseWeights
): number {
const totalWeight =
weights.day + weights.dawn + weights.dusk + weights.night;
if (totalWeight <= 1e-6) {
return dayValue;
}
return (
dayValue * weights.day +
dawnValue * weights.dawn +
duskValue * weights.dusk +
nightValue * weights.night
) / totalWeight;
}
function resolveNoonSunDirection(direction: Vec3): Vec3 {
const normalizedDirection = normalizeVec3(direction);
@@ -158,8 +225,141 @@ function resolveNoonSunDirection(direction: Vec3): Vec3 {
});
}
function unwrapTimeAroundReference(hours: number, referenceHours: number): number {
let unwrappedHours = normalizeTimeOfDayHours(hours);
while (unwrappedHours - referenceHours > HOURS_PER_DAY / 2) {
unwrappedHours -= HOURS_PER_DAY;
}
while (referenceHours - unwrappedHours > HOURS_PER_DAY / 2) {
unwrappedHours += HOURS_PER_DAY;
}
return unwrappedHours;
}
function resolveRuntimeDayNightPhaseWeights(
settings: ProjectTimeSettings,
timeOfDayHours: number
): RuntimeDayNightPhaseWeights {
const normalizedTime = normalizeTimeOfDayHours(timeOfDayHours);
const sunrise = unwrapTimeAroundReference(
settings.sunriseTimeOfDayHours,
normalizedTime
);
const sunset = unwrapTimeAroundReference(
settings.sunsetTimeOfDayHours,
normalizedTime
);
const dawnHalfDuration = Math.max(settings.dawnDurationHours, 0.001) / 2;
const duskHalfDuration = Math.max(settings.duskDurationHours, 0.001) / 2;
const dawnStart = sunrise - dawnHalfDuration;
const dawnEnd = sunrise + dawnHalfDuration;
const duskStart = sunset - duskHalfDuration;
const duskEnd = sunset + duskHalfDuration;
if (normalizedTime < dawnStart || normalizedTime >= duskEnd) {
return {
day: 0,
dawn: 0,
dusk: 0,
night: 1
};
}
if (normalizedTime < sunrise) {
const amount = smoothstep(dawnStart, sunrise, normalizedTime);
return {
day: 0,
dawn: amount,
dusk: 0,
night: 1 - amount
};
}
if (normalizedTime < dawnEnd) {
const amount = smoothstep(sunrise, dawnEnd, normalizedTime);
return {
day: amount,
dawn: 1 - amount,
dusk: 0,
night: 0
};
}
if (normalizedTime < duskStart) {
return {
day: 1,
dawn: 0,
dusk: 0,
night: 0
};
}
if (normalizedTime < sunset) {
const amount = smoothstep(duskStart, sunset, normalizedTime);
return {
day: 1 - amount,
dawn: 0,
dusk: amount,
night: 0
};
}
const amount = smoothstep(sunset, duskEnd, normalizedTime);
return {
day: 0,
dawn: 0,
dusk: 1 - amount,
night: amount
};
}
function resolveTimeDrivenSunOrbitRadians(
settings: ProjectTimeSettings,
timeOfDayHours: number
): number {
const normalizedTime = normalizeTimeOfDayHours(timeOfDayHours);
const sunrise = unwrapTimeAroundReference(
settings.sunriseTimeOfDayHours,
normalizedTime
);
const sunset = unwrapTimeAroundReference(
settings.sunsetTimeOfDayHours,
normalizedTime
);
if (normalizedTime >= sunrise && normalizedTime <= sunset) {
const daytimeDuration = Math.max(sunset - sunrise, 0.001);
const daytimeProgress = clamp(
(normalizedTime - sunrise) / daytimeDuration,
0,
1
);
return lerp(-Math.PI / 2, Math.PI / 2, daytimeProgress);
}
const previousSunset = sunset > normalizedTime ? sunset - HOURS_PER_DAY : sunset;
const nextSunrise = sunrise <= normalizedTime ? sunrise + HOURS_PER_DAY : sunrise;
const nighttimeDuration = Math.max(nextSunrise - previousSunset, 0.001);
const nighttimeProgress = clamp(
(normalizedTime - previousSunset) / nighttimeDuration,
0,
1
);
return lerp(Math.PI / 2, Math.PI * 1.5, nighttimeProgress);
}
function resolveTimeDrivenSunDirection(
noonDirection: Vec3,
settings: ProjectTimeSettings,
timeOfDayHours: number
): Vec3 {
const orbitAxisCandidate = cross(noonDirection, UP_AXIS);
@@ -175,24 +375,32 @@ function resolveTimeDrivenSunDirection(
z: 0
}
: normalizeVec3(orbitAxisCandidate);
const orbitRadians =
((normalizeTimeOfDayHours(timeOfDayHours) - 12) / HOURS_PER_DAY) *
Math.PI * 2;
const orbitRadians = resolveTimeDrivenSunOrbitRadians(
settings,
timeOfDayHours
);
return rotateAroundAxis(noonDirection, orbitAxis, orbitRadians);
}
function resolvePhaseSolidBackgroundColor(profile: ProjectTimePhaseProfile): string {
return blendHexColors(profile.skyTopColorHex, profile.skyBottomColorHex, 0.65);
}
function resolveTimeDrivenBackground(
background: WorldBackgroundSettings,
daylightFactor: number
settings: ProjectTimeSettings,
weights: RuntimeDayNightPhaseWeights
): WorldBackgroundSettings {
if (background.mode === "solid") {
return {
mode: "solid",
colorHex: blendHexColors(
NIGHT_SOLID_BACKGROUND_COLOR,
colorHex: blendHexColorsByWeights(
background.colorHex,
daylightFactor
resolvePhaseSolidBackgroundColor(settings.dawn),
resolvePhaseSolidBackgroundColor(settings.dusk),
resolvePhaseSolidBackgroundColor(settings.night),
weights
)
};
}
@@ -200,15 +408,19 @@ function resolveTimeDrivenBackground(
if (background.mode === "verticalGradient") {
return {
mode: "verticalGradient",
topColorHex: blendHexColors(
NIGHT_GRADIENT_TOP_COLOR,
topColorHex: blendHexColorsByWeights(
background.topColorHex,
daylightFactor
settings.dawn.skyTopColorHex,
settings.dusk.skyTopColorHex,
settings.night.skyTopColorHex,
weights
),
bottomColorHex: blendHexColors(
NIGHT_GRADIENT_BOTTOM_COLOR,
bottomColorHex: blendHexColorsByWeights(
background.bottomColorHex,
daylightFactor
settings.dawn.skyBottomColorHex,
settings.dusk.skyBottomColorHex,
settings.night.skyBottomColorHex,
weights
)
};
}
@@ -221,7 +433,7 @@ export function createRuntimeClockState(
): RuntimeClockState {
return {
timeOfDayHours: normalizeTimeOfDayHours(settings.startTimeOfDayHours),
dayCount: 0,
dayCount: settings.startDayNumber - 1,
dayLengthMinutes: settings.dayLengthMinutes
};
}
@@ -283,9 +495,10 @@ export function formatRuntimeClockTime(state: RuntimeClockState): string {
export function resolveRuntimeDayNightWorldState(
world: WorldSettings,
settings: ProjectTimeSettings,
clock: RuntimeClockState | null
): RuntimeDayNightWorldState {
if (clock === null) {
if (clock === null || !world.projectTimeLightingEnabled) {
return {
ambientLight: {
colorHex: world.ambientLight.colorHex,
@@ -298,39 +511,119 @@ export function resolveRuntimeDayNightWorldState(
...world.sunLight.direction
}
},
moonLight: null,
background: cloneWorldBackgroundSettings(world.background),
daylightFactor: 1
};
}
const normalizedTime = normalizeTimeOfDayHours(clock.timeOfDayHours);
const phaseWeights = resolveRuntimeDayNightPhaseWeights(
settings,
normalizedTime
);
const noonDirection = resolveNoonSunDirection(world.sunLight.direction);
const sunDirection = resolveTimeDrivenSunDirection(
noonDirection,
clock.timeOfDayHours
settings,
normalizedTime
);
const daylightFactor = smoothstep(-0.2, 0.18, sunDirection.y);
const ambientFactor = lerp(0.18, 1, daylightFactor);
const sunFactor = lerp(0.02, 1, smoothstep(-0.05, 0.22, sunDirection.y));
const daylightFactor = clamp(
phaseWeights.day +
phaseWeights.dawn * 0.7 +
phaseWeights.dusk * 0.5 +
phaseWeights.night * 0.08,
0,
1
);
const sunFactor =
blendScalarByWeights(
1,
settings.dawn.lightIntensityFactor,
settings.dusk.lightIntensityFactor,
0,
phaseWeights
) * smoothstep(-0.16, 0.18, sunDirection.y);
const ambientFactor = blendScalarByWeights(
1,
settings.dawn.ambientIntensityFactor,
settings.dusk.ambientIntensityFactor,
settings.night.ambientIntensityFactor,
phaseWeights
);
const beforeSunrise = normalizedTime < settings.sunriseTimeOfDayHours;
const afterSunset = normalizedTime >= settings.sunsetTimeOfDayHours;
let moonLight: WorldSunLightSettings | null = null;
if (beforeSunrise || afterSunset || phaseWeights.night > 0) {
let moonColorHex = settings.night.lightColorHex;
let moonVisibilityFactor = phaseWeights.night;
if (beforeSunrise && phaseWeights.dawn > 0) {
const twilightBlend =
phaseWeights.dawn /
Math.max(phaseWeights.dawn + phaseWeights.night, 0.001);
moonColorHex = blendHexColors(
settings.night.lightColorHex,
settings.dawn.lightColorHex,
twilightBlend
);
moonVisibilityFactor = phaseWeights.night + phaseWeights.dawn * 0.45;
} else if (afterSunset && phaseWeights.dusk > 0) {
const twilightBlend =
phaseWeights.dusk /
Math.max(phaseWeights.dusk + phaseWeights.night, 0.001);
moonColorHex = blendHexColors(
settings.night.lightColorHex,
settings.dusk.lightColorHex,
twilightBlend
);
moonVisibilityFactor = phaseWeights.night + phaseWeights.dusk * 0.45;
}
moonVisibilityFactor = clamp(moonVisibilityFactor, 0, 1);
if (moonVisibilityFactor > 1e-4) {
moonLight = {
colorHex: moonColorHex,
intensity:
world.sunLight.intensity *
settings.night.lightIntensityFactor *
moonVisibilityFactor,
direction: scaleVec3(sunDirection, -1)
};
}
}
return {
ambientLight: {
colorHex: blendHexColors(
NIGHT_AMBIENT_COLOR,
colorHex: blendHexColorsByWeights(
world.ambientLight.colorHex,
lerp(0.15, 1, daylightFactor)
settings.dawn.ambientColorHex,
settings.dusk.ambientColorHex,
settings.night.ambientColorHex,
phaseWeights
),
intensity: world.ambientLight.intensity * ambientFactor
},
sunLight: {
colorHex: blendHexColors(
NIGHT_SUN_COLOR,
colorHex: blendHexColorsByWeights(
world.sunLight.colorHex,
lerp(0.1, 1, daylightFactor)
settings.dawn.lightColorHex,
settings.dusk.lightColorHex,
settings.night.lightColorHex,
phaseWeights
),
intensity: world.sunLight.intensity * sunFactor,
direction: sunDirection
},
background: resolveTimeDrivenBackground(world.background, daylightFactor),
moonLight,
background: resolveTimeDrivenBackground(
world.background,
settings,
phaseWeights
),
daylightFactor
};
}