瀏覽代碼

WebGLRenderer: Add NodeMaterial compatibility layer (#32851)

Garrett Johnson 1 月之前
父節點
當前提交
b431608199

+ 6 - 0
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",

+ 605 - 0
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 );
+
+		}
+
+	}
+
+}

二進制
examples/screenshots/webgl_tsl_clearcoat.jpg


二進制
examples/screenshots/webgl_tsl_instancing.jpg


二進制
examples/screenshots/webgl_tsl_shadowmap.jpg


二進制
examples/screenshots/webgl_tsl_skinning.jpg


+ 243 - 0
examples/webgl_tsl_clearcoat.html

@@ -0,0 +1,243 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - materials - clearcoat</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="example.css">
+	</head>
+	<body>
+		<div id="info">
+			<a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>
+
+			<div class="title-wrapper">
+				<a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>Clearcoat</span>
+			</div>
+
+			<small>PBR Clearcoat effect.</small>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/webgpu": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.tsl.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three/webgpu';
+			import { WebGLNodesHandler } from 'three/addons/tsl/WebGLNodesHandler.js';
+			import { WebGLRenderer } from 'three';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { HDRCubeTextureLoader } from 'three/addons/loaders/HDRCubeTextureLoader.js';
+
+			import { FlakesTexture } from 'three/addons/textures/FlakesTexture.js';
+
+			let camera, scene, renderer;
+
+			let particleLight;
+			let group;
+
+			init();
+
+			function init() {
+
+				camera = new THREE.PerspectiveCamera( 27, window.innerWidth / window.innerHeight, 0.25, 50 );
+				camera.position.z = 10;
+
+				scene = new THREE.Scene();
+
+				group = new THREE.Group();
+				scene.add( group );
+
+				new HDRCubeTextureLoader()
+					.setPath( 'textures/cube/pisaHDR/' )
+					.load( [ 'px.hdr', 'nx.hdr', 'py.hdr', 'ny.hdr', 'pz.hdr', 'nz.hdr' ],
+						function ( texture ) {
+
+							const geometry = new THREE.SphereGeometry( .8, 64, 32 );
+
+							const textureLoader = new THREE.TextureLoader();
+
+							const diffuse = textureLoader.load( 'textures/carbon/Carbon.png' );
+							diffuse.colorSpace = THREE.SRGBColorSpace;
+							diffuse.wrapS = THREE.RepeatWrapping;
+							diffuse.wrapT = THREE.RepeatWrapping;
+							diffuse.repeat.x = 10;
+							diffuse.repeat.y = 10;
+
+							const normalMap = textureLoader.load( 'textures/carbon/Carbon_Normal.png' );
+							normalMap.wrapS = THREE.RepeatWrapping;
+							normalMap.wrapT = THREE.RepeatWrapping;
+							normalMap.repeat.x = 10;
+							normalMap.repeat.y = 10;
+
+							const normalMap2 = textureLoader.load( 'textures/water/Water_1_M_Normal.jpg' );
+
+							const normalMap3 = new THREE.CanvasTexture( new FlakesTexture() );
+							normalMap3.wrapS = THREE.RepeatWrapping;
+							normalMap3.wrapT = THREE.RepeatWrapping;
+							normalMap3.repeat.x = 10;
+							normalMap3.repeat.y = 6;
+							normalMap3.anisotropy = 16;
+
+							const normalMap4 = textureLoader.load( 'textures/golfball.jpg' );
+
+							const clearcoatNormalMap = textureLoader.load( 'textures/pbr/Scratched_gold/Scratched_gold_01_1K_Normal.png' );
+
+							// car paint
+
+							let material = new THREE.MeshPhysicalNodeMaterial( {
+								clearcoat: 1.0,
+								clearcoatRoughness: 0.1,
+								metalness: 0.9,
+								roughness: 0.5,
+								color: 0x0000ff,
+								normalMap: normalMap3,
+								normalScale: new THREE.Vector2( 0.15, 0.15 )
+							} );
+							let mesh = new THREE.Mesh( geometry, material );
+							mesh.position.x = - 1;
+							mesh.position.y = 1;
+							group.add( mesh );
+
+							// fibers
+
+							material = new THREE.MeshPhysicalNodeMaterial( {
+								roughness: 0.5,
+								clearcoat: 1.0,
+								clearcoatRoughness: 0.1,
+								map: diffuse,
+								normalMap: normalMap
+							} );
+							mesh = new THREE.Mesh( geometry, material );
+							mesh.position.x = 1;
+							mesh.position.y = 1;
+							group.add( mesh );
+
+							// golf
+
+							material = new THREE.MeshPhysicalNodeMaterial( {
+								metalness: 0.0,
+								roughness: 0.1,
+								clearcoat: 1.0,
+								normalMap: normalMap4,
+								clearcoatNormalMap: clearcoatNormalMap,
+
+								// y scale is negated to compensate for normal map handedness.
+								clearcoatNormalScale: new THREE.Vector2( 2.0, - 2.0 )
+							} );
+							mesh = new THREE.Mesh( geometry, material );
+							mesh.position.x = - 1;
+							mesh.position.y = - 1;
+							group.add( mesh );
+
+							// clearcoat + normalmap
+
+							material = new THREE.MeshPhysicalNodeMaterial( {
+								clearcoat: 1.0,
+								metalness: 1.0,
+								color: 0xff0000,
+								normalMap: normalMap2,
+								normalScale: new THREE.Vector2( 0.15, 0.15 ),
+								clearcoatNormalMap: clearcoatNormalMap,
+
+								// y scale is negated to compensate for normal map handedness.
+								clearcoatNormalScale: new THREE.Vector2( 2.0, - 2.0 )
+							} );
+							mesh = new THREE.Mesh( geometry, material );
+							mesh.position.x = 1;
+							mesh.position.y = - 1;
+							group.add( mesh );
+
+							//
+
+							scene.background = texture;
+							scene.environment = texture;
+
+						}
+
+					);
+
+				// LIGHTS
+
+				particleLight = new THREE.Mesh(
+					new THREE.SphereGeometry( .05, 8, 8 ),
+					new THREE.MeshBasicMaterial( { color: 0xffffff } )
+				);
+				scene.add( particleLight );
+
+				particleLight.add( new THREE.PointLight( 0xffffff, 30 ) );
+
+				renderer = new WebGLRenderer( { antialias: true } );
+				renderer.setNodesHandler( new WebGLNodesHandler() );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				document.body.appendChild( renderer.domElement );
+
+				//
+
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
+				renderer.toneMappingExposure = 1.25;
+
+				// EVENTS
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 3;
+				controls.maxDistance = 30;
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			//
+
+			function onWindowResize() {
+
+				const width = window.innerWidth;
+				const height = window.innerHeight;
+
+				camera.aspect = width / height;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( width, height );
+
+			}
+
+			//
+
+			function animate() {
+
+				render();
+
+			}
+
+			function render() {
+
+				const timer = Date.now() * 0.00025;
+
+				particleLight.position.x = Math.sin( timer * 7 ) * 3;
+				particleLight.position.y = Math.cos( timer * 5 ) * 4;
+				particleLight.position.z = Math.cos( timer * 3 ) * 3;
+
+				for ( let i = 0; i < group.children.length; i ++ ) {
+
+					const child = group.children[ i ];
+					child.rotation.y += 0.005;
+
+				}
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 375 - 0
examples/webgl_tsl_instancing.html

@@ -0,0 +1,375 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<title>three.js webgl - instancing - performance</title>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+	<link type="text/css" rel="stylesheet" href="main.css">
+	<style>
+		#info {
+			background-color: rgba(0,0,0,0.75);
+		}
+
+		.lil-gui .gui-stats {
+			line-height: var(--widget-height);
+			padding: var(--padding);
+		}
+	</style>
+</head>
+<body>
+
+	<div id="info">
+
+		<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - instancing - performance
+
+	</div>
+
+	<div id="container"></div>
+
+	<script type="importmap">
+		{
+			"imports": {
+				"three": "../build/three.module.js",
+				"three/webgpu": "../build/three.webgpu.js",
+				"three/addons/": "./jsm/",
+				"three/tsl": "../build/three.tsl.js"
+			}
+		}
+	</script>
+
+	<script type="module">
+		import * as THREE from 'three';
+		import { WebGLNodesHandler } from 'three/addons/tsl/WebGLNodesHandler.js';
+		
+		import Stats from 'three/addons/libs/stats.module.js';
+		import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+		import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+		import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
+		import { MeshPhongNodeMaterial } from 'three/webgpu';
+		import { sin, time, vec3, positionWorld } from 'three/tsl';
+
+		let container, stats, gui, guiStatsEl;
+		let camera, controls, scene, renderer, material;
+
+		// gui
+
+		const Method = {
+			INSTANCED: 'INSTANCED',
+			MERGED: 'MERGED',
+			NAIVE: 'NAIVE'
+		};
+
+		const api = {
+			method: Method.INSTANCED,
+			count: 1000
+		};
+
+		//
+
+		init();
+		initMesh();
+
+		//
+
+		function clean() {
+
+			const meshes = [];
+
+			scene.traverse( function ( object ) {
+
+				if ( object.isMesh ) meshes.push( object );
+
+			} );
+
+			for ( let i = 0; i < meshes.length; i ++ ) {
+
+				const mesh = meshes[ i ];
+				mesh.material.dispose();
+				mesh.geometry.dispose();
+
+				scene.remove( mesh );
+
+			}
+
+		}
+
+		const randomizeMatrix = function () {
+
+			const position = new THREE.Vector3();
+			const quaternion = new THREE.Quaternion();
+			const scale = new THREE.Vector3();
+
+			return function ( matrix ) {
+
+				position.x = Math.random() * 40 - 20;
+				position.y = Math.random() * 40 - 20;
+				position.z = Math.random() * 40 - 20;
+
+				quaternion.random();
+
+				scale.x = scale.y = scale.z = Math.random() * 1;
+
+				matrix.compose( position, quaternion, scale );
+
+			};
+
+		}();
+
+		function initMesh() {
+
+			clean();
+
+			// make instances
+			new THREE.BufferGeometryLoader()
+				.setPath( 'models/json/' )
+				.load( 'suzanne_buffergeometry.json', function ( geometry ) {
+
+					const posY = positionWorld;
+					const t = time.mul( 4 );
+					material = new MeshPhongNodeMaterial();
+					material.colorNode = vec3(
+						sin( posY.mul( 0.1 ).add( t ) ).mul( 0.5 ).add( 0.5 ).x,
+						sin( posY.mul( 0.1 ).add( t.mul( 0.5 ).add( 2 ) ) ).mul( 0.5 ).add( 0.5 ).y,
+						sin( posY.mul( 0.1 ).add( t.mul( 1.5 ).add( 4 ) ) ).mul( 0.5 ).add( 0.5 ).z,
+					);
+
+					geometry.computeVertexNormals();
+
+					console.time( api.method + ' (build)' );
+
+					switch ( api.method ) {
+
+						case Method.INSTANCED:
+							makeInstanced( geometry );
+							break;
+
+						case Method.MERGED:
+							makeMerged( geometry );
+							break;
+
+						case Method.NAIVE:
+							makeNaive( geometry );
+							break;
+
+					}
+
+					console.timeEnd( api.method + ' (build)' );
+
+				} );
+
+		}
+
+		function makeInstanced( geometry ) {
+
+			const matrix = new THREE.Matrix4();
+			const mesh = new THREE.InstancedMesh( geometry, material, api.count );
+
+			for ( let i = 0; i < api.count; i ++ ) {
+
+				randomizeMatrix( matrix );
+				mesh.setMatrixAt( i, matrix );
+
+			}
+
+			scene.add( mesh );
+
+			//
+
+			const geometryByteLength = getGeometryByteLength( geometry );
+
+			guiStatsEl.innerHTML = [
+
+				'<i>GPU draw calls</i>: 1',
+				'<i>GPU memory</i>: ' + formatBytes( api.count * 16 + geometryByteLength, 2 )
+
+			].join( '<br/>' );
+
+		}
+
+		function makeMerged( geometry ) {
+
+			const geometries = [];
+			const matrix = new THREE.Matrix4();
+
+			for ( let i = 0; i < api.count; i ++ ) {
+
+				randomizeMatrix( matrix );
+
+				const instanceGeometry = geometry.clone();
+				instanceGeometry.applyMatrix4( matrix );
+
+				geometries.push( instanceGeometry );
+
+			}
+
+			const mergedGeometry = BufferGeometryUtils.mergeGeometries( geometries );
+
+			scene.add( new THREE.Mesh( mergedGeometry, material ) );
+
+			//
+
+			guiStatsEl.innerHTML = [
+
+				'<i>GPU draw calls</i>: 1',
+				'<i>GPU memory</i>: ' + formatBytes( getGeometryByteLength( mergedGeometry ), 2 )
+
+			].join( '<br/>' );
+
+		}
+
+		function makeNaive( geometry ) {
+
+			const matrix = new THREE.Matrix4();
+
+			for ( let i = 0; i < api.count; i ++ ) {
+
+				randomizeMatrix( matrix );
+
+				const mesh = new THREE.Mesh( geometry, material );
+				mesh.applyMatrix4( matrix );
+
+				scene.add( mesh );
+
+			}
+
+			//
+
+			const geometryByteLength = getGeometryByteLength( geometry );
+
+			guiStatsEl.innerHTML = [
+
+				'<i>GPU draw calls</i>: ' + api.count,
+				'<i>GPU memory</i>: ' + formatBytes( api.count * 16 + geometryByteLength, 2 )
+
+			].join( '<br/>' );
+
+		}
+
+		function init() {
+
+			const width = window.innerWidth;
+			const height = window.innerHeight;
+
+			// camera
+
+			camera = new THREE.PerspectiveCamera( 70, width / height, 1, 100 );
+			camera.position.z = 30;
+
+			// renderer
+
+			renderer = new THREE.WebGLRenderer( { antialias: true } );
+			renderer.setNodesHandler( new WebGLNodesHandler() );
+
+			renderer.setPixelRatio( window.devicePixelRatio );
+			renderer.setSize( width, height );
+			renderer.setAnimationLoop( animate );
+			container = document.getElementById( 'container' );
+			container.appendChild( renderer.domElement );
+
+			// scene
+
+			scene = new THREE.Scene();
+			scene.background = new THREE.Color( 0xffffff );
+
+			// controls
+
+			controls = new OrbitControls( camera, renderer.domElement );
+			controls.autoRotate = true;
+			controls.autoRotateSpeed = 0.5;
+
+			// light
+
+			const ambientLight = new THREE.AmbientLight( 0xffffff, 2 );
+			const directionalLight = new THREE.DirectionalLight( 0xffffff, 2 );
+			directionalLight.position.set( 1, 1, 1 );
+			scene.add( directionalLight, ambientLight );
+
+			// stats
+
+			stats = new Stats();
+			container.appendChild( stats.dom );
+
+			// gui
+
+			gui = new GUI();
+			gui.add( api, 'method', Method ).onChange( initMesh );
+			gui.add( api, 'count', 1, 10000 ).step( 1 ).onChange( initMesh );
+
+			const perfFolder = gui.addFolder( 'Performance' );
+
+			guiStatsEl = document.createElement( 'div' );
+			guiStatsEl.classList.add( 'gui-stats' );
+
+			perfFolder.$children.appendChild( guiStatsEl );
+			perfFolder.open();
+
+			// listeners
+
+			window.addEventListener( 'resize', onWindowResize );
+
+			Object.assign( window, { scene } );
+
+		}
+
+		//
+
+		function onWindowResize() {
+
+			const width = window.innerWidth;
+			const height = window.innerHeight;
+
+			camera.aspect = width / height;
+			camera.updateProjectionMatrix();
+
+			renderer.setSize( width, height );
+
+		}
+
+		function animate() {
+
+			controls.update();
+
+			renderer.render( scene, camera );
+
+			stats.update();
+
+		}
+
+		//
+
+		function getGeometryByteLength( geometry ) {
+
+			let total = 0;
+
+			if ( geometry.index ) total += geometry.index.array.byteLength;
+
+			for ( const name in geometry.attributes ) {
+
+				total += geometry.attributes[ name ].array.byteLength;
+
+			}
+
+			return total;
+
+		}
+
+		// Source: https://stackoverflow.com/a/18650828/1314762
+		function formatBytes( bytes, decimals ) {
+
+			if ( bytes === 0 ) return '0 bytes';
+
+			const k = 1024;
+			const dm = decimals < 0 ? 0 : decimals;
+			const sizes = [ 'bytes', 'KB', 'MB' ];
+
+			const i = Math.floor( Math.log( bytes ) / Math.log( k ) );
+
+			return parseFloat( ( bytes / Math.pow( k, i ) ).toFixed( dm ) ) + ' ' + sizes[ i ];
+
+		}
+
+	</script>
+
+</body>
+</html>

+ 208 - 0
examples/webgl_tsl_shadowmap.html

@@ -0,0 +1,208 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - shadow map</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="example.css">
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>
+
+			<div class="title-wrapper">
+				<a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>Shadow Map</span>
+			</div>
+
+			<small>
+				Shadow map example.
+			</small>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/webgpu": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.tsl.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three/webgpu';
+			import { WebGLNodesHandler } from 'three/addons/tsl/WebGLNodesHandler.js';
+			import { WebGLRenderer } from 'three';
+			import { mx_fractal_noise_float, mx_fractal_noise_vec3, positionLocal, positionWorld, Fn, color } from 'three/tsl';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			let camera, scene, renderer, timer;
+			let dirLight, spotLight;
+			let torusKnot, dirGroup;
+
+			init();
+
+			function init() {
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 );
+				camera.position.set( 0, 10, 20 );
+
+				scene = new THREE.Scene();
+				scene.backgroundNode = color( 0x222244 );
+				scene.fog = new THREE.Fog( 0x222244, 50, 100 );
+
+				// lights
+
+				scene.add( new THREE.AmbientLight( 0x444444, 2 ) );
+
+				spotLight = new THREE.SpotLight( 0xff8888, 400 );
+				spotLight.angle = Math.PI / 5;
+				spotLight.penumbra = 0.3;
+				spotLight.position.set( 8, 10, 5 );
+				spotLight.castShadow = true;
+				spotLight.shadow.camera.near = 8;
+				spotLight.shadow.camera.far = 200;
+				spotLight.shadow.mapSize.width = 2048;
+				spotLight.shadow.mapSize.height = 2048;
+				spotLight.shadow.radius = 4;
+				scene.add( spotLight );
+
+
+				dirLight = new THREE.DirectionalLight( 0x8888ff, 3 );
+				dirLight.position.set( 3, 12, 17 );
+				dirLight.castShadow = true;
+				dirLight.shadow.camera.near = 0.1;
+				dirLight.shadow.camera.far = 500;
+				dirLight.shadow.camera.right = 17;
+				dirLight.shadow.camera.left = - 17;
+				dirLight.shadow.camera.top	= 17;
+				dirLight.shadow.camera.bottom = - 17;
+				dirLight.shadow.mapSize.width = 2048;
+				dirLight.shadow.mapSize.height = 2048;
+				dirLight.shadow.radius = 4;
+
+				dirGroup = new THREE.Group();
+				dirGroup.add( dirLight );
+				scene.add( dirGroup );
+
+				// geometry
+
+				const geometry = new THREE.TorusKnotGeometry( 25, 8, 75, 80 );
+				const material = new THREE.MeshPhongNodeMaterial( {
+					color: 0x999999,
+					shininess: 0,
+					specular: 0x222222
+				} );
+
+				const materialCustomShadow = material.clone();
+				materialCustomShadow.transparent = true;
+
+				const discardNode = mx_fractal_noise_float( positionLocal.mul( 0.1 ) ).x.greaterThan( 0.0 );
+
+				materialCustomShadow.maskNode = discardNode;
+
+				torusKnot = new THREE.Mesh( geometry, materialCustomShadow );
+				torusKnot.scale.multiplyScalar( 1 / 18 );
+				torusKnot.position.y = 3;
+				torusKnot.castShadow = true;
+				torusKnot.receiveShadow = true;
+				scene.add( torusKnot );
+
+				const cylinderGeometry = new THREE.CylinderGeometry( 0.75, 0.75, 7, 32 );
+
+				const pillar1 = new THREE.Mesh( cylinderGeometry, material );
+				pillar1.position.set( 8, 3.5, 8 );
+				pillar1.castShadow = true;
+
+				const pillar2 = pillar1.clone();
+				pillar2.position.set( 8, 3.5, - 8 );
+				const pillar3 = pillar1.clone();
+				pillar3.position.set( - 8, 3.5, 8 );
+				const pillar4 = pillar1.clone();
+				pillar4.position.set( - 8, 3.5, - 8 );
+
+				scene.add( pillar1 );
+				scene.add( pillar2 );
+				scene.add( pillar3 );
+				scene.add( pillar4 );
+
+				const planeGeometry = new THREE.PlaneGeometry( 200, 200 );
+
+				const planeMaterial = new THREE.MeshPhongNodeMaterial();
+				planeMaterial.color.setHex( 0x999999 );
+				planeMaterial.shininess = 0;
+				planeMaterial.specular.setHex( 0x111111 );
+
+				planeMaterial.colorNode = Fn( () => {
+
+					const pos = positionWorld.toVar();
+					pos.xz.addAssign( mx_fractal_noise_vec3( positionWorld.mul( 2 ) ).saturate().xz );
+					return mx_fractal_noise_vec3( positionWorld.mul( 2 ) ).saturate().zzz.mul( 0.2 ).add( .5 );
+
+				} )();
+
+				const ground = new THREE.Mesh( planeGeometry, planeMaterial );
+				ground.rotation.x = - Math.PI / 2;
+				ground.scale.multiplyScalar( 3 );
+				ground.castShadow = true;
+				ground.receiveShadow = true;
+				scene.add( ground );
+
+				// renderer
+
+				renderer = new WebGLRenderer( { antialias: true } );
+				renderer.setNodesHandler( new WebGLNodesHandler() );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				renderer.shadowMap.enabled = true;
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
+				document.body.appendChild( renderer.domElement );
+
+				// Mouse control
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.target.set( 0, 2, 0 );
+				controls.minDistance = 7;
+				controls.maxDistance = 40;
+				controls.update();
+
+				timer = new THREE.Timer();
+				timer.connect( document );
+
+				window.addEventListener( 'resize', resize );
+
+			}
+
+			function resize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate( time ) {
+
+				timer.update();
+
+				const delta = timer.getDelta();
+
+				torusKnot.rotation.x += 0.25 * delta;
+				torusKnot.rotation.y += 0.5 * delta;
+				torusKnot.rotation.z += 1 * delta;
+
+				dirGroup.rotation.y += 0.7 * delta;
+				dirLight.position.z = 17 + Math.sin( time * 0.001 ) * 5;
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 121 - 0
examples/webgl_tsl_skinning.html

@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - skinning</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="example.css">
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>
+
+			<div class="title-wrapper">
+				<a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>Skinning</span>
+			</div>
+
+			<small>
+				Basic skinning example using a model from Mixamo.
+			</small>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/webgpu": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.tsl.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three/webgpu';
+			import { WebGLNodesHandler } from 'three/addons/tsl/WebGLNodesHandler.js';
+			import { WebGLRenderer } from 'three';
+
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+
+			let camera, scene, renderer;
+
+			let mixer, timer;
+
+			init();
+
+			function init() {
+
+				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.01, 100 );
+				camera.position.set( 1, 2, 3 );
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0x3355aa );
+				camera.lookAt( 0, 1, 0 );
+
+				timer = new THREE.Timer();
+				timer.connect( document );
+
+				//lights
+
+				const light = new THREE.PointLight( 0xffffff, 1, 100 );
+				light.power = 2500;
+				camera.add( light );
+				scene.add( camera );
+
+				const ambient = new THREE.AmbientLight( 0x4466ff, 1 );
+				scene.add( ambient );
+
+				const loader = new GLTFLoader();
+				loader.load( 'models/gltf/Michelle.glb', function ( gltf ) {
+
+					const object = gltf.scene;
+					mixer = new THREE.AnimationMixer( object );
+
+					const action = mixer.clipAction( gltf.animations[ 0 ] );
+					action.play();
+
+					scene.add( object );
+
+				} );
+
+				//renderer
+
+				renderer = new WebGLRenderer( { antialias: true } );
+				renderer.setNodesHandler( new WebGLNodesHandler() );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				renderer.toneMapping = THREE.LinearToneMapping;
+				renderer.toneMappingExposure = 0.4;
+				document.body.appendChild( renderer.domElement );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				timer.update();
+
+				const delta = timer.getDelta();
+
+				if ( mixer ) mixer.update( delta );
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 3 - 0
src/Three.WebGPU.js

