540 lines
14 KiB
TypeScript
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);
|
|
}
|