Prechádzať zdrojové kódy

WebGLRenderer: Add `compileAsync()`. (#19752)

* Add compileAsync

The compileAsync method adds a way to allow apps to wait on shader
compilation before adding objects to a scene.

* Update webgl_loader_gltf.html

Fix typo.

* Update webgl_loader_gltf_transmission.html

Fix typo.

* Update WebGLProgram.js

Code style improvements.

* Update WebGLRenderer.js

Keep signature of `compileAsync()` similar to `compile()`.

* Update webgl_loader_gltf.html

* Update webgl_loader_gltf_transmission.html

---------

Co-authored-by: Michael Herzog <[email protected]>
Brandon Jones 1 rok pred
rodič
commit
d47a43ad39

+ 7 - 1
examples/webgl_loader_gltf.html

@@ -61,7 +61,13 @@
 						// model
 
 						const loader = new GLTFLoader().setPath( 'models/gltf/DamagedHelmet/glTF/' );
-						loader.load( 'DamagedHelmet.gltf', function ( gltf ) {
+						loader.load( 'DamagedHelmet.gltf', async function ( gltf ) {
+
+							// Calling compileAsync returns a promise that resolves when gltf.scene can be added
+							// to scene without unnecessary stalling on shader compilation. This helps the page
+							// stay responsive during startup.
+
+							await renderer.compileAsync( gltf.scene, camera );
 
 							scene.add( gltf.scene );
 

+ 8 - 1
examples/webgl_loader_gltf_transmission.html

@@ -65,10 +65,17 @@
 						new GLTFLoader()
 							.setPath( 'models/gltf/' )
 							.setDRACOLoader( new DRACOLoader().setDecoderPath( 'jsm/libs/draco/gltf/' ) )
-							.load( 'IridescentDishWithOlives.glb', function ( gltf ) {
+							.load( 'IridescentDishWithOlives.glb', async function ( gltf ) {
 
 								mixer = new THREE.AnimationMixer( gltf.scene );
 								mixer.clipAction( gltf.animations[ 0 ] ).play();
+
+								// Calling compileAsync returns a promise that resolves when gltf.scene can be added
+								// to scene without unnecessary stalling on shader compilation. This helps the page
+								// stay responsive during startup.
+
+								await renderer.compileAsync( gltf.scene, camera );
+
 								scene.add( gltf.scene );
 
 							} );

+ 130 - 14
src/renderers/WebGLRenderer.js

@@ -917,29 +917,87 @@ class WebGLRenderer {
 
 		// Compile
 
+		function prepareMaterial( material, scene, object ) {
+
+			if ( material.transparent === true && material.side === DoubleSide && material.forceSinglePass === false ) {
+
+				material.side = BackSide;
+				material.needsUpdate = true;
+				getProgram( material, scene, object );
+
+				material.side = FrontSide;
+				material.needsUpdate = true;
+				getProgram( material, scene, object );
+
+				material.side = DoubleSide;
+
+			} else {
+
+				getProgram( material, scene, object );
+
+			}
+
+		}
+
 		this.compile = function ( scene, camera ) {
 
-			function prepare( material, scene, object ) {
+			currentRenderState = renderStates.get( scene );
+			currentRenderState.init();
 
-				if ( material.transparent === true && material.side === DoubleSide && material.forceSinglePass === false ) {
+			renderStateStack.push( currentRenderState );
 
-					material.side = BackSide;
-					material.needsUpdate = true;
-					getProgram( material, scene, object );
+			scene.traverseVisible( function ( object ) {
 
-					material.side = FrontSide;
-					material.needsUpdate = true;
-					getProgram( material, scene, object );
+				if ( object.isLight && object.layers.test( camera.layers ) ) {
 
-					material.side = DoubleSide;
+					currentRenderState.pushLight( object );
 
-				} else {
+					if ( object.castShadow ) {
 
-					getProgram( material, scene, object );
+						currentRenderState.pushShadow( object );
+
+					}
 
 				}
 
-			}
+			} );
+
+			currentRenderState.setupLights( _this._useLegacyLights );
+
+			scene.traverse( function ( object ) {
+
+				const material = object.material;
+
+				if ( material ) {
+
+					if ( Array.isArray( material ) ) {
+
+						for ( let i = 0; i < material.length; i ++ ) {
+
+							const material2 = material[ i ];
+
+							prepareMaterial( material2, scene, object );
+
+						}
+
+					} else {
+
+						prepareMaterial( material, scene, object );
+
+					}
+
+				}
+
+			} );
+
+			renderStateStack.pop();
+			currentRenderState = null;
+
+		};
+
+		// compileAsync
+
+		this.compileAsync = function ( scene, camera ) {
 
 			currentRenderState = renderStates.get( scene );
 			currentRenderState.init();
@@ -964,6 +1022,8 @@ class WebGLRenderer {
 
 			currentRenderState.setupLights( _this._useLegacyLights );
 
+			const compiling = new Set();
+
 			scene.traverse( function ( object ) {
 
 				const material = object.material;
@@ -976,13 +1036,15 @@ class WebGLRenderer {
 
 							const material2 = material[ i ];
 
-							prepare( material2, scene, object );
+							prepareMaterial( material2, scene, object );
+							compiling.add( material2 );
 
 						}
 
 					} else {
 
-						prepare( material, scene, object );
+						prepareMaterial( material, scene, object );
+						compiling.add( material );
 
 					}
 
@@ -993,6 +1055,60 @@ class WebGLRenderer {
 			renderStateStack.pop();
 			currentRenderState = null;
 
+			// Wait for all the materials in the new object to indicate that they're
+			// ready to be used before resolving the promise.
+
+			return new Promise( ( resolve ) => {
+
+				function checkMaterialsReady() {
+
+					compiling.forEach( function ( material ) {
+
+						const materialProperties = properties.get( material );
+						const program = materialProperties.currentProgram;
+
+						if ( program.isReady() ) {
+
+							// remove any programs that report they're ready to use from the list
+							compiling.delete( material );
+
+						}
+
+					} );
+
+					// once the list of compiling materials is empty, call the callback
+
+					if ( compiling.size === 0 ) {
+
+						resolve( scene );
+						return;
+
+					}
+
+					// if some materials are still not ready, wait a bit and check again
+
+					setTimeout( checkMaterialsReady, 10 );
+
+				}
+
+				if ( extensions.get( 'KHR_parallel_shader_compile' ) !== null ) {
+
+					// If we can check the compilation status of the materials without
+					// blocking then do so right away.
+
+					checkMaterialsReady();
+
+				} else {
+
+					// Otherwise start by waiting a bit to give the materials we just
+					// initialized a chance to finish.
+
+					setTimeout( checkMaterialsReady, 10 );
+
+				}
+
+			} );
+
 		};
 
 		// Animation Loop

+ 20 - 0
src/renderers/webgl/WebGLProgram.js

@@ -4,6 +4,9 @@ import { ShaderChunk } from '../shaders/ShaderChunk.js';
 import { NoToneMapping, AddOperation, MixOperation, MultiplyOperation, CubeRefractionMapping, CubeUVReflectionMapping, CubeReflectionMapping, PCFSoftShadowMap, PCFShadowMap, VSMShadowMap, ACESFilmicToneMapping, CineonToneMapping, CustomToneMapping, ReinhardToneMapping, LinearToneMapping, GLSL3, LinearSRGBColorSpace, SRGBColorSpace, LinearDisplayP3ColorSpace, DisplayP3ColorSpace, P3Primaries, Rec709Primaries } from '../../constants.js';
 import { ColorManagement } from '../../math/ColorManagement.js';
 
+// From https://www.khronos.org/registry/webgl/extensions/KHR_parallel_shader_compile/
+const COMPLETION_STATUS_KHR = 0x91B1;
+
 let programIdCount = 0;
 
 function handleSource( string, errorLine ) {
@@ -1014,6 +1017,23 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
 
 	};
 
+	// indicate when the program is ready to be used. if the KHR_parallel_shader_compile extension isn't supported,
+	// flag the program as ready immediately. It may cause a stall when it's first used.
+
+	let programReady = ( parameters.rendererExtensionParallelShaderCompile === false );
+
+	this.isReady = function () {
+
+		if ( programReady === false ) {
+
+			programReady = gl.getProgramParameter( program, COMPLETION_STATUS_KHR );
+
+		}
+
+		return programReady;
+
+	};
+
 	// free resource
 
 	this.destroy = function () {

+ 1 - 0
src/renderers/webgl/WebGLPrograms.js

@@ -358,6 +358,7 @@ function WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities
 			rendererExtensionFragDepth: IS_WEBGL2 || extensions.has( 'EXT_frag_depth' ),
 			rendererExtensionDrawBuffers: IS_WEBGL2 || extensions.has( 'WEBGL_draw_buffers' ),
 			rendererExtensionShaderTextureLod: IS_WEBGL2 || extensions.has( 'EXT_shader_texture_lod' ),
+			rendererExtensionParallelShaderCompile: extensions.has( 'KHR_parallel_shader_compile' ),
 
 			customProgramCacheKey: material.customProgramCacheKey()