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 };