auto-git:

[add] src/rendering/world-shader-sky.ts
This commit is contained in:
2026-04-22 15:17:36 +02:00
parent 57bbc6fda6
commit 62edf5aa78

View File

@@ -0,0 +1,305 @@
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;
};
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) =>
Math.round(clamp(value, 0, 255)).toString(16).padStart(2, "0");
return `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`;
}
function blendHexColorsByWeights(
colors: {
day: string;
dawn: string;
dusk: string;
night: string;
},
weights: RuntimeDayNightPhaseWeights
): string {
const totalWeight =
weights.day + weights.dawn + weights.dusk + weights.night;
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
};
}
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
}
);
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
)
},
celestial: {
sunDirection,
sunColorHex: resolvedWorld.sunLight.colorHex,
sunIntensity: resolvedWorld.sunLight.intensity,
sunDiscSizeDegrees: world.shaderSky.celestial.sunDiscSizeDegrees,
sunVisible:
world.showCelestialBodies &&
resolvedWorld.sunLight.intensity > 1e-4 &&
sunDirection.y > -0.06,
moonDirection,
moonColorHex: resolvedWorld.moonLight?.colorHex ?? "#d7e4ff",
moonIntensity: resolvedWorld.moonLight?.intensity ?? 0,
moonDiscSizeDegrees: world.shaderSky.celestial.moonDiscSizeDegrees,
moonVisible:
world.showCelestialBodies &&
(resolvedWorld.moonLight?.intensity ?? 0) > 1e-4 &&
moonDirection.y > -0.06
},
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
}
}
};
}