658 lines
19 KiB
TypeScript
658 lines
19 KiB
TypeScript
import type { Vec2, Vec3 } from "../core/vector";
|
|
import {
|
|
HOURS_PER_DAY,
|
|
normalizeTimeOfDayHours,
|
|
type ProjectTimeSettings
|
|
} from "../document/project-time-settings";
|
|
import {
|
|
createDefaultWorldTimePhaseProfile,
|
|
resolveWorldCelestialOrbitPeakDirection,
|
|
type WorldCelestialOrbitSettings,
|
|
type WorldBackgroundSettings,
|
|
type WorldSettings
|
|
} from "../document/world-settings";
|
|
import {
|
|
resolveRuntimeDayNightWorldState,
|
|
resolveRuntimeDayNightPhaseWeights,
|
|
resolveRuntimeTimeState,
|
|
type RuntimeDayNightPhaseWeights,
|
|
type RuntimeDayPhase,
|
|
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;
|
|
horizonHeight: number;
|
|
};
|
|
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;
|
|
horizonFadeOffset: number;
|
|
rotationAxis: Vec3;
|
|
rotationRadians: number;
|
|
};
|
|
clouds: {
|
|
coverage: number;
|
|
density: number;
|
|
softness: number;
|
|
scale: number;
|
|
height: number;
|
|
heightVariation: number;
|
|
tintHex: string;
|
|
opacity: number;
|
|
opacityRandomness: number;
|
|
driftOffset: Vec2;
|
|
};
|
|
aurora: {
|
|
visibility: number;
|
|
intensity: number;
|
|
height: number;
|
|
thickness: number;
|
|
speed: number;
|
|
primaryColorHex: string;
|
|
secondaryColorHex: string;
|
|
rotationRadians: number;
|
|
timeHours: number;
|
|
};
|
|
}
|
|
|
|
export interface WorldShaderSkyEnvironmentPhaseStates {
|
|
day: WorldShaderSkyRenderState | null;
|
|
dawn: WorldShaderSkyRenderState | null;
|
|
dusk: WorldShaderSkyRenderState | null;
|
|
night: WorldShaderSkyRenderState | null;
|
|
}
|
|
|
|
export interface WorldShaderSkyEnvironmentPhaseBlend {
|
|
basePhase: RuntimeDayPhase;
|
|
overlayPhase: RuntimeDayPhase | null;
|
|
blendAmount: number;
|
|
}
|
|
|
|
function clamp(value: number, min: number, max: number): number {
|
|
return Math.min(Math.max(value, min), max);
|
|
}
|
|
|
|
function wrapTimeForward(hours: number, originHours: number): number {
|
|
let wrappedHours = normalizeTimeOfDayHours(hours);
|
|
|
|
while (wrappedHours < originHours) {
|
|
wrappedHours += HOURS_PER_DAY;
|
|
}
|
|
|
|
return wrappedHours;
|
|
}
|
|
|
|
function resolveTimeWindowMidpoint(
|
|
startTimeOfDayHours: number,
|
|
endTimeOfDayHours: number
|
|
): number {
|
|
const wrappedEndTime = wrapTimeForward(
|
|
endTimeOfDayHours,
|
|
startTimeOfDayHours
|
|
);
|
|
|
|
return normalizeTimeOfDayHours(
|
|
startTimeOfDayHours + (wrappedEndTime - startTimeOfDayHours) / 2
|
|
);
|
|
}
|
|
|
|
function quantizeNumberToBucket(value: number, step: number): number {
|
|
return Math.round(value / step);
|
|
}
|
|
|
|
const AURORA_WORLD_ROTATION_RADIANS = 0;
|
|
const DEFAULT_SKY_ROTATION_AXIS: Vec3 = {
|
|
x: 0,
|
|
y: 1,
|
|
z: 0
|
|
};
|
|
|
|
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
|
|
};
|
|
}
|
|
|
|
export function resolveWorldShaderSkyStarRotation(
|
|
timeOfDayHours: number,
|
|
sunOrbit: WorldCelestialOrbitSettings
|
|
): {
|
|
rotationAxis: Vec3;
|
|
rotationRadians: number;
|
|
} {
|
|
const sunPeakDirection = resolveWorldCelestialOrbitPeakDirection(sunOrbit);
|
|
const rotationAxis = normalizeDirection(
|
|
{
|
|
x: -sunPeakDirection.x * sunPeakDirection.y,
|
|
y: 1 - sunPeakDirection.y * sunPeakDirection.y,
|
|
z: -sunPeakDirection.z * sunPeakDirection.y
|
|
},
|
|
DEFAULT_SKY_ROTATION_AXIS
|
|
);
|
|
|
|
return {
|
|
rotationAxis,
|
|
rotationRadians:
|
|
(normalizeTimeOfDayHours(timeOfDayHours) / HOURS_PER_DAY) * Math.PI * 2
|
|
};
|
|
}
|
|
|
|
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
|
|
);
|
|
}
|
|
|
|
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 sunRenderDirection = offsetDirectionForHorizon(
|
|
sunDirection,
|
|
world.shaderSky.horizonHeight
|
|
);
|
|
const moonRenderDirection = offsetDirectionForHorizon(
|
|
moonDirection,
|
|
world.shaderSky.horizonHeight
|
|
);
|
|
const starVisibility =
|
|
clamp(phaseWeights.night + twilightFactor * 0.45, 0, 1) *
|
|
clamp(1 - resolvedWorld.daylightFactor * 0.85, 0, 1);
|
|
const auroraVisibility = world.shaderSky.aurora.enabled
|
|
? clamp(phaseWeights.night + twilightFactor * 0.32, 0, 1) *
|
|
clamp(1 - resolvedWorld.daylightFactor * 1.15, 0, 1)
|
|
: 0;
|
|
const cloudDriftDirectionRadians =
|
|
(world.shaderSky.clouds.driftDirectionDegrees * Math.PI) / 180;
|
|
const cloudDriftDistance =
|
|
continuousHours * world.shaderSky.clouds.driftSpeed;
|
|
const starRotation = resolveWorldShaderSkyStarRotation(
|
|
timeOfDayHours,
|
|
world.celestialOrbits.sun
|
|
);
|
|
|
|
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
|
|
),
|
|
horizonHeight: world.shaderSky.horizonHeight
|
|
},
|
|
celestial: {
|
|
sunDirection: sunRenderDirection,
|
|
sunColorHex: resolvedWorld.sunLight.colorHex,
|
|
sunIntensity: resolvedWorld.sunLight.intensity,
|
|
sunDiscSizeDegrees: world.shaderSky.celestial.sunDiscSizeDegrees,
|
|
sunVisible:
|
|
world.showCelestialBodies && resolvedWorld.sunLight.intensity > 1e-4,
|
|
moonDirection: moonRenderDirection,
|
|
moonColorHex: resolvedWorld.moonLight?.colorHex ?? "#d7e4ff",
|
|
moonIntensity: resolvedWorld.moonLight?.intensity ?? 0,
|
|
moonDiscSizeDegrees: world.shaderSky.celestial.moonDiscSizeDegrees,
|
|
moonVisible:
|
|
world.showCelestialBodies &&
|
|
(resolvedWorld.moonLight?.intensity ?? 0) > 1e-4
|
|
},
|
|
stars: {
|
|
density: world.shaderSky.stars.density,
|
|
brightness: world.shaderSky.stars.brightness,
|
|
visibility: starVisibility,
|
|
horizonFadeOffset: world.shaderSky.stars.horizonFadeOffset,
|
|
rotationAxis: starRotation.rotationAxis,
|
|
rotationRadians: starRotation.rotationRadians
|
|
},
|
|
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
|
|
}
|
|
},
|
|
aurora: {
|
|
visibility: auroraVisibility,
|
|
intensity: world.shaderSky.aurora.intensity,
|
|
height: world.shaderSky.aurora.height,
|
|
thickness: world.shaderSky.aurora.thickness,
|
|
speed: world.shaderSky.aurora.speed,
|
|
primaryColorHex: world.shaderSky.aurora.primaryColorHex,
|
|
secondaryColorHex: world.shaderSky.aurora.secondaryColorHex,
|
|
rotationRadians: AURORA_WORLD_ROTATION_RADIANS,
|
|
timeHours: continuousHours
|
|
}
|
|
};
|
|
}
|
|
|
|
export function resolveWorldShaderSkyEnvironmentPhaseStates(
|
|
world: WorldSettings,
|
|
timeSettings: ProjectTimeSettings | null
|
|
): WorldShaderSkyEnvironmentPhaseStates {
|
|
if (world.background.mode !== "shader") {
|
|
return {
|
|
day: null,
|
|
dawn: null,
|
|
dusk: null,
|
|
night: null
|
|
};
|
|
}
|
|
|
|
if (!world.projectTimeLightingEnabled || timeSettings === null) {
|
|
const resolvedWorld: RuntimeDayNightWorldState = {
|
|
ambientLight: {
|
|
...world.ambientLight
|
|
},
|
|
sunLight: {
|
|
...world.sunLight,
|
|
direction: {
|
|
...world.sunLight.direction
|
|
}
|
|
},
|
|
moonLight: null,
|
|
background: {
|
|
...world.background
|
|
},
|
|
nightBackgroundOverlay: null,
|
|
daylightFactor: 1
|
|
};
|
|
const dayState = resolveWorldShaderSkyRenderState(
|
|
world,
|
|
resolvedWorld,
|
|
null,
|
|
null
|
|
);
|
|
|
|
return {
|
|
day: dayState,
|
|
dawn: dayState,
|
|
dusk: dayState,
|
|
night: dayState
|
|
};
|
|
}
|
|
|
|
const dawnHalfDuration = Math.max(timeSettings.dawnDurationHours, 0.001) / 2;
|
|
const duskHalfDuration = Math.max(timeSettings.duskDurationHours, 0.001) / 2;
|
|
const dawnStart = timeSettings.sunriseTimeOfDayHours - dawnHalfDuration;
|
|
const dawnEnd = timeSettings.sunriseTimeOfDayHours + dawnHalfDuration;
|
|
const duskStart = timeSettings.sunsetTimeOfDayHours - duskHalfDuration;
|
|
const duskEnd = timeSettings.sunsetTimeOfDayHours + duskHalfDuration;
|
|
const representativeTimes = {
|
|
dawn: resolveTimeWindowMidpoint(dawnStart, dawnEnd),
|
|
day: resolveTimeWindowMidpoint(dawnEnd, duskStart),
|
|
dusk: resolveTimeWindowMidpoint(duskStart, duskEnd),
|
|
night: resolveTimeWindowMidpoint(duskEnd, dawnStart)
|
|
};
|
|
const resolveRepresentativeState = (
|
|
timeOfDayHours: number
|
|
): WorldShaderSkyRenderState | null => {
|
|
const resolvedTime = resolveRuntimeTimeState(timeSettings, {
|
|
timeOfDayHours,
|
|
dayCount: 0,
|
|
dayLengthMinutes: timeSettings.dayLengthMinutes
|
|
});
|
|
const resolvedWorld = resolveRuntimeDayNightWorldState(
|
|
world,
|
|
timeSettings,
|
|
{
|
|
timeOfDayHours,
|
|
dayCount: 0,
|
|
dayLengthMinutes: timeSettings.dayLengthMinutes
|
|
},
|
|
resolvedTime
|
|
);
|
|
|
|
return resolveWorldShaderSkyRenderState(
|
|
world,
|
|
resolvedWorld,
|
|
resolvedTime,
|
|
timeSettings
|
|
);
|
|
};
|
|
|
|
return {
|
|
day: resolveRepresentativeState(representativeTimes.day),
|
|
dawn: resolveRepresentativeState(representativeTimes.dawn),
|
|
dusk: resolveRepresentativeState(representativeTimes.dusk),
|
|
night: resolveRepresentativeState(representativeTimes.night)
|
|
};
|
|
}
|
|
|
|
export function createWorldShaderSkyEnvironmentCacheKey(
|
|
state: WorldShaderSkyRenderState
|
|
): string {
|
|
return JSON.stringify({
|
|
presetId: state.presetId,
|
|
sky: [
|
|
state.sky.topColorHex,
|
|
state.sky.bottomColorHex,
|
|
quantizeNumberToBucket(state.sky.horizonHeight, 0.01)
|
|
],
|
|
sun: [
|
|
state.celestial.sunColorHex,
|
|
quantizeNumberToBucket(state.celestial.sunDirection.x, 0.01),
|
|
quantizeNumberToBucket(state.celestial.sunDirection.y, 0.01),
|
|
quantizeNumberToBucket(state.celestial.sunDirection.z, 0.01),
|
|
quantizeNumberToBucket(state.celestial.sunIntensity, 0.05),
|
|
quantizeNumberToBucket(state.celestial.sunDiscSizeDegrees, 0.05),
|
|
state.celestial.sunVisible ? 1 : 0
|
|
],
|
|
moon: [
|
|
state.celestial.moonColorHex,
|
|
quantizeNumberToBucket(state.celestial.moonDirection.x, 0.01),
|
|
quantizeNumberToBucket(state.celestial.moonDirection.y, 0.01),
|
|
quantizeNumberToBucket(state.celestial.moonDirection.z, 0.01),
|
|
quantizeNumberToBucket(state.celestial.moonIntensity, 0.05),
|
|
quantizeNumberToBucket(state.celestial.moonDiscSizeDegrees, 0.05),
|
|
state.celestial.moonVisible ? 1 : 0
|
|
],
|
|
stars: [
|
|
quantizeNumberToBucket(state.stars.density, 0.05),
|
|
quantizeNumberToBucket(state.stars.brightness, 0.05),
|
|
quantizeNumberToBucket(state.stars.visibility, 0.05),
|
|
quantizeNumberToBucket(state.stars.horizonFadeOffset, 0.01),
|
|
quantizeNumberToBucket(state.stars.rotationAxis.x, 0.01),
|
|
quantizeNumberToBucket(state.stars.rotationAxis.y, 0.01),
|
|
quantizeNumberToBucket(state.stars.rotationAxis.z, 0.01),
|
|
quantizeNumberToBucket(state.stars.rotationRadians, 0.02)
|
|
],
|
|
clouds: [
|
|
state.clouds.tintHex,
|
|
quantizeNumberToBucket(state.clouds.coverage, 0.05),
|
|
quantizeNumberToBucket(state.clouds.density, 0.05),
|
|
quantizeNumberToBucket(state.clouds.softness, 0.05),
|
|
quantizeNumberToBucket(state.clouds.scale, 0.05),
|
|
quantizeNumberToBucket(state.clouds.height, 0.05),
|
|
quantizeNumberToBucket(state.clouds.heightVariation, 0.05),
|
|
quantizeNumberToBucket(state.clouds.opacity, 0.05),
|
|
quantizeNumberToBucket(state.clouds.opacityRandomness, 0.05),
|
|
quantizeNumberToBucket(state.clouds.driftOffset.x, 0.02),
|
|
quantizeNumberToBucket(state.clouds.driftOffset.y, 0.02)
|
|
],
|
|
aurora: [
|
|
state.aurora.primaryColorHex,
|
|
state.aurora.secondaryColorHex,
|
|
quantizeNumberToBucket(state.aurora.visibility, 0.05),
|
|
quantizeNumberToBucket(state.aurora.intensity, 0.05),
|
|
quantizeNumberToBucket(state.aurora.height, 0.05),
|
|
quantizeNumberToBucket(state.aurora.thickness, 0.05),
|
|
quantizeNumberToBucket(state.aurora.speed, 0.05),
|
|
quantizeNumberToBucket(state.aurora.rotationRadians, 0.02)
|
|
]
|
|
});
|
|
}
|
|
|
|
export function createWorldShaderSkyEnvironmentPhaseCacheKey(
|
|
states: WorldShaderSkyEnvironmentPhaseStates
|
|
): string {
|
|
return JSON.stringify({
|
|
day:
|
|
states.day === null
|
|
? null
|
|
: createWorldShaderSkyEnvironmentCacheKey(states.day),
|
|
dawn:
|
|
states.dawn === null
|
|
? null
|
|
: createWorldShaderSkyEnvironmentCacheKey(states.dawn),
|
|
dusk:
|
|
states.dusk === null
|
|
? null
|
|
: createWorldShaderSkyEnvironmentCacheKey(states.dusk),
|
|
night:
|
|
states.night === null
|
|
? null
|
|
: createWorldShaderSkyEnvironmentCacheKey(states.night)
|
|
});
|
|
}
|
|
|
|
export function resolveWorldShaderSkyEnvironmentPhaseBlend(
|
|
state: Pick<WorldShaderSkyRenderState, "time">
|
|
): WorldShaderSkyEnvironmentPhaseBlend | null {
|
|
const activePhases = (["day", "dawn", "dusk", "night"] as const).filter(
|
|
(phase) => state.time.phaseWeights[phase] > 1e-4
|
|
);
|
|
|
|
if (activePhases.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
if (activePhases.length === 1) {
|
|
return {
|
|
basePhase: activePhases[0],
|
|
overlayPhase: null,
|
|
blendAmount: 0
|
|
};
|
|
}
|
|
|
|
const [basePhase, overlayPhase] = activePhases;
|
|
const baseWeight = state.time.phaseWeights[basePhase];
|
|
const overlayWeight = state.time.phaseWeights[overlayPhase];
|
|
const totalWeight = Math.max(baseWeight + overlayWeight, 1e-6);
|
|
|
|
return {
|
|
basePhase,
|
|
overlayPhase,
|
|
blendAmount: overlayWeight / totalWeight
|
|
};
|
|
}
|