2026-04-22 15:28:13 +02:00
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
|
|
|
|
|
|
import { createDefaultProjectTimeSettings } from "../../src/document/project-time-settings";
|
|
|
|
|
import { createDefaultWorldSettings } from "../../src/document/world-settings";
|
2026-04-22 17:55:16 +02:00
|
|
|
import {
|
|
|
|
|
createWorldShaderSkyEnvironmentCacheKey,
|
|
|
|
|
resolveWorldShaderSkyEnvironmentPhaseStates,
|
|
|
|
|
resolveWorldShaderSkyRenderState
|
|
|
|
|
} from "../../src/rendering/world-shader-sky";
|
2026-04-22 15:28:13 +02:00
|
|
|
import {
|
|
|
|
|
resolveRuntimeDayNightWorldState,
|
|
|
|
|
resolveRuntimeTimeState
|
|
|
|
|
} from "../../src/runtime-three/runtime-project-time";
|
|
|
|
|
|
|
|
|
|
describe("resolveWorldShaderSkyRenderState", () => {
|
|
|
|
|
it("blends authored day, dawn, dusk, and night color bases continuously", () => {
|
|
|
|
|
const world = createDefaultWorldSettings();
|
|
|
|
|
const time = createDefaultProjectTimeSettings();
|
|
|
|
|
world.background = {
|
|
|
|
|
mode: "shader"
|
|
|
|
|
};
|
|
|
|
|
world.showCelestialBodies = true;
|
|
|
|
|
world.shaderSky.dayTopColorHex = "#88ccff";
|
|
|
|
|
world.shaderSky.dayBottomColorHex = "#dff3ff";
|
2026-04-22 16:00:18 +02:00
|
|
|
world.shaderSky.horizonHeight = -0.08;
|
2026-04-22 16:23:26 +02:00
|
|
|
world.shaderSky.stars.horizonFadeOffset = 0.06;
|
2026-04-22 15:28:13 +02:00
|
|
|
world.timeOfDay.dawn.background = {
|
|
|
|
|
mode: "verticalGradient",
|
|
|
|
|
topColorHex: "#ff8855",
|
|
|
|
|
bottomColorHex: "#ffd4a8"
|
|
|
|
|
};
|
|
|
|
|
world.timeOfDay.dusk.background = {
|
|
|
|
|
mode: "verticalGradient",
|
|
|
|
|
topColorHex: "#5e62c8",
|
|
|
|
|
bottomColorHex: "#ff9966"
|
|
|
|
|
};
|
|
|
|
|
world.timeOfDay.night.background = {
|
|
|
|
|
mode: "verticalGradient",
|
|
|
|
|
topColorHex: "#04101d",
|
|
|
|
|
bottomColorHex: "#17263b"
|
|
|
|
|
};
|
|
|
|
|
time.sunriseTimeOfDayHours = 7;
|
|
|
|
|
time.sunsetTimeOfDayHours = 20;
|
|
|
|
|
time.dawnDurationHours = 2;
|
|
|
|
|
time.duskDurationHours = 2;
|
|
|
|
|
|
|
|
|
|
const dawnTime = resolveRuntimeTimeState(time, {
|
|
|
|
|
timeOfDayHours: 6.5,
|
|
|
|
|
dayCount: 0,
|
|
|
|
|
dayLengthMinutes: 24
|
|
|
|
|
});
|
|
|
|
|
const dawnWorld = resolveRuntimeDayNightWorldState(
|
|
|
|
|
world,
|
|
|
|
|
time,
|
|
|
|
|
{
|
|
|
|
|
timeOfDayHours: 6.5,
|
|
|
|
|
dayCount: 0,
|
|
|
|
|
dayLengthMinutes: 24
|
|
|
|
|
},
|
|
|
|
|
dawnTime
|
|
|
|
|
);
|
|
|
|
|
const noonTime = resolveRuntimeTimeState(time, {
|
|
|
|
|
timeOfDayHours: 12,
|
|
|
|
|
dayCount: 0,
|
|
|
|
|
dayLengthMinutes: 24
|
|
|
|
|
});
|
|
|
|
|
const noonWorld = resolveRuntimeDayNightWorldState(
|
|
|
|
|
world,
|
|
|
|
|
time,
|
|
|
|
|
{
|
|
|
|
|
timeOfDayHours: 12,
|
|
|
|
|
dayCount: 0,
|
|
|
|
|
dayLengthMinutes: 24
|
|
|
|
|
},
|
|
|
|
|
noonTime
|
|
|
|
|
);
|
|
|
|
|
const midnightTime = resolveRuntimeTimeState(time, {
|
|
|
|
|
timeOfDayHours: 0,
|
|
|
|
|
dayCount: 0,
|
|
|
|
|
dayLengthMinutes: 24
|
|
|
|
|
});
|
|
|
|
|
const midnightWorld = resolveRuntimeDayNightWorldState(
|
|
|
|
|
world,
|
|
|
|
|
time,
|
|
|
|
|
{
|
|
|
|
|
timeOfDayHours: 0,
|
|
|
|
|
dayCount: 0,
|
|
|
|
|
dayLengthMinutes: 24
|
|
|
|
|
},
|
|
|
|
|
midnightTime
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const dawnSky = resolveWorldShaderSkyRenderState(
|
|
|
|
|
world,
|
|
|
|
|
dawnWorld,
|
|
|
|
|
dawnTime,
|
|
|
|
|
time
|
|
|
|
|
);
|
|
|
|
|
const noonSky = resolveWorldShaderSkyRenderState(
|
|
|
|
|
world,
|
|
|
|
|
noonWorld,
|
|
|
|
|
noonTime,
|
|
|
|
|
time
|
|
|
|
|
);
|
|
|
|
|
const midnightSky = resolveWorldShaderSkyRenderState(
|
|
|
|
|
world,
|
|
|
|
|
midnightWorld,
|
|
|
|
|
midnightTime,
|
|
|
|
|
time
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(dawnSky).not.toBeNull();
|
|
|
|
|
expect(noonSky?.sky.topColorHex).toBe("#88ccff");
|
|
|
|
|
expect(noonSky?.sky.bottomColorHex).toBe("#dff3ff");
|
2026-04-22 16:00:18 +02:00
|
|
|
expect(noonSky?.sky.horizonHeight).toBe(-0.08);
|
2026-04-22 16:23:26 +02:00
|
|
|
expect(noonSky?.stars.horizonFadeOffset).toBe(0.06);
|
2026-04-22 15:28:13 +02:00
|
|
|
expect(dawnSky?.sky.topColorHex).not.toBe(noonSky?.sky.topColorHex);
|
2026-04-22 15:30:37 +02:00
|
|
|
expect(dawnSky?.sky.topColorHex).not.toBe(midnightSky?.sky.topColorHex);
|
2026-04-22 15:28:13 +02:00
|
|
|
expect(midnightSky?.stars.visibility ?? 0).toBeGreaterThan(
|
|
|
|
|
dawnSky?.stars.visibility ?? 0
|
|
|
|
|
);
|
|
|
|
|
expect(dawnSky?.stars.visibility ?? 0).toBeGreaterThan(0);
|
|
|
|
|
expect(noonSky?.stars.visibility ?? 1).toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("keeps shader sky drift and celestial visibility coherent across days", () => {
|
|
|
|
|
const world = createDefaultWorldSettings();
|
|
|
|
|
const time = createDefaultProjectTimeSettings();
|
|
|
|
|
world.background = {
|
|
|
|
|
mode: "shader"
|
|
|
|
|
};
|
|
|
|
|
world.showCelestialBodies = true;
|
|
|
|
|
|
|
|
|
|
const firstNightTime = resolveRuntimeTimeState(time, {
|
|
|
|
|
timeOfDayHours: 0,
|
|
|
|
|
dayCount: 0,
|
|
|
|
|
dayLengthMinutes: 24
|
|
|
|
|
});
|
|
|
|
|
const firstNightWorld = resolveRuntimeDayNightWorldState(
|
|
|
|
|
world,
|
|
|
|
|
time,
|
|
|
|
|
{
|
|
|
|
|
timeOfDayHours: 0,
|
|
|
|
|
dayCount: 0,
|
|
|
|
|
dayLengthMinutes: 24
|
|
|
|
|
},
|
|
|
|
|
firstNightTime
|
|
|
|
|
);
|
|
|
|
|
const secondNightTime = resolveRuntimeTimeState(time, {
|
|
|
|
|
timeOfDayHours: 0,
|
|
|
|
|
dayCount: 1,
|
|
|
|
|
dayLengthMinutes: 24
|
|
|
|
|
});
|
|
|
|
|
const secondNightWorld = resolveRuntimeDayNightWorldState(
|
|
|
|
|
world,
|
|
|
|
|
time,
|
|
|
|
|
{
|
|
|
|
|
timeOfDayHours: 0,
|
|
|
|
|
dayCount: 1,
|
|
|
|
|
dayLengthMinutes: 24
|
|
|
|
|
},
|
|
|
|
|
secondNightTime
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const firstNightSky = resolveWorldShaderSkyRenderState(
|
|
|
|
|
world,
|
|
|
|
|
firstNightWorld,
|
|
|
|
|
firstNightTime,
|
|
|
|
|
time
|
|
|
|
|
);
|
|
|
|
|
const secondNightSky = resolveWorldShaderSkyRenderState(
|
|
|
|
|
world,
|
|
|
|
|
secondNightWorld,
|
|
|
|
|
secondNightTime,
|
|
|
|
|
time
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(firstNightSky?.celestial.sunVisible).toBe(false);
|
|
|
|
|
expect(firstNightSky?.celestial.moonVisible).toBe(true);
|
|
|
|
|
expect(secondNightSky?.clouds.driftOffset.x).not.toBeCloseTo(
|
|
|
|
|
firstNightSky?.clouds.driftOffset.x ?? 0
|
|
|
|
|
);
|
|
|
|
|
});
|
2026-04-22 16:12:10 +02:00
|
|
|
|
|
|
|
|
it("offsets shader-rendered celestial positions when the horizon height changes", () => {
|
|
|
|
|
const world = createDefaultWorldSettings();
|
|
|
|
|
const time = createDefaultProjectTimeSettings();
|
|
|
|
|
world.background = {
|
|
|
|
|
mode: "shader"
|
|
|
|
|
};
|
|
|
|
|
world.showCelestialBodies = true;
|
|
|
|
|
|
|
|
|
|
const noonTime = resolveRuntimeTimeState(time, {
|
|
|
|
|
timeOfDayHours: 12,
|
|
|
|
|
dayCount: 0,
|
|
|
|
|
dayLengthMinutes: 24
|
|
|
|
|
});
|
|
|
|
|
const noonWorld = resolveRuntimeDayNightWorldState(
|
|
|
|
|
world,
|
|
|
|
|
time,
|
|
|
|
|
{
|
|
|
|
|
timeOfDayHours: 12,
|
|
|
|
|
dayCount: 0,
|
|
|
|
|
dayLengthMinutes: 24
|
|
|
|
|
},
|
|
|
|
|
noonTime
|
|
|
|
|
);
|
|
|
|
|
const baseSky = resolveWorldShaderSkyRenderState(
|
|
|
|
|
world,
|
|
|
|
|
noonWorld,
|
|
|
|
|
noonTime,
|
|
|
|
|
time
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
world.shaderSky.horizonHeight = -0.12;
|
|
|
|
|
|
|
|
|
|
const shiftedSky = resolveWorldShaderSkyRenderState(
|
|
|
|
|
world,
|
|
|
|
|
noonWorld,
|
|
|
|
|
noonTime,
|
|
|
|
|
time
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(baseSky).not.toBeNull();
|
|
|
|
|
expect(shiftedSky).not.toBeNull();
|
|
|
|
|
expect(shiftedSky?.celestial.sunDirection.y ?? 0).toBeGreaterThan(
|
|
|
|
|
baseSky?.celestial.sunDirection.y ?? 0
|
|
|
|
|
);
|
|
|
|
|
expect(shiftedSky?.celestial.moonDirection.y ?? 0).toBeGreaterThan(
|
|
|
|
|
baseSky?.celestial.moonDirection.y ?? 0
|
|
|
|
|
);
|
|
|
|
|
});
|
2026-04-22 17:55:16 +02:00
|
|
|
|
|
|
|
|
it("resolves representative shader environment states for day, dawn, dusk, and night", () => {
|
|
|
|
|
const world = createDefaultWorldSettings();
|
|
|
|
|
const time = createDefaultProjectTimeSettings();
|
|
|
|
|
world.background = {
|
|
|
|
|
mode: "shader"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const states = resolveWorldShaderSkyEnvironmentPhaseStates(world, time);
|
|
|
|
|
|
|
|
|
|
expect(states.day?.time.dayPhase).toBe("day");
|
|
|
|
|
expect(states.dawn?.time.dayPhase).toBe("dawn");
|
|
|
|
|
expect(states.dusk?.time.dayPhase).toBe("dusk");
|
|
|
|
|
expect(states.night?.time.dayPhase).toBe("night");
|
|
|
|
|
expect(states.day?.sky.topColorHex).not.toBe(states.night?.sky.topColorHex);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("quantizes small shader-environment state changes into the same cache key", () => {
|
|
|
|
|
const world = createDefaultWorldSettings();
|
|
|
|
|
const time = createDefaultProjectTimeSettings();
|
|
|
|
|
world.background = {
|
|
|
|
|
mode: "shader"
|
|
|
|
|
};
|
|
|
|
|
const noonTime = resolveRuntimeTimeState(time, {
|
|
|
|
|
timeOfDayHours: 12,
|
|
|
|
|
dayCount: 0,
|
|
|
|
|
dayLengthMinutes: 24
|
|
|
|
|
});
|
|
|
|
|
const noonWorld = resolveRuntimeDayNightWorldState(
|
|
|
|
|
world,
|
|
|
|
|
time,
|
|
|
|
|
{
|
|
|
|
|
timeOfDayHours: 12,
|
|
|
|
|
dayCount: 0,
|
|
|
|
|
dayLengthMinutes: 24
|
|
|
|
|
},
|
|
|
|
|
noonTime
|
|
|
|
|
);
|
|
|
|
|
const baseSky = resolveWorldShaderSkyRenderState(
|
|
|
|
|
world,
|
|
|
|
|
noonWorld,
|
|
|
|
|
noonTime,
|
|
|
|
|
time
|
|
|
|
|
);
|
|
|
|
|
const nearSky =
|
|
|
|
|
baseSky === null
|
|
|
|
|
? null
|
|
|
|
|
: {
|
|
|
|
|
...baseSky,
|
|
|
|
|
celestial: {
|
|
|
|
|
...baseSky.celestial,
|
|
|
|
|
sunDirection: {
|
|
|
|
|
...baseSky.celestial.sunDirection,
|
|
|
|
|
x: baseSky.celestial.sunDirection.x + 0.004
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
clouds: {
|
|
|
|
|
...baseSky.clouds,
|
|
|
|
|
driftOffset: {
|
2026-04-22 17:58:17 +02:00
|
|
|
x: baseSky.clouds.driftOffset.x + 0.004,
|
2026-04-22 17:55:16 +02:00
|
|
|
y: baseSky.clouds.driftOffset.y
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const shiftedSky =
|
|
|
|
|
baseSky === null
|
|
|
|
|
? null
|
|
|
|
|
: {
|
|
|
|
|
...baseSky,
|
|
|
|
|
celestial: {
|
|
|
|
|
...baseSky.celestial,
|
|
|
|
|
sunIntensity: baseSky.celestial.sunIntensity + 0.2
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
expect(baseSky).not.toBeNull();
|
|
|
|
|
expect(nearSky).not.toBeNull();
|
|
|
|
|
expect(shiftedSky).not.toBeNull();
|
|
|
|
|
expect(
|
|
|
|
|
createWorldShaderSkyEnvironmentCacheKey(
|
|
|
|
|
nearSky ?? (baseSky as NonNullable<typeof baseSky>)
|
|
|
|
|
)
|
|
|
|
|
).toBe(
|
|
|
|
|
createWorldShaderSkyEnvironmentCacheKey(
|
|
|
|
|
baseSky as NonNullable<typeof baseSky>
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
expect(
|
|
|
|
|
createWorldShaderSkyEnvironmentCacheKey(
|
|
|
|
|
shiftedSky ?? (baseSky as NonNullable<typeof baseSky>)
|
|
|
|
|
)
|
|
|
|
|
).not.toBe(
|
|
|
|
|
createWorldShaderSkyEnvironmentCacheKey(
|
|
|
|
|
baseSky as NonNullable<typeof baseSky>
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
});
|
2026-04-22 15:28:13 +02:00
|
|
|
});
|