diff --git a/examples/files.json b/examples/files.json index 6294a84efbf8fc..0d5047fcf322aa 100644 --- a/examples/files.json +++ b/examples/files.json @@ -295,6 +295,12 @@ "webgl_worker_offscreencanvas", "webgl_performance" ], + "webgl / tsl": [ + "webgl_tsl_shadowmap", + "webgl_tsl_skinning", + "webgl_tsl_clearcoat", + "webgl_tsl_instancing" + ], "webgpu (wip)": [ "webgpu_animation_retargeting", "webgpu_animation_retargeting_readyplayer", diff --git a/examples/jsm/tsl/WebGLNodesHandler.js b/examples/jsm/tsl/WebGLNodesHandler.js new file mode 100644 index 00000000000000..584b93d0e2c176 --- /dev/null +++ b/examples/jsm/tsl/WebGLNodesHandler.js @@ -0,0 +1,605 @@ +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 ); + + } + + } + +} diff --git a/examples/screenshots/webgl_tsl_clearcoat.jpg b/examples/screenshots/webgl_tsl_clearcoat.jpg new file mode 100644 index 00000000000000..3f23098f88b9ca Binary files /dev/null and b/examples/screenshots/webgl_tsl_clearcoat.jpg differ diff --git a/examples/screenshots/webgl_tsl_instancing.jpg b/examples/screenshots/webgl_tsl_instancing.jpg new file mode 100644 index 00000000000000..d6181ff421bd6a Binary files /dev/null and b/examples/screenshots/webgl_tsl_instancing.jpg differ diff --git a/examples/screenshots/webgl_tsl_shadowmap.jpg b/examples/screenshots/webgl_tsl_shadowmap.jpg new file mode 100644 index 00000000000000..9d068f4adc3c68 Binary files /dev/null and b/examples/screenshots/webgl_tsl_shadowmap.jpg differ diff --git a/examples/screenshots/webgl_tsl_skinning.jpg b/examples/screenshots/webgl_tsl_skinning.jpg new file mode 100644 index 00000000000000..218485520bd7fc Binary files /dev/null and b/examples/screenshots/webgl_tsl_skinning.jpg differ diff --git a/examples/webgl_tsl_clearcoat.html b/examples/webgl_tsl_clearcoat.html new file mode 100644 index 00000000000000..76414bcd508d3a --- /dev/null +++ b/examples/webgl_tsl_clearcoat.html @@ -0,0 +1,243 @@ + + +
+