diff --git a/examples/jsm/postprocessing/OutputPass.js b/examples/jsm/postprocessing/OutputPass.js index 1148de644671f9..1366c6e1906e22 100644 --- a/examples/jsm/postprocessing/OutputPass.js +++ b/examples/jsm/postprocessing/OutputPass.js @@ -39,6 +39,15 @@ class OutputPass extends Pass { super(); + /** + * This flag indicates that this is an output pass. + * + * @type {boolean} + * @readonly + * @default true + */ + this.isOutputPass = true; + /** * The pass uniforms. * diff --git a/examples/jsm/postprocessing/RenderPass.js b/examples/jsm/postprocessing/RenderPass.js index c85f0e559a7a78..e8428a1c7ef849 100644 --- a/examples/jsm/postprocessing/RenderPass.js +++ b/examples/jsm/postprocessing/RenderPass.js @@ -93,6 +93,16 @@ class RenderPass extends Pass { * @default false */ this.needsSwap = false; + + /** + * This flag indicates that this pass renders the scene itself. + * + * @type {boolean} + * @readonly + * @default true + */ + this.isRenderPass = true; + this._oldClearColor = new Color(); } diff --git a/examples/screenshots/webgl_loader_ldraw.jpg b/examples/screenshots/webgl_loader_ldraw.jpg index 2b17a519f19bc7..54fa76e1c98871 100644 Binary files a/examples/screenshots/webgl_loader_ldraw.jpg and b/examples/screenshots/webgl_loader_ldraw.jpg differ diff --git a/examples/screenshots/webxr_xr_controls_transform.jpg b/examples/screenshots/webxr_xr_controls_transform.jpg index 1eb4306a715ef0..1931b1b51cea28 100644 Binary files a/examples/screenshots/webxr_xr_controls_transform.jpg and b/examples/screenshots/webxr_xr_controls_transform.jpg differ diff --git a/examples/webgl_loader_ldraw.html b/examples/webgl_loader_ldraw.html index f6c10569cf5474..8a2e0d6f60b4db 100644 --- a/examples/webgl_loader_ldraw.html +++ b/examples/webgl_loader_ldraw.html @@ -81,7 +81,7 @@ // - renderer = new THREE.WebGLRenderer( { antialias: true } ); + renderer = new THREE.WebGLRenderer( { antialias: true, outputBufferType: THREE.HalfFloatType } ); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); renderer.setAnimationLoop( animate ); diff --git a/examples/webgl_watch.html b/examples/webgl_watch.html index 262ab6b9586b8c..b4ff698edb1336 100644 --- a/examples/webgl_watch.html +++ b/examples/webgl_watch.html @@ -35,12 +35,10 @@ import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; - import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; - import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; import { TAARenderPass } from 'three/addons/postprocessing/TAARenderPass.js'; - let composer, camera, scene, renderer; + let camera, scene, renderer; let gui, dirLight, pointLight, controls, bloomPass, taaPass; let ready = false; @@ -53,7 +51,7 @@ metalness: 1.0, opacity: 0.4, threshold: 0, - strength: 0.08, + strength: 0.007, radius: 0.0, postProcess: false }; @@ -69,7 +67,7 @@ scene = new THREE.Scene(); - renderer = new THREE.WebGLRenderer( { antialias: true } ); + renderer = new THREE.WebGLRenderer( { antialias: true, outputBufferType: THREE.HalfFloatType } ); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); renderer.setAnimationLoop( animate ); @@ -79,6 +77,14 @@ renderer.shadowMap.type = THREE.VSMShadowMap; container.appendChild( renderer.domElement ); + taaPass = new TAARenderPass( scene, camera ); + taaPass.sampleLevel = 2; + + bloomPass = new UnrealBloomPass( new THREE.Vector2( window.innerWidth, window.innerHeight ), 1.5, 0.4, 0.85 ); + bloomPass.threshold = setting.threshold; + bloomPass.strength = setting.strength; + bloomPass.radius = setting.radius; + new HDRLoader() .setPath( 'textures/equirectangular/' ) .load( 'lobe.hdr', function ( texture ) { @@ -213,32 +219,11 @@ if ( b ) { - if ( composer ) return; - - bloomPass = new UnrealBloomPass( new THREE.Vector2( window.innerWidth, window.innerHeight ), 1.5, 0.4, 0.85 ); - bloomPass.threshold = setting.threshold; - bloomPass.strength = setting.strength; - bloomPass.radius = setting.radius; - - taaPass = new TAARenderPass( scene, camera ); - taaPass.sampleLevel = 2; - taaPass.unbiased = false; - - composer = new EffectComposer( renderer ); - composer.setPixelRatio( window.devicePixelRatio ); - composer.setSize( window.innerWidth, window.innerHeight ); - - composer.addPass( taaPass ); - composer.addPass( bloomPass ); - composer.addPass( new OutputPass() ); + renderer.setEffects( [ taaPass, bloomPass ] ); } else { - if ( ! composer ) return; - composer.dispose(); - composer = null; - bloomPass = null; - taaPass = null; + renderer.setEffects( null ); } @@ -255,7 +240,7 @@ gui.add( setting, 'postProcess' ).onChange( postProcess ); gui.add( setting, 'threshold', 0, 1, 0.01 ).onChange( upBloom ); - gui.add( setting, 'strength', 0, 3, 0.01 ).onChange( upBloom ); + gui.add( setting, 'strength', 0, 0.1, 0.001 ).onChange( upBloom ); gui.add( setting, 'radius', 0, 1, 0.01 ).onChange( upBloom ); } @@ -306,11 +291,6 @@ camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize( width, height ); - if ( composer ) { - - composer.setSize( width, height ); - - } } @@ -322,8 +302,7 @@ TWEEN.update(); - if ( composer ) composer.render(); - else renderer.render( scene, camera ); + renderer.render( scene, camera ); if ( ready ) getTime(); diff --git a/examples/webxr_xr_controls_transform.html b/examples/webxr_xr_controls_transform.html index 7e535b08fa0346..3dc27cec8c4973 100644 --- a/examples/webxr_xr_controls_transform.html +++ b/examples/webxr_xr_controls_transform.html @@ -27,9 +27,11 @@ import { TransformControls } from 'three/addons/controls/TransformControls.js'; import { XRButton } from 'three/addons/webxr/XRButton.js'; import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js'; + import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; let container; let camera, scene, renderer; + let bloomPass; let controller1, controller2, line; let controllerGrip1, controllerGrip2; @@ -110,14 +112,20 @@ // - renderer = new THREE.WebGLRenderer( { antialias: true } ); + renderer = new THREE.WebGLRenderer( { antialias: true, outputBufferType: THREE.HalfFloatType } ); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); renderer.setAnimationLoop( animate ); renderer.shadowMap.enabled = true; + renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.xr.enabled = true; container.appendChild( renderer.domElement ); + // post-processing + + bloomPass = new UnrealBloomPass( new THREE.Vector2( window.innerWidth, window.innerHeight ), 1.5, 0.4, 0.85 ); + renderer.setEffects( [ bloomPass ] ); + document.body.appendChild( XRButton.createButton( renderer ) ); // controllers diff --git a/src/renderers/WebGLRenderer.js b/src/renderers/WebGLRenderer.js index 9cd5b4c43fee84..4c523e3259e96f 100644 --- a/src/renderers/WebGLRenderer.js +++ b/src/renderers/WebGLRenderer.js @@ -39,6 +39,7 @@ import { WebGLIndexedBufferRenderer } from './webgl/WebGLIndexedBufferRenderer.j import { WebGLInfo } from './webgl/WebGLInfo.js'; import { WebGLMorphtargets } from './webgl/WebGLMorphtargets.js'; import { WebGLObjects } from './webgl/WebGLObjects.js'; +import { WebGLOutput } from './webgl/WebGLOutput.js'; import { WebGLPrograms } from './webgl/WebGLPrograms.js'; import { WebGLProperties } from './webgl/WebGLProperties.js'; import { WebGLRenderLists } from './webgl/WebGLRenderLists.js'; @@ -82,6 +83,7 @@ class WebGLRenderer { powerPreference = 'default', failIfMajorPerformanceCaveat = false, reversedDepthBuffer = false, + outputBufferType = UnsignedByteType, } = parameters; /** @@ -111,6 +113,8 @@ class WebGLRenderer { } + const _outputBufferType = outputBufferType; + const INTEGER_FORMATS = new Set( [ RGBAIntegerFormat, RGIntegerFormat, @@ -138,6 +142,10 @@ class WebGLRenderer { const renderListStack = []; const renderStateStack = []; + // internal render target for non-UnsignedByteType color buffer + + let output = null; + // public properties /** @@ -533,6 +541,14 @@ class WebGLRenderer { initGLContext(); + // initialize internal render target for non-UnsignedByteType color buffer + + if ( _outputBufferType !== UnsignedByteType ) { + + output = new WebGLOutput( _outputBufferType, canvas.width, canvas.height, depth, stencil ); + + } + // xr const xr = new WebXRManager( _this, _gl ); @@ -655,6 +671,12 @@ class WebGLRenderer { } + if ( output !== null ) { + + output.setSize( canvas.width, canvas.height ); + + } + this.setViewport( 0, 0, width, height ); }; @@ -698,6 +720,39 @@ class WebGLRenderer { }; + /** + * Sets the post-processing effects to be applied after rendering. + * + * @param {Array} effects - An array of post-processing effects. + */ + this.setEffects = function ( effects ) { + + if ( _outputBufferType === UnsignedByteType ) { + + console.error( 'THREE.WebGLRenderer: setEffects() requires outputBufferType set to HalfFloatType or FloatType.' ); + return; + + } + + if ( effects ) { + + for ( let i = 0; i < effects.length; i ++ ) { + + if ( effects[ i ].isOutputPass === true ) { + + console.warn( 'THREE.WebGLRenderer: OutputPass is not needed in setEffects(). Tone mapping and color space conversion are applied automatically.' ); + break; + + } + + } + + } + + output.setEffects( effects || [] ); + + }; + /** * Returns the current viewport definition. * @@ -1547,6 +1602,12 @@ class WebGLRenderer { if ( _isContextLost === true ) return; + // use internal render target for HalfFloatType color buffer (only when tone mapping is enabled) + + const isXRPresenting = xr.enabled === true && xr.isPresenting === true; + + const useOutput = output !== null && ( _currentRenderTarget === null || isXRPresenting ) && output.begin( _this, _currentRenderTarget ); + // update scene graph if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld(); @@ -1555,7 +1616,7 @@ class WebGLRenderer { if ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld(); - if ( xr.enabled === true && xr.isPresenting === true ) { + if ( xr.enabled === true && xr.isPresenting === true && ( output === null || output.isCompositing() === false ) ) { if ( xr.cameraAutoUpdate === true ) xr.updateCamera( camera ); @@ -1627,46 +1688,52 @@ class WebGLRenderer { if ( this.info.autoReset === true ) this.info.reset(); - // render scene + // render scene (skip if first effect is a render pass - it will render the scene itself) - const opaqueObjects = currentRenderList.opaque; - const transmissiveObjects = currentRenderList.transmissive; + const skipSceneRender = useOutput && output.hasRenderPass(); - currentRenderState.setupLights(); + if ( skipSceneRender === false ) { - if ( camera.isArrayCamera ) { + const opaqueObjects = currentRenderList.opaque; + const transmissiveObjects = currentRenderList.transmissive; - const cameras = camera.cameras; + currentRenderState.setupLights(); - if ( transmissiveObjects.length > 0 ) { + if ( camera.isArrayCamera ) { - for ( let i = 0, l = cameras.length; i < l; i ++ ) { + const cameras = camera.cameras; - const camera2 = cameras[ i ]; + if ( transmissiveObjects.length > 0 ) { + + for ( let i = 0, l = cameras.length; i < l; i ++ ) { + + const camera2 = cameras[ i ]; + + renderTransmissionPass( opaqueObjects, transmissiveObjects, scene, camera2 ); - renderTransmissionPass( opaqueObjects, transmissiveObjects, scene, camera2 ); + } } - } + if ( _renderBackground ) background.render( scene ); - if ( _renderBackground ) background.render( scene ); + for ( let i = 0, l = cameras.length; i < l; i ++ ) { - for ( let i = 0, l = cameras.length; i < l; i ++ ) { + const camera2 = cameras[ i ]; - const camera2 = cameras[ i ]; + renderScene( currentRenderList, scene, camera2, camera2.viewport ); - renderScene( currentRenderList, scene, camera2, camera2.viewport ); + } - } + } else { - } else { + if ( transmissiveObjects.length > 0 ) renderTransmissionPass( opaqueObjects, transmissiveObjects, scene, camera ); - if ( transmissiveObjects.length > 0 ) renderTransmissionPass( opaqueObjects, transmissiveObjects, scene, camera ); + if ( _renderBackground ) background.render( scene ); - if ( _renderBackground ) background.render( scene ); + renderScene( currentRenderList, scene, camera ); - renderScene( currentRenderList, scene, camera ); + } } @@ -1684,6 +1751,14 @@ class WebGLRenderer { } + // copy from internal render target to canvas using fullscreen quad + + if ( useOutput ) { + + output.end( _this ); + + } + // if ( scene.isScene === true ) scene.onAfterRender( _this, scene, camera ); @@ -1872,9 +1947,11 @@ class WebGLRenderer { if ( currentRenderState.state.transmissionRenderTarget[ camera.id ] === undefined ) { + const hasHalfFloatSupport = extensions.has( 'EXT_color_buffer_half_float' ) || extensions.has( 'EXT_color_buffer_float' ); + currentRenderState.state.transmissionRenderTarget[ camera.id ] = new WebGLRenderTarget( 1, 1, { generateMipmaps: true, - type: ( extensions.has( 'EXT_color_buffer_half_float' ) || extensions.has( 'EXT_color_buffer_float' ) ) ? HalfFloatType : UnsignedByteType, + type: hasHalfFloatSupport ? HalfFloatType : UnsignedByteType, minFilter: LinearMipmapLinearFilter, samples: capabilities.samples, stencilBuffer: stencil, @@ -2722,7 +2799,6 @@ class WebGLRenderer { _currentActiveCubeFace = activeCubeFace; _currentActiveMipmapLevel = activeMipmapLevel; - let useDefaultFramebuffer = true; let framebuffer = null; let isCube = false; let isRenderTarget3D = false; @@ -2733,9 +2809,21 @@ class WebGLRenderer { if ( renderTargetProperties.__useDefaultFramebuffer !== undefined ) { - // We need to make sure to rebind the framebuffer. - state.bindFramebuffer( _gl.FRAMEBUFFER, null ); - useDefaultFramebuffer = false; + // Externally-managed framebuffer (e.g. XR) + // Bind to the stored framebuffer (may be null for default, or a WebGLFramebuffer) + state.bindFramebuffer( _gl.FRAMEBUFFER, renderTargetProperties.__webglFramebuffer ); + + _currentViewport.copy( renderTarget.viewport ); + _currentScissor.copy( renderTarget.scissor ); + _currentScissorTest = renderTarget.scissorTest; + + state.viewport( _currentViewport ); + state.scissor( _currentScissor ); + state.setScissorTest( _currentScissorTest ); + + _currentMaterialId = - 1; + + return; } else if ( renderTargetProperties.__webglFramebuffer === undefined ) { @@ -2834,7 +2922,7 @@ class WebGLRenderer { const framebufferBound = state.bindFramebuffer( _gl.FRAMEBUFFER, framebuffer ); - if ( framebufferBound && useDefaultFramebuffer ) { + if ( framebufferBound ) { state.drawBuffers( renderTarget, framebuffer ); @@ -3469,6 +3557,7 @@ class WebGLRenderer { * Note that this setting uses `gl_FragDepth` if available which disables the Early Fragment Test optimization and can cause a decrease in performance. * @property {boolean} [reversedDepthBuffer=false] Whether to use a reverse depth buffer. Requires the `EXT_clip_control` extension. * This is a more faster and accurate version than logarithmic depth buffer. + * @property {number} [outputBufferType=UnsignedByteType] Defines the type of the output buffer. Use `HalfFloatType` for HDR rendering with tone mapping and post-processing support. **/ /** diff --git a/src/renderers/webgl/WebGLOutput.js b/src/renderers/webgl/WebGLOutput.js new file mode 100644 index 00000000000000..711411fb2a8f95 --- /dev/null +++ b/src/renderers/webgl/WebGLOutput.js @@ -0,0 +1,267 @@ +import { + NoToneMapping, + LinearToneMapping, + ReinhardToneMapping, + CineonToneMapping, + ACESFilmicToneMapping, + AgXToneMapping, + NeutralToneMapping, + CustomToneMapping, + SRGBTransfer, + HalfFloatType +} from '../../constants.js'; +import { BufferGeometry } from '../../core/BufferGeometry.js'; +import { Float32BufferAttribute } from '../../core/BufferAttribute.js'; +import { RawShaderMaterial } from '../../materials/RawShaderMaterial.js'; +import { Mesh } from '../../objects/Mesh.js'; +import { OrthographicCamera } from '../../cameras/OrthographicCamera.js'; +import { WebGLRenderTarget } from '../WebGLRenderTarget.js'; +import { ColorManagement } from '../../math/ColorManagement.js'; + +const toneMappingMap = { + [ LinearToneMapping ]: 'LINEAR_TONE_MAPPING', + [ ReinhardToneMapping ]: 'REINHARD_TONE_MAPPING', + [ CineonToneMapping ]: 'CINEON_TONE_MAPPING', + [ ACESFilmicToneMapping ]: 'ACES_FILMIC_TONE_MAPPING', + [ AgXToneMapping ]: 'AGX_TONE_MAPPING', + [ NeutralToneMapping ]: 'NEUTRAL_TONE_MAPPING', + [ CustomToneMapping ]: 'CUSTOM_TONE_MAPPING' +}; + +function WebGLOutput( type, width, height, depth, stencil ) { + + // render targets for scene and post-processing + const targetA = new WebGLRenderTarget( width, height, { + type: type, + depthBuffer: depth, + stencilBuffer: stencil + } ); + + const targetB = new WebGLRenderTarget( width, height, { + type: HalfFloatType, + depthBuffer: false, + stencilBuffer: false + } ); + + // create fullscreen triangle geometry + const geometry = new BufferGeometry(); + geometry.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) ); + geometry.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) ); + + // create output material with tone mapping support + const material = new RawShaderMaterial( { + uniforms: { + tDiffuse: { value: null } + }, + vertexShader: /* glsl */` + precision highp float; + + uniform mat4 modelViewMatrix; + uniform mat4 projectionMatrix; + + attribute vec3 position; + attribute vec2 uv; + + varying vec2 vUv; + + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); + }`, + fragmentShader: /* glsl */` + precision highp float; + + uniform sampler2D tDiffuse; + + varying vec2 vUv; + + #include + #include + + void main() { + gl_FragColor = texture2D( tDiffuse, vUv ); + + #ifdef LINEAR_TONE_MAPPING + gl_FragColor.rgb = LinearToneMapping( gl_FragColor.rgb ); + #elif defined( REINHARD_TONE_MAPPING ) + gl_FragColor.rgb = ReinhardToneMapping( gl_FragColor.rgb ); + #elif defined( CINEON_TONE_MAPPING ) + gl_FragColor.rgb = CineonToneMapping( gl_FragColor.rgb ); + #elif defined( ACES_FILMIC_TONE_MAPPING ) + gl_FragColor.rgb = ACESFilmicToneMapping( gl_FragColor.rgb ); + #elif defined( AGX_TONE_MAPPING ) + gl_FragColor.rgb = AgXToneMapping( gl_FragColor.rgb ); + #elif defined( NEUTRAL_TONE_MAPPING ) + gl_FragColor.rgb = NeutralToneMapping( gl_FragColor.rgb ); + #elif defined( CUSTOM_TONE_MAPPING ) + gl_FragColor.rgb = CustomToneMapping( gl_FragColor.rgb ); + #endif + + #ifdef SRGB_TRANSFER + gl_FragColor = sRGBTransferOETF( gl_FragColor ); + #endif + }`, + depthTest: false, + depthWrite: false + } ); + + const mesh = new Mesh( geometry, material ); + const camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 ); + + let _outputColorSpace = null; + let _outputToneMapping = null; + let _isCompositing = false; + let _savedToneMapping; + let _savedRenderTarget = null; + let _effects = []; + let _hasRenderPass = false; + + this.setSize = function ( width, height ) { + + targetA.setSize( width, height ); + targetB.setSize( width, height ); + + for ( let i = 0; i < _effects.length; i ++ ) { + + const effect = _effects[ i ]; + if ( effect.setSize ) effect.setSize( width, height ); + + } + + }; + + this.setEffects = function ( effects ) { + + _effects = effects; + _hasRenderPass = _effects.length > 0 && _effects[ 0 ].isRenderPass === true; + + const width = targetA.width; + const height = targetA.height; + + for ( let i = 0; i < _effects.length; i ++ ) { + + const effect = _effects[ i ]; + if ( effect.setSize ) effect.setSize( width, height ); + + } + + }; + + this.begin = function ( renderer, renderTarget ) { + + // Don't begin during compositing phase (post-processing effects call render()) + if ( _isCompositing ) return false; + + if ( renderer.toneMapping === NoToneMapping && _effects.length === 0 ) return false; + + _savedRenderTarget = renderTarget; + + // resize internal buffers to match render target (e.g. XR resolution) + if ( renderTarget !== null ) { + + const width = renderTarget.width; + const height = renderTarget.height; + + if ( targetA.width !== width || targetA.height !== height ) { + + this.setSize( width, height ); + + } + + } + + // if first effect is a RenderPass, it will set its own render target + if ( _hasRenderPass === false ) { + + renderer.setRenderTarget( targetA ); + + } + + // disable tone mapping during render - it will be applied in end() + _savedToneMapping = renderer.toneMapping; + renderer.toneMapping = NoToneMapping; + + return true; + + }; + + this.hasRenderPass = function () { + + return _hasRenderPass; + + }; + + this.end = function ( renderer, deltaTime ) { + + // restore tone mapping + renderer.toneMapping = _savedToneMapping; + + _isCompositing = true; + + // run post-processing effects + let readBuffer = targetA; + let writeBuffer = targetB; + + for ( let i = 0; i < _effects.length; i ++ ) { + + const effect = _effects[ i ]; + + if ( effect.enabled === false ) continue; + + effect.render( renderer, writeBuffer, readBuffer, deltaTime ); + + if ( effect.needsSwap !== false ) { + + const temp = readBuffer; + readBuffer = writeBuffer; + writeBuffer = temp; + + } + + } + + // update output material defines if settings changed + if ( _outputColorSpace !== renderer.outputColorSpace || _outputToneMapping !== renderer.toneMapping ) { + + _outputColorSpace = renderer.outputColorSpace; + _outputToneMapping = renderer.toneMapping; + + material.defines = {}; + + if ( ColorManagement.getTransfer( _outputColorSpace ) === SRGBTransfer ) material.defines.SRGB_TRANSFER = ''; + + const toneMapping = toneMappingMap[ _outputToneMapping ]; + if ( toneMapping ) material.defines[ toneMapping ] = ''; + + material.needsUpdate = true; + + } + + // final output to canvas (or XR render target) + material.uniforms.tDiffuse.value = readBuffer.texture; + renderer.setRenderTarget( _savedRenderTarget ); + renderer.render( mesh, camera ); + + _savedRenderTarget = null; + _isCompositing = false; + + }; + + this.isCompositing = function () { + + return _isCompositing; + + }; + + this.dispose = function () { + + targetA.dispose(); + targetB.dispose(); + geometry.dispose(); + material.dispose(); + + }; + +} + +export { WebGLOutput };