Enhance ambient occlusion effect with dual-layer SSAO and additional parameters
This commit is contained in:
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user