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()