diff --git a/docs/api/en/renderers/WebGLRenderer.html b/docs/api/en/renderers/WebGLRenderer.html index c80c7eeafcb4bd..3b5617a160cc02 100644 --- a/docs/api/en/renderers/WebGLRenderer.html +++ b/docs/api/en/renderers/WebGLRenderer.html @@ -511,16 +511,23 @@

This is a wrapper around [link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/readPixels WebGLRenderingContext.readPixels]().

-

- See the [example:webgl_interactive_cubes_gpu interactive / cubes / gpu] - example. -

For reading out a [page:WebGLCubeRenderTarget WebGLCubeRenderTarget] use the optional parameter activeCubeFaceIndex to determine which face should be read.

+

+ [method:Promise readRenderTargetPixelsAsync]( [param:WebGLRenderTarget renderTarget], [param:Float x], [param:Float y], [param:Float width], [param:Float height], [param:TypedArray buffer], [param:Integer activeCubeFaceIndex] ) +

+

+ Asynchronous, non-blocking version of [page:WebGLRenderer.readRenderTargetPixels .readRenderTargetPixels]. The + returned promise resolves once the buffer data is ready to be used. +

+

+ See the [example:webgl_interactive_cubes_gpu interactive / cubes / gpu] example. +

+

[method:undefined render]( [param:Object3D scene], [param:Camera camera] )

diff --git a/examples/webgl_interactive_cubes_gpu.html b/examples/webgl_interactive_cubes_gpu.html index 013f82acaf0ff8..813ce836f6c628 100644 --- a/examples/webgl_interactive_cubes_gpu.html +++ b/examples/webgl_interactive_cubes_gpu.html @@ -261,23 +261,27 @@ const pixelBuffer = new Int32Array( 4 ); // read the pixel - renderer.readRenderTargetPixels( pickingTexture, 0, 0, 1, 1, pixelBuffer ); + renderer + .readRenderTargetPixelsAsync( pickingTexture, 0, 0, 1, 1, pixelBuffer ) + .then( () => { - const id = pixelBuffer[ 0 ]; - if ( id !== - 1 ) { + const id = pixelBuffer[ 0 ]; + if ( id !== - 1 ) { - // move our highlightBox so that it surrounds the picked object - const data = pickingData[ id ]; - highlightBox.position.copy( data.position ); - highlightBox.rotation.copy( data.rotation ); - highlightBox.scale.copy( data.scale ).add( offset ); - highlightBox.visible = true; + // move our highlightBox so that it surrounds the picked object + const data = pickingData[ id ]; + highlightBox.position.copy( data.position ); + highlightBox.rotation.copy( data.rotation ); + highlightBox.scale.copy( data.scale ).add( offset ); + highlightBox.visible = true; - } else { + } else { - highlightBox.visible = false; + highlightBox.visible = false; - } + } + + } ); } diff --git a/src/renderers/WebGLRenderer.js b/src/renderers/WebGLRenderer.js index 6fe5a52fa19f08..7227d7c00553c8 100644 --- a/src/renderers/WebGLRenderer.js +++ b/src/renderers/WebGLRenderer.js @@ -54,7 +54,7 @@ import { WebGLUtils } from './webgl/WebGLUtils.js'; import { WebXRManager } from './webxr/WebXRManager.js'; import { WebGLMaterials } from './webgl/WebGLMaterials.js'; import { WebGLUniformsGroups } from './webgl/WebGLUniformsGroups.js'; -import { createCanvasElement } from '../utils.js'; +import { createCanvasElement, probeAsync } from '../utils.js'; import { ColorManagement } from '../math/ColorManagement.js'; class WebGLRenderer { @@ -2357,6 +2357,85 @@ class WebGLRenderer { }; + this.readRenderTargetPixelsAsync = async function ( renderTarget, x, y, width, height, buffer, activeCubeFaceIndex ) { + + if ( ! ( renderTarget && renderTarget.isWebGLRenderTarget ) ) { + + throw new Error( 'THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.' ); + + } + + let framebuffer = properties.get( renderTarget ).__webglFramebuffer; + if ( renderTarget.isWebGLCubeRenderTarget && activeCubeFaceIndex !== undefined ) { + + framebuffer = framebuffer[ activeCubeFaceIndex ]; + + } + + if ( framebuffer ) { + + state.bindFramebuffer( _gl.FRAMEBUFFER, framebuffer ); + + try { + + const texture = renderTarget.texture; + const textureFormat = texture.format; + const textureType = texture.type; + + if ( ! capabilities.textureFormatReadable( textureFormat ) ) { + + throw new Error( 'THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in RGBA or implementation defined format.' ); + + } + + if ( ! capabilities.textureTypeReadable( textureType ) ) { + + throw new Error( 'THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in UnsignedByteType or implementation defined type.' ); + + } + + // the following if statement ensures valid read requests (no out-of-bounds pixels, see #8604) + if ( ( x >= 0 && x <= ( renderTarget.width - width ) ) && ( y >= 0 && y <= ( renderTarget.height - height ) ) ) { + + const glBuffer = _gl.createBuffer(); + _gl.bindBuffer( _gl.PIXEL_PACK_BUFFER, glBuffer ); + _gl.bufferData( _gl.PIXEL_PACK_BUFFER, buffer.byteLength, _gl.STREAM_READ ); + _gl.readPixels( x, y, width, height, utils.convert( textureFormat ), utils.convert( textureType ), 0 ); + _gl.flush(); + + // check if the commands have finished every 8 ms + const sync = _gl.fenceSync( _gl.SYNC_GPU_COMMANDS_COMPLETE, 0 ); + await probeAsync( _gl, sync, 4 ); + + try { + + _gl.bindBuffer( _gl.PIXEL_PACK_BUFFER, glBuffer ); + _gl.getBufferSubData( _gl.PIXEL_PACK_BUFFER, 0, buffer ); + + } finally { + + _gl.deleteBuffer( glBuffer ); + _gl.deleteSync( sync ); + + } + + return buffer; + + } + + } finally { + + // restore framebuffer of current render target if necessary + + const framebuffer = ( _currentRenderTarget !== null ) ? properties.get( _currentRenderTarget ).__webglFramebuffer : null; + state.bindFramebuffer( _gl.FRAMEBUFFER, framebuffer ); + + } + + } + + }; + this.copyFramebufferToTexture = function ( position, texture, level = 0 ) { const levelScale = Math.pow( 2, - level ); diff --git a/src/utils.js b/src/utils.js index 6fd13ca9dee638..3c333e5085f402 100644 --- a/src/utils.js +++ b/src/utils.js @@ -88,4 +88,33 @@ function warnOnce( message ) { } -export { arrayMin, arrayMax, arrayNeedsUint32, getTypedArray, createElementNS, createCanvasElement, warnOnce }; +function probeAsync( gl, sync, interval ) { + + return new Promise( function ( resolve, reject ) { + + function probe() { + + switch ( gl.clientWaitSync( sync, gl.SYNC_FLUSH_COMMANDS_BIT, 0 ) ) { + + case gl.WAIT_FAILED: + reject(); + break; + + case gl.TIMEOUT_EXPIRED: + setTimeout( probe, interval ); + break; + + default: + resolve(); + + } + + } + + setTimeout( probe, interval ); + + } ); + +} + +export { arrayMin, arrayMax, arrayNeedsUint32, getTypedArray, createElementNS, createCanvasElement, warnOnce, probeAsync };