606 lines
13 KiB
JavaScript
606 lines
13 KiB
JavaScript
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 );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|