@@ -4,6 +4,7 @@ export * from './materials/nodes/NodeMaterials.js';
 export { default as WebGPURenderer } from './renderers/webgpu/WebGPURenderer.js';
 export { default as WebGPUBackend } from './renderers/webgpu/WebGPUBackend.js';
 export { default as WebGLBackend } from './renderers/webgl-fallback/WebGLBackend.js';
+export { default as WebGLCapabilities } from './renderers/webgl-fallback/utils/WebGLCapabilities.js';
 export { default as Lighting } from './renderers/common/Lighting.js';
 export { default as BundleGroup } from './renderers/common/BundleGroup.js';
 export { default as QuadMesh } from './renderers/common/QuadMesh.js';
@@ -27,6 +28,8 @@ export { default as NodeMaterialLoader } from './loaders/nodes/NodeMaterialLoade
 export { default as InspectorBase } from './renderers/common/InspectorBase.js';
 export { default as CanvasTarget } from './renderers/common/CanvasTarget.js';
 export { default as BlendMode } from './renderers/common/BlendMode.js';
+export { default as GLSLNodeBuilder } from './renderers/webgl-fallback/nodes/GLSLNodeBuilder.js';
+export { default as BasicNodeLibrary } from './renderers/webgpu/nodes/BasicNodeLibrary.js';
 export { ClippingGroup } from './objects/ClippingGroup.js';
 export * from './nodes/Nodes.js';
 import * as TSL from './nodes/TSL.js';

