11 KiB
11 KiB
TSL Post-Processing
Post-processing applies effects to the rendered image. TSL provides both built-in effects and the ability to create custom effects.
Basic Setup
import * as THREE from 'three/webgpu';
import { pass } from 'three/tsl';
// Create renderer
const renderer = new THREE.WebGPURenderer();
await renderer.init();
// Create post-processing
const postProcessing = new THREE.PostProcessing(renderer);
// Create scene pass
const scenePass = pass(scene, camera);
const scenePassColor = scenePass.getTextureNode('output');
// Output (passthrough)
postProcessing.outputNode = scenePassColor;
// Render with post-processing
function animate() {
postProcessing.render(); // Not renderer.render()
}
Built-in Effects
Bloom
import { bloom } from 'three/addons/tsl/display/BloomNode.js';
const scenePass = pass(scene, camera);
const scenePassColor = scenePass.getTextureNode('output');
// Add bloom
const bloomPass = bloom(scenePassColor);
// Configure
bloomPass.threshold.value = 0.5; // Brightness threshold
bloomPass.strength.value = 1.0; // Bloom intensity
bloomPass.radius.value = 0.5; // Blur radius
// Combine original + bloom
postProcessing.outputNode = scenePassColor.add(bloomPass);
Gaussian Blur
import { gaussianBlur } from 'three/addons/tsl/display/GaussianBlurNode.js';
const blurred = gaussianBlur(scenePassColor, vec2(2.0)); // Blur strength
postProcessing.outputNode = blurred;
FXAA (Anti-aliasing)
import { fxaa } from 'three/addons/tsl/display/FXAANode.js';
postProcessing.outputNode = fxaa(scenePassColor);
SMAA (Anti-aliasing)
import { smaa } from 'three/addons/tsl/display/SMAANode.js';
postProcessing.outputNode = smaa(scenePassColor);
Depth of Field
import { dof } from 'three/addons/tsl/display/DepthOfFieldNode.js';
const scenePass = pass(scene, camera);
const colorNode = scenePass.getTextureNode('output');
const depthNode = scenePass.getTextureNode('depth');
const dofPass = dof(colorNode, depthNode, {
focus: 5.0, // Focus distance
aperture: 0.025, // Aperture size
maxblur: 0.01 // Maximum blur
});
postProcessing.outputNode = dofPass;
Motion Blur
import { motionBlur } from 'three/addons/tsl/display/MotionBlurNode.js';
const scenePass = pass(scene, camera);
const velocityPass = scenePass.getTextureNode('velocity');
const motionBlurPass = motionBlur(scenePassColor, velocityPass);
postProcessing.outputNode = motionBlurPass;
Screen Space Reflections (SSR)
import { ssr } from 'three/addons/tsl/display/SSRNode.js';
const scenePass = pass(scene, camera);
const colorNode = scenePass.getTextureNode('output');
const depthNode = scenePass.getTextureNode('depth');
const normalNode = scenePass.getTextureNode('normal');
const ssrPass = ssr(colorNode, depthNode, normalNode, camera);
postProcessing.outputNode = ssrPass;
Ambient Occlusion (SSAO)
import { ao } from 'three/addons/tsl/display/AmbientOcclusionNode.js';
const scenePass = pass(scene, camera);
const depthNode = scenePass.getTextureNode('depth');
const normalNode = scenePass.getTextureNode('normal');
const aoPass = ao(depthNode, normalNode, camera);
postProcessing.outputNode = scenePassColor.mul(aoPass);
Film Grain
import { film } from 'three/addons/tsl/display/FilmNode.js';
const filmPass = film(scenePassColor, {
intensity: 0.5,
grayscale: false
});
postProcessing.outputNode = filmPass;
Outline
import { outline } from 'three/addons/tsl/display/OutlineNode.js';
const outlinePass = outline(scene, camera, selectedObjects, {
edgeStrength: 3.0,
edgeGlow: 0.0,
edgeThickness: 1.0,
visibleEdgeColor: new THREE.Color(0xffffff),
hiddenEdgeColor: new THREE.Color(0x190a05)
});
postProcessing.outputNode = scenePassColor.add(outlinePass);
Chromatic Aberration
import { chromaticAberration } from 'three/addons/tsl/display/ChromaticAberrationNode.js';
const caPass = chromaticAberration(scenePassColor, {
offset: vec2(0.002, 0.002)
});
postProcessing.outputNode = caPass;
Color Adjustments
Grayscale
import { grayscale } from 'three/tsl';
postProcessing.outputNode = grayscale(scenePassColor);
Saturation
import { saturation } from 'three/tsl';
// 0 = grayscale, 1 = normal, 2 = oversaturated
postProcessing.outputNode = saturation(scenePassColor, 1.5);
Hue Shift
import { hue } from 'three/tsl';
// Shift hue by radians
postProcessing.outputNode = hue(scenePassColor, time.mul(0.5));
Vibrance
import { vibrance } from 'three/tsl';
postProcessing.outputNode = vibrance(scenePassColor, 0.5);
Posterize
import { posterize } from 'three/tsl';
// Reduce color levels
postProcessing.outputNode = posterize(scenePassColor, 8);
Sepia
import { sepia } from 'three/addons/tsl/display/SepiaNode.js';
postProcessing.outputNode = sepia(scenePassColor);
3D LUT
import { lut3D } from 'three/addons/tsl/display/Lut3DNode.js';
const lutTexture = new THREE.Data3DTexture(lutData, size, size, size);
postProcessing.outputNode = lut3D(scenePassColor, lutTexture, size);
Custom Post-Processing
Basic Custom Effect
import { Fn, screenUV, float, vec4 } from 'three/tsl';
const customEffect = Fn(() => {
const color = scenePassColor.toVar();
// Invert colors
color.rgb.assign(float(1.0).sub(color.rgb));
return color;
});
postProcessing.outputNode = customEffect();
Vignette Effect
const vignette = Fn(() => {
const color = scenePassColor.toVar();
// Distance from center
const uv = screenUV;
const dist = uv.sub(0.5).length();
// Vignette falloff
const vignette = float(1.0).sub(dist.mul(1.5)).clamp(0, 1);
color.rgb.mulAssign(vignette);
return color;
});
postProcessing.outputNode = vignette();
CRT/Scanline Effect
import { viewportSharedTexture } from 'three/tsl';
const crtEffect = Fn(() => {
const uv = screenUV;
// Sample scene at offset UVs for RGB separation (chromatic aberration)
const uvR = uv.add(vec2(0.002, 0));
const uvG = uv;
const uvB = uv.sub(vec2(0.002, 0));
// Use viewportSharedTexture to sample at different UV coordinates
const r = viewportSharedTexture(uvR).r;
const g = viewportSharedTexture(uvG).g;
const b = viewportSharedTexture(uvB).b;
const color = vec4(r, g, b, 1.0).toVar();
// Scanlines
const scanline = uv.y.mul(screenSize.y).mul(0.5).sin().mul(0.1).add(0.9);
color.rgb.mulAssign(scanline);
// Vignette
const dist = uv.sub(0.5).length();
color.rgb.mulAssign(float(1.0).sub(dist.mul(0.5)));
return color;
});
// Note: For this effect, apply after scene rendering
postProcessing.outputNode = crtEffect();
Pixelate Effect
const pixelSize = uniform(8.0);
const pixelate = Fn(() => {
const uv = screenUV;
const pixelUV = uv.mul(screenSize).div(pixelSize).floor().mul(pixelSize).div(screenSize);
return texture(scenePassColor, pixelUV);
});
postProcessing.outputNode = pixelate();
Edge Detection (Sobel)
const sobelEdge = Fn(() => {
const uv = screenUV;
const texelSize = vec2(1.0).div(screenSize);
// Sample 3x3 kernel
const tl = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(-1, -1)))));
const tc = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(0, -1)))));
const tr = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(1, -1)))));
const ml = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(-1, 0)))));
const mr = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(1, 0)))));
const bl = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(-1, 1)))));
const bc = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(0, 1)))));
const br = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(1, 1)))));
// Sobel operators
const gx = tl.add(ml.mul(2)).add(bl).sub(tr).sub(mr.mul(2)).sub(br);
const gy = tl.add(tc.mul(2)).add(tr).sub(bl).sub(bc.mul(2)).sub(br);
const edge = sqrt(gx.mul(gx).add(gy.mul(gy)));
return vec4(vec3(edge), 1.0);
});
postProcessing.outputNode = sobelEdge();
Multiple Render Targets (MRT)
Access multiple buffers from the scene pass:
import { mrt, output } from 'three/tsl';
const scenePass = pass(scene, camera);
// Set up MRT
scenePass.setMRT(mrt({
output: output, // Color output
normal: normalView, // View-space normals
depth: depth // Depth buffer
}));
// Access individual targets
const colorTexture = scenePass.getTextureNode('output');
const normalTexture = scenePass.getTextureNode('normal');
const depthTexture = scenePass.getTextureNode('depth');
Selective Bloom with MRT
Bloom only emissive objects by rendering emissive to a separate target:
import { pass, mrt, output, emissive } from 'three/tsl';
import { bloom } from 'three/addons/tsl/display/BloomNode.js';
const postProcessing = new THREE.PostProcessing(renderer);
const scenePass = pass(scene, camera);
// Render both color and emissive to separate targets
scenePass.setMRT(mrt({
output: output,
emissive: emissive
}));
// Get the texture nodes
const colorTexture = scenePass.getTextureNode('output');
const emissiveTexture = scenePass.getTextureNode('emissive');
// Apply bloom only to emissive
const bloomPass = bloom(emissiveTexture);
bloomPass.threshold.value = 0.0; // Bloom all emissive
bloomPass.strength.value = 1.5;
bloomPass.radius.value = 0.5;
// Combine: original color + bloomed emissive
postProcessing.outputNode = colorTexture.add(bloomPass);
This approach prevents non-emissive bright areas (like white surfaces) from blooming.
Chaining Effects
const scenePass = pass(scene, camera);
const color = scenePass.getTextureNode('output');
// Chain multiple effects
let output = color;
// 1. Apply bloom
const bloomPass = bloom(output);
output = output.add(bloomPass.mul(0.5));
// 2. Apply color grading
output = saturation(output, 1.2);
// 3. Apply vignette
const dist = screenUV.sub(0.5).length();
const vignette = float(1.0).sub(dist.mul(0.5));
output = output.mul(vignette);
// 4. Apply FXAA
output = fxaa(output);
postProcessing.outputNode = output;
Conditional Effects
const effectEnabled = uniform(true);
const conditionalEffect = Fn(() => {
const color = scenePassColor;
return select(effectEnabled, grayscale(color), color);
});
postProcessing.outputNode = conditionalEffect();
// Toggle at runtime
effectEnabled.value = false;
Transitions
import { transition } from 'three/addons/tsl/display/TransitionNode.js';
const scenePassA = pass(sceneA, camera);
const scenePassB = pass(sceneB, camera);
const transitionProgress = uniform(0);
const transitionPass = transition(
scenePassA.getTextureNode('output'),
scenePassB.getTextureNode('output'),
transitionProgress,
texture(transitionTexture) // Optional transition texture
);
postProcessing.outputNode = transitionPass;
// Animate transition
function animate() {
transitionProgress.value = Math.sin(time) * 0.5 + 0.5;
postProcessing.render();
}