|
| 1 | +--- |
| 2 | +layout: single |
| 3 | +collection: sections |
| 4 | +title: Custom Passes |
| 5 | +draft: false |
| 6 | +menu: main |
| 7 | +weight: 40 |
| 8 | +--- |
| 9 | + |
| 10 | +# Custom Passes |
| 11 | + |
| 12 | +## Introduction |
| 13 | + |
| 14 | +At a closer look, passes can be divided into four groups. The first group consists of passes that render normal scenes like the `GeometryPass`. The second type doesn't render anything but performs supporting operations like the `ClearPass` or `LambdaPass`. Passes that render textures for further use make up the third group. One example would be the `LuminancePass`. The fourth and most prominent group contains the fullscreen effect passes. If you want to make a pass that belongs to the last group, you should consider [creating an Effect]({{< relref "custom-effects" >}}) instead. |
| 15 | + |
| 16 | +There are two options for creating custom passes. You can either rely on the general-purpose `ShaderPass` or extend `Pass`. |
| 17 | + |
| 18 | +## ShaderPass |
| 19 | + |
| 20 | +<details><summary>TL;DR</summary> |
| 21 | +<p> |
| 22 | + |
| 23 | +```js |
| 24 | +import { ShaderMaterial, Uniform } from "three"; |
| 25 | +import { ShaderPass } from "postprocessing"; |
| 26 | + |
| 27 | +const myShaderMaterial = new ShaderMaterial({ |
| 28 | + |
| 29 | + defines: { SOMETHING: "value" }, |
| 30 | + uniforms: { tDiffuse: new Uniform(null) }, |
| 31 | + vertexShader: "...", |
| 32 | + fragmentShader: "..." |
| 33 | + |
| 34 | +}); |
| 35 | + |
| 36 | +const myShaderPass = new ShaderPass(myShaderMaterial, "tDiffuse"); |
| 37 | +``` |
| 38 | + |
| 39 | +</p> |
| 40 | +</details> |
| 41 | + |
| 42 | +The `ShaderPass` expects an instance of `ShaderMaterial` as its first argument. The second argument specifies the name of the texture sampler uniform of the shader you provide. This name defaults to `"inputBuffer"` and the `ShaderPass` binds its `input.defaultBuffer` to this uniform. |
| 43 | + |
| 44 | +In order to render a simple `ShaderMaterial`, you have to pass your shader object (uniforms, defines, fragment and vertex shader code) to `ShaderMaterial` and then pass that material instance to `ShaderPass`. Depending on the material you use, you may have to adjust the name of the input texture. |
| 45 | + |
| 46 | +## Extending Pass |
| 47 | + |
| 48 | +<details><summary>TL;DR</summary> |
| 49 | +<p> |
| 50 | + |
| 51 | +##### shader.frag |
| 52 | + |
| 53 | +```glsl |
| 54 | +#include <pp_default_output_pars_fragment> |
| 55 | +#include <pp_input_buffer_pars_fragment> |
| 56 | +
|
| 57 | +uniform vec3 weights; |
| 58 | +
|
| 59 | +in vec2 vUv; |
| 60 | +
|
| 61 | +void main() { |
| 62 | +
|
| 63 | + vec4 texel = texture(inputBuffer, vUv); |
| 64 | + out_Color = vec4(texel.rgb * weights, texel.a); |
| 65 | +
|
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +##### CustomMaterial.ts |
| 70 | + |
| 71 | +```ts |
| 72 | +import { ShaderMaterial, Uniform, Vector3 } from "three"; |
| 73 | +import { FullscreenMaterial, Uniform, Vector3 } from "postprocessing"; |
| 74 | + |
| 75 | +// Tip: Use a bundler plugin like esbuild-plugin-glsl to import shaders as text. |
| 76 | +import fragmentShader from "./shader.frag"; |
| 77 | + |
| 78 | +export class CustomMaterial extends FullscreenMaterial { |
| 79 | + |
| 80 | + constructor() { |
| 81 | + |
| 82 | + super({ |
| 83 | + name: "LuminanceMaterial", |
| 84 | + fragmentShader, |
| 85 | + uniforms: { |
| 86 | + weights: new Uniform(new Vector3()) |
| 87 | + } |
| 88 | + }); |
| 89 | + |
| 90 | + } |
| 91 | + |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +##### CustomPass.js |
| 96 | + |
| 97 | +```ts |
| 98 | +import { Pass } from "postprocessing"; |
| 99 | +import { CustomMaterial } from "./CustomMaterial.js"; |
| 100 | + |
| 101 | +export class CustomPass extends Pass<CustomMaterial> { |
| 102 | + |
| 103 | + constructor() { |
| 104 | + |
| 105 | + super("CustomPass"); |
| 106 | + this.fullscreenMaterial = new CustomMaterial(); |
| 107 | + |
| 108 | + } |
| 109 | + |
| 110 | + override render(): void { |
| 111 | + |
| 112 | + this.setRenderTarget(this.output.defaultBuffer?.value); |
| 113 | + this.renderFullscreen(); |
| 114 | + |
| 115 | + } |
| 116 | + |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | +</p> |
| 121 | +</details> |
| 122 | + |
| 123 | +By extending `Pass`, you can decide what happens during resizing, initialization and rendering. There are also several lifecycle hooks that you can take advantage of. Passes in postprocessing receive various input data from the main `GeometryPass` and the preceding pass in a render pipeline. |
| 124 | + |
| 125 | +The minimum requirement to create a custom pass is to override the `render` method. If you're creating a fullscreen effect, you'll need to assign a `fullscreenMaterial`: |
| 126 | + |
| 127 | +```ts |
| 128 | +this.fullscreenMaterial = new MyMaterial(); |
| 129 | +``` |
| 130 | + |
| 131 | +> [!TIP] |
| 132 | +> If your pass uses multiple materials, add them to the `materials` set so that they can be precompiled. The `fullscreenMaterial` is added automatically. |
| 133 | +
|
| 134 | +### Resources |
| 135 | + |
| 136 | +Framebuffers can be created manually or via the `createFrambuffer` method. All framebuffers should be added to the `output` buffer resources so that the pipeline can optimize them: |
| 137 | + |
| 138 | +```ts |
| 139 | +this.output.setBuffer(MyPass.BUFFER_ID, this.createFramebuffer()); |
| 140 | +``` |
| 141 | + |
| 142 | +A convenience getter can be defined to retrieve the buffer as needed: |
| 143 | + |
| 144 | +```ts |
| 145 | +private get renderTarget(): WebGLRenderTarget { |
| 146 | + |
| 147 | + return this.output.getBuffer(MyPass.BUFFER_ID)!; |
| 148 | + |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +> [!TIP] |
| 153 | +> If your pass uses disposable resources that don't fit into the existing `input` and `output` resources, add them to the `disposables` set instead. |
| 154 | +
|
| 155 | +### G-Buffer |
| 156 | + |
| 157 | +Passes can request [GBuffer]() components via `input.gBuffer`. The actual textures will be supplied via `input.buffers` and can be retrieved by using the `GBuffer` value as the key. Passes should override the `onInputChange` hook to fetch and utilize the requested textures. |
| 158 | + |
| 159 | +#### G-Buffer Packing |
| 160 | + |
| 161 | +WebGL 2 guarantees that a compatible device supports at least 4 texture attachments per render target. For a broad device support, postprocessing stays within this limitation and packs certain combinations of G-Buffer components into a single texture attachment. To be able to unpack this data, special shader macros that control predefined unpacking functions are provided to the requesting passes via input `defines`. If a pass uses a fullscreen material that extends `FullscreenMaterial`, these `defines` will automatically be integrated into the shaders. To finally read the data, the following shader chunk must be included in the fragment shader: |
| 162 | + |
| 163 | +```glsl |
| 164 | +#include <pp_gbuffer_packing> |
| 165 | +``` |
| 166 | + |
| 167 | +This include adds the following utility functions that should be used to read the respective G-Buffer data: |
| 168 | + |
| 169 | +```glsl |
| 170 | +float readDepth(sampler2D depthBuffer, vec2 uv); |
| 171 | +vec3 readNormal(sampler2D normalBuffer, vec2 uv); |
| 172 | +vec2 readVelocity(sampler2D velocityBuffer, vec2 uv); |
| 173 | +``` |
| 174 | + |
| 175 | + |
| 176 | +### Lifecycle Hooks |
| 177 | + |
| 178 | +The `Pass` base class defines lifecycle methods that can be overridden to react to various events: |
| 179 | +* `checkRequirements(): void;` |
| 180 | +* `onInputChange(): void;` |
| 181 | +* `onOutputChange(): void;` |
| 182 | +* `onResolutionChange(): void;` |
| 183 | +* `onViewportChange(): void;` |
| 184 | +* `onScissorChange(): void;` |
| 185 | +* `onSceneChildAdded(): void;` |
| 186 | +* `onSceneChildRemoved(): void;` |
| 187 | + |
| 188 | +It depends on the use case which of these hooks will actually be needed. |
| 189 | + |
| 190 | +### Fullscreen Passes |
| 191 | + |
| 192 | +It's recommended to use materials that extend [FullscreenMaterial]() when writing passes that perform fullscreen render operations. Materials of this type will benefit from the following automatic features: |
| 193 | +* The `inputBuffer` will be set based on `input.defaultBuffer`. |
| 194 | +* Input `defines` and `uniforms` will be assigned. |
| 195 | +* If the geometry pass uses a float type color buffer, the macro `FRAMEBUFFER_PRECISION_HIGH` will be defined. |
| 196 | + |
| 197 | +To render a fullscreen material, set the render target and use the `renderFullscreen` method: |
| 198 | + |
| 199 | +```ts |
| 200 | +override render(): void { |
| 201 | + |
| 202 | + this.setRenderTarget(this.output.defaultBuffer?.value); |
| 203 | + this.renderFullscreen(); |
| 204 | + |
| 205 | +} |
| 206 | +``` |
0 commit comments