Enhance ambient occlusion effect with dual-layer SSAO and additional parameters

This commit is contained in:
2026-04-07 12:16:56 +02:00
parent 486f0f3f3c
commit 6e28024097
3 changed files with 89 additions and 18 deletions

View File

@@ -1,7 +1,14 @@
import { BasicShadowMap, DirectionalLight, HalfFloatType, Mesh, NoToneMapping, PCFShadowMap, PCFSoftShadowMap, PointLight, SpotLight, UnsignedByteType } from "three"; import { BasicShadowMap, DirectionalLight, HalfFloatType, Mesh, NoToneMapping, PCFShadowMap, PCFSoftShadowMap, PointLight, SpotLight, UnsignedByteType } from "three";
import { BloomEffect, DepthOfFieldEffect, EffectComposer, EffectPass, NormalPass, 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 AMBIENT_OCCLUSION_LUMINANCE_INFLUENCE = 0.15;
const MIN_AMBIENT_OCCLUSION_EFFECT_RADIUS = 0.02;
const MAX_AMBIENT_OCCLUSION_EFFECT_RADIUS = 0.2; const MAX_AMBIENT_OCCLUSION_EFFECT_RADIUS = 0.2;
const MIN_AMBIENT_OCCLUSION_SAMPLES = 12;
const COARSE_AMBIENT_OCCLUSION_RESOLUTION_SCALE = 0.5;
const DETAIL_AMBIENT_OCCLUSION_RESOLUTION_SCALE = 0.75;
const DETAIL_AMBIENT_OCCLUSION_RADIUS_SCALE = 0.35;
const COARSE_AMBIENT_OCCLUSION_INTENSITY_SCALE = 0.45;
const DETAIL_AMBIENT_OCCLUSION_INTENSITY_SCALE = 0.35;
export function resolveBoxVolumeRenderPaths(settings) { export function resolveBoxVolumeRenderPaths(settings) {
if (!settings.enabled) { if (!settings.enabled) {
return { return {
@@ -44,6 +51,12 @@ export function configureAdvancedRenderingRenderer(renderer, settings) {
renderer.toneMapping = NoToneMapping; renderer.toneMapping = NoToneMapping;
renderer.toneMappingExposure = settings.toneMapping.exposure; renderer.toneMappingExposure = settings.toneMapping.exposure;
} }
function clampAmbientOcclusionEffectRadius(radius) {
return Math.min(Math.max(radius, MIN_AMBIENT_OCCLUSION_EFFECT_RADIUS), MAX_AMBIENT_OCCLUSION_EFFECT_RADIUS);
}
function getAmbientOcclusionSampleCount(samples) {
return Math.max(samples, MIN_AMBIENT_OCCLUSION_SAMPLES);
}
export function createAdvancedRenderingComposer(renderer, scene, camera, settings) { export function createAdvancedRenderingComposer(renderer, scene, camera, settings) {
// The scene is always rendered into the composer's offscreen targets first, // 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. // so those targets need depth for correct visibility even when no effect samples it.
@@ -60,13 +73,24 @@ export function createAdvancedRenderingComposer(renderer, scene, camera, setting
// a real normal buffer is supplied, which turns SSAO into speckled noise. // a real normal buffer is supplied, which turns SSAO into speckled noise.
const normalPass = new NormalPass(scene, camera); const normalPass = new NormalPass(scene, camera);
composer.addPass(normalPass); composer.addPass(normalPass);
effects.push(new SSAOEffect(camera, normalPass.texture, { const ambientOcclusionRadius = clampAmbientOcclusionEffectRadius(settings.ambientOcclusion.radius);
const ambientOcclusionSamples = getAmbientOcclusionSampleCount(settings.ambientOcclusion.samples);
const detailAmbientOcclusionRadius = Math.max(ambientOcclusionRadius * DETAIL_AMBIENT_OCCLUSION_RADIUS_SCALE, MIN_AMBIENT_OCCLUSION_EFFECT_RADIUS);
composer.addPass(new EffectPass(camera, new SSAOEffect(camera, normalPass.texture, {
depthAwareUpsampling: true, depthAwareUpsampling: true,
luminanceInfluence: AMBIENT_OCCLUSION_LUMINANCE_INFLUENCE, luminanceInfluence: AMBIENT_OCCLUSION_LUMINANCE_INFLUENCE,
samples: settings.ambientOcclusion.samples, resolutionScale: COARSE_AMBIENT_OCCLUSION_RESOLUTION_SCALE,
radius: Math.min(settings.ambientOcclusion.radius, MAX_AMBIENT_OCCLUSION_EFFECT_RADIUS), samples: ambientOcclusionSamples,
intensity: settings.ambientOcclusion.intensity radius: ambientOcclusionRadius,
})); intensity: settings.ambientOcclusion.intensity * COARSE_AMBIENT_OCCLUSION_INTENSITY_SCALE
}), new SSAOEffect(camera, normalPass.texture, {
depthAwareUpsampling: true,
luminanceInfluence: AMBIENT_OCCLUSION_LUMINANCE_INFLUENCE,
resolutionScale: DETAIL_AMBIENT_OCCLUSION_RESOLUTION_SCALE,
samples: ambientOcclusionSamples,
radius: detailAmbientOcclusionRadius,
intensity: settings.ambientOcclusion.intensity * DETAIL_AMBIENT_OCCLUSION_INTENSITY_SCALE
})));
} }
if (settings.bloom.enabled) { if (settings.bloom.enabled) {
effects.push(new BloomEffect({ effects.push(new BloomEffect({

View File

@@ -37,7 +37,14 @@ import type {
} from "../document/world-settings"; } from "../document/world-settings";
const AMBIENT_OCCLUSION_LUMINANCE_INFLUENCE = 0.15; const AMBIENT_OCCLUSION_LUMINANCE_INFLUENCE = 0.15;
const MIN_AMBIENT_OCCLUSION_EFFECT_RADIUS = 0.02;
const MAX_AMBIENT_OCCLUSION_EFFECT_RADIUS = 0.2; const MAX_AMBIENT_OCCLUSION_EFFECT_RADIUS = 0.2;
const MIN_AMBIENT_OCCLUSION_SAMPLES = 12;
const COARSE_AMBIENT_OCCLUSION_RESOLUTION_SCALE = 0.5;
const DETAIL_AMBIENT_OCCLUSION_RESOLUTION_SCALE = 0.75;
const DETAIL_AMBIENT_OCCLUSION_RADIUS_SCALE = 0.35;
const COARSE_AMBIENT_OCCLUSION_INTENSITY_SCALE = 0.45;
const DETAIL_AMBIENT_OCCLUSION_INTENSITY_SCALE = 0.35;
export interface ResolvedBoxVolumeRenderPaths { export interface ResolvedBoxVolumeRenderPaths {
fog: BoxVolumeRenderPath; fog: BoxVolumeRenderPath;
@@ -91,6 +98,14 @@ export function configureAdvancedRenderingRenderer(renderer: WebGLRenderer, sett
renderer.toneMappingExposure = settings.toneMapping.exposure; renderer.toneMappingExposure = settings.toneMapping.exposure;
} }
function clampAmbientOcclusionEffectRadius(radius: number) {
return Math.min(Math.max(radius, MIN_AMBIENT_OCCLUSION_EFFECT_RADIUS), MAX_AMBIENT_OCCLUSION_EFFECT_RADIUS);
}
function getAmbientOcclusionSampleCount(samples: number) {
return Math.max(samples, MIN_AMBIENT_OCCLUSION_SAMPLES);
}
export function createAdvancedRenderingComposer( export function createAdvancedRenderingComposer(
renderer: WebGLRenderer, renderer: WebGLRenderer,
scene: Scene, scene: Scene,
@@ -108,7 +123,7 @@ export function createAdvancedRenderingComposer(
composer.addPass(new RenderPass(scene, camera)); composer.addPass(new RenderPass(scene, camera));
const effects: Array<SSAOEffect | BloomEffect | DepthOfFieldEffect | ToneMappingEffect | SMAAEffect> = []; const effects: Array<BloomEffect | DepthOfFieldEffect | ToneMappingEffect | SMAAEffect> = [];
if (settings.ambientOcclusion.enabled) { if (settings.ambientOcclusion.enabled) {
// postprocessing's internal depth-downsampling path writes zero normals unless // postprocessing's internal depth-downsampling path writes zero normals unless
@@ -116,14 +131,33 @@ export function createAdvancedRenderingComposer(
const normalPass = new NormalPass(scene, camera); const normalPass = new NormalPass(scene, camera);
composer.addPass(normalPass); composer.addPass(normalPass);
effects.push( const ambientOcclusionRadius = clampAmbientOcclusionEffectRadius(settings.ambientOcclusion.radius);
new SSAOEffect(camera, normalPass.texture, { const ambientOcclusionSamples = getAmbientOcclusionSampleCount(settings.ambientOcclusion.samples);
depthAwareUpsampling: true, const detailAmbientOcclusionRadius = Math.max(
luminanceInfluence: AMBIENT_OCCLUSION_LUMINANCE_INFLUENCE, ambientOcclusionRadius * DETAIL_AMBIENT_OCCLUSION_RADIUS_SCALE,
samples: settings.ambientOcclusion.samples, MIN_AMBIENT_OCCLUSION_EFFECT_RADIUS
radius: Math.min(settings.ambientOcclusion.radius, MAX_AMBIENT_OCCLUSION_EFFECT_RADIUS), );
intensity: settings.ambientOcclusion.intensity
}) composer.addPass(
new EffectPass(
camera,
new SSAOEffect(camera, normalPass.texture, {
depthAwareUpsampling: true,
luminanceInfluence: AMBIENT_OCCLUSION_LUMINANCE_INFLUENCE,
resolutionScale: COARSE_AMBIENT_OCCLUSION_RESOLUTION_SCALE,
samples: ambientOcclusionSamples,
radius: ambientOcclusionRadius,
intensity: settings.ambientOcclusion.intensity * COARSE_AMBIENT_OCCLUSION_INTENSITY_SCALE
}),
new SSAOEffect(camera, normalPass.texture, {
depthAwareUpsampling: true,
luminanceInfluence: AMBIENT_OCCLUSION_LUMINANCE_INFLUENCE,
resolutionScale: DETAIL_AMBIENT_OCCLUSION_RESOLUTION_SCALE,
samples: ambientOcclusionSamples,
radius: detailAmbientOcclusionRadius,
intensity: settings.ambientOcclusion.intensity * DETAIL_AMBIENT_OCCLUSION_INTENSITY_SCALE
})
)
); );
} }

View File

@@ -140,9 +140,10 @@ describe("createAdvancedRenderingComposer", () => {
depthBuffer: true, depthBuffer: true,
frameBufferType: UnsignedByteType frameBufferType: UnsignedByteType
}); });
expect(postprocessingState.ssaoCalls).toHaveLength(0);
}); });
it("feeds SSAO a normal pass and clamps broad occlusion into a corner-shading range", () => { it("builds a dual-layer SSAO stack from one normal pass", () => {
postprocessingState.composerOptions.length = 0; postprocessingState.composerOptions.length = 0;
postprocessingState.composerPasses.length = 0; postprocessingState.composerPasses.length = 0;
postprocessingState.normalPassTextures.length = 0; postprocessingState.normalPassTextures.length = 0;
@@ -151,7 +152,7 @@ describe("createAdvancedRenderingComposer", () => {
const settings = createDefaultWorldSettings().advancedRendering; const settings = createDefaultWorldSettings().advancedRendering;
settings.enabled = true; settings.enabled = true;
settings.ambientOcclusion.enabled = true; settings.ambientOcclusion.enabled = true;
settings.ambientOcclusion.samples = 12; settings.ambientOcclusion.samples = 8;
settings.ambientOcclusion.radius = 0.5; settings.ambientOcclusion.radius = 0.5;
settings.ambientOcclusion.intensity = 0.85; settings.ambientOcclusion.intensity = 0.85;
@@ -167,7 +168,7 @@ describe("createAdvancedRenderingComposer", () => {
); );
expect(postprocessingState.normalPassTextures).toHaveLength(1); expect(postprocessingState.normalPassTextures).toHaveLength(1);
expect(postprocessingState.ssaoCalls).toHaveLength(1); expect(postprocessingState.ssaoCalls).toHaveLength(2);
expect(postprocessingState.ssaoCalls[0]).toMatchObject({ expect(postprocessingState.ssaoCalls[0]).toMatchObject({
normalBuffer: postprocessingState.normalPassTextures[0], normalBuffer: postprocessingState.normalPassTextures[0],
options: { options: {
@@ -175,7 +176,19 @@ describe("createAdvancedRenderingComposer", () => {
luminanceInfluence: 0.15, luminanceInfluence: 0.15,
samples: 12, samples: 12,
radius: 0.2, radius: 0.2,
intensity: 0.85 intensity: 0.3825,
resolutionScale: 0.5
}
});
expect(postprocessingState.ssaoCalls[1]).toMatchObject({
normalBuffer: postprocessingState.normalPassTextures[0],
options: {
depthAwareUpsampling: true,
luminanceInfluence: 0.15,
samples: 12,
radius: 0.07,
intensity: 0.2975,
resolutionScale: 0.75
} }
}); });
}); });