diff --git a/examples/webgl_loader_gltf.html b/examples/webgl_loader_gltf.html index 54d6a58cf82b28..e9cb8470ffed74 100644 --- a/examples/webgl_loader_gltf.html +++ b/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 compiation. This helps the page + // stay responsive during startup. + + await renderer.compileAsync( gltf.scene, scene ); scene.add( gltf.scene ); diff --git a/examples/webgl_loader_gltf_transmission.html b/examples/webgl_loader_gltf_transmission.html index 92979c1d123558..d9afbf3c36a484 100644 --- a/examples/webgl_loader_gltf_transmission.html +++ b/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 compiation. This helps the page + // stay responsive during startup. + + await renderer.compileAsync( gltf.scene, scene ); + scene.add( gltf.scene ); } ); diff --git a/src/renderers/WebGLRenderer.js b/src/renderers/WebGLRenderer.js index dc640341d37277..f6a8bd7c1dbc52 100644 --- a/src/renderers/WebGLRenderer.js +++ b/src/renderers/WebGLRenderer.js @@ -917,29 +917,29 @@ class WebGLRenderer { // Compile - this.compile = function ( scene, camera ) { + function prepareMaterial( material, scene, object ) { - function prepare( material, scene, object ) { + if ( material.transparent === true && material.side === DoubleSide && material.forceSinglePass === false ) { - if ( material.transparent === true && material.side === DoubleSide && material.forceSinglePass === false ) { + material.side = BackSide; + material.needsUpdate = true; + getProgram( material, scene, object ); - material.side = BackSide; - material.needsUpdate = true; - getProgram( material, scene, object ); + material.side = FrontSide; + material.needsUpdate = true; + getProgram( material, scene, object ); - material.side = FrontSide; - material.needsUpdate = true; - getProgram( material, scene, object ); + material.side = DoubleSide; - material.side = DoubleSide; + } else { - } else { + getProgram( material, scene, object ); - getProgram( material, scene, object ); + } - } + } - } + this.compile = function ( scene, camera ) { currentRenderState = renderStates.get( scene ); currentRenderState.init(); @@ -976,13 +976,116 @@ class WebGLRenderer { const material2 = material[ i ]; - prepare( material2, scene, object ); + prepareMaterial( material2, scene, object ); + + } + + } else { + + prepareMaterial( material, scene, object ); + + } + + } + + } ); + + renderStateStack.pop(); + currentRenderState = null; + + }; + + // compileAsync + + this.compileAsync = function ( scene, targetScene = null ) { + + // If no explicit targetScene was given use the scene instead + if ( ! targetScene ) { + + targetScene = scene; + + } + + currentRenderState = renderStates.get( targetScene ); + currentRenderState.init(); + + renderStateStack.push( currentRenderState ); + + let foundScene = scene === targetScene; + + // Gather lights from both the scene and the new object that will be added + // to the scene. + targetScene.traverseVisible( function ( object ) { + + if ( object === scene ) { + + foundScene = true; + + } + + if ( object.isLight ) { + + currentRenderState.pushLight( object ); + + if ( object.castShadow ) { + + currentRenderState.pushShadow( object ); + + } + + } + + } ); + + // If the scene wasn't already part of the targetScene, add any lights it + // contains as well. + if ( ! foundScene ) { + + scene.traverseVisible( function ( object ) { + + if ( object.isLight ) { + + currentRenderState.pushLight( object ); + + if ( object.castShadow ) { + + currentRenderState.pushShadow( object ); + + } + + } + + } ); + + } + + currentRenderState.setupLights( _this._useLegacyLights ); + + const compiling = new Set(); + + // Only initialize materials in the new scene, not the targetScene. + + 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, targetScene, object ); + compiling.add( material2 ); } } else { - prepare( material, scene, object ); + prepareMaterial( material, targetScene, object ); + compiling.add( material ); } @@ -993,6 +1096,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 diff --git a/src/renderers/webgl/WebGLProgram.js b/src/renderers/webgl/WebGLProgram.js index 49a2035e8da351..24fd68f6ce56c1 100644 --- a/src/renderers/webgl/WebGLProgram.js +++ b/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,24 @@ 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; + + this.isReady = function () { + + if ( ! programReady ) { + + programReady = gl.getProgramParameter( program, COMPLETION_STATUS_KHR ); + + } + + return programReady; + + }; + // free resource this.destroy = function () { diff --git a/src/renderers/webgl/WebGLPrograms.js b/src/renderers/webgl/WebGLPrograms.js index ac6fc0ea3c45f8..af1826eaf4a748 100644 --- a/src/renderers/webgl/WebGLPrograms.js +++ b/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()