2026-04-22 15:17:36 +02:00
|
|
|
import type { Vec2, Vec3 } from "../core/vector";
|
|
|
|
|
import type { ProjectTimeSettings } from "../document/project-time-settings";
|
|
|
|
|
import {
|
|
|
|
|
createDefaultWorldTimePhaseProfile,
|
|
|
|
|
type WorldBackgroundSettings,
|
|
|
|
|
type WorldSettings
|
|
|
|
|
} from "../document/world-settings";
|
|
|
|
|
import {
|
|
|
|
|
resolveRuntimeDayNightPhaseWeights,
|
|
|
|
|
type RuntimeDayNightPhaseWeights,
|
|
|
|
|
type RuntimeDayNightWorldState,
|
|
|
|
|
type RuntimeResolvedTimeState
|
|
|
|
|
} from "../runtime-three/runtime-project-time";
|
|
|
|
|
|
|
|
|
|
export interface WorldShaderSkyRenderState {
|
|
|
|
|
presetId: "defaultSky";
|
|
|
|
|
time: {
|
|
|
|
|
dayCount: number;
|
|
|
|
|
timeOfDayHours: number;
|
|
|
|
|
dayPhase: RuntimeResolvedTimeState["dayPhase"];
|
|
|
|
|
daylightFactor: number;
|
|
|
|
|
twilightFactor: number;
|
|
|
|
|
phaseWeights: RuntimeDayNightPhaseWeights;
|
|
|
|
|
};
|
|
|
|
|
sky: {
|
|
|
|
|
topColorHex: string;
|
|
|
|
|
bottomColorHex: string;
|
2026-04-22 15:49:32 +02:00
|
|
|
horizonHeight: number;
|
2026-04-22 15:17:36 +02:00
|
|
|
};
|
|
|
|
|
celestial: {
|
|
|
|
|
sunDirection: Vec3;
|
|
|
|
|
sunColorHex: string;
|
|
|
|
|
sunIntensity: number;
|
|
|
|
|
sunDiscSizeDegrees: number;
|
|
|
|
|
sunVisible: boolean;
|
|
|
|
|
moonDirection: Vec3;
|
|
|
|
|
moonColorHex: string;
|
|
|
|
|
moonIntensity: number;
|
|
|
|
|
moonDiscSizeDegrees: number;
|
|
|
|
|
moonVisible: boolean;
|
|
|
|
|
};
|
|
|
|
|
stars: {
|
|
|
|
|
density: number;
|
|
|
|
|
brightness: number;
|
|
|
|
|
visibility: number;
|
|
|
|
|
rotationRadians: number;
|
|
|
|
|
};
|
|
|
|
|
clouds: {
|
|
|
|
|
coverage: number;
|
|
|
|
|
density: number;
|
|
|
|
|
softness: number;
|
|
|
|
|
scale: number;
|
|
|
|
|
height: number;
|
|
|
|
|
heightVariation: number;
|
|
|
|
|
tintHex: string;
|
|
|
|
|
opacity: number;
|
|
|
|
|
opacityRandomness: number;
|
|
|
|
|
driftOffset: Vec2;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clamp(value: number, min: number, max: number): number {
|
|
|
|
|
return Math.min(Math.max(value, min), max);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseHexColor(colorHex: string): { r: number; g: number; b: number } {
|
|
|
|
|
return {
|
|
|
|
|
r: Number.parseInt(colorHex.slice(1, 3), 16),
|
|
|
|
|
g: Number.parseInt(colorHex.slice(3, 5), 16),
|
|
|
|
|
b: Number.parseInt(colorHex.slice(5, 7), 16)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatHexColor(color: { r: number; g: number; b: number }): string {
|
|
|
|
|
const toHex = (value: number) =>
|
2026-04-22 15:30:37 +02:00
|
|
|
Math.round(clamp(value, 0, 255))
|
|
|
|
|
.toString(16)
|
|
|
|
|
.padStart(2, "0");
|
2026-04-22 15:17:36 +02:00
|
|
|
|
|
|
|
|
return `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function blendHexColorsByWeights(
|
|
|
|
|
colors: {
|
|
|
|
|
day: string;
|
|
|
|
|
dawn: string;
|
|
|
|
|
dusk: string;
|
|
|
|
|
night: string;
|
|
|
|
|
},
|
|
|
|
|
weights: RuntimeDayNightPhaseWeights
|
|
|
|
|
): string {
|
2026-04-22 15:30:37 +02:00
|
|
|
const totalWeight = weights.day + weights.dawn + weights.dusk + weights.night;
|
2026-04-22 15:17:36 +02:00
|
|
|
|
|
|
|
|
if (totalWeight <= 1e-6) {
|
|
|
|
|
return colors.day;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const day = parseHexColor(colors.day);
|
|
|
|
|
const dawn = parseHexColor(colors.dawn);
|
|
|
|
|
const dusk = parseHexColor(colors.dusk);
|
|
|
|
|
const night = parseHexColor(colors.night);
|
|
|
|
|
|
|
|
|
|
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 normalizeDirection(direction: Vec3, fallback: Vec3): Vec3 {
|
|
|
|
|
const length = Math.hypot(direction.x, direction.y, direction.z);
|
|
|
|
|
|
|
|
|
|
if (length <= 1e-6) {
|
|
|
|
|
return {
|
|
|
|
|
...fallback
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
x: direction.x / length,
|
|
|
|
|
y: direction.y / length,
|
|
|
|
|
z: direction.z / length
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 16:12:00 +02:00
|
|
|
function offsetDirectionForHorizon(direction: Vec3, horizonHeight: number): Vec3 {
|
|
|
|
|
if (Math.abs(horizonHeight) <= 1e-6) {
|
|
|
|
|
return direction;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return normalizeDirection(
|
|
|
|
|
{
|
|
|
|
|
x: direction.x,
|
|
|
|
|
y: direction.y - horizonHeight,
|
|
|
|
|
z: direction.z
|
|
|
|
|
},
|
|
|
|
|
direction
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 15:17:36 +02:00
|
|
|
function resolveGradientBasis(
|
|
|
|
|
background: WorldBackgroundSettings,
|
|
|
|
|
fallbackTopColorHex: string,
|
|
|
|
|
fallbackBottomColorHex: string
|
|
|
|
|
): {
|
|
|
|
|
topColorHex: string;
|
|
|
|
|
bottomColorHex: string;
|
|
|
|
|
} {
|
|
|
|
|
if (background.mode === "solid") {
|
|
|
|
|
return {
|
|
|
|
|
topColorHex: background.colorHex,
|
|
|
|
|
bottomColorHex: background.colorHex
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (background.mode === "verticalGradient") {
|
|
|
|
|
return {
|
|
|
|
|
topColorHex: background.topColorHex,
|
|
|
|
|
bottomColorHex: background.bottomColorHex
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
topColorHex: fallbackTopColorHex,
|
|
|
|
|
bottomColorHex: fallbackBottomColorHex
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resolveWorldShaderSkyRenderState(
|
|
|
|
|
world: WorldSettings,
|
|
|
|
|
resolvedWorld: RuntimeDayNightWorldState,
|
|
|
|
|
resolvedTime: RuntimeResolvedTimeState | null,
|
|
|
|
|
timeSettings: ProjectTimeSettings | null
|
|
|
|
|
): WorldShaderSkyRenderState | null {
|
|
|
|
|
if (world.background.mode !== "shader") {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const phaseWeights =
|
|
|
|
|
world.projectTimeLightingEnabled &&
|
|
|
|
|
resolvedTime !== null &&
|
|
|
|
|
timeSettings !== null
|
|
|
|
|
? resolveRuntimeDayNightPhaseWeights(
|
|
|
|
|
timeSettings,
|
|
|
|
|
resolvedTime.timeOfDayHours
|
|
|
|
|
)
|
|
|
|
|
: {
|
|
|
|
|
day: 1,
|
|
|
|
|
dawn: 0,
|
|
|
|
|
dusk: 0,
|
|
|
|
|
night: 0
|
|
|
|
|
};
|
|
|
|
|
const twilightFactor = clamp(phaseWeights.dawn + phaseWeights.dusk, 0, 1);
|
|
|
|
|
const dayCount = resolvedTime?.dayCount ?? 0;
|
|
|
|
|
const timeOfDayHours = resolvedTime?.timeOfDayHours ?? 12;
|
|
|
|
|
const continuousHours = dayCount * 24 + timeOfDayHours;
|
|
|
|
|
const dawnGradient = resolveGradientBasis(
|
|
|
|
|
world.timeOfDay.dawn.background,
|
|
|
|
|
world.timeOfDay.dawn.skyTopColorHex,
|
|
|
|
|
world.timeOfDay.dawn.skyBottomColorHex
|
|
|
|
|
);
|
|
|
|
|
const duskGradient = resolveGradientBasis(
|
|
|
|
|
world.timeOfDay.dusk.background,
|
|
|
|
|
world.timeOfDay.dusk.skyTopColorHex,
|
|
|
|
|
world.timeOfDay.dusk.skyBottomColorHex
|
|
|
|
|
);
|
|
|
|
|
const defaultNightGradient = createDefaultWorldTimePhaseProfile("night");
|
|
|
|
|
const nightGradient = resolveGradientBasis(
|
|
|
|
|
world.timeOfDay.night.background,
|
|
|
|
|
defaultNightGradient.skyTopColorHex,
|
|
|
|
|
defaultNightGradient.skyBottomColorHex
|
|
|
|
|
);
|
|
|
|
|
const sunDirection = normalizeDirection(
|
|
|
|
|
resolvedWorld.sunLight.direction,
|
|
|
|
|
world.sunLight.direction
|
|
|
|
|
);
|
|
|
|
|
const moonDirection = normalizeDirection(
|
|
|
|
|
resolvedWorld.moonLight?.direction ?? {
|
|
|
|
|
x: -sunDirection.x,
|
|
|
|
|
y: -sunDirection.y,
|
|
|
|
|
z: -sunDirection.z
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
x: -sunDirection.x,
|
|
|
|
|
y: -sunDirection.y,
|
|
|
|
|
z: -sunDirection.z
|
|
|
|
|
}
|
|
|
|
|
);
|
2026-04-22 16:12:00 +02:00
|
|
|
const sunRenderDirection = offsetDirectionForHorizon(
|
|
|
|
|
sunDirection,
|
|
|
|
|
world.shaderSky.horizonHeight
|
|
|
|
|
);
|
|
|
|
|
const moonRenderDirection = offsetDirectionForHorizon(
|
|
|
|
|
moonDirection,
|
|
|
|
|
world.shaderSky.horizonHeight
|
|
|
|
|
);
|
2026-04-22 15:17:36 +02:00
|
|
|
const starVisibility =
|
|
|
|
|
clamp(phaseWeights.night + twilightFactor * 0.45, 0, 1) *
|
|
|
|
|
clamp(1 - resolvedWorld.daylightFactor * 0.85, 0, 1);
|
|
|
|
|
const cloudDriftDirectionRadians =
|
|
|
|
|
(world.shaderSky.clouds.driftDirectionDegrees * Math.PI) / 180;
|
|
|
|
|
const cloudDriftDistance =
|
|
|
|
|
continuousHours * world.shaderSky.clouds.driftSpeed;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
presetId: world.shaderSky.presetId,
|
|
|
|
|
time: {
|
|
|
|
|
dayCount,
|
|
|
|
|
timeOfDayHours,
|
|
|
|
|
dayPhase: resolvedTime?.dayPhase ?? "day",
|
|
|
|
|
daylightFactor: resolvedWorld.daylightFactor,
|
|
|
|
|
twilightFactor,
|
|
|
|
|
phaseWeights
|
|
|
|
|
},
|
|
|
|
|
sky: {
|
|
|
|
|
topColorHex: blendHexColorsByWeights(
|
|
|
|
|
{
|
|
|
|
|
day: world.shaderSky.dayTopColorHex,
|
|
|
|
|
dawn: dawnGradient.topColorHex,
|
|
|
|
|
dusk: duskGradient.topColorHex,
|
|
|
|
|
night: nightGradient.topColorHex
|
|
|
|
|
},
|
|
|
|
|
phaseWeights
|
|
|
|
|
),
|
|
|
|
|
bottomColorHex: blendHexColorsByWeights(
|
|
|
|
|
{
|
|
|
|
|
day: world.shaderSky.dayBottomColorHex,
|
|
|
|
|
dawn: dawnGradient.bottomColorHex,
|
|
|
|
|
dusk: duskGradient.bottomColorHex,
|
|
|
|
|
night: nightGradient.bottomColorHex
|
|
|
|
|
},
|
|
|
|
|
phaseWeights
|
2026-04-22 15:49:32 +02:00
|
|
|
),
|
|
|
|
|
horizonHeight: world.shaderSky.horizonHeight
|
2026-04-22 15:17:36 +02:00
|
|
|
},
|
|
|
|
|
celestial: {
|
2026-04-22 16:12:00 +02:00
|
|
|
sunDirection: sunRenderDirection,
|
2026-04-22 15:17:36 +02:00
|
|
|
sunColorHex: resolvedWorld.sunLight.colorHex,
|
|
|
|
|
sunIntensity: resolvedWorld.sunLight.intensity,
|
|
|
|
|
sunDiscSizeDegrees: world.shaderSky.celestial.sunDiscSizeDegrees,
|
|
|
|
|
sunVisible:
|
2026-04-22 15:49:32 +02:00
|
|
|
world.showCelestialBodies && resolvedWorld.sunLight.intensity > 1e-4,
|
2026-04-22 16:12:00 +02:00
|
|
|
moonDirection: moonRenderDirection,
|
2026-04-22 15:17:36 +02:00
|
|
|
moonColorHex: resolvedWorld.moonLight?.colorHex ?? "#d7e4ff",
|
|
|
|
|
moonIntensity: resolvedWorld.moonLight?.intensity ?? 0,
|
|
|
|
|
moonDiscSizeDegrees: world.shaderSky.celestial.moonDiscSizeDegrees,
|
|
|
|
|
moonVisible:
|
|
|
|
|
world.showCelestialBodies &&
|
2026-04-22 15:49:32 +02:00
|
|
|
(resolvedWorld.moonLight?.intensity ?? 0) > 1e-4
|
2026-04-22 15:17:36 +02:00
|
|
|
},
|
|
|
|
|
stars: {
|
|
|
|
|
density: world.shaderSky.stars.density,
|
|
|
|
|
brightness: world.shaderSky.stars.brightness,
|
|
|
|
|
visibility: starVisibility,
|
|
|
|
|
rotationRadians: Math.atan2(sunDirection.z, sunDirection.x)
|
|
|
|
|
},
|
|
|
|
|
clouds: {
|
|
|
|
|
coverage: world.shaderSky.clouds.coverage,
|
|
|
|
|
density: world.shaderSky.clouds.density,
|
|
|
|
|
softness: world.shaderSky.clouds.softness,
|
|
|
|
|
scale: world.shaderSky.clouds.scale,
|
|
|
|
|
height: world.shaderSky.clouds.height,
|
|
|
|
|
heightVariation: world.shaderSky.clouds.heightVariation,
|
|
|
|
|
tintHex: world.shaderSky.clouds.tintHex,
|
|
|
|
|
opacity: world.shaderSky.clouds.opacity,
|
|
|
|
|
opacityRandomness: world.shaderSky.clouds.opacityRandomness,
|
|
|
|
|
driftOffset: {
|
|
|
|
|
x: Math.cos(cloudDriftDirectionRadians) * cloudDriftDistance,
|
|
|
|
|
y: Math.sin(cloudDriftDirectionRadians) * cloudDriftDistance * 0.35
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|