476 lines
11 KiB
Markdown
476 lines
11 KiB
Markdown
# 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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
import { gaussianBlur } from 'three/addons/tsl/display/GaussianBlurNode.js';
|
|
|
|
const blurred = gaussianBlur(scenePassColor, vec2(2.0)); // Blur strength
|
|
postProcessing.outputNode = blurred;
|
|
```
|
|
|
|
### FXAA (Anti-aliasing)
|
|
|
|
```javascript
|
|
import { fxaa } from 'three/addons/tsl/display/FXAANode.js';
|
|
|
|
postProcessing.outputNode = fxaa(scenePassColor);
|
|
```
|
|
|
|
### SMAA (Anti-aliasing)
|
|
|
|
```javascript
|
|
import { smaa } from 'three/addons/tsl/display/SMAANode.js';
|
|
|
|
postProcessing.outputNode = smaa(scenePassColor);
|
|
```
|
|
|
|
### Depth of Field
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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)
|
|
|
|
```javascript
|
|
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)
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
import { film } from 'three/addons/tsl/display/FilmNode.js';
|
|
|
|
const filmPass = film(scenePassColor, {
|
|
intensity: 0.5,
|
|
grayscale: false
|
|
});
|
|
postProcessing.outputNode = filmPass;
|
|
```
|
|
|
|
### Outline
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
import { grayscale } from 'three/tsl';
|
|
|
|
postProcessing.outputNode = grayscale(scenePassColor);
|
|
```
|
|
|
|
### Saturation
|
|
|
|
```javascript
|
|
import { saturation } from 'three/tsl';
|
|
|
|
// 0 = grayscale, 1 = normal, 2 = oversaturated
|
|
postProcessing.outputNode = saturation(scenePassColor, 1.5);
|
|
```
|
|
|
|
### Hue Shift
|
|
|
|
```javascript
|
|
import { hue } from 'three/tsl';
|
|
|
|
// Shift hue by radians
|
|
postProcessing.outputNode = hue(scenePassColor, time.mul(0.5));
|
|
```
|
|
|
|
### Vibrance
|
|
|
|
```javascript
|
|
import { vibrance } from 'three/tsl';
|
|
|
|
postProcessing.outputNode = vibrance(scenePassColor, 0.5);
|
|
```
|
|
|
|
### Posterize
|
|
|
|
```javascript
|
|
import { posterize } from 'three/tsl';
|
|
|
|
// Reduce color levels
|
|
postProcessing.outputNode = posterize(scenePassColor, 8);
|
|
```
|
|
|
|
### Sepia
|
|
|
|
```javascript
|
|
import { sepia } from 'three/addons/tsl/display/SepiaNode.js';
|
|
|
|
postProcessing.outputNode = sepia(scenePassColor);
|
|
```
|
|
|
|
### 3D LUT
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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)
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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();
|
|
}
|
|
```
|