Enhance advanced rendering by adding normal pass and refining SSAO effect settings
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { BasicShadowMap, DirectionalLight, HalfFloatType, Mesh, NoToneMapping, PCFShadowMap, PCFSoftShadowMap, PointLight, SpotLight, UnsignedByteType } from "three";
|
||||
import { BloomEffect, DepthOfFieldEffect, EffectComposer, EffectPass, RenderPass, SMAAEffect, SMAAPreset, SSAOEffect, ToneMappingEffect, ToneMappingMode } from "postprocessing";
|
||||
import { BloomEffect, DepthOfFieldEffect, EffectComposer, EffectPass, NormalPass, RenderPass, SMAAEffect, SMAAPreset, SSAOEffect, ToneMappingEffect, ToneMappingMode } from "postprocessing";
|
||||
const AMBIENT_OCCLUSION_LUMINANCE_INFLUENCE = 0.15;
|
||||
const MAX_AMBIENT_OCCLUSION_EFFECT_RADIUS = 0.2;
|
||||
export function resolveBoxVolumeRenderPaths(settings) {
|
||||
if (!settings.enabled) {
|
||||
return {
|
||||
@@ -43,9 +45,10 @@ export function configureAdvancedRenderingRenderer(renderer, settings) {
|
||||
renderer.toneMappingExposure = settings.toneMapping.exposure;
|
||||
}
|
||||
export function createAdvancedRenderingComposer(renderer, scene, camera, settings) {
|
||||
const requiresDepthBuffer = settings.ambientOcclusion.enabled || settings.depthOfField.enabled;
|
||||
// The scene is always rendered into the composer's offscreen targets first,
|
||||
// so those targets need depth for correct visibility even when no effect samples it.
|
||||
const composer = new EffectComposer(renderer, {
|
||||
depthBuffer: requiresDepthBuffer,
|
||||
depthBuffer: true,
|
||||
stencilBuffer: false,
|
||||
multisampling: 0,
|
||||
frameBufferType: renderer.capabilities.isWebGL2 ? HalfFloatType : UnsignedByteType
|
||||
@@ -53,9 +56,15 @@ export function createAdvancedRenderingComposer(renderer, scene, camera, setting
|
||||
composer.addPass(new RenderPass(scene, camera));
|
||||
const effects = [];
|
||||
if (settings.ambientOcclusion.enabled) {
|
||||
effects.push(new SSAOEffect(camera, undefined, {
|
||||
// postprocessing's internal depth-downsampling path writes zero normals unless
|
||||
// a real normal buffer is supplied, which turns SSAO into speckled noise.
|
||||
const normalPass = new NormalPass(scene, camera);
|
||||
composer.addPass(normalPass);
|
||||
effects.push(new SSAOEffect(camera, normalPass.texture, {
|
||||
depthAwareUpsampling: true,
|
||||
luminanceInfluence: AMBIENT_OCCLUSION_LUMINANCE_INFLUENCE,
|
||||
samples: settings.ambientOcclusion.samples,
|
||||
radius: settings.ambientOcclusion.radius,
|
||||
radius: Math.min(settings.ambientOcclusion.radius, MAX_AMBIENT_OCCLUSION_EFFECT_RADIUS),
|
||||
intensity: settings.ambientOcclusion.intensity
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
DepthOfFieldEffect,
|
||||
EffectComposer,
|
||||
EffectPass,
|
||||
NormalPass,
|
||||
RenderPass,
|
||||
SMAAEffect,
|
||||
SMAAPreset,
|
||||
@@ -35,6 +36,9 @@ import type {
|
||||
AdvancedRenderingToneMappingMode
|
||||
} from "../document/world-settings";
|
||||
|
||||
const AMBIENT_OCCLUSION_LUMINANCE_INFLUENCE = 0.15;
|
||||
const MAX_AMBIENT_OCCLUSION_EFFECT_RADIUS = 0.2;
|
||||
|
||||
export interface ResolvedBoxVolumeRenderPaths {
|
||||
fog: BoxVolumeRenderPath;
|
||||
water: BoxVolumeRenderPath;
|
||||
@@ -93,9 +97,10 @@ export function createAdvancedRenderingComposer(
|
||||
camera: PerspectiveCamera,
|
||||
settings: AdvancedRenderingSettings
|
||||
): EffectComposer {
|
||||
const requiresDepthBuffer = settings.ambientOcclusion.enabled || settings.depthOfField.enabled;
|
||||
// The scene is always rendered into the composer's offscreen targets first,
|
||||
// so those targets need depth for correct visibility even when no effect samples it.
|
||||
const composer = new EffectComposer(renderer, {
|
||||
depthBuffer: requiresDepthBuffer,
|
||||
depthBuffer: true,
|
||||
stencilBuffer: false,
|
||||
multisampling: 0,
|
||||
frameBufferType: renderer.capabilities.isWebGL2 ? HalfFloatType : UnsignedByteType
|
||||
@@ -106,10 +111,17 @@ export function createAdvancedRenderingComposer(
|
||||
const effects: Array<SSAOEffect | BloomEffect | DepthOfFieldEffect | ToneMappingEffect | SMAAEffect> = [];
|
||||
|
||||
if (settings.ambientOcclusion.enabled) {
|
||||
// postprocessing's internal depth-downsampling path writes zero normals unless
|
||||
// a real normal buffer is supplied, which turns SSAO into speckled noise.
|
||||
const normalPass = new NormalPass(scene, camera);
|
||||
composer.addPass(normalPass);
|
||||
|
||||
effects.push(
|
||||
new SSAOEffect(camera, undefined, {
|
||||
new SSAOEffect(camera, normalPass.texture, {
|
||||
depthAwareUpsampling: true,
|
||||
luminanceInfluence: AMBIENT_OCCLUSION_LUMINANCE_INFLUENCE,
|
||||
samples: settings.ambientOcclusion.samples,
|
||||
radius: settings.ambientOcclusion.radius,
|
||||
radius: Math.min(settings.ambientOcclusion.radius, MAX_AMBIENT_OCCLUSION_EFFECT_RADIUS),
|
||||
intensity: settings.ambientOcclusion.intensity
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,7 +1,90 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { PerspectiveCamera, Scene, UnsignedByteType } from "three";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const postprocessingState = vi.hoisted(() => ({
|
||||
composerOptions: [] as Array<Record<string, unknown>>,
|
||||
composerPasses: [] as unknown[],
|
||||
normalPassTextures: [] as unknown[],
|
||||
ssaoCalls: [] as Array<{ normalBuffer: unknown; options: Record<string, unknown> }>
|
||||
}));
|
||||
|
||||
vi.mock("postprocessing", () => {
|
||||
class MockEffectComposer {
|
||||
constructor(_renderer: unknown, options: Record<string, unknown>) {
|
||||
postprocessingState.composerOptions.push(options);
|
||||
}
|
||||
|
||||
addPass(pass: unknown) {
|
||||
postprocessingState.composerPasses.push(pass);
|
||||
}
|
||||
}
|
||||
|
||||
class MockRenderPass {
|
||||
constructor(_scene: unknown, _camera: unknown) {}
|
||||
}
|
||||
|
||||
class MockNormalPass {
|
||||
texture: Record<string, unknown>;
|
||||
|
||||
constructor(_scene: unknown, _camera: unknown) {
|
||||
this.texture = {
|
||||
kind: "normal-pass-texture",
|
||||
index: postprocessingState.normalPassTextures.length
|
||||
};
|
||||
postprocessingState.normalPassTextures.push(this.texture);
|
||||
}
|
||||
}
|
||||
|
||||
class MockEffectPass {
|
||||
constructor(_camera: unknown, ..._effects: unknown[]) {}
|
||||
}
|
||||
|
||||
class MockSSAOEffect {
|
||||
constructor(_camera: unknown, normalBuffer: unknown, options: Record<string, unknown>) {
|
||||
postprocessingState.ssaoCalls.push({ normalBuffer, options });
|
||||
}
|
||||
}
|
||||
|
||||
class MockBloomEffect {
|
||||
constructor(_options: Record<string, unknown>) {}
|
||||
}
|
||||
|
||||
class MockDepthOfFieldEffect {
|
||||
constructor(_camera: unknown, _options: Record<string, unknown>) {}
|
||||
}
|
||||
|
||||
class MockToneMappingEffect {
|
||||
constructor(_options: Record<string, unknown>) {}
|
||||
}
|
||||
|
||||
class MockSMAAEffect {
|
||||
constructor(_options: Record<string, unknown>) {}
|
||||
}
|
||||
|
||||
return {
|
||||
BloomEffect: MockBloomEffect,
|
||||
DepthOfFieldEffect: MockDepthOfFieldEffect,
|
||||
EffectComposer: MockEffectComposer,
|
||||
EffectPass: MockEffectPass,
|
||||
NormalPass: MockNormalPass,
|
||||
RenderPass: MockRenderPass,
|
||||
SMAAEffect: MockSMAAEffect,
|
||||
SMAAPreset: {
|
||||
MEDIUM: "medium"
|
||||
},
|
||||
SSAOEffect: MockSSAOEffect,
|
||||
ToneMappingEffect: MockToneMappingEffect,
|
||||
ToneMappingMode: {
|
||||
ACES_FILMIC: "ACES_FILMIC",
|
||||
CINEON: "CINEON",
|
||||
LINEAR: "LINEAR",
|
||||
REINHARD: "REINHARD"
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
import { createDefaultWorldSettings } from "../../src/document/world-settings";
|
||||
import { resolveBoxVolumeRenderPaths } from "../../src/rendering/advanced-rendering";
|
||||
import { createAdvancedRenderingComposer, resolveBoxVolumeRenderPaths } from "../../src/rendering/advanced-rendering";
|
||||
|
||||
describe("resolveBoxVolumeRenderPaths", () => {
|
||||
it("uses authored fog and water paths when advanced rendering is enabled", () => {
|
||||
@@ -28,3 +111,72 @@ describe("resolveBoxVolumeRenderPaths", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAdvancedRenderingComposer", () => {
|
||||
it("keeps depth buffering enabled when the post stack only uses color effects", () => {
|
||||
postprocessingState.composerOptions.length = 0;
|
||||
postprocessingState.composerPasses.length = 0;
|
||||
postprocessingState.normalPassTextures.length = 0;
|
||||
postprocessingState.ssaoCalls.length = 0;
|
||||
|
||||
const settings = createDefaultWorldSettings().advancedRendering;
|
||||
settings.enabled = true;
|
||||
settings.ambientOcclusion.enabled = false;
|
||||
settings.depthOfField.enabled = false;
|
||||
|
||||
createAdvancedRenderingComposer(
|
||||
{
|
||||
capabilities: {
|
||||
isWebGL2: false
|
||||
}
|
||||
} as unknown as never,
|
||||
new Scene(),
|
||||
new PerspectiveCamera(),
|
||||
settings
|
||||
);
|
||||
|
||||
expect(postprocessingState.composerOptions).toHaveLength(1);
|
||||
expect(postprocessingState.composerOptions[0]).toMatchObject({
|
||||
depthBuffer: true,
|
||||
frameBufferType: UnsignedByteType
|
||||
});
|
||||
});
|
||||
|
||||
it("feeds SSAO a normal pass and clamps broad occlusion into a corner-shading range", () => {
|
||||
postprocessingState.composerOptions.length = 0;
|
||||
postprocessingState.composerPasses.length = 0;
|
||||
postprocessingState.normalPassTextures.length = 0;
|
||||
postprocessingState.ssaoCalls.length = 0;
|
||||
|
||||
const settings = createDefaultWorldSettings().advancedRendering;
|
||||
settings.enabled = true;
|
||||
settings.ambientOcclusion.enabled = true;
|
||||
settings.ambientOcclusion.samples = 12;
|
||||
settings.ambientOcclusion.radius = 0.5;
|
||||
settings.ambientOcclusion.intensity = 0.85;
|
||||
|
||||
createAdvancedRenderingComposer(
|
||||
{
|
||||
capabilities: {
|
||||
isWebGL2: true
|
||||
}
|
||||
} as unknown as never,
|
||||
new Scene(),
|
||||
new PerspectiveCamera(),
|
||||
settings
|
||||
);
|
||||
|
||||
expect(postprocessingState.normalPassTextures).toHaveLength(1);
|
||||
expect(postprocessingState.ssaoCalls).toHaveLength(1);
|
||||
expect(postprocessingState.ssaoCalls[0]).toMatchObject({
|
||||
normalBuffer: postprocessingState.normalPassTextures[0],
|
||||
options: {
|
||||
depthAwareUpsampling: true,
|
||||
luminanceInfluence: 0.15,
|
||||
samples: 12,
|
||||
radius: 0.2,
|
||||
intensity: 0.85
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user