From d118b89e92803befa37ea706d62f7d538d9c79a2 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Mon, 13 Apr 2026 14:57:17 +0200 Subject: [PATCH] Refactor time-driven background resolution and phase gradient creation --- src/runtime-three/runtime-project-time.ts | 306 +++++++++++++++++----- 1 file changed, 235 insertions(+), 71 deletions(-) diff --git a/src/runtime-three/runtime-project-time.ts b/src/runtime-three/runtime-project-time.ts index 1d5cee2b..1da4e572 100644 --- a/src/runtime-three/runtime-project-time.ts +++ b/src/runtime-three/runtime-project-time.ts @@ -2,15 +2,15 @@ import type { Vec3 } from "../core/vector"; import { formatTimeOfDayHours, HOURS_PER_DAY, - type ProjectTimeNightBackgroundSettings, normalizeTimeOfDayHours, - type ProjectTimePhaseProfile, type ProjectTimeSettings } from "../document/project-time-settings"; import { cloneWorldBackgroundSettings, type WorldAmbientLightSettings, type WorldBackgroundSettings, + type WorldTimeOfDaySettings, + type WorldTimePhaseProfile, type WorldSettings, type WorldSunLightSettings } from "../document/world-settings"; @@ -369,76 +369,241 @@ function resolveTimeDrivenSunDirection( return rotateAroundAxis(noonDirection, orbitAxis, orbitRadians); } -function resolvePhaseSolidBackgroundColor(profile: ProjectTimePhaseProfile): string { +function resolvePhaseSolidBackgroundColor(profile: WorldTimePhaseProfile): string { return blendHexColors(profile.skyTopColorHex, profile.skyBottomColorHex, 0.65); } -function resolveTimeDrivenBackground( - background: WorldBackgroundSettings, - settings: ProjectTimeSettings, - weights: RuntimeDayNightPhaseWeights +function createPhaseGradientBackground( + profile: WorldTimePhaseProfile ): WorldBackgroundSettings { + return { + mode: "verticalGradient", + topColorHex: profile.skyTopColorHex, + bottomColorHex: profile.skyBottomColorHex + }; +} + +function resolveBackgroundTopColor(background: WorldBackgroundSettings): string { if (background.mode === "solid") { - return { - mode: "solid", - colorHex: blendHexColorsByWeights( - background.colorHex, - resolvePhaseSolidBackgroundColor(settings.dawn), - resolvePhaseSolidBackgroundColor(settings.dusk), - resolvePhaseSolidBackgroundColor(settings.night), - weights - ) - }; + return background.colorHex; } if (background.mode === "verticalGradient") { + return background.topColorHex; + } + + return "#000000"; +} + +function resolveBackgroundBottomColor(background: WorldBackgroundSettings): string { + if (background.mode === "solid") { + return background.colorHex; + } + + if (background.mode === "verticalGradient") { + return background.bottomColorHex; + } + + return "#000000"; +} + +function blendNonImageBackgrounds( + contributions: Array<{ + background: WorldBackgroundSettings; + weight: number; + }> +): WorldBackgroundSettings { + const activeContributions = contributions.filter( + ({ background, weight }) => background.mode !== "image" && weight > 1e-6 + ); + + if (activeContributions.length === 0) { + const fallbackBackground = contributions.find( + ({ background }) => background.mode !== "image" + )?.background; + + return fallbackBackground === undefined + ? { + mode: "solid", + colorHex: "#000000" + } + : cloneWorldBackgroundSettings(fallbackBackground); + } + + const totalWeight = activeContributions.reduce( + (sum, { weight }) => sum + weight, + 0 + ); + + if (totalWeight <= 1e-6) { + return cloneWorldBackgroundSettings(activeContributions[0].background); + } + + const hasGradient = activeContributions.some( + ({ background }) => background.mode === "verticalGradient" + ); + const blendedTopColorHex = blendHexColorsByWeights( + resolveBackgroundTopColor(activeContributions[0].background), + resolveBackgroundTopColor(activeContributions[Math.min(1, activeContributions.length - 1)].background), + resolveBackgroundTopColor(activeContributions[Math.min(2, activeContributions.length - 1)].background), + resolveBackgroundTopColor(activeContributions[Math.min(3, activeContributions.length - 1)].background), + { + day: activeContributions[0]?.weight ?? 0, + dawn: activeContributions[1]?.weight ?? 0, + dusk: activeContributions[2]?.weight ?? 0, + night: activeContributions[3]?.weight ?? 0 + } + ); + + if (!hasGradient) { return { - mode: "verticalGradient", - topColorHex: blendHexColorsByWeights( - background.topColorHex, - settings.dawn.skyTopColorHex, - settings.dusk.skyTopColorHex, - settings.night.skyTopColorHex, - weights - ), - bottomColorHex: blendHexColorsByWeights( - background.bottomColorHex, - settings.dawn.skyBottomColorHex, - settings.dusk.skyBottomColorHex, - settings.night.skyBottomColorHex, - weights - ) + mode: "solid", + colorHex: blendedTopColorHex }; } - return cloneWorldBackgroundSettings(background); + const blendedBottomColorHex = blendHexColorsByWeights( + resolveBackgroundBottomColor(activeContributions[0].background), + resolveBackgroundBottomColor(activeContributions[Math.min(1, activeContributions.length - 1)].background), + resolveBackgroundBottomColor(activeContributions[Math.min(2, activeContributions.length - 1)].background), + resolveBackgroundBottomColor(activeContributions[Math.min(3, activeContributions.length - 1)].background), + { + day: activeContributions[0]?.weight ?? 0, + dawn: activeContributions[1]?.weight ?? 0, + dusk: activeContributions[2]?.weight ?? 0, + night: activeContributions[3]?.weight ?? 0 + } + ); + + return { + mode: "verticalGradient", + topColorHex: blendedTopColorHex, + bottomColorHex: blendedBottomColorHex + }; } -function resolveNightBackgroundOverlay( - nightBackground: ProjectTimeNightBackgroundSettings, - weights: RuntimeDayNightPhaseWeights +function resolveBackgroundImageOverlay( + background: WorldBackgroundSettings, + opacity: number ): RuntimeDayNightWorldState["nightBackgroundOverlay"] { - if (nightBackground.assetId === null) { + if (background.mode !== "image") { return null; } - const opacity = clamp( - weights.night + weights.dawn * 0.5 + weights.dusk * 0.5, - 0, - 1 - ); + const clampedOpacity = clamp(opacity, 0, 1); - if (opacity <= 1e-4) { + if (clampedOpacity <= 1e-4) { return null; } return { - assetId: nightBackground.assetId, - environmentIntensity: nightBackground.environmentIntensity, - opacity + assetId: background.assetId, + environmentIntensity: background.environmentIntensity, + opacity: clampedOpacity }; } +function resolveTimeDrivenBackground( + dayBackground: WorldBackgroundSettings, + timeOfDay: WorldTimeOfDaySettings, + weights: RuntimeDayNightPhaseWeights +): { + background: WorldBackgroundSettings; + nightBackgroundOverlay: RuntimeDayNightWorldState["nightBackgroundOverlay"]; +} { + const dawnBackground = createPhaseGradientBackground(timeOfDay.dawn); + const duskBackground = createPhaseGradientBackground(timeOfDay.dusk); + const nightBackground = timeOfDay.night.background; + const twilightNightOpacity = + weights.night + weights.dawn * 0.5 + weights.dusk * 0.5; + const twilightDayOpacity = weights.day + weights.dawn * 0.5 + weights.dusk * 0.5; + + if (dayBackground.mode === "image" && nightBackground.mode === "image") { + return { + background: cloneWorldBackgroundSettings(dayBackground), + nightBackgroundOverlay: resolveBackgroundImageOverlay( + nightBackground, + twilightNightOpacity + ) + }; + } + + if (dayBackground.mode === "image") { + return { + background: blendNonImageBackgrounds([ + { + background: dawnBackground, + weight: weights.dawn + }, + { + background: duskBackground, + weight: weights.dusk + }, + { + background: nightBackground, + weight: weights.night + } + ]), + nightBackgroundOverlay: resolveBackgroundImageOverlay( + dayBackground, + twilightDayOpacity + ) + }; + } + + if (nightBackground.mode === "image") { + return { + background: blendNonImageBackgrounds([ + { + background: dayBackground, + weight: weights.day + }, + { + background: dawnBackground, + weight: weights.dawn + }, + { + background: duskBackground, + weight: weights.dusk + } + ]), + nightBackgroundOverlay: resolveBackgroundImageOverlay( + nightBackground, + twilightNightOpacity + ) + }; + } + + return { + background: blendNonImageBackgrounds([ + { + background: dayBackground, + weight: weights.day + }, + { + background: dawnBackground, + weight: weights.dawn + }, + { + background: duskBackground, + weight: weights.dusk + }, + { + background: nightBackground, + weight: weights.night + } + ]), + nightBackgroundOverlay: null + }; +} + +function resolveNightBackgroundOverlay( + _nightBackground: WorldBackgroundSettings, + _weights: RuntimeDayNightPhaseWeights +): RuntimeDayNightWorldState["nightBackgroundOverlay"] { + return null; +} + export function createRuntimeClockState( settings: ProjectTimeSettings ): RuntimeClockState { @@ -534,6 +699,7 @@ export function resolveRuntimeDayNightWorldState( settings, normalizedTime ); + const timeOfDay = world.timeOfDay; const noonDirection = resolveNoonSunDirection(world.sunLight.direction); const sunDirection = resolveTimeDrivenSunDirection( noonDirection, @@ -551,16 +717,21 @@ export function resolveRuntimeDayNightWorldState( const sunFactor = blendScalarByWeights( 1, - settings.dawn.lightIntensityFactor, - settings.dusk.lightIntensityFactor, + timeOfDay.dawn.lightIntensityFactor, + timeOfDay.dusk.lightIntensityFactor, 0, phaseWeights ) * smoothstep(-0.16, 0.18, sunDirection.y); const ambientFactor = blendScalarByWeights( 1, - settings.dawn.ambientIntensityFactor, - settings.dusk.ambientIntensityFactor, - settings.night.ambientIntensityFactor, + timeOfDay.dawn.ambientIntensityFactor, + timeOfDay.dusk.ambientIntensityFactor, + timeOfDay.night.ambientIntensityFactor, + phaseWeights + ); + const resolvedBackground = resolveTimeDrivenBackground( + world.background, + timeOfDay, phaseWeights ); let moonLight: WorldSunLightSettings | null = null; @@ -574,10 +745,10 @@ export function resolveRuntimeDayNightWorldState( if (moonVisibilityFactor > 1e-4) { moonLight = { colorHex: blendHexColorsByWeights( - settings.night.lightColorHex, - settings.dawn.lightColorHex, - settings.dusk.lightColorHex, - settings.night.lightColorHex, + timeOfDay.night.lightColorHex, + timeOfDay.dawn.lightColorHex, + timeOfDay.dusk.lightColorHex, + timeOfDay.night.lightColorHex, { day: 0, dawn: phaseWeights.dawn, @@ -587,7 +758,7 @@ export function resolveRuntimeDayNightWorldState( ), intensity: world.sunLight.intensity * - settings.night.lightIntensityFactor * + timeOfDay.night.lightIntensityFactor * moonVisibilityFactor, direction: scaleVec3(sunDirection, -1) }; @@ -597,9 +768,9 @@ export function resolveRuntimeDayNightWorldState( ambientLight: { colorHex: blendHexColorsByWeights( world.ambientLight.colorHex, - settings.dawn.ambientColorHex, - settings.dusk.ambientColorHex, - settings.night.ambientColorHex, + timeOfDay.dawn.ambientColorHex, + timeOfDay.dusk.ambientColorHex, + timeOfDay.night.ambientColorHex, phaseWeights ), intensity: world.ambientLight.intensity * ambientFactor @@ -607,24 +778,17 @@ export function resolveRuntimeDayNightWorldState( sunLight: { colorHex: blendHexColorsByWeights( world.sunLight.colorHex, - settings.dawn.lightColorHex, - settings.dusk.lightColorHex, - settings.night.lightColorHex, + timeOfDay.dawn.lightColorHex, + timeOfDay.dusk.lightColorHex, + timeOfDay.night.lightColorHex, phaseWeights ), intensity: world.sunLight.intensity * sunFactor, direction: sunDirection }, moonLight, - background: resolveTimeDrivenBackground( - world.background, - settings, - phaseWeights - ), - nightBackgroundOverlay: resolveNightBackgroundOverlay( - settings.nightBackground, - phaseWeights - ), + background: resolvedBackground.background, + nightBackgroundOverlay: resolvedBackground.nightBackgroundOverlay, daylightFactor }; } \ No newline at end of file