Implement and test God Rays advanced rendering feature
This commit is contained in:
@@ -155,6 +155,12 @@ import {
|
||||
resolveAdvancedRenderingPerspectiveCameraFar,
|
||||
resolveDistanceFogParameters
|
||||
} from "../../src/rendering/distance-fog-pass";
|
||||
import {
|
||||
createScreenSpaceGodRaysLightSource,
|
||||
projectScreenSpaceGodRaysLight,
|
||||
resolveGodRaysParameters,
|
||||
syncScreenSpaceGodRaysLightSource
|
||||
} from "../../src/rendering/screen-space-god-rays";
|
||||
import { resolveDynamicGlobalIlluminationParameters } from "../../src/rendering/screen-space-global-illumination";
|
||||
import {
|
||||
ALL_RENDER_LAYER_MASK,
|
||||
@@ -301,6 +307,105 @@ describe("distance fog parameters", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("god rays parameters", () => {
|
||||
it("keeps the pass disabled by default", () => {
|
||||
const settings = createDefaultWorldSettings().advancedRendering;
|
||||
|
||||
expect(resolveGodRaysParameters(settings.godRays)).toMatchObject({
|
||||
enabled: false
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves bounded screen-space shaft parameters", () => {
|
||||
const settings = createDefaultWorldSettings().advancedRendering.godRays;
|
||||
settings.enabled = true;
|
||||
settings.intensity = 12;
|
||||
settings.decay = 1.5;
|
||||
settings.exposure = 6;
|
||||
settings.density = 3;
|
||||
settings.samples = 999;
|
||||
|
||||
expect(resolveGodRaysParameters(settings)).toEqual({
|
||||
enabled: true,
|
||||
intensity: 3,
|
||||
decay: 1,
|
||||
exposure: 2,
|
||||
density: 1.5,
|
||||
samples: 64
|
||||
});
|
||||
});
|
||||
|
||||
it("syncs the active celestial light source", () => {
|
||||
const lightSource = createScreenSpaceGodRaysLightSource();
|
||||
|
||||
syncScreenSpaceGodRaysLightSource(lightSource, {
|
||||
colorHex: "#ffd8aa",
|
||||
intensity: 1.6,
|
||||
direction: {
|
||||
x: 0.1,
|
||||
y: 0.8,
|
||||
z: -0.2
|
||||
}
|
||||
});
|
||||
|
||||
expect(lightSource).toEqual({
|
||||
colorHex: "#ffd8aa",
|
||||
intensity: 1.6,
|
||||
direction: {
|
||||
x: 0.1,
|
||||
y: 0.8,
|
||||
z: -0.2
|
||||
}
|
||||
});
|
||||
|
||||
syncScreenSpaceGodRaysLightSource(lightSource, {
|
||||
colorHex: "#ffd8aa",
|
||||
intensity: 0,
|
||||
direction: {
|
||||
x: 0.1,
|
||||
y: 0.8,
|
||||
z: -0.2
|
||||
}
|
||||
});
|
||||
|
||||
expect(lightSource).toEqual({
|
||||
colorHex: "#ffffff",
|
||||
intensity: 0,
|
||||
direction: null
|
||||
});
|
||||
});
|
||||
|
||||
it("projects the celestial light direction and rejects behind-camera lights", () => {
|
||||
const camera = new PerspectiveCamera(60, 1, 0.1, 1000);
|
||||
|
||||
const projection = projectScreenSpaceGodRaysLight(camera, {
|
||||
colorHex: "#ffffff",
|
||||
intensity: 1,
|
||||
direction: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: -1
|
||||
}
|
||||
});
|
||||
|
||||
expect(projection?.screenPosition.x).toBeCloseTo(0.5, 6);
|
||||
expect(projection?.screenPosition.y).toBeCloseTo(0.5, 6);
|
||||
expect(projection?.visibility).toBeCloseTo(1, 6);
|
||||
|
||||
expect(
|
||||
projectScreenSpaceGodRaysLight(camera, {
|
||||
colorHex: "#ffffff",
|
||||
intensity: 1,
|
||||
direction: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 1
|
||||
}
|
||||
})
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAdvancedRenderingComposer", () => {
|
||||
it("keeps depth buffering enabled when the post stack only uses color effects", () => {
|
||||
postprocessingState.composerOptions.length = 0;
|
||||
@@ -391,6 +496,100 @@ describe("createAdvancedRenderingComposer", () => {
|
||||
).toBe(OVERLAY_RENDER_LAYER_MASK);
|
||||
});
|
||||
|
||||
it("adds god rays before post-world overlay layers when a celestial light source is available", () => {
|
||||
postprocessingState.composerOptions.length = 0;
|
||||
postprocessingState.composerPasses.length = 0;
|
||||
postprocessingState.normalPassTextures.length = 0;
|
||||
postprocessingState.ssaoCalls.length = 0;
|
||||
|
||||
const settings = createDefaultWorldSettings().advancedRendering;
|
||||
const lightSource = createScreenSpaceGodRaysLightSource();
|
||||
settings.enabled = true;
|
||||
settings.godRays.enabled = true;
|
||||
syncScreenSpaceGodRaysLightSource(lightSource, {
|
||||
colorHex: "#fff3cc",
|
||||
intensity: 1,
|
||||
direction: {
|
||||
x: 0,
|
||||
y: 0.25,
|
||||
z: -1
|
||||
}
|
||||
});
|
||||
|
||||
createAdvancedRenderingComposer(
|
||||
{
|
||||
capabilities: {
|
||||
isWebGL2: true
|
||||
}
|
||||
} as unknown as never,
|
||||
new Scene(),
|
||||
new PerspectiveCamera(),
|
||||
settings,
|
||||
null,
|
||||
lightSource
|
||||
);
|
||||
|
||||
expect(
|
||||
postprocessingState.composerPasses.map(
|
||||
(pass) => (pass as { name: string }).name
|
||||
)
|
||||
).toEqual([
|
||||
"RenderPass",
|
||||
"ScreenSpaceGodRaysPass",
|
||||
"RenderPass",
|
||||
"RenderPass",
|
||||
"EffectPass"
|
||||
]);
|
||||
expect(
|
||||
(postprocessingState.composerPasses[0] as { renderLayerMask?: number })
|
||||
.renderLayerMask
|
||||
).toBe(AO_WORLD_RENDER_LAYER_MASK);
|
||||
expect(
|
||||
(postprocessingState.composerPasses[1] as { needsDepthTexture?: boolean })
|
||||
.needsDepthTexture
|
||||
).toBe(true);
|
||||
expect(
|
||||
(postprocessingState.composerPasses[2] as { renderLayerMask?: number })
|
||||
.renderLayerMask
|
||||
).toBe(POST_AO_TRANSPARENT_RENDER_LAYER_MASK);
|
||||
expect(
|
||||
(postprocessingState.composerPasses[3] as { renderLayerMask?: number })
|
||||
.renderLayerMask
|
||||
).toBe(OVERLAY_RENDER_LAYER_MASK);
|
||||
});
|
||||
|
||||
it("does not add god rays when the feature is enabled without a light source", () => {
|
||||
postprocessingState.composerOptions.length = 0;
|
||||
postprocessingState.composerPasses.length = 0;
|
||||
postprocessingState.normalPassTextures.length = 0;
|
||||
postprocessingState.ssaoCalls.length = 0;
|
||||
|
||||
const settings = createDefaultWorldSettings().advancedRendering;
|
||||
settings.enabled = true;
|
||||
settings.godRays.enabled = true;
|
||||
|
||||
createAdvancedRenderingComposer(
|
||||
{
|
||||
capabilities: {
|
||||
isWebGL2: true
|
||||
}
|
||||
} as unknown as never,
|
||||
new Scene(),
|
||||
new PerspectiveCamera(),
|
||||
settings
|
||||
);
|
||||
|
||||
expect(
|
||||
postprocessingState.composerPasses.map(
|
||||
(pass) => (pass as { name: string }).name
|
||||
)
|
||||
).toEqual(["RenderPass", "EffectPass"]);
|
||||
expect(
|
||||
(postprocessingState.composerPasses[0] as { renderLayerMask?: number })
|
||||
.renderLayerMask
|
||||
).toBe(ALL_RENDER_LAYER_MASK);
|
||||
});
|
||||
|
||||
it("adds the dynamic GI pass only when dynamic GI is enabled", () => {
|
||||
postprocessingState.composerOptions.length = 0;
|
||||
postprocessingState.composerPasses.length = 0;
|
||||
|
||||
@@ -1677,6 +1677,15 @@ describe("validateSceneDocument", () => {
|
||||
farDistance: 0,
|
||||
strength: 1.5,
|
||||
renderDistance: -2
|
||||
},
|
||||
godRays: {
|
||||
...document.world.advancedRendering.godRays,
|
||||
enabled: "yes",
|
||||
intensity: -0.1,
|
||||
decay: 1.5,
|
||||
exposure: -0.2,
|
||||
density: Number.NaN,
|
||||
samples: 0
|
||||
}
|
||||
} as any;
|
||||
|
||||
@@ -1776,6 +1785,30 @@ describe("validateSceneDocument", () => {
|
||||
code: "invalid-advanced-rendering-distance-fog-render-distance",
|
||||
path: "world.advancedRendering.distanceFog.renderDistance"
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: "invalid-advanced-rendering-god-rays-enabled",
|
||||
path: "world.advancedRendering.godRays.enabled"
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: "invalid-advanced-rendering-god-rays-intensity",
|
||||
path: "world.advancedRendering.godRays.intensity"
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: "invalid-advanced-rendering-god-rays-decay",
|
||||
path: "world.advancedRendering.godRays.decay"
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: "invalid-advanced-rendering-god-rays-exposure",
|
||||
path: "world.advancedRendering.godRays.exposure"
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: "invalid-advanced-rendering-god-rays-density",
|
||||
path: "world.advancedRendering.godRays.density"
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: "invalid-advanced-rendering-god-rays-samples",
|
||||
path: "world.advancedRendering.godRays.samples"
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: "invalid-advanced-rendering-fog-path",
|
||||
path: "world.advancedRendering.fogPath"
|
||||
|
||||
@@ -35,6 +35,12 @@ describe("world settings helpers", () => {
|
||||
expect(clone.advancedRendering.whiteboxBevel).not.toBe(
|
||||
source.advancedRendering.whiteboxBevel
|
||||
);
|
||||
expect(clone.advancedRendering.distanceFog).not.toBe(
|
||||
source.advancedRendering.distanceFog
|
||||
);
|
||||
expect(clone.advancedRendering.godRays).not.toBe(
|
||||
source.advancedRendering.godRays
|
||||
);
|
||||
});
|
||||
|
||||
it("switches a solid background into a gradient while preserving the authored color as the top edge", () => {
|
||||
@@ -168,6 +174,15 @@ describe("world settings helpers", () => {
|
||||
expect(areWorldSettingsEqual(left, right)).toBe(false);
|
||||
});
|
||||
|
||||
it("treats god rays settings as part of authored world equality", () => {
|
||||
const left = createDefaultWorldSettings();
|
||||
const right = cloneWorldSettings(left);
|
||||
|
||||
right.advancedRendering.godRays.enabled = true;
|
||||
|
||||
expect(areWorldSettingsEqual(left, right)).toBe(false);
|
||||
});
|
||||
|
||||
it("treats the scene project-time lighting toggle as part of authored world equality", () => {
|
||||
const left = createDefaultWorldSettings();
|
||||
const right = cloneWorldSettings(left);
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
CAMERA_RIG_ENTITY_SCENE_DOCUMENT_VERSION,
|
||||
CELESTIAL_BODY_OVERLAY_SCENE_DOCUMENT_VERSION,
|
||||
DAWN_DUSK_BACKGROUND_IMAGE_SCENE_DOCUMENT_VERSION,
|
||||
DISTANCE_FOG_SCENE_DOCUMENT_VERSION,
|
||||
DYNAMIC_GLOBAL_ILLUMINATION_SCENE_DOCUMENT_VERSION,
|
||||
FOLLOW_ACTOR_PATH_SMOOTH_SCENE_DOCUMENT_VERSION,
|
||||
ANIMATION_PLAYBACK_SCENE_DOCUMENT_VERSION,
|
||||
@@ -814,6 +815,14 @@ describe("scene document JSON", () => {
|
||||
strength: 0.72,
|
||||
renderDistance: 160
|
||||
},
|
||||
godRays: {
|
||||
enabled: true,
|
||||
intensity: 0.55,
|
||||
decay: 0.9,
|
||||
exposure: 0.35,
|
||||
density: 0.8,
|
||||
samples: 40
|
||||
},
|
||||
fogPath: "quality",
|
||||
waterPath: "performance",
|
||||
waterReflectionMode: "world",
|
||||
@@ -927,6 +936,39 @@ describe("scene document JSON", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("migrates v86 scene documents without god rays settings to defaults", () => {
|
||||
const emptyScene = createEmptySceneDocument({
|
||||
name: "Legacy God Rays Scene"
|
||||
});
|
||||
const { godRays: _godRays, ...legacyAdvancedRendering } =
|
||||
emptyScene.world.advancedRendering;
|
||||
|
||||
const migratedDocument = migrateSceneDocument({
|
||||
version: DISTANCE_FOG_SCENE_DOCUMENT_VERSION,
|
||||
name: emptyScene.name,
|
||||
time: emptyScene.time,
|
||||
scheduler: emptyScene.scheduler,
|
||||
world: {
|
||||
...emptyScene.world,
|
||||
advancedRendering: legacyAdvancedRendering
|
||||
},
|
||||
materials: emptyScene.materials,
|
||||
textures: emptyScene.textures,
|
||||
assets: emptyScene.assets,
|
||||
brushes: emptyScene.brushes,
|
||||
terrains: emptyScene.terrains,
|
||||
paths: emptyScene.paths,
|
||||
modelInstances: emptyScene.modelInstances,
|
||||
entities: emptyScene.entities,
|
||||
interactionLinks: emptyScene.interactionLinks
|
||||
});
|
||||
|
||||
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
||||
expect(migratedDocument.world.advancedRendering.godRays).toEqual(
|
||||
emptyScene.world.advancedRendering.godRays
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults missing water reflection mode and clamps legacy foam limits during migration", () => {
|
||||
const migratedDocument = migrateSceneDocument({
|
||||
version: SCENE_DOCUMENT_VERSION,
|
||||
|
||||
Reference in New Issue
Block a user