Skip to content

Commit

Permalink
WebGLRenderer: Add "readRenderTargetPixelsAsync" function (#28291)
Browse files Browse the repository at this point in the history
* Copy to async implementation from #24466

* probeSync cleanup

* More simplification

* Simplification

* Remove tangential functions

* More simplification

* Convert to thrown errors

* Remove comma

* Update docs, probe frequency
  • Loading branch information
gkjohnson authored May 7, 2024
1 parent bff37d7 commit 7a02475
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 18 deletions.
15 changes: 11 additions & 4 deletions docs/api/en/renderers/WebGLRenderer.html
Original file line number Diff line number Diff line change
Expand Up @@ -511,16 +511,23 @@ <h3>
This is a wrapper around
[link:https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/readPixels WebGLRenderingContext.readPixels]().
</p>
<p>
See the [example:webgl_interactive_cubes_gpu interactive / cubes / gpu]
example.
</p>
<p>
For reading out a [page:WebGLCubeRenderTarget WebGLCubeRenderTarget] use
the optional parameter activeCubeFaceIndex to determine which face should
be read.
</p>

<h3>
[method:Promise readRenderTargetPixelsAsync]( [param:WebGLRenderTarget renderTarget], [param:Float x], [param:Float y], [param:Float width], [param:Float height], [param:TypedArray buffer], [param:Integer activeCubeFaceIndex] )
</h3>
<p>
Asynchronous, non-blocking version of [page:WebGLRenderer.readRenderTargetPixels .readRenderTargetPixels]. The
returned promise resolves once the buffer data is ready to be used.
</p>
<p>
See the [example:webgl_interactive_cubes_gpu interactive / cubes / gpu] example.
</p>

<h3>
[method:undefined render]( [param:Object3D scene], [param:Camera camera] )
</h3>
Expand Down
28 changes: 16 additions & 12 deletions examples/webgl_interactive_cubes_gpu.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;

}
}

} );

}

Expand Down
81 changes: 80 additions & 1 deletion src/renderers/WebGLRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 );
Expand Down
31 changes: 30 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

0 comments on commit 7a02475

Please sign in to comment.