Make the depth of field blur effect 2x faster (#9186)

This commit is contained in:
Florian Rival
2026-10-06 08:55:22 +02:00
committed by GitHub
parent 25a183b2f2
commit 9b1314bfa0
5 changed files with 313 additions and 151 deletions
+5 -150
View File
@@ -6,142 +6,6 @@ namespace gdjs {
mb: number;
}
/**
* A bokeh blur adapted from Three.js `BokehShader`, with:
* - a range around the focus distance staying sharp,
* - a blur growing linearly over a transition distance,
* - samples weighted to avoid sharp objects bleeding on the blurred
* background behind them.
*/
const depthOfFieldShader = {
defines: {
PERSPECTIVE_CAMERA: 1,
},
uniforms: {
tDiffuse: { value: null },
tDepth: { value: null },
focusDistance: { value: 10.0 },
focusRange: { value: 1.0 },
transitionDistance: { value: 10.0 },
maxBlur: { value: 0.01 },
aspect: { value: 1.0 },
nearClip: { value: 1.0 },
farClip: { value: 1000.0 },
},
vertexShader: /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`,
fragmentShader: /* glsl */ `
#include <common>
#include <packing>
varying vec2 vUv;
uniform sampler2D tDiffuse;
uniform sampler2D tDepth;
uniform float focusDistance;
uniform float focusRange;
uniform float transitionDistance;
uniform float maxBlur;
uniform float aspect;
uniform float nearClip;
uniform float farClip;
float getDistance(const in vec2 uv) {
float depth = texture2D(tDepth, uv).x;
#if PERSPECTIVE_CAMERA == 1
return -perspectiveDepthToViewZ(depth, nearClip, farClip);
#else
return -orthographicDepthToViewZ(depth, nearClip, farClip);
#endif
}
float getBlur(const in float distance) {
float outOfFocusDistance =
abs(distance - focusDistance) - focusRange * 0.5;
return clamp(outOfFocusDistance / transitionDistance, 0.0, 1.0);
}
vec4 color;
float totalWeight;
float centerDistance;
float centerBlur;
vec2 blurRadius;
void addSample(const in vec2 offset) {
vec2 uv = vUv + offset * blurRadius;
float sampleDistance = getDistance(uv);
// A sharper object in front of the pixel must not be spread on it.
float weight = sampleDistance < centerDistance
? clamp(getBlur(sampleDistance) / centerBlur, 0.0, 1.0)
: 1.0;
color += texture2D(tDiffuse, uv) * weight;
totalWeight += weight;
}
void main() {
color = texture2D(tDiffuse, vUv);
centerDistance = getDistance(vUv);
centerBlur = getBlur(centerDistance);
if (centerBlur <= 0.0) {
gl_FragColor = color;
return;
}
totalWeight = 1.0;
blurRadius = vec2(1.0, aspect) * maxBlur * centerBlur;
addSample(vec2( 0.0, 1.0 ));
addSample(vec2( 0.375, 0.925));
addSample(vec2( 0.725, 0.725));
addSample(vec2(-0.925, 0.375));
addSample(vec2( 1.0, 0.0 ));
addSample(vec2( 0.925, -0.375));
addSample(vec2( 0.725, -0.725));
addSample(vec2(-0.375, -0.925));
addSample(vec2( 0.0, -1.0 ));
addSample(vec2(-0.375, 0.925));
addSample(vec2(-0.725, 0.725));
addSample(vec2( 0.925, 0.375));
addSample(vec2(-1.0, 0.0 ));
addSample(vec2(-0.925, -0.375));
addSample(vec2(-0.725, -0.725));
addSample(vec2( 0.375, -0.925));
addSample(vec2( 0.375, 0.925) * 0.9);
addSample(vec2(-0.925, 0.375) * 0.9);
addSample(vec2( 0.925, -0.375) * 0.9);
addSample(vec2(-0.375, -0.925) * 0.9);
addSample(vec2(-0.375, 0.925) * 0.9);
addSample(vec2( 0.925, 0.375) * 0.9);
addSample(vec2(-0.925, -0.375) * 0.9);
addSample(vec2( 0.375, -0.925) * 0.9);
addSample(vec2( 0.725, 0.725) * 0.7);
addSample(vec2( 1.0, 0.0 ) * 0.7);
addSample(vec2( 0.725, -0.725) * 0.7);
addSample(vec2( 0.0, -1.0 ) * 0.7);
addSample(vec2(-0.725, 0.725) * 0.7);
addSample(vec2(-1.0, 0.0 ) * 0.7);
addSample(vec2(-0.725, -0.725) * 0.7);
addSample(vec2( 0.0, 1.0 ) * 0.7);
addSample(vec2( 0.725, 0.725) * 0.4);
addSample(vec2( 1.0, 0.0 ) * 0.4);
addSample(vec2( 0.725, -0.725) * 0.4);
addSample(vec2( 0.0, -1.0 ) * 0.4);
addSample(vec2(-0.725, 0.725) * 0.4);
addSample(vec2(-1.0, 0.0 ) * 0.4);
addSample(vec2(-0.725, -0.725) * 0.4);
addSample(vec2( 0.0, 1.0 ) * 0.4);
gl_FragColor = color / totalWeight;
}`,
};
gdjs.PixiFiltersTools.registerFilterCreator(
'Scene3D::DepthOfField',
new (class implements gdjs.PixiFiltersTools.FilterCreator {
@@ -153,7 +17,7 @@ namespace gdjs {
return new gdjs.PixiFiltersTools.EmptyFilter();
}
return new (class implements gdjs.PixiFiltersTools.Filter {
shaderPass: THREE_ADDONS.ShaderPass;
shaderPass: THREE_ADDONS.DepthOfFieldPass;
_isEnabled: boolean = false;
// Distances are in scene units: they are converted to the Three.js
// world units before each rendering, as the world scale can change.
@@ -164,9 +28,7 @@ namespace gdjs {
_maxBlur: float = 6;
constructor() {
this.shaderPass = new THREE_ADDONS.ShaderPass(
gdjs.PixiFiltersTools.clampThreeShaderOutput(depthOfFieldShader)
);
this.shaderPass = new THREE_ADDONS.DepthOfFieldPass();
}
isEnabled(target: EffectsTarget): boolean {
@@ -228,16 +90,9 @@ namespace gdjs {
uniforms.tDepth.value = depthTexture;
uniforms.nearClip.value = camera.near;
uniforms.farClip.value = camera.far;
const isPerspectiveCamera =
camera instanceof THREE.PerspectiveCamera;
if (
this.shaderPass.material.defines.PERSPECTIVE_CAMERA !==
(isPerspectiveCamera ? 1 : 0)
) {
this.shaderPass.material.defines.PERSPECTIVE_CAMERA =
isPerspectiveCamera ? 1 : 0;
this.shaderPass.material.needsUpdate = true;
}
this.shaderPass.setPerspectiveCamera(
camera instanceof THREE.PerspectiveCamera
);
const inverseWorldScale = target
.getRuntimeScene()
File diff suppressed because one or more lines are too long
+2
View File
@@ -24,6 +24,7 @@ import { ExposureShader } from 'three/examples/jsm/shaders/ExposureShader';
import { N8AOPass } from '../../../SharedLibs/ThreeAddons/src/n8ao/N8AOPass.js';
import { SceneDepthRenderPass } from '../../../SharedLibs/ThreeAddons/src/SceneDepthRenderPass.js';
import { DepthOfFieldPass } from '../../../SharedLibs/ThreeAddons/src/DepthOfFieldPass.js';
import { LowResolutionBloomPass } from '../../../SharedLibs/ThreeAddons/src/LowResolutionBloomPass.js';
declare global {
@@ -51,6 +52,7 @@ declare global {
ExposureShader,
N8AOPass,
SceneDepthRenderPass,
DepthOfFieldPass,
LowResolutionBloomPass,
};
}
@@ -0,0 +1,304 @@
import { HalfFloatType, ShaderMaterial, WebGLRenderTarget } from "three";
import { Pass, FullScreenQuad } from "./examples/jsm/postprocessing/Pass.js";
const vertexShader = /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`;
const blurAmountShader = /* glsl */ `
#include <common>
#include <packing>
varying vec2 vUv;
uniform sampler2D tDiffuse;
uniform sampler2D tDepth;
uniform float focusDistance;
uniform float focusRange;
uniform float transitionDistance;
uniform float maxBlur;
uniform float aspect;
uniform float nearClip;
uniform float farClip;
float getDistance(const in vec2 uv) {
float depth = texture2D(tDepth, uv).x;
#if PERSPECTIVE_CAMERA == 1
return -perspectiveDepthToViewZ(depth, nearClip, farClip);
#else
return -orthographicDepthToViewZ(depth, nearClip, farClip);
#endif
}
float getBlur(const in float distance) {
float outOfFocusDistance =
abs(distance - focusDistance) - focusRange * 0.5;
return clamp(outOfFocusDistance / transitionDistance, 0.0, 1.0);
}
// A sharper object in front of the pixel must not be spread on it.
float getSampleWeight(
const in float sampleDistance,
const in float centerDistance,
const in float centerBlur
) {
return sampleDistance < centerDistance
? clamp(getBlur(sampleDistance) / centerBlur, 0.0, 1.0)
: 1.0;
}
// The full resolution pixel giving its depth to a pixel of the half
// resolution blur.
vec2 getBlurDepthUv(const in vec2 blurUv) {
return blurUv - 0.5 / vec2(textureSize(tDiffuse, 0));
}`;
/**
* A bokeh blur adapted from Three.js `BokehShader`, with samples weighted to
* avoid sharp objects bleeding on the blurred background behind them.
*/
const blurFragmentShader = /* glsl */ `
${blurAmountShader}
vec4 color;
float totalWeight;
float centerDistance;
float centerBlur;
vec2 blurRadius;
void addPixel(const in vec2 uv, const in float weight) {
float pixelWeight = weight *
getSampleWeight(getDistance(uv), centerDistance, centerBlur);
color += texture2D(tDiffuse, uv) * pixelWeight;
totalWeight += pixelWeight;
}
void addSample(const in vec2 offset) {
addPixel(vUv + offset * blurRadius, 1.0);
}
void main() {
vec2 depthUv = getBlurDepthUv(vUv);
centerDistance = getDistance(depthUv);
centerBlur = getBlur(centerDistance);
if (centerBlur <= 0.0) {
gl_FragColor = texture2D(tDiffuse, vUv);
return;
}
// Start from the 4 pixels under this one, without the sharper objects
// in front.
vec2 pixelSize = 1.0 / vec2(textureSize(tDiffuse, 0));
color = vec4(0.0);
totalWeight = 0.0;
addPixel(depthUv, 0.25);
addPixel(depthUv + vec2(pixelSize.x, 0.0), 0.25);
addPixel(depthUv + vec2(0.0, pixelSize.y), 0.25);
addPixel(depthUv + pixelSize, 0.25);
blurRadius = vec2(1.0, aspect) * maxBlur * centerBlur;
addSample(vec2( 0.0, 1.0 ));
addSample(vec2( 0.375, 0.925));
addSample(vec2( 0.725, 0.725));
addSample(vec2(-0.925, 0.375));
addSample(vec2( 1.0, 0.0 ));
addSample(vec2( 0.925, -0.375));
addSample(vec2( 0.725, -0.725));
addSample(vec2(-0.375, -0.925));
addSample(vec2( 0.0, -1.0 ));
addSample(vec2(-0.375, 0.925));
addSample(vec2(-0.725, 0.725));
addSample(vec2( 0.925, 0.375));
addSample(vec2(-1.0, 0.0 ));
addSample(vec2(-0.925, -0.375));
addSample(vec2(-0.725, -0.725));
addSample(vec2( 0.375, -0.925));
addSample(vec2( 0.375, 0.925) * 0.9);
addSample(vec2(-0.925, 0.375) * 0.9);
addSample(vec2( 0.925, -0.375) * 0.9);
addSample(vec2(-0.375, -0.925) * 0.9);
addSample(vec2(-0.375, 0.925) * 0.9);
addSample(vec2( 0.925, 0.375) * 0.9);
addSample(vec2(-0.925, -0.375) * 0.9);
addSample(vec2( 0.375, -0.925) * 0.9);
addSample(vec2( 0.725, 0.725) * 0.7);
addSample(vec2( 1.0, 0.0 ) * 0.7);
addSample(vec2( 0.725, -0.725) * 0.7);
addSample(vec2( 0.0, -1.0 ) * 0.7);
addSample(vec2(-0.725, 0.725) * 0.7);
addSample(vec2(-1.0, 0.0 ) * 0.7);
addSample(vec2(-0.725, -0.725) * 0.7);
addSample(vec2( 0.0, 1.0 ) * 0.7);
addSample(vec2( 0.725, 0.725) * 0.4);
addSample(vec2( 1.0, 0.0 ) * 0.4);
addSample(vec2( 0.725, -0.725) * 0.4);
addSample(vec2( 0.0, -1.0 ) * 0.4);
addSample(vec2(-0.725, 0.725) * 0.4);
addSample(vec2(-1.0, 0.0 ) * 0.4);
addSample(vec2(-0.725, -0.725) * 0.4);
addSample(vec2( 0.0, 1.0 ) * 0.4);
gl_FragColor = color / totalWeight;
}`;
const compositeFragmentShader = /* glsl */ `
${blurAmountShader}
uniform sampler2D tBlurred;
vec4 blurredColor;
float totalWeight;
float centerDistance;
float centerBlur;
vec2 blurredPosition;
void addBlurredSample(const in vec2 offset) {
vec2 blurredSize = vec2(textureSize(tBlurred, 0));
// Clamped at the borders, so that the color and the depth are read for
// the same pixel.
vec2 blurPixel = clamp(
floor(blurredPosition) + offset,
vec2(0.0),
blurredSize - 1.0
);
vec2 blurUv = (blurPixel + 0.5) / blurredSize;
vec2 bilinearWeights = mix(
1.0 - fract(blurredPosition),
fract(blurredPosition),
offset
);
float weight = bilinearWeights.x * bilinearWeights.y * getSampleWeight(
getDistance(getBlurDepthUv(blurUv)),
centerDistance,
centerBlur
);
blurredColor += texture2D(tBlurred, blurUv) * weight;
totalWeight += weight;
}
void main() {
vec4 sharpColor = texture2D(tDiffuse, vUv);
centerDistance = getDistance(vUv);
centerBlur = getBlur(centerDistance);
float blurInPixels =
centerBlur * maxBlur * float(textureSize(tDiffuse, 0).x);
if (blurInPixels <= 0.0) {
gl_FragColor = sharpColor;
return;
}
// Bilinear upsampling of the blur, without the pixels of sharper objects
// in front of this one.
blurredPosition = vUv * vec2(textureSize(tBlurred, 0)) - 0.5;
blurredColor = vec4(0.0);
totalWeight = 0.0;
addBlurredSample(vec2(0.0, 0.0));
addBlurredSample(vec2(1.0, 0.0));
addBlurredSample(vec2(0.0, 1.0));
addBlurredSample(vec2(1.0, 1.0));
if (totalWeight <= 0.0) {
gl_FragColor = sharpColor;
return;
}
// The blur computed at half the resolution is too coarse for blurs
// under 2 pixels: mix it with the sharp image.
gl_FragColor = mix(
sharpColor,
blurredColor / totalWeight,
clamp(blurInPixels * 0.5, 0.0, 1.0)
);
}`;
/**
* A depth of field blurring at half the resolution, then mixing the blur
* with the sharp image at full resolution according to the depth of each
* pixel, so that objects in focus stay sharp.
*/
class DepthOfFieldPass extends Pass {
constructor() {
super();
/**
* The uniforms of both shaders. Distances are in Three.js world units,
* `maxBlur` is a fraction of the width of the screen.
* @type {Record<string, THREE.IUniform>}
*/
this.uniforms = {
tDiffuse: { value: null },
tBlurred: { value: null },
tDepth: { value: null },
focusDistance: { value: 10.0 },
focusRange: { value: 1.0 },
transitionDistance: { value: 10.0 },
maxBlur: { value: 0.01 },
aspect: { value: 1.0 },
nearClip: { value: 1.0 },
farClip: { value: 1000.0 },
};
this._isPerspectiveCamera = true;
const defines = { PERSPECTIVE_CAMERA: 1 };
this._blurRenderTarget = new WebGLRenderTarget(1, 1, {
type: HalfFloatType,
});
this.uniforms.tBlurred.value = this._blurRenderTarget.texture;
this._blurMaterial = new ShaderMaterial({
uniforms: this.uniforms,
defines,
vertexShader,
fragmentShader: blurFragmentShader,
});
this._compositeMaterial = new ShaderMaterial({
uniforms: this.uniforms,
defines,
vertexShader,
fragmentShader: compositeFragmentShader,
});
this._fullScreenQuad = new FullScreenQuad(this._blurMaterial);
}
/** @param {boolean} isPerspectiveCamera */
setPerspectiveCamera(isPerspectiveCamera) {
if (this._isPerspectiveCamera === isPerspectiveCamera) {
return;
}
this._isPerspectiveCamera = isPerspectiveCamera;
for (const material of [this._blurMaterial, this._compositeMaterial]) {
material.defines.PERSPECTIVE_CAMERA = isPerspectiveCamera ? 1 : 0;
material.needsUpdate = true;
}
}
setSize(width, height) {
this._blurRenderTarget.setSize(Math.ceil(width / 2), Math.ceil(height / 2));
}
render(renderer, writeBuffer, readBuffer) {
this.uniforms.tDiffuse.value = readBuffer.texture;
this._fullScreenQuad.material = this._blurMaterial;
renderer.setRenderTarget(this._blurRenderTarget);
this._fullScreenQuad.render(renderer);
this._fullScreenQuad.material = this._compositeMaterial;
renderer.setRenderTarget(this.renderToScreen ? null : writeBuffer);
if (this.clear) renderer.clear();
this._fullScreenQuad.render(renderer);
}
dispose() {
this._blurRenderTarget.dispose();
this._blurMaterial.dispose();
this._compositeMaterial.dispose();
this._fullScreenQuad.dispose();
}
}
export { DepthOfFieldPass };
+1
View File
@@ -5,6 +5,7 @@
export { N8AOPass } from "./n8ao/N8AOPass";
export { SceneDepthRenderPass } from "./SceneDepthRenderPass";
export { DepthOfFieldPass } from "./DepthOfFieldPass";
export { GLTFLoader } from "./examples/jsm/loaders/GLTFLoader";
export { DRACOLoader } from "./examples/jsm/loaders/DRACOLoader";