src/core/EffectComposer.js
- import {
- DepthStencilFormat,
- DepthTexture,
- LinearFilter,
- SRGBColorSpace,
- UnsignedByteType,
- UnsignedIntType,
- UnsignedInt248Type,
- Vector2,
- WebGLRenderTarget
- } from "three";
-
- import { Timer } from "./Timer.js";
- import { ClearMaskPass } from "../passes/ClearMaskPass.js";
- import { CopyPass } from "../passes/CopyPass.js";
- import { MaskPass } from "../passes/MaskPass.js";
- import { Pass } from "../passes/Pass.js";
-
- /**
- * The EffectComposer may be used in place of a normal WebGLRenderer.
- *
- * The auto clear behaviour of the provided renderer will be disabled to prevent unnecessary clear operations.
- *
- * It is common practice to use a {@link RenderPass} as the first pass to automatically clear the buffers and render a
- * scene for further processing.
- *
- * @implements {Resizable}
- * @implements {Disposable}
- */
-
- export class EffectComposer {
-
- /**
- * Constructs a new effect composer.
- *
- * @param {WebGLRenderer} renderer - The renderer that should be used.
- * @param {Object} [options] - The options.
- * @param {Boolean} [options.depthBuffer=true] - Whether the main render targets should have a depth buffer.
- * @param {Boolean} [options.stencilBuffer=false] - Whether the main render targets should have a stencil buffer.
- * @param {Boolean} [options.alpha] - Deprecated. Buffers are always RGBA since three r137.
- * @param {Number} [options.multisampling=0] - The number of samples used for multisample antialiasing. Requires WebGL 2.
- * @param {Number} [options.frameBufferType] - The type of the internal frame buffers. It's recommended to use HalfFloatType if possible.
- */
-
- constructor(renderer = null, {
- depthBuffer = true,
- stencilBuffer = false,
- multisampling = 0,
- frameBufferType
- } = {}) {
-
- /**
- * The renderer.
- *
- * @type {WebGLRenderer}
- * @private
- */
-
- this.renderer = null;
-
- /**
- * The input buffer.
- *
- * Two identical buffers are used to avoid reading from and writing to the same render target.
- *
- * @type {WebGLRenderTarget}
- * @private
- */
-
- this.inputBuffer = this.createBuffer(depthBuffer, stencilBuffer, frameBufferType, multisampling);
-
- /**
- * The output buffer.
- *
- * @type {WebGLRenderTarget}
- * @private
- */
-
- this.outputBuffer = this.inputBuffer.clone();
-
- /**
- * A copy pass used for copying masked scenes.
- *
- * @type {CopyPass}
- * @private
- */
-
- this.copyPass = new CopyPass();
-
- /**
- * A depth texture.
- *
- * @type {DepthTexture}
- * @private
- */
-
- this.depthTexture = null;
-
- /**
- * The passes.
- *
- * @type {Pass[]}
- * @private
- */
-
- this.passes = [];
-
- /**
- * A timer.
- *
- * @type {Timer}
- * @private
- */
-
- this.timer = new Timer();
-
- /**
- * Determines whether the last pass automatically renders to screen.
- *
- * @type {Boolean}
- */
-
- this.autoRenderToScreen = true;
-
- this.setRenderer(renderer);
-
- }
-
- /**
- * The current amount of samples used for multisample anti-aliasing.
- *
- * @type {Number}
- */
-
- get multisampling() {
-
- // TODO Raise min three version to 138 and remove || 0.
- return this.inputBuffer.samples || 0;
-
- }
-
- /**
- * Sets the amount of MSAA samples.
- *
- * Requires WebGL 2. Set to zero to disable multisampling.
- *
- * @type {Number}
- */
-
- set multisampling(value) {
-
- const buffer = this.inputBuffer;
- const multisampling = this.multisampling;
-
- if(multisampling > 0 && value > 0) {
-
- this.inputBuffer.samples = value;
- this.outputBuffer.samples = value;
- this.inputBuffer.dispose();
- this.outputBuffer.dispose();
-
- } else if(multisampling !== value) {
-
- this.inputBuffer.dispose();
- this.outputBuffer.dispose();
-
- // Enable or disable MSAA.
- this.inputBuffer = this.createBuffer(
- buffer.depthBuffer,
- buffer.stencilBuffer,
- buffer.texture.type,
- value
- );
-
- this.inputBuffer.depthTexture = this.depthTexture;
- this.outputBuffer = this.inputBuffer.clone();
-
- }
-
- }
-
- /**
- * Returns the internal timer.
- *
- * @return {Timer} The timer.
- */
-
- getTimer() {
-
- return this.timer;
-
- }
-
- /**
- * Returns the renderer.
- *
- * @return {WebGLRenderer} The renderer.
- */
-
- getRenderer() {
-
- return this.renderer;
-
- }
-
- /**
- * Sets the renderer.
- *
- * @param {WebGLRenderer} renderer - The renderer.
- */
-
- setRenderer(renderer) {
-
- this.renderer = renderer;
-
- if(renderer !== null) {
-
- const size = renderer.getSize(new Vector2());
- const alpha = renderer.getContext().getContextAttributes().alpha;
- const frameBufferType = this.inputBuffer.texture.type;
-
- if(frameBufferType === UnsignedByteType && renderer.outputColorSpace === SRGBColorSpace) {
-
- this.inputBuffer.texture.colorSpace = SRGBColorSpace;
- this.outputBuffer.texture.colorSpace = SRGBColorSpace;
-
- this.inputBuffer.dispose();
- this.outputBuffer.dispose();
-
- }
-
- renderer.autoClear = false;
- this.setSize(size.width, size.height);
-
- for(const pass of this.passes) {
-
- pass.initialize(renderer, alpha, frameBufferType);
-
- }
-
- }
-
- }
-
- /**
- * Replaces the current renderer with the given one.
- *
- * The auto clear mechanism of the provided renderer will be disabled. If the new render size differs from the
- * previous one, all passes will be updated.
- *
- * By default, the DOM element of the current renderer will automatically be removed from its parent node and the DOM
- * element of the new renderer will take its place.
- *
- * @deprecated Use setRenderer instead.
- * @param {WebGLRenderer} renderer - The new renderer.
- * @param {Boolean} updateDOM - Indicates whether the old canvas should be replaced by the new one in the DOM.
- * @return {WebGLRenderer} The old renderer.
- */
-
- replaceRenderer(renderer, updateDOM = true) {
-
- const oldRenderer = this.renderer;
- const parent = oldRenderer.domElement.parentNode;
-
- this.setRenderer(renderer);
-
- if(updateDOM && parent !== null) {
-
- parent.removeChild(oldRenderer.domElement);
- parent.appendChild(renderer.domElement);
-
- }
-
- return oldRenderer;
-
- }
-
- /**
- * Creates a depth texture attachment that will be provided to all passes.
- *
- * Note: When a shader reads from a depth texture and writes to a render target that uses the same depth texture
- * attachment, the depth information will be lost. This happens even if `depthWrite` is disabled.
- *
- * @private
- * @return {DepthTexture} The depth texture.
- */
-
- createDepthTexture() {
-
- const depthTexture = this.depthTexture = new DepthTexture();
-
- // Hack: Make sure the input buffer uses the depth texture.
- this.inputBuffer.depthTexture = depthTexture;
- this.inputBuffer.dispose();
-
- if(this.inputBuffer.stencilBuffer) {
-
- depthTexture.format = DepthStencilFormat;
- depthTexture.type = UnsignedInt248Type;
-
- } else {
-
- depthTexture.type = UnsignedIntType;
-
- }
-
- return depthTexture;
-
- }
-
- /**
- * Deletes the current depth texture.
- *
- * @private
- */
-
- deleteDepthTexture() {
-
- if(this.depthTexture !== null) {
-
- this.depthTexture.dispose();
- this.depthTexture = null;
-
- // Update the input buffer.
- this.inputBuffer.depthTexture = null;
- this.inputBuffer.dispose();
-
- for(const pass of this.passes) {
-
- pass.setDepthTexture(null);
-
- }
-
- }
-
- }
-
- /**
- * Creates a new render target.
- *
- * @deprecated Create buffers manually via WebGLRenderTarget instead.
- * @param {Boolean} depthBuffer - Whether the render target should have a depth buffer.
- * @param {Boolean} stencilBuffer - Whether the render target should have a stencil buffer.
- * @param {Number} type - The frame buffer type.
- * @param {Number} multisampling - The number of samples to use for antialiasing.
- * @return {WebGLRenderTarget} A new render target that equals the renderer's canvas.
- */
-
- createBuffer(depthBuffer, stencilBuffer, type, multisampling) {
-
- const renderer = this.renderer;
- const size = (renderer === null) ? new Vector2() : renderer.getDrawingBufferSize(new Vector2());
-
- const options = {
- minFilter: LinearFilter,
- magFilter: LinearFilter,
- stencilBuffer,
- depthBuffer,
- type
- };
-
- const renderTarget = new WebGLRenderTarget(size.width, size.height, options);
-
- if(multisampling > 0) {
-
- renderTarget.ignoreDepthForMultisampleCopy = false;
- renderTarget.samples = multisampling;
-
- }
-
- if(type === UnsignedByteType && renderer !== null && renderer.outputColorSpace === SRGBColorSpace) {
-
- renderTarget.texture.colorSpace = SRGBColorSpace;
-
- }
-
- renderTarget.texture.name = "EffectComposer.Buffer";
- renderTarget.texture.generateMipmaps = false;
-
- return renderTarget;
-
- }
-
- /**
- * Can be used to change the main scene for all registered passes and effects.
- *
- * @param {Scene} scene - The scene.
- */
-
- setMainScene(scene) {
-
- for(const pass of this.passes) {
-
- pass.mainScene = scene;
-
- }
-
- }
-
- /**
- * Can be used to change the main camera for all registered passes and effects.
- *
- * @param {Camera} camera - The camera.
- */
-
- setMainCamera(camera) {
-
- for(const pass of this.passes) {
-
- pass.mainCamera = camera;
-
- }
-
- }
-
- /**
- * Adds a pass, optionally at a specific index.
- *
- * @param {Pass} pass - A new pass.
- * @param {Number} [index] - An index at which the pass should be inserted.
- */
-
- addPass(pass, index) {
-
- const passes = this.passes;
- const renderer = this.renderer;
-
- const drawingBufferSize = renderer.getDrawingBufferSize(new Vector2());
- const alpha = renderer.getContext().getContextAttributes().alpha;
- const frameBufferType = this.inputBuffer.texture.type;
-
- pass.setRenderer(renderer);
- pass.setSize(drawingBufferSize.width, drawingBufferSize.height);
- pass.initialize(renderer, alpha, frameBufferType);
-
- if(this.autoRenderToScreen) {
-
- if(passes.length > 0) {
-
- passes[passes.length - 1].renderToScreen = false;
-
- }
-
- if(pass.renderToScreen) {
-
- this.autoRenderToScreen = false;
-
- }
-
- }
-
- if(index !== undefined) {
-
- passes.splice(index, 0, pass);
-
- } else {
-
- passes.push(pass);
-
- }
-
- if(this.autoRenderToScreen) {
-
- passes[passes.length - 1].renderToScreen = true;
-
- }
-
- if(pass.needsDepthTexture || this.depthTexture !== null) {
-
- if(this.depthTexture === null) {
-
- const depthTexture = this.createDepthTexture();
-
- for(pass of passes) {
-
- pass.setDepthTexture(depthTexture);
-
- }
-
- } else {
-
- pass.setDepthTexture(this.depthTexture);
-
- }
-
- }
-
- }
-
- /**
- * Removes a pass.
- *
- * @param {Pass} pass - The pass.
- */
-
- removePass(pass) {
-
- const passes = this.passes;
- const index = passes.indexOf(pass);
- const exists = (index !== -1);
- const removed = exists && (passes.splice(index, 1).length > 0);
-
- if(removed) {
-
- if(this.depthTexture !== null) {
-
- // Check if the depth texture is still required.
- const reducer = (a, b) => (a || b.needsDepthTexture);
- const depthTextureRequired = passes.reduce(reducer, false);
-
- if(!depthTextureRequired) {
-
- if(pass.getDepthTexture() === this.depthTexture) {
-
- pass.setDepthTexture(null);
-
- }
-
- this.deleteDepthTexture();
-
- }
-
- }
-
- if(this.autoRenderToScreen) {
-
- // Check if the removed pass was the last one.
- if(index === passes.length) {
-
- pass.renderToScreen = false;
-
- if(passes.length > 0) {
-
- passes[passes.length - 1].renderToScreen = true;
-
- }
-
- }
-
- }
-
- }
-
- }
-
- /**
- * Removes all passes.
- */
-
- removeAllPasses() {
-
- const passes = this.passes;
-
- this.deleteDepthTexture();
-
- if(passes.length > 0) {
-
- if(this.autoRenderToScreen) {
-
- passes[passes.length - 1].renderToScreen = false;
-
- }
-
- this.passes = [];
-
- }
-
- }
-
- /**
- * Renders all enabled passes in the order in which they were added.
- *
- * @param {Number} [deltaTime] - The time since the last frame in seconds.
- */
-
- render(deltaTime) {
-
- const renderer = this.renderer;
- const copyPass = this.copyPass;
-
- let inputBuffer = this.inputBuffer;
- let outputBuffer = this.outputBuffer;
-
- let stencilTest = false;
- let context, stencil, buffer;
-
- if(deltaTime === undefined) {
-
- this.timer.update();
- deltaTime = this.timer.getDelta();
-
- }
-
- for(const pass of this.passes) {
-
- if(pass.enabled) {
-
- pass.render(renderer, inputBuffer, outputBuffer, deltaTime, stencilTest);
-
- if(pass.needsSwap) {
-
- if(stencilTest) {
-
- copyPass.renderToScreen = pass.renderToScreen;
- context = renderer.getContext();
- stencil = renderer.state.buffers.stencil;
-
- // Preserve the unaffected pixels.
- stencil.setFunc(context.NOTEQUAL, 1, 0xffffffff);
- copyPass.render(renderer, inputBuffer, outputBuffer, deltaTime, stencilTest);
- stencil.setFunc(context.EQUAL, 1, 0xffffffff);
-
- }
-
- buffer = inputBuffer;
- inputBuffer = outputBuffer;
- outputBuffer = buffer;
-
- }
-
- if(pass instanceof MaskPass) {
-
- stencilTest = true;
-
- } else if(pass instanceof ClearMaskPass) {
-
- stencilTest = false;
-
- }
-
- }
-
- }
-
- }
-
- /**
- * Sets the size of the buffers, passes and the renderer.
- *
- * @param {Number} width - The width.
- * @param {Number} height - The height.
- * @param {Boolean} [updateStyle] - Determines whether the style of the canvas should be updated.
- */
-
- setSize(width, height, updateStyle) {
-
- const renderer = this.renderer;
- const currentSize = renderer.getSize(new Vector2());
-
- if(width === undefined || height === undefined) {
-
- width = currentSize.width;
- height = currentSize.height;
-
- }
-
- if(currentSize.width !== width || currentSize.height !== height) {
-
- // Update the logical render size.
- renderer.setSize(width, height, updateStyle);
-
- }
-
- // The drawing buffer size takes the device pixel ratio into account.
- const drawingBufferSize = renderer.getDrawingBufferSize(new Vector2());
- this.inputBuffer.setSize(drawingBufferSize.width, drawingBufferSize.height);
- this.outputBuffer.setSize(drawingBufferSize.width, drawingBufferSize.height);
-
- for(const pass of this.passes) {
-
- pass.setSize(drawingBufferSize.width, drawingBufferSize.height);
-
- }
-
- }
-
- /**
- * Resets this composer by deleting all passes and creating new buffers.
- */
-
- reset() {
-
- this.dispose();
- this.autoRenderToScreen = true;
-
- }
-
- /**
- * Disposes this composer and all passes.
- */
-
- dispose() {
-
- for(const pass of this.passes) {
-
- pass.dispose();
-
- }
-
- this.passes = [];
-
- if(this.inputBuffer !== null) {
-
- this.inputBuffer.dispose();
-
- }
-
- if(this.outputBuffer !== null) {
-
- this.outputBuffer.dispose();
-
- }
-
- this.deleteDepthTexture();
- this.copyPass.dispose();
- this.timer.dispose();
-
- Pass.fullscreenGeometry.dispose();
-
- }
-
- }