import { GLSL3, UniformsGroup, Compatibility, Color, UniformsLib, UniformsUtils, } from 'three'; import { context, cubeTexture, reference, texture, fog, rangeFogFactor, densityFogFactor, workingToColorSpace, } from 'three/tsl'; import { NodeUtils, NodeFrame, Lighting, InspectorBase, GLSLNodeBuilder, BasicNodeLibrary, WebGLCapabilities, } from 'three/webgpu'; // Limitations // - VSM shadows not supported // - MRT not supported // - Transmission not supported // - WebGPU postprocessing stack not supported // - Storage textures not supported // - Fog / environment do not automatically update - must call "dispose" // - instanced mesh geometry cannot be shared // - Node materials cannot be used with "compile" function // hash any object parameters that will impact the resulting shader so we can force // a program update function getObjectHash( object ) { return '' + object.receiveShadow; } // Mirrors WebGLUniforms.seqWithValue from WebGLRenderer function generateUniformsList( program, uniforms ) { const progUniforms = program.getUniforms(); const uniformsList = []; for ( let i = 0; i < progUniforms.seq.length; i ++ ) { const u = progUniforms.seq[ i ]; if ( u.id in uniforms ) uniformsList.push( u ); } return uniformsList; } // overrides shadow nodes to use the built in shadow textures class WebGLNodeBuilder extends GLSLNodeBuilder { addNode( node ) { if ( node.isShadowNode ) { node.setupRenderTarget = shadow => { return { shadowMap: shadow.map, depthTexture: shadow.map.depthTexture }; }; node.updateBefore = () => { // no need to rerender shadows since WebGLRenderer is handling it }; } super.addNode( node ); } } // produce and update reusable nodes for a scene class SceneContext { constructor( renderer, scene ) { // TODO: can / should we update the fog and environment node every frame for recompile? this.renderer = renderer; this.scene = scene; this.lightsNode = renderer.lighting.getNode( scene ); this.fogNode = null; this.environmentNode = null; this.prevFog = null; this.prevEnvironment = null; } getCacheKey() { const { lightsNode, environmentNode, fogNode } = this; const lightsHash = lightsNode.getCacheKey(); const envHash = environmentNode ? environmentNode.getCacheKey : 0; const fogHash = fogNode ? fogNode.getCacheKey() : 0; return NodeUtils.hashArray( [ lightsHash, envHash, fogHash ] ); } update() { const { scene, lightsNode } = this; // update lighting const sceneLights = []; scene.traverse( object => { if ( object.isLight ) { sceneLights.push( object ); } } ); lightsNode.setLights( sceneLights ); // update fog if ( this.prevFog !== scene.fog ) { this.fogNode = this.getFogNode(); this.prevFog = scene.fog; } // update environment if ( this.prevEnvironment !== scene.environment ) { this.environmentNode = this.getEnvironmentNode(); this.prevEnvironment = scene.environment; } } getFogNode() { const { scene } = this; if ( scene.fog && scene.fog.isFogExp2 ) { const color = reference( 'color', 'color', scene.fog ); const density = reference( 'density', 'float', scene.fog ); return fog( color, densityFogFactor( density ) ); } else if ( scene.fog && scene.fog.isFog ) { const color = reference( 'color', 'color', scene.fog ); const near = reference( 'near', 'float', scene.fog ); const far = reference( 'far', 'float', scene.fog ); return fog( color, rangeFogFactor( near, far ) ); } else { return null; } } getEnvironmentNode() { const { scene } = this; if ( scene.environment && scene.environment.isCubeTexture ) { return cubeTexture( scene.environment ); } else if ( scene.environment && scene.environment.isTexture ) { return texture( scene.environment ); } else { return null; } } } class RendererProxy { constructor( renderer ) { const backend = { isWebGPUBackend: false, extensions: renderer.extensions, gl: renderer.getContext(), capabilities: null, }; backend.capabilities = new WebGLCapabilities( backend ); this.contextNode = context(); this.inspector = new InspectorBase(); this.library = new BasicNodeLibrary(); this.lighting = new Lighting(); this.backend = backend; const self = this; return new Proxy( renderer, { get( target, property ) { return Reflect.get( property in self ? self : target, property ); }, set( target, property, value ) { return Reflect.set( property in self ? self : target, property, value ); } } ); } hasInitialized() { return true; } getMRT() { return null; } hasCompatibility( name ) { if ( name === Compatibility.TEXTURE_COMPARE ) { return true; } return false; } getCacheKey() { return this.toneMapping + this.outputColorSpace; } } /** * Compatibility loader and builder for TSL Node materials in WebGLRenderer. */ export class WebGLNodesHandler { /** * Constructs a new WebGL node adapter. */ constructor() { this.renderer = null; this.nodeFrame = new NodeFrame(); this.sceneContexts = new WeakMap(); this.programCache = new Map(); this.renderStack = []; const self = this; this.onDisposeMaterialCallback = function () { // dispose of all the uniform groups const { programCache } = self; if ( programCache.has( this ) ) { self.programCache.get( this ).forEach( ( { uniformsGroups } ) => { uniformsGroups.forEach( u => u.dispose() ); } ); self.programCache.delete( this ); } this.removeEventListener( 'dispose', self.onDisposeMaterialCallback ); }; this.getOutputCallback = function ( outputNode ) { // apply tone mapping and color spaces to the output const { outputColorSpace, toneMapping } = self.renderer; outputNode = outputNode.toneMapping( toneMapping ); outputNode = workingToColorSpace( outputNode, outputColorSpace ); return outputNode; }; this.onBeforeRenderCallback = function ( renderer, scene, camera, geometry, object ) { // update node frame references for update nodes const { nodeFrame } = self; nodeFrame.material = this; nodeFrame.object = object; // increment "frame" here to force uniform buffers to update for the material, which otherwise only get // updated once per frame. renderer.info.render.frame ++; // update the uniform groups and nodes for the program if they're available before rendering if ( renderer.properties.has( this ) ) { const currentProgram = renderer.properties.get( this ).currentProgram; const programs = self.programCache.get( this ); if ( programs && programs.has( currentProgram ) ) { // update the nodes for the current object const { updateNodes } = programs.get( currentProgram ); self.updateNodes( updateNodes ); } } const objectHash = getObjectHash( object ); if ( this.prevObjectHash !== objectHash ) { this.prevObjectHash = objectHash; this.needsUpdate = true; } }; this.customProgramCacheKeyCallback = function () { const { renderStack, renderer, nodeFrame } = self; const sceneHash = renderStack[ renderStack.length - 1 ].sceneContext.getCacheKey(); const materialHash = this.constructor.prototype.customProgramCacheKey.call( this ); const rendererHash = renderer.getCacheKey(); return materialHash + sceneHash + rendererHash + getObjectHash( nodeFrame.object ); }; } setRenderer( renderer ) { const rendererProxy = new RendererProxy( renderer ); this.nodeFrame.renderer = rendererProxy; this.renderer = rendererProxy; } onUpdateProgram( material, program, materialProperties ) { const { programCache } = this; if ( ! programCache.has( material ) ) { programCache.set( material, new Map() ); } const programs = programCache.get( material ); if ( ! programs.has( program ) ) { const builder = material._latestBuilder; const uniforms = materialProperties.uniforms; programs.set( program, { uniformsGroups: this.collectUniformsGroups( builder ), uniforms: uniforms, uniformsList: generateUniformsList( program, uniforms ), updateNodes: builder.updateNodes, } ); } const { uniformsGroups, uniforms, uniformsList, updateNodes } = programs.get( program ); material.uniformsGroups = uniformsGroups; materialProperties.uniforms = uniforms; materialProperties.uniformsList = uniformsList; this.updateNodes( updateNodes ); } renderStart( scene, camera ) { const { nodeFrame, renderStack, renderer, sceneContexts } = this; nodeFrame.update(); nodeFrame.camera = camera; nodeFrame.scene = scene; nodeFrame.frameId ++; let sceneContext = sceneContexts.get( scene ); if ( ! sceneContext ) { sceneContext = new SceneContext( renderer, scene ); sceneContexts.set( scene, sceneContext ); } sceneContext.update(); renderStack.push( { sceneContext, camera } ); // ensure all node material callbacks are initialized before // traversal and build const { customProgramCacheKeyCallback, onBeforeRenderCallback, } = this; scene.traverse( object => { if ( object.material && object.material.isNodeMaterial ) { object.material.customProgramCacheKey = customProgramCacheKeyCallback; object.material.onBeforeRender = onBeforeRenderCallback; } } ); } renderEnd() { const { nodeFrame, renderStack } = this; renderStack.pop(); const frame = renderStack[ renderStack.length - 1 ]; if ( frame ) { const { camera, sceneContext } = frame; nodeFrame.camera = camera; nodeFrame.scene = sceneContext.scene; } } build( material, object, parameters ) { const { nodeFrame, renderer, getOutputCallback, onDisposeMaterialCallback, renderStack, } = this; const { camera, sceneContext, } = renderStack[ renderStack.length - 1 ]; const { fogNode, environmentNode, lightsNode, scene, } = sceneContext; // prepare the frame nodeFrame.material = material; nodeFrame.object = object; // create & run the builder const builder = new WebGLNodeBuilder( object, renderer ); builder.scene = scene; builder.camera = camera; builder.material = material; builder.fogNode = fogNode; builder.environmentNode = environmentNode; builder.lightsNode = lightsNode; builder.context.getOutput = getOutputCallback; builder.build(); // update the shader parameters and geometry for program creation and rendering this.updateShaderParameters( builder, parameters ); this.updateGeometryAttributes( builder, object.geometry ); // reset node frame settings to account for any intermediate renders nodeFrame.material = material; nodeFrame.object = object; // set up callbacks for uniforms and node updates material._latestBuilder = builder; material.addEventListener( 'dispose', onDisposeMaterialCallback ); this.updateNodes( builder.updateNodes ); } updateGeometryAttributes( builder, geometry ) { // TODO: this may cause issues if the material / geometry is used in multiple places // add instancing attributes builder.bufferAttributes.forEach( v => { geometry.setAttribute( v.name, v.node.attribute ); } ); // force WebGLAttributes & WebGLBindingStates to refresh // could be fixed by running "build" sooner? Or calling "WebGLAttributes" separately for those // associated with a material? queueMicrotask( () => geometry.dispose() ); } updateShaderParameters( builder, parameters ) { // set up shaders parameters.isRawShaderMaterial = true; parameters.glslVersion = GLSL3; parameters.vertexShader = builder.vertexShader.replace( /#version 300 es/, '' ); parameters.fragmentShader = builder.fragmentShader.replace( /#version 300 es/, '' ); // add uniforms accessed by WebGLRenderer parameters.uniforms = { fogColor: { value: new Color() }, fogNear: { value: 0 }, fogFar: { value: 0 }, envMapIntensity: { value: 0 }, ...UniformsUtils.clone( UniformsLib.lights ) }; // init uniforms const builderUniforms = [ ...builder.uniforms.vertex, ...builder.uniforms.fragment ]; for ( const uniform of builderUniforms ) { parameters.uniforms[ uniform.name ] = uniform.node; } } collectUniformsGroups( builder ) { // create UniformsGroups for regular grouped uniforms const uniformsGroups = []; for ( const key in builder.uniformGroups ) { const { uniforms } = builder.uniformGroups[ key ]; const group = new UniformsGroup(); group.name = key; group.uniforms = uniforms.map( node => node.nodeUniform ); uniformsGroups.push( group ); } // init uniforms const builderUniforms = [ ...builder.uniforms.vertex, ...builder.uniforms.fragment ]; for ( const uniform of builderUniforms ) { if ( uniform.type === 'buffer' ) { // buffer uniforms are all nested in groups const group = new UniformsGroup(); group.name = uniform.node.name; group.uniforms = [ uniform ]; uniformsGroups.push( group ); } } return uniformsGroups; } updateNodes( updateNodes ) { // update nodes for render const { nodeFrame } = this; nodeFrame.renderId ++; for ( const node of updateNodes ) { nodeFrame.updateNode( node ); } } }