Files
webeditor3d/src/rendering/advanced-rendering.ts

540 lines
14 KiB
TypeScript

import {
BasicShadowMap,
DepthStencilFormat,
DepthTexture,
DirectionalLight,
FloatType,
HalfFloatType,
Mesh,
NoToneMapping,
PCFShadowMap,
PCFSoftShadowMap,
PointLight,
SpotLight,
type Camera,
type Object3D,
type Material,
type PerspectiveCamera,
type Scene,
UnsignedInt248Type,
WebGLRenderTarget,
type WebGLRenderer,
UnsignedByteType
} from "three";
import {
BloomEffect,
CopyMaterial,
DepthOfFieldEffect,
EffectComposer,
EffectPass,
NormalPass,
RenderPass,
ShaderPass,
SMAAEffect,
SMAAPreset,
SSAOEffect,
ToneMappingEffect,
ToneMappingMode
} from "postprocessing";
import type {
AdvancedRenderingSettings,
BoxVolumeRenderPath,
AdvancedRenderingShadowType,
AdvancedRenderingToneMappingMode
} from "../document/world-settings";
import {
ALL_RENDER_LAYER_MASK,
AO_WORLD_RENDER_LAYER_MASK,
OVERLAY_RENDER_LAYER_MASK,
POST_AO_TRANSPARENT_RENDER_LAYER_MASK,
isMaterialEligibleForAmbientOcclusion
} from "./render-layers";
import {
DistanceFogPass,
resolveDistanceFogParameters,
shouldApplyDistanceFog
} from "./distance-fog-pass";
import {
ScreenSpaceGlobalIlluminationPass,
resolveDynamicGlobalIlluminationParameters
} from "./screen-space-global-illumination";
import {
ScreenSpaceGodRaysPass,
resolveGodRaysParameters,
shouldApplyGodRays,
type ScreenSpaceGodRaysLightSource
} from "./screen-space-god-rays";
const AMBIENT_OCCLUSION_LUMINANCE_INFLUENCE = 0.15;
const MIN_AMBIENT_OCCLUSION_EFFECT_RADIUS = 0.02;
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;
function renderWithCameraLayerMask(
camera: Camera,
layerMask: number,
render: () => void
) {
const previousLayerMask = camera.layers.mask;
camera.layers.mask = layerMask;
try {
render();
} finally {
camera.layers.mask = previousLayerMask;
}
}
class RenderLayerPass extends RenderPass {
readonly renderLayerMask: number;
private readonly renderLayerCamera: Camera;
constructor(
scene: Scene,
camera: Camera,
renderLayerMask: number,
overrideMaterial?: Material
) {
super(scene, camera, overrideMaterial);
this.renderLayerCamera = camera;
this.renderLayerMask = renderLayerMask;
}
override render(
renderer: WebGLRenderer,
inputBuffer: WebGLRenderTarget | null,
outputBuffer: WebGLRenderTarget | null,
deltaTime?: number,
stencilTest?: boolean
) {
renderWithCameraLayerMask(
this.renderLayerCamera,
this.renderLayerMask,
() =>
super.render(
renderer,
inputBuffer,
outputBuffer,
deltaTime,
stencilTest
)
);
}
}
class RenderLayerNormalPass extends NormalPass {
readonly renderLayerMask: number;
private readonly renderLayerCamera: Camera;
constructor(scene: Scene, camera: Camera, renderLayerMask: number) {
super(scene, camera);
this.renderLayerCamera = camera;
this.renderLayerMask = renderLayerMask;
}
override render(
renderer: WebGLRenderer,
inputBuffer: WebGLRenderTarget | null,
outputBuffer: WebGLRenderTarget | null,
deltaTime?: number,
stencilTest?: boolean
) {
renderWithCameraLayerMask(
this.renderLayerCamera,
this.renderLayerMask,
() =>
super.render(
renderer,
inputBuffer,
outputBuffer,
deltaTime,
stencilTest
)
);
}
}
function createMainRenderPass(
scene: Scene,
camera: Camera,
layerMask: number,
clear: boolean
) {
const pass = new RenderLayerPass(scene, camera, layerMask);
pass.clear = clear;
return pass;
}
function createPostAmbientOcclusionRenderPass(
scene: Scene,
camera: Camera,
layerMask: number
) {
const pass = new RenderLayerPass(scene, camera, layerMask);
pass.clear = false;
pass.ignoreBackground = true;
pass.skipShadowMapUpdate = true;
return pass;
}
export interface ResolvedBoxVolumeRenderPaths {
fog: BoxVolumeRenderPath;
water: BoxVolumeRenderPath;
}
export function resolveBoxVolumeRenderPaths(
settings: AdvancedRenderingSettings
): ResolvedBoxVolumeRenderPaths {
if (!settings.enabled) {
return {
fog: "performance",
water: "performance"
};
}
return {
fog: settings.fogPath,
water: settings.waterPath
};
}
export function getAdvancedRenderingShadowMapType(
shadowType: AdvancedRenderingShadowType
) {
switch (shadowType) {
case "basic":
return BasicShadowMap;
case "pcf":
return PCFShadowMap;
case "pcfSoft":
return PCFSoftShadowMap;
}
}
export function getAdvancedRenderingToneMappingMode(
mode: AdvancedRenderingToneMappingMode
): ToneMappingMode {
switch (mode) {
case "none":
return ToneMappingMode.LINEAR;
case "linear":
return ToneMappingMode.LINEAR;
case "reinhard":
return ToneMappingMode.REINHARD;
case "cineon":
return ToneMappingMode.CINEON;
case "acesFilmic":
return ToneMappingMode.ACES_FILMIC;
}
}
export function configureAdvancedRenderingRenderer(
renderer: WebGLRenderer,
settings: AdvancedRenderingSettings
) {
renderer.shadowMap.enabled = settings.enabled && settings.shadows.enabled;
renderer.shadowMap.type = getAdvancedRenderingShadowMapType(
settings.shadows.type
);
renderer.toneMapping = NoToneMapping;
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);
}
interface EffectComposerDepthTextureInternals extends EffectComposer {
depthTexture: DepthTexture | null;
depthRenderTarget: WebGLRenderTarget | null;
createDepthTexture(): DepthTexture;
}
function configureEffectComposerDepthTextureIsolation(
composer: EffectComposer
) {
const composerInternals = composer as EffectComposerDepthTextureInternals;
composerInternals.createDepthTexture = function createDepthTexture() {
const inputBuffer = this.inputBuffer;
// postprocessing clones this source depth texture by default. Three.js
// texture clones share a Source, which can alias the source and stable
// depth attachments during gl.blitFramebuffer on WebKit.
const sourceDepthTexture = new DepthTexture(1, 1);
const stableDepthTexture = new DepthTexture(1, 1);
this.depthTexture = sourceDepthTexture;
if (inputBuffer.stencilBuffer) {
sourceDepthTexture.format = DepthStencilFormat;
sourceDepthTexture.type = UnsignedInt248Type;
stableDepthTexture.format = DepthStencilFormat;
stableDepthTexture.type = UnsignedInt248Type;
} else {
sourceDepthTexture.type = FloatType;
stableDepthTexture.type = FloatType;
}
stableDepthTexture.name = "EffectComposer.StableDepth";
this.depthRenderTarget = new WebGLRenderTarget(
inputBuffer.width,
inputBuffer.height,
{
depthBuffer: true,
stencilBuffer: inputBuffer.stencilBuffer,
depthTexture: stableDepthTexture
}
);
return stableDepthTexture;
};
}
export function createAdvancedRenderingComposer(
renderer: WebGLRenderer,
scene: Scene,
camera: PerspectiveCamera,
settings: AdvancedRenderingSettings,
backgroundScene: Scene | null = null,
godRaysLightSource: ScreenSpaceGodRaysLightSource | null = null
): EffectComposer {
// 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: true,
stencilBuffer: false,
multisampling: 0,
frameBufferType: renderer.capabilities.isWebGL2
? HalfFloatType
: UnsignedByteType
});
configureEffectComposerDepthTextureIsolation(composer);
const dynamicGlobalIlluminationParameters =
resolveDynamicGlobalIlluminationParameters(
settings.dynamicGlobalIllumination
);
const dynamicGlobalIlluminationEnabled =
settings.enabled && dynamicGlobalIlluminationParameters.enabled;
const distanceFogParameters = resolveDistanceFogParameters(
settings.distanceFog
);
const distanceFogEnabled = shouldApplyDistanceFog(settings);
const godRaysParameters = resolveGodRaysParameters(settings.godRays);
const godRaysEnabled =
shouldApplyGodRays(settings) && godRaysLightSource !== null;
const postWorldLayerIsolationEnabled =
settings.ambientOcclusion.enabled ||
dynamicGlobalIlluminationEnabled ||
distanceFogEnabled ||
godRaysEnabled;
const mainRenderLayerMask = postWorldLayerIsolationEnabled
? AO_WORLD_RENDER_LAYER_MASK
: ALL_RENDER_LAYER_MASK;
if (backgroundScene !== null) {
composer.addPass(
createMainRenderPass(backgroundScene, camera, mainRenderLayerMask, true)
);
composer.addPass(
createMainRenderPass(scene, camera, mainRenderLayerMask, false)
);
} else {
composer.addPass(
createMainRenderPass(scene, camera, mainRenderLayerMask, true)
);
}
const effects: Array<
BloomEffect | DepthOfFieldEffect | ToneMappingEffect | SMAAEffect
> = [];
if (settings.ambientOcclusion.enabled || dynamicGlobalIlluminationEnabled) {
// 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 RenderLayerNormalPass(
scene,
camera,
AO_WORLD_RENDER_LAYER_MASK
);
composer.addPass(normalPass);
if (dynamicGlobalIlluminationEnabled) {
composer.addPass(
new ScreenSpaceGlobalIlluminationPass(
camera,
normalPass.texture,
dynamicGlobalIlluminationParameters
)
);
}
if (settings.ambientOcclusion.enabled) {
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,
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
})
)
);
}
composer.addPass(new ShaderPass(new CopyMaterial()));
}
if (distanceFogEnabled) {
composer.addPass(new DistanceFogPass(camera, distanceFogParameters));
}
if (godRaysEnabled && godRaysLightSource !== null) {
composer.addPass(
new ScreenSpaceGodRaysPass(
camera,
godRaysLightSource,
godRaysParameters,
distanceFogEnabled
? {
nearDistance: distanceFogParameters.nearDistance,
farDistance: distanceFogParameters.farDistance,
strength: distanceFogParameters.strength,
horizonStrength: distanceFogParameters.horizonStrength
}
: null,
scene,
AO_WORLD_RENDER_LAYER_MASK
)
);
}
if (postWorldLayerIsolationEnabled) {
composer.addPass(
createPostAmbientOcclusionRenderPass(
scene,
camera,
POST_AO_TRANSPARENT_RENDER_LAYER_MASK
)
);
composer.addPass(
createPostAmbientOcclusionRenderPass(
scene,
camera,
OVERLAY_RENDER_LAYER_MASK
)
);
}
if (settings.bloom.enabled) {
effects.push(
new BloomEffect({
intensity: settings.bloom.intensity,
luminanceThreshold: settings.bloom.threshold,
radius: settings.bloom.radius
})
);
}
if (settings.depthOfField.enabled) {
effects.push(
new DepthOfFieldEffect(camera, {
focusDistance: settings.depthOfField.focusDistance,
focalLength: settings.depthOfField.focalLength,
bokehScale: settings.depthOfField.bokehScale
})
);
}
effects.push(
new ToneMappingEffect({
mode: getAdvancedRenderingToneMappingMode(settings.toneMapping.mode)
})
);
effects.push(
new SMAAEffect({
preset: SMAAPreset.MEDIUM
})
);
composer.addPass(new EffectPass(camera, ...effects));
return composer;
}
export function applyAdvancedRenderingRenderableShadowFlags(
root: Object3D,
enabled: boolean
) {
root.traverse((object) => {
if ((object as Mesh).isMesh === true) {
const mesh = object as Mesh;
const shadowEligible =
enabled &&
object.userData.shadowIgnored !== true &&
isMaterialEligibleForAmbientOcclusion(mesh.material);
mesh.castShadow = shadowEligible;
mesh.receiveShadow = shadowEligible;
}
});
}
export function configureAdvancedRenderingShadowLight(
light: DirectionalLight | PointLight | SpotLight,
settings: Pick<AdvancedRenderingSettings, "enabled" | "shadows">,
castShadow: boolean,
normalBias = 0
) {
const shadowEnabled =
settings.enabled && settings.shadows.enabled && castShadow;
light.castShadow = shadowEnabled;
light.shadow.autoUpdate = shadowEnabled;
light.shadow.bias = settings.shadows.bias;
light.shadow.normalBias = shadowEnabled ? Math.max(0, normalBias) : 0;
light.shadow.mapSize.set(settings.shadows.mapSize, settings.shadows.mapSize);
}