Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/webgpu_compute_particles_fluid.html
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@

} );

await renderer.init();
await renderer.compileComputeAsync( [ clearGridKernel, p2g1Kernel, p2g2Kernel, updateGridKernel, g2pKernel ] );
await renderer.compileAsync( scene, camera );

window.addEventListener( 'resize', onWindowResize );
controls.update();
renderer.setAnimationLoop( render );
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/renderers/common/Backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,9 @@ class Backend {
* @abstract
* @param {ComputePipeline} computePipeline - The compute pipeline.
* @param {Array<BindGroup>} bindings - The bindings.
* @param {?Array<Promise>} [promises=null] - Optional compilation promises.
*/
createComputePipeline( /*computePipeline, bindings*/ ) { }
createComputePipeline( /*computePipeline, bindings, promises*/ ) { }

// cache key

Expand Down
10 changes: 6 additions & 4 deletions src/renderers/common/Pipelines.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ class Pipelines extends DataMap {
*
* @param {Node} computeNode - The compute node.
* @param {Array<BindGroup>} bindings - The bindings.
* @param {?Array<Promise>} [promises=null] - An array of compilation promises which is only relevant in context of `Renderer.compileComputeAsync()`.
* @return {ComputePipeline} The compute pipeline.
*/
getForCompute( computeNode, bindings ) {
getForCompute( computeNode, bindings, promises = null ) {

const { backend } = this;

Expand Down Expand Up @@ -130,7 +131,7 @@ class Pipelines extends DataMap {

if ( previousPipeline && previousPipeline.usedTimes === 0 ) this._releasePipeline( previousPipeline );

pipeline = this._getComputePipeline( computeNode, stageCompute, cacheKey, bindings );
pipeline = this._getComputePipeline( computeNode, stageCompute, cacheKey, bindings, promises );

}

Expand Down Expand Up @@ -342,9 +343,10 @@ class Pipelines extends DataMap {
* @param {ProgrammableStage} stageCompute - The programmable stage representing the compute shader.
* @param {string} cacheKey - The cache key.
* @param {Array<BindGroup>} bindings - The bindings.
* @param {?Array<Promise>} promises - An array of compilation promises which is only relevant in context of `Renderer.compileComputeAsync()`.
* @return {ComputePipeline} The compute pipeline.
*/
_getComputePipeline( computeNode, stageCompute, cacheKey, bindings ) {
_getComputePipeline( computeNode, stageCompute, cacheKey, bindings, promises ) {

// check for existing pipeline

Expand All @@ -358,7 +360,7 @@ class Pipelines extends DataMap {

this.caches.set( cacheKey, pipeline );

this.backend.createComputePipeline( pipeline, bindings );
this.backend.createComputePipeline( pipeline, bindings, promises );

}

Expand Down
117 changes: 109 additions & 8 deletions src/renderers/common/Renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -896,9 +896,10 @@ class Renderer {
* @param {Object3D} scene - The scene or 3D object to precompile.
* @param {Camera} camera - The camera that is used to render the scene.
* @param {?Scene} targetScene - If the first argument is a 3D object, this parameter must represent the scene the 3D object is going to be added.
* @param {onProgressCallback} [onProgress] - Executed while the compilation is in progress.
* @return {Promise} A Promise that resolves when the compile has been finished.
*/
async compileAsync( scene, camera, targetScene = null ) {
async compileAsync( scene, camera, targetScene = null, onProgress = null ) {

if ( this._isDeviceLost === true ) return;

Expand Down Expand Up @@ -932,7 +933,9 @@ class Renderer {

// Match render()'s logic: use frameBufferTarget when needsFrameBufferTarget is true
const useFrameBufferTarget = this.needsFrameBufferTarget && this._renderTarget === null;
const renderTarget = useFrameBufferTarget ? this._getFrameBufferTarget() : ( this._renderTarget || this._outputRenderTarget );
const outputRenderTarget = this._renderTarget || this._outputRenderTarget;
const useXRCamera = outputRenderTarget !== null && outputRenderTarget.isXRRenderTarget === true;
const renderTarget = useFrameBufferTarget ? this._getFrameBufferTarget() : outputRenderTarget;
const renderContext = this._renderContexts.get( renderTarget, this._mrt );
const activeMipmapLevel = this._activeMipmapLevel;

Expand Down Expand Up @@ -963,7 +966,7 @@ class Renderer {

if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();

camera = this._updateCamera( camera );
camera = this._updateCamera( camera, useXRCamera );

//

Expand Down Expand Up @@ -1058,6 +1061,9 @@ class Renderer {
// Process compilation work items sequentially to avoid freezing
// Yields between objects to keep animation smooth

const total = compilationPromises.length;
let loaded = 0;

for ( const item of compilationPromises ) {

const renderObject = this._objects.get( item.object, item.material, item.scene, item.camera, item.lightsNode, item.renderContext, item.clippingContext, item.passId );
Expand Down Expand Up @@ -1087,13 +1093,105 @@ class Renderer {
this._nodes.updateAfter( renderObject );
this._isPreCompiling = false;

loaded ++;

if ( onProgress !== null ) {

onProgress( new ProgressEvent( 'progress', { lengthComputable: true, loaded, total } ) );

}

// Yield between objects to allow animation frames
await yieldToMain();

}

}

/**
* Compile compute programs. This can be useful to avoid a
* phenomenon which is called "shader compilation stutter", which occurs when
* rendering an object with a new shader for the first time.
*
* @async
* @param {Node|Array<Node>} computeNodes - The compute node(s).
* @param {onProgressCallback} [onProgress] - Executed while the compilation is in progress.
* @return {Promise} A Promise that resolves when the compile has been finished.
*/
async compileComputeAsync( computeNodes, onProgress = null ) {

if ( this._isDeviceLost === true ) return;

if ( this._initialized === false ) await this.init();

const computeList = Array.isArray( computeNodes ) ? computeNodes : [ computeNodes ];

if ( computeList.length === 0 || computeList.some( ( computeNode ) => computeNode === undefined || computeNode === null || computeNode.isComputeNode !== true ) ) {

throw new Error( 'THREE.Renderer: .compileComputeAsync() expects a ComputeNode.' );

}

const total = computeList.length;
let loaded = 0;

//

const pipelines = this._pipelines;
const bindings = this._bindings;
const nodes = this._nodes;

for ( const computeNode of computeList ) {

if ( pipelines.has( computeNode ) === false ) {

const dispose = () => {

computeNode.removeEventListener( 'dispose', dispose );

pipelines.delete( computeNode );
bindings.deleteForCompute( computeNode );
nodes.delete( computeNode );

};

computeNode.addEventListener( 'dispose', dispose );

const onInitFn = computeNode.onInitFunction;

if ( onInitFn !== null ) {

onInitFn.call( computeNode, { renderer: this } );

}

}

await nodes.getForComputeAsync( computeNode );

nodes.updateForCompute( computeNode );
bindings.updateForCompute( computeNode );

const computeBindings = bindings.getForCompute( computeNode );
const compilationPromises = [];

pipelines.getForCompute( computeNode, computeBindings, compilationPromises );
await Promise.all( compilationPromises );

loaded ++;

if ( onProgress !== null ) {

onProgress( new ProgressEvent( 'progress', { lengthComputable: true, loaded, total } ) );

}

if ( loaded < total ) await yieldToMain();

}

}

/**
* Renders the scene in an async fashion.
*
Expand Down Expand Up @@ -1582,6 +1680,7 @@ class Renderer {
const sceneRef = ( scene.isScene === true ) ? scene : _scene;

const outputRenderTarget = this._renderTarget || this._outputRenderTarget;
const useXRCamera = outputRenderTarget !== null && outputRenderTarget.isXRRenderTarget === true;

const activeCubeFace = this._activeCubeFace;
const activeMipmapLevel = this._activeMipmapLevel;
Expand Down Expand Up @@ -1650,7 +1749,7 @@ class Renderer {

if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();

camera = this._updateCamera( camera );
camera = this._updateCamera( camera, useXRCamera );

//

Expand Down Expand Up @@ -3497,13 +3596,14 @@ class Renderer {
*
* @private
* @param {Camera} camera - The camera to update.
* @param {boolean} useXRCamera - Whether the XR camera should be used when presenting.
* @return {Camera} The returned camera might be different depending on whether XR is used or not.
*/
_updateCamera( camera ) {
_updateCamera( camera, useXRCamera ) {

const xr = this.xr;

if ( xr.isPresenting === false ) {
if ( xr.isPresenting === false || useXRCamera === false ) {

let projectionMatrixNeedsUpdate = false;

Expand Down Expand Up @@ -3573,7 +3673,7 @@ class Renderer {

// handle XR

if ( xr.enabled === true && xr.isPresenting === true ) {
if ( useXRCamera === true && xr.enabled === true && xr.isPresenting === true ) {

if ( xr.cameraAutoUpdate === true ) xr.updateCamera( camera );
camera = xr.getCamera(); // use XR camera for rendering
Expand Down Expand Up @@ -3864,7 +3964,8 @@ class Renderer {
* @param {Object3D} scene - The scene or 3D object to precompile.
* @param {Camera} camera - The camera that is used to render the scene.
* @param {Scene} targetScene - If the first argument is a 3D object, this parameter must represent the scene the 3D object is going to be added.
* @return {function(Object3D, Camera, ?Scene): Promise|undefined} A Promise that resolves when the compile has been finished.
* @param {onProgressCallback} [onProgress] - Executed while the compilation is in progress.
* @return {function(Object3D, Camera, ?Scene, ?onProgressCallback): Promise|undefined} A Promise that resolves when the compile has been finished.
*/
get compile() {

Expand Down
4 changes: 2 additions & 2 deletions src/renderers/common/XRManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -648,11 +648,11 @@ class XRManager extends EventDispatcher {
* Browser-side `XRWebGLBinding.foveateBoundTexture()` failures are treated as
* non-fatal so they do not interrupt rendering.
*
* @param {RenderTarget} renderTarget - The internal render target.
* @param {?RenderTarget} renderTarget - The internal render target.
*/
foveateBoundTexture( renderTarget ) {

if ( renderTarget.isPostProcessingRenderTarget !== true ) return;
if ( renderTarget === null || renderTarget.isPostProcessingRenderTarget !== true ) return;
if ( this.isPresenting !== true ) return;
if ( this._glProjLayer === null ) return;

Expand Down
62 changes: 60 additions & 2 deletions src/renderers/common/nodes/NodeManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -450,9 +450,10 @@ class NodeManager extends DataMap {
* Returns a node builder state for the given compute node.
*
* @param {Node} computeNode - The compute node.
* @return {NodeBuilderState} The node builder state.
* @param {boolean} [useAsync=false] - Whether to use async build with yielding.
* @return {NodeBuilderState|Promise<NodeBuilderState>} The node builder state (or Promise if async).
*/
getForCompute( computeNode ) {
getForCompute( computeNode, useAsync = false ) {

const computeData = this.get( computeNode );

Expand All @@ -465,6 +466,21 @@ class NodeManager extends DataMap {

if ( onNodeBuilderCreated !== null ) onNodeBuilderCreated( nodeBuilder, computeNode );

if ( useAsync ) {

return nodeBuilder.buildAsync().then( () => {

nodeBuilderState = this._createNodeBuilderState( nodeBuilder );

computeData.nodeBuilderState = nodeBuilderState;
computeData.version = computeNode.version;

return nodeBuilderState;

} );

}

nodeBuilder.build();

nodeBuilderState = this._createNodeBuilderState( nodeBuilder );
Expand All @@ -478,6 +494,27 @@ class NodeManager extends DataMap {

}

/**
* Async version of getForCompute() that yields to main thread during build.
* Use this in compileComputeAsync() to prevent blocking the main thread.
*
* @param {Node} computeNode - The compute node.
* @return {Promise<NodeBuilderState>} A promise that resolves to the node builder state.
*/
getForComputeAsync( computeNode ) {

const result = this.getForCompute( computeNode, true );

if ( result.then ) {

return result;

}

return Promise.resolve( result );

}

/**
* Creates a node builder state for the given node builder.
*
Expand Down Expand Up @@ -740,6 +777,27 @@ class NodeManager extends DataMap {

if ( node === undefined || forceUpdate ) {

if ( node === undefined && object.isTexture === true ) {

const onTextureDispose = () => {

object.removeEventListener( 'dispose', onTextureDispose );

const node = nodeCache.get( object );

if ( node !== undefined ) {

nodeCache.delete( object );
node.dispose();

}

};

object.addEventListener( 'dispose', onTextureDispose );

}

node = callback();
nodeCache.set( object, node );

Expand Down
Loading