This is an overview of the planned changes for v7. The actual implementation details are subject to change during development.
Feel free to leave feedback, questions or suggestions.
The Current Design
The API in postprocessing is based on three's postprocessing examples; at its core it provides an EffectComposer that uses a WebGLRenderer to render passes. A Pass performs a set of tasks to either render consumable textures or to draw the final result to screen. The RenderPass renders the scene colors to a texture and serves as a starting point for most render pipelines.
A few years ago, #82 introduced the EffectPass which can merge Effect instances into a single fullscreen pass for optimal shader performance. Since then, passes and effects have been added and improved, but some effects like SSR and motion blur are still missing. This is because the current API doesn't provide a good way to implement such effects efficiently. Modern effects need additional scene geometry data such as depth, normals, positions, roughness, velocity, etc. This currently requires the scene to be rendered multiple times using three's limited material override system.
Problems
- The
EffectComposer creates two internal render targets to store intermediate results.
- The purpose of this is to avoid reading from and rendering to the same buffer (feedback loop).
- The composer provides access to these buffers via the
render method of the passes.
- Passes need to tell the composer whether these buffers should be swapped when they're done.
- The
multisampling (MSAA) setting affects both of these buffers because the composer doesn't know which of them will actually be used by a RenderPass.
- The composer provides a
DepthTexture to passes that need one, but this feature is partially broken and too limited.
- Passes and effects create several render targets internally which makes optimizations impossible.
- Some passes and effects re-render the main scene to obtain additional data or to mask objects based on depth.
- This leads to poor performance because rendering the main scene is expensive.
Implementation Goals
The buffer management in postprocessing needs to become more sophisticated to support modern requirements.
- Rename
RenderPass to GeometryPass.
- Replace
EffectComposer with a more lightweight RenderPipeline class.
- Pipelines are used to group and run passes.
- The user may create many pipelines. Resources may be shared if they use the same renderer.
- Common setups only require one pipeline that contains a
ClearPass, a GeometryPass and one or more EffectPass instances.
- The first
GeometryPass in a pipeline will be considered the main pass (regarding the main scene & camera).
- Passes and effects declare
input and output resources.
input will include uniforms and textures (alias buffers).
output will include uniforms and renderTargets (alias buffers).
- Resources are declared as key-value pairs.
- Keys are strings and values are either
Texture or WebGLRenderTarget.
- Keys can also be of type
GBuffer (string enum).
- GBuffer inputs will be filled with the actual textures at runtime.
Input and Output both define BUFFER_DEFAULT which will be used to auto connect passes.
- All GBuffer textures must be rendered by the
GeometryPass with MRT.
- The configuration of the render targets is controlled by the passes and effects that produce them.
- Declaring input buffers in effects automatically makes these textures available to the respective effect shaders.
- Inputs and outputs of internal effects or passes must be propagated to the outermost pass.
- Render targets will be managed by a
BufferManager (shared private static instance in RenderPipeline).
- This manager reacts to configuration changes and determines whether render targets can be shared among passes.
- Buffer changes are tracked via events that are dispatched automatically for lifecycle hooks.
- The flags
renderToScreen and needsSwap will be removed.
- The parameters of interface methods will be reduced to the bare minimum.
- The last pass in a pipeline will render to screen (default output buffer is
null) if pipeline.autoRenderToScreen is true (default).
The general IO concept is similar to other node-based systems like Blender's shader nodes which allow users to define named inputs and outputs. Three's built-in materials must be modified with onBeforeCompile to use MRT effectively (possibly the biggest challenge). Since MRT requires WebGL 2, effects that make use of the GBuffer may use GLSL 300.
Use Case Examples
Common Setup
import { ... } from "three";
import { ... } from "postprocessing";
const renderer = ...;
const scene = ...;
const camera = ...;
const pipeline = new RenderPipeline(renderer);
pipeline.addPass(new ClearPass());
pipeline.addPass(new GeometryPass(scene, camera, { frameBufferType: HalfFloatType, samples: 4 }));
pipeline.addPass(new EffectPass(new BloomEffect(), ...));
requestAnimationFrame(function render(timestamp: number): void {
requestAnimationFrame(render);
pipeline.render(timestamp);
});
Multiple Scenes
The first GeometryPass in a pipeline produces the GBuffer. Other GeometryPass instances in the same pipeline render to the same GBuffer. To render to separate GBuffers, multiple pipelines must be created.
const mainPass = new GeometryPass(scene, camera, { frameBufferType: HalfFloatType, samples: 4 }));
const hudPass = new GeometryPass(hudScene, hudCamera));
const effectPass = new EffectPass(new BloomEffect(), ...);
pipeline.addPass(new ClearPass());
pipeline.addPass(mainPass);
pipeline.addPass(hudPass); // Renders to the same buffer as mainPass by default.
pipeline.addPass(effectPass);
// defaultBuffer is an alias for output.buffers.get(Output.BUFFER_DEFAULT)
hudPass.output.defaultBuffer = effectPass.output.defaultBuffer;
pipeline.addPass(new ClearPass());
pipeline.addPass(mainPass);
pipeline.addPass(effectPass);
pipeline.addPass(hudPass); // Renders to the same buffer as effectPass.
const pipelineA = new RenderPipeline(renderer);
const pipelineB = new RenderPipeline(renderer);
const geoPassA = new GeometryPass(sceneA, cameraA, { samples: 4 }));
const geoPassB = new GeometryPass(sceneB, cameraB, { samples: 4 }));
const blendEffect = new TextureEffect({ texture: geoPassA.output.defaultBuffer.texture });
pipelineA.addPass(new ClearPass());
pipelineA.addPass(geoPassA);
pipelineB.addPass(new ClearPass());
pipelineB.addPass(geoPassB);
pipelineB.addPass(new EffectPass(blendEffect, ...));
IO Management
class ExamplePass extends Pass {
// Temporary buffers are outputs with private names.
// Buffer names will be prefixed internally to avoid collisions.
private static BUFFER_TMP_0 = "buffer.tmp0";
private static BUFFER_TMP_1 = "buffer.tmp1";
constructor() {
super();
this.input.buffers.set(ExampleEffect.BUFFER_TMP_0, null);
this.input.buffers.set(ExampleEffect.BUFFER_TMP_1, null);
// input.defaultBuffer will automatically be set to previousPass.output.defaultBuffer.texture
this.output.defaultBuffer = new WebGLRenderTarget(...);
this.output.buffers.set(ExamplePass.BUFFER_TMP_0, new WebGLRenderTarget(...));
this.output.buffers.set(ExamplePass.BUFFER_TMP_1, new WebGLRenderTarget(...));
...
}
protected override onInputChange(): void {
this.copyMaterial.inputBuffer = this.input.buffers.get(ExampleEffect.BUFFER_TMP_1);
}
override onResolutionChange(resolution: Resolution): void {
const { width, height } = resolution;
this.output.buffers.get(BUFFER_TMP_0).setSize(width, height);
this.output.buffers.get(BUFFER_TMP_1).setSize(width, height);
this.output.setChanged();
}
render(): void {
const { renderer, output } = this;
this.fullscreenMaterial = this.customMaterial;
this.customMaterial.inputBuffer = this.input.defaultBuffer;
renderer.setRenderTarget(output.buffers.get(ExamplePass.BUFFER_TMP_0));
this.renderFullscreen();
this.customMaterial.inputBuffer = this.input.buffers.get(ExampleEffect.BUFFER_TMP_0);
renderer.setRenderTarget(output.buffers.get(ExamplePass.BUFFER_TMP_1));
this.renderFullscreen();
this.fullscreenMaterial = this.copyMaterial;
renderer.setRenderTarget(output.defaultBuffer);
this.renderFullscreen();
}
}
GBuffer Usage
class ExampleEffect extends Effect {
private static BUFFER_TMP = "buffer.tmp";
constructor() {
super();
this.fragmentShader = fragmentShader;
this.uniforms.set(..., ...);
this.input.buffers.set(GBuffer.DEPTH, null);
this.input.buffers.set(GBuffer.NORMAL, null);
// GeometryPass provides optimization options for things like normal-depth downsampling.
//this.input.buffers.set(GBuffer.NORMAL_DEPTH, null);
this.input.buffers.set(ExampleEffect.BUFFER_TMP, null);
// Note: Using the default output buffer in an Effect would result in an error.
this.output.buffers.set(ExampleEffect.BUFFER_TMP, new WebGLRenderTarget(1, 1, {
depthBuffer: false
}));
this.exampleMaterial = ...;
}
protected override onInputChange(): void {
// Refresh uniforms...
const buffers = this.input.buffers;
this.exampleMaterial.depthBuffer = this.input.buffers.get(GBuffer.DEPTH);
this.exampleMaterial.normalBuffer = this.input.buffers.get(GBuffer.NORMAL);
this.uniforms.get("exampleBuffer").value = buffers.get(ExampleEffect.BUFFER_TMP);
}
...
}
Effect Shader Changes
Shader Function Signatures
Fragment Shader
vec4 mainImage(in vec4 inputColor, in vec2 uv, in GData data);
void mainUv(inout vec2 uv);
Vertex Shader
void mainSupport(in vec2 uv);
Geometry Data
Effects have access to the geometry data of the current fragment via the data parameter of the mainImage function. The EffectPass detects whether an effect reads a value from this struct and only fetches the relevant data from the respective textures when it's actually needed. Sampling depth at another coordinate can be done via float readDepth(in vec2 uv). To calculate the view Z based on depth, the function float getViewZ(in float depth) can be used. GData is defined as follows:
struct GData {
vec3 position;
vec3 normal;
float depth;
float roughness;
float metalness;
float luminance;
}
Uniforms, Macros and Varyings
All shaders have access to the following uniforms:
uniform vec4 resolution; // screen resolution (xy), texel size (zw)
uniform vec3 cameraParams; // near, far, aspect
uniform float time;
The fragment shader has access to the following additional uniforms:
// Availability of actual buffers depends on the input configuration.
struct GBuffer {
sampler2D color;
sampler2D position;
sampler2D depth;
sampler2D normal;
sampler2D normalDepth;
sampler2D roughnessMetalness;
}
uniform GBuffer gBuffer;
The following varyings are reserved:
Available vertex attributes:
Available macros:
- If the main camera is a
PerspectiveCamera, the macro PERSPECTIVE_CAMERA will be defined.
- If the geometry pass uses a float type color buffer, the macro
FRAMEBUFFER_PRECISION_HIGH will be defined.
This is an overview of the planned changes for v7. The actual implementation details are subject to change during development.
Feel free to leave feedback, questions or suggestions.
The Current Design
The API in
postprocessingis based on three's postprocessing examples; at its core it provides anEffectComposerthat uses aWebGLRendererto render passes. APassperforms a set of tasks to either render consumable textures or to draw the final result to screen. TheRenderPassrenders the scene colors to a texture and serves as a starting point for most render pipelines.A few years ago, #82 introduced the
EffectPasswhich can mergeEffectinstances into a single fullscreen pass for optimal shader performance. Since then, passes and effects have been added and improved, but some effects like SSR and motion blur are still missing. This is because the current API doesn't provide a good way to implement such effects efficiently. Modern effects need additional scene geometry data such as depth, normals, positions, roughness, velocity, etc. This currently requires the scene to be rendered multiple times using three's limited material override system.Problems
EffectComposercreates two internal render targets to store intermediate results.rendermethod of the passes.multisampling(MSAA) setting affects both of these buffers because the composer doesn't know which of them will actually be used by aRenderPass.DepthTextureto passes that need one, but this feature is partially broken and too limited.Implementation Goals
The buffer management in
postprocessingneeds to become more sophisticated to support modern requirements.RenderPasstoGeometryPass.EffectComposerwith a more lightweightRenderPipelineclass.ClearPass, aGeometryPassand one or moreEffectPassinstances.GeometryPassin a pipeline will be considered the main pass (regarding the main scene & camera).inputandoutputresources.inputwill includeuniformsandtextures(aliasbuffers).outputwill includeuniformsandrenderTargets(aliasbuffers).TextureorWebGLRenderTarget.GBuffer(string enum).InputandOutputboth defineBUFFER_DEFAULTwhich will be used to auto connect passes.GeometryPasswith MRT.BufferManager(shared private static instance inRenderPipeline).renderToScreenandneedsSwapwill be removed.null) ifpipeline.autoRenderToScreenistrue(default).The general IO concept is similar to other node-based systems like Blender's shader nodes which allow users to define named inputs and outputs. Three's built-in materials must be modified with
onBeforeCompileto use MRT effectively (possibly the biggest challenge). Since MRT requires WebGL 2, effects that make use of the GBuffer may use GLSL 300.Use Case Examples
Common Setup
Multiple Scenes
The first
GeometryPassin a pipeline produces the GBuffer. OtherGeometryPassinstances in the same pipeline render to the same GBuffer. To render to separate GBuffers, multiple pipelines must be created.IO Management
GBuffer Usage
Effect Shader Changes
Shader Function Signatures
Fragment Shader
Vertex Shader
Geometry Data
Effects have access to the geometry data of the current fragment via the
dataparameter of themainImagefunction. TheEffectPassdetects whether an effect reads a value from this struct and only fetches the relevant data from the respective textures when it's actually needed. Sampling depth at another coordinate can be done viafloat readDepth(in vec2 uv). To calculate the view Z based on depth, the functionfloat getViewZ(in float depth)can be used.GDatais defined as follows:Uniforms, Macros and Varyings
All shaders have access to the following uniforms:
The fragment shader has access to the following additional uniforms:
The following varyings are reserved:
Available vertex attributes:
Available macros:
PerspectiveCamera, the macroPERSPECTIVE_CAMERAwill be defined.FRAMEBUFFER_PRECISION_HIGHwill be defined.