+ 44 - 1
src/renderers/WebGLRenderer.js

@@ -292,6 +292,7 @@ class WebGLRenderer {
 		const _this = this;
 
 		let _isContextLost = false;
+		let _nodesHandler = null;
 
 		// internal state cache
 
@@ -1042,6 +1043,20 @@ class WebGLRenderer {
 
 		};
 
+		/**
+		 * Sets a compatibility node builder for rendering node materials with WebGLRenderer.
+		 * This enables using TSL (Three.js Shading Language) node materials to prepare
+		 * for migration to WebGPURenderer.
+		 *
+		 * @param {WebGLNodesHandler} nodesHandler - The node builder instance.
+		 */
+		this.setNodesHandler = function ( nodesHandler ) {
+
+			nodesHandler.setRenderer( this );
+			_nodesHandler = nodesHandler;
+
+		};
+
 		/**
 		 * Frees the GPU-related resources allocated by this instance. Call this
 		 * method whenever this instance is no longer used in your app.
@@ -1602,6 +1617,13 @@ class WebGLRenderer {
 
 			if ( _isContextLost === true ) return;
 
+			// update node builder if available
+			if ( _nodesHandler !== null ) {
+
+				_nodesHandler.renderStart( scene, camera );
+
+			}
+
 			// use internal render target for HalfFloatType color buffer (only when tone mapping is enabled)
 
 			const isXRPresenting = xr.enabled === true && xr.isPresenting === true;
@@ -1795,6 +1817,12 @@ class WebGLRenderer {
 
 			}
 
+			if ( _nodesHandler !== null ) {
+
+				_nodesHandler.renderEnd();
+
+			}
+
 		};
 
 		function projectObject( object, camera, groupOrder, sortObjects ) {
@@ -2171,6 +2199,13 @@ class WebGLRenderer {
 
 				parameters.uniforms = programCache.getUniforms( material );
 
+				// Use node builder for node materials if available
+				if ( _nodesHandler !== null && material.isNodeMaterial ) {
+
+					_nodesHandler.build( material, object, parameters );
+
+				}
+
 				material.onBeforeCompile( parameters, _this );
 
 				program = programCache.acquireProgram( parameters, programCacheKey );
@@ -2436,6 +2471,14 @@ class WebGLRenderer {
 
 				program = getProgram( material, scene, object );
 
+				// notify the node builder that the program has changed so uniforms and update nodes can
+				// be cached and triggered.
+				if ( _nodesHandler && material.isNodeMaterial ) {
+
+					_nodesHandler.onUpdateProgram( material, program, materialProperties );
+
+				}
+
 			}
 
 			let refreshProgram = false;
@@ -2665,7 +2708,7 @@ class WebGLRenderer {
 
 			// UBOs
 
-			if ( material.isShaderMaterial || material.isRawShaderMaterial ) {
+			if ( material.uniformsGroups !== undefined ) {
 
 				const groups = material.uniformsGroups;
 

+ 23 - 15
src/renderers/common/extras/PMREMGenerator.js

@@ -410,7 +410,7 @@ class PMREMGenerator {
 
 		this._renderer.setRenderTarget( _oldTarget, _oldActiveCubeFace, _oldActiveMipmapLevel );
 		outputTarget.scissorTest = false;
-		_setViewport( outputTarget, 0, 0, outputTarget.width, outputTarget.height );
+		this._setViewport( outputTarget, 0, 0, outputTarget.width, outputTarget.height );
 
 	}
 
@@ -566,7 +566,7 @@ class PMREMGenerator {
 
 			const size = this._cubeSize;
 
-			_setViewport( cubeUVRenderTarget, col * size, i > 2 ? size : 0, size, size );
+			this._setViewport( cubeUVRenderTarget, col * size, i > 2 ? size : 0, size, size );
 
 			renderer.render( scene, cubeCamera );
 
@@ -608,9 +608,7 @@ class PMREMGenerator {
 		mesh.material = material;
 
 		const size = this._cubeSize;
-
-		_setViewport( cubeUVRenderTarget, 0, 0, 3 * size, 2 * size );
-
+		this._setViewport( cubeUVRenderTarget, 0, 0, 3 * size, 2 * size );
 		renderer.setRenderTarget( cubeUVRenderTarget );
 		renderer.render( mesh, _flatCamera );
 
@@ -678,7 +676,7 @@ class PMREMGenerator {
 		ggxUniforms.roughness.value = adjustedRoughness;
 		ggxUniforms.mipInt.value = _lodMax - lodIn; // Sample from input LOD
 
-		_setViewport( pingPongRenderTarget, x, y, 3 * outputSize, 2 * outputSize );
+		this._setViewport( pingPongRenderTarget, x, y, 3 * outputSize, 2 * outputSize );
 		renderer.setRenderTarget( pingPongRenderTarget );
 		renderer.render( ggxMesh, _flatCamera );
 
@@ -688,7 +686,7 @@ class PMREMGenerator {
 		ggxUniforms.roughness.value = 0.0; // Direct copy
 		ggxUniforms.mipInt.value = _lodMax - lodOut; // Read from the level we just wrote
 
-		_setViewport( cubeUVRenderTarget, x, y, 3 * outputSize, 2 * outputSize );
+		this._setViewport( cubeUVRenderTarget, x, y, 3 * outputSize, 2 * outputSize );
 		renderer.setRenderTarget( cubeUVRenderTarget );
 		renderer.render( ggxMesh, _flatCamera );
 
@@ -814,12 +812,29 @@ class PMREMGenerator {
 		const x = 3 * outputSize * ( lodOut > _lodMax - LOD_MIN ? lodOut - _lodMax + LOD_MIN : 0 );
 		const y = 4 * ( this._cubeSize - outputSize );
 
-		_setViewport( targetOut, x, y, 3 * outputSize, 2 * outputSize );
+		this._setViewport( targetOut, x, y, 3 * outputSize, 2 * outputSize );
 		renderer.setRenderTarget( targetOut );
 		renderer.render( blurMesh, _flatCamera );
 
 	}
 
+	_setViewport( target, x, y, width, height ) {
+
+		if ( this._renderer.isWebGLRenderer ) {
+
+			target.viewport.set( x, target.height - height - y, width, height );
+			target.scissor.set( x, target.height - height - y, width, height );
+
+		} else {
+
+			target.viewport.set( x, y, width, height );
+			target.scissor.set( x, y, width, height );
+
+		}
+
+	}
+
+
 }
 
 function _createPlanes( lodMax ) {
@@ -925,13 +940,6 @@ function _createRenderTarget( width, height ) {
 
 }
 
-function _setViewport( target, x, y, width, height ) {
-
-	target.viewport.set( x, y, width, height );
-	target.scissor.set( x, y, width, height );
-
-}
-
 function _getMaterial( type ) {
 
 	const material = new NodeMaterial();

+ 5 - 1
src/renderers/webgl/WebGLMaterials.js

@@ -41,7 +41,11 @@ function WebGLMaterials( renderer, properties ) {
 
 	function refreshMaterialUniforms( uniforms, material, pixelRatio, height, transmissionRenderTarget ) {
 
-		if ( material.isMeshBasicMaterial ) {
+		if ( material.isNodeMaterial ) {
+
+			material.uniformsNeedUpdate = false;
+
+		} else if ( material.isMeshBasicMaterial ) {
 
 			refreshUniformsCommon( uniforms, material );
 

+ 19 - 0
src/renderers/webgl/WebGLUniformsGroups.js

@@ -141,6 +141,11 @@ function WebGLUniformsGroups( gl, info, capabilities, state ) {
 							uniform.__data[ 10 ] = value.elements[ 8 ];
 							uniform.__data[ 11 ] = 0;
 
+						} else if ( ArrayBuffer.isView( value ) ) {
+
+							// copy the buffer data using "set"
+							uniform.__data.set( new value.constructor( value.buffer, value.byteOffset, uniform.__data.length ) );
+
 						} else {
 
 							value.toArray( uniform.__data, arrayOffset );
@@ -176,6 +181,10 @@ function WebGLUniformsGroups( gl, info, capabilities, state ) {
 
 				cache[ indexString ] = value;
 
+			} else if ( ArrayBuffer.isView( value ) ) {
+
+				cache[ indexString ] = value.slice();
+
 			} else {
 
 				cache[ indexString ] = value.clone();
@@ -199,6 +208,11 @@ function WebGLUniformsGroups( gl, info, capabilities, state ) {
 
 				}
 
+			} else if ( ArrayBuffer.isView( value ) ) {
+
+				// always update the array buffers
+				return true;
+
 			} else {
 
 				if ( cachedObject.equals( value ) === false ) {
@@ -339,6 +353,11 @@ function WebGLUniformsGroups( gl, info, capabilities, state ) {
 
 			warn( 'WebGLRenderer: Texture samplers can not be part of an uniforms group.' );
 
+		} else if ( ArrayBuffer.isView( value ) ) {
+
+			info.boundary = 16;
+			info.storage = value.byteLength;
+
 		} else {
 
 			warn( 'WebGLRenderer: Unsupported uniform value type.', value );

粤ICP备19079148号