文章目录

  • 1. 声明一个实例必要的属性`instanceMatrix`同级别的属性
  • 2. 在设置位置矩阵的时候填充这个数组
  • 3. 在shader中获取当前的索引
  • 4. 增加uniform
  • 5. 对比当前着色的实例是否是选中的实例
  • 6. 如果是选中的实例
  • 7. 影响片元着色器透明度参数
  • 其他 - 渐入渐出动画
  • 8.源码


写在前面
本文环境是 原生js 没使用框架
因为目前r167节点材质系统还不太稳定试了几个打包工具对有些特性支持不好 遂不在框架中写代码
并且直接引用 three.webgpu.js文件 更方便更改源代码 插入自己的元素 也是本文的实现方式

官方instances案例

Three.js & WebGPU 节点材质系统 控制instances的某个实例单独的透明度,颜色等属性_WebGPU


实现效果如图 第二个实例透明度为0.1 其他的为1

Three.js & WebGPU 节点材质系统 控制instances的某个实例单独的透明度,颜色等属性_html_02

实现思路:

1. 声明一个实例必要的属性instanceMatrix同级别的属性
child.instanceIndex = new THREE.InstancedBufferAttribute(
	new Float32Array(实例数量),
	1
);
2. 在设置位置矩阵的时候填充这个数组
for (let i = 0; i < 实例数量; i++) {
	//,,,
	child.instanceIndex.array[i] = i ;
}
3. 在shader中获取当前的索引

修改InstanceNode的源码的setup函数

if(instanceMesh.instanceIndex){

	const indexBuffer = new InstancedBufferAttribute( instanceMesh.instanceIndex.array, 1 );
	
	const _index = instancedBufferAttribute( indexBuffer ) 
}

_index就是当前着色的实例索引

4. 增加uniform
// 提供uniform
// 选中的实例索引
child.selectInstanceIndex = uniform(1, "float");
// 选中的实例索引的透明度
child.selectInstanceIndexOpacity = uniform(0.1, "float");
5. 对比当前着色的实例是否是选中的实例
if(instanceMesh.instanceIndex){

	const indexBuffer = new InstancedBufferAttribute( instanceMesh.instanceIndex.array, 1 );
	
	const _index = instancedBufferAttribute( indexBuffer ) 
	
	If(_index.equal(instanceMesh.selectInstanceIndex),() => {
		//...			
	})
}
6. 如果是选中的实例

加入一个varying变量vInstanceIndexOpacity影响选中的实例的透明度(也可以影响其他材质参数 这里以透明度为例)

if(instanceMesh.instanceIndex){

	const indexBuffer = new InstancedBufferAttribute( instanceMesh.instanceIndex.array, 1 );
	
	const _index = instancedBufferAttribute( indexBuffer ) 
	
	If(_index.equal(instanceMesh.selectInstanceIndex),() => {
+		varyingProperty( 'float', 'vInstanceIndexOpacity' ).assign(instanceMesh.selectInstanceIndexOpacity );
	})
}
7. 影响片元着色器透明度参数

NodeMaterial对象的setupDiffuseColor方法中将透明度乘以vInstanceIndexOpacity的值或者直接设置为vInstanceIndexOpacity的值

const vInstanceIndexOpacity = varyingProperty( 'float', 'vInstanceIndexOpacity' ); 

// OPACITY

const opacityNode = this.opacityNode ? float( this.opacityNode ) : materialOpacity;

diffuseColor.a.assign( diffuseColor.a.mul( opacityNode ).mul(vInstanceIndexOpacity) );

如此便可通过更改uniform来决定某个实例的透明度了
以此思路其他材质属性也均可单独指定

其他 - 渐入渐出动画

如果想让透明度的值 自动变化 可以如下

const oscNode = abs(oscSine(timerLocal(0.1)));
// 选中的实例索引的透明度
- child.selectInstanceIndexOpacity = uniform(0.1, "float");
+ child.selectInstanceIndexOpacity = oscNode;

ocsNode的值就是时间放慢10倍并且使用sin函数约束值[-1,1 ]再使用abs取绝对值 使之在[0-1-0]之间循环
这样渐入渐出的动画就巧妙的完成了 这也是 节点材质系统的优越性和趣味性的体现

8.源码

html

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgpu - skinning instancing</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" />
	</head>
	<body>
		<div id="info">
			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a>
			webgpu - skinning instancing
		</div>

		<script type="importmap">
			{
				"imports": {
					"three": "../../build/three.webgpu.js",
					"three/tsl": "../../build/three.webgpu.js",
					"three/addons/": "../jsm/"
				}
			}
		</script>

		<script type="module">
			import * as THREE from "three";
			import {
				pass,
				mix,
				range,
				color,
				oscSine,
				timerLocal,
				texture,
				TextureNode,
				normalLocal,
				min,
				max,
				abs,
				uniform
			} from "three/tsl";

			import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
			import { OrbitControls } from "three/addons/controls/OrbitControls.js";
			import { RectAreaLightHelper } from "three/addons/helpers/RectAreaLightHelper.js";
			import { RectAreaLightTexturesLib } from "three/addons/lights/RectAreaLightTexturesLib.js";

			let camera, scene, renderer, controls;
			let postProcessing;

			let mixer, clock;

			init();

			function init() {
				THREE.RectAreaLightNode.setLTC(RectAreaLightTexturesLib.init());

				camera = new THREE.PerspectiveCamera(
					50,
					window.innerWidth / window.innerHeight,
					0.01,
					40
				);
				// camera.position.set( 1, 2, 3 );
				camera.position.set(0, 0, 0);

				scene = new THREE.Scene();
				scene.add(new THREE.AxesHelper(1));
				camera.lookAt(0, 1, 0);

				clock = new THREE.Clock();

				// lights

				const centerLight = new THREE.PointLight(0xff9900, 2, 100);
				centerLight.position.y = 4.5;
				centerLight.power = 400;
				// scene.add(centerLight);

				const cameraLight = new THREE.PointLight(0xffffff, 1, 100);
				cameraLight.power = 400;
				cameraLight.position.set(0, 2, 3);
				// camera.add(cameraLight);
				// scene.add(camera);
				// scene.add(cameraLight);

				const rectLight1 = new THREE.RectAreaLight(0xffffff, 10, 10, 0.5);
				rectLight1.position.set(0, 2, 0);
				rectLight1.lookAt(0, -1, 0);
				scene.add(rectLight1);
				{
					const rectLight1 = new THREE.RectAreaLight(0xffffff, 10, 10, 0.1);
					rectLight1.position.set(0, 0, 2);
					rectLight1.lookAt(0, 0, 0);
					scene.add(rectLight1);
				}
				scene.add(new RectAreaLightHelper(rectLight1));

				const thickness = 10;
				const geometry = new THREE.BoxGeometry(100, 2, thickness);
				geometry.translate(0, 0, -thickness / 2);
				geometry.rotateX(-Math.PI / 2);

				const plane = new THREE.Mesh(
					geometry,
					new THREE.MeshStandardMaterial({
						color: 0x000000,
						roughness: 1,
						metalness: 0.6,
					})
				);
				scene.add(plane);

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

					const instanceCount = 3;
					const dummy = new THREE.Object3D();

					object.traverse((child) => {
						if (child.isMesh) {
							// const oscNode = max(0,oscSine(timerLocal(0.1)));
							const oscNode = abs(oscSine(timerLocal(0.1)));
							// const oscNode = oscSine(timerLocal(0.1));

							const randomColors = range(
								new THREE.Color(0x0000),
								new THREE.Color(0xffffff)
							);

							const randomMetalness = range(0, 1);
							const prevMap = child.material.map;
							child.material = new THREE.MeshStandardNodeMaterial({
								transparent: true,
							});

							// child.material.onBeforeCompile = (shader) => {
							// 	console.log("onBeforeCompile:", shader);
							// };

							// roughnessNode是变化的 roughness是固定的
							child.material.roughnessNode = oscNode;

							child.material.metalnessNode =
								0.5 || mix(0.0, randomMetalness, oscNode);

							child.material.colorNode = mix(
								texture(prevMap),
								randomColors,
								oscNode
							);

							child.isInstancedMesh = true;
							child.instanceMatrix = new THREE.InstancedBufferAttribute(
								new Float32Array(instanceCount * 16),
								16
							);
							child.instanceIndex = new THREE.InstancedBufferAttribute(
								new Float32Array(instanceCount),
								1
							);

							// 提供uniform
							// 选中的实例索引
							child.selectInstanceIndex = uniform(1, "float");
							// 选中的实例索引的透明度
							child.selectInstanceIndexOpacity = uniform(0.1, "float");
							
							child.count = instanceCount;

							for (let i = 0; i < instanceCount; i++) {
								dummy.position.x = i * 70;

								dummy.position.y = Math.floor(i / 5) * -200;

								dummy.updateMatrix();

								dummy.matrix.toArray(child.instanceMatrix.array, i * 16);

								child.instanceIndex.array[i] = i ;
							}
							// child.instanceIndex.array[0] = 0 ;
							// child.instanceIndex.array[1] = 1 ;
							// child.instanceIndex.array[2] = 5 ;
						}
					});

					scene.add(object);
				});

				// renderer

				renderer = new THREE.WebGPURenderer({ antialias: true });
				renderer.setPixelRatio(window.devicePixelRatio);
				renderer.setSize(window.innerWidth, window.innerHeight);
				renderer.setAnimationLoop(animate);
				document.body.appendChild(renderer.domElement);

				controls = new OrbitControls(camera, renderer.domElement);

				controls.target.set(0, 1, 0);
				controls.object.position.set(0, 1, 4);

				// post processing

				const scenePass = pass(scene, camera);
				const scenePassColor = scenePass.getTextureNode();
				const scenePassDepth = scenePass
					.getLinearDepthNode()
					.remapClamp(0.15, 0.3);

				const scenePassColorBlurred = scenePassColor.gaussianBlur();
				scenePassColorBlurred.directionNode = scenePassDepth;

				// postProcessing = new THREE.PostProcessing(renderer);
				// postProcessing.outputNode = scenePassColorBlurred;

				// events

				window.addEventListener("resize", onWindowResize);
			}

			function onWindowResize() {
				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize(window.innerWidth, window.innerHeight);
			}

			function animate() {
				const delta = clock.getDelta();

				if (mixer) mixer.update(delta);

				// postProcessing.render();
				renderer.render(scene, camera);
			}
		</script>
	</body>
</html>

两个three模块核心函数修改后的代码
NodeMaterial.setupDiffuseColor

setupDiffuseColor( { object, geometry } ) {

		let colorNode = this.colorNode ? vec4( this.colorNode ) : materialColor;

		// VERTEX COLORS

		if ( this.vertexColors === true && geometry.hasAttribute( 'color' ) ) {

			colorNode = vec4( colorNode.xyz.mul( attribute( 'color', 'vec3' ) ), colorNode.a );

		}

		// Instanced colors

		if ( object.instanceColor ) {

			const instanceColor = varyingProperty( 'vec3', 'vInstanceColor' );

			colorNode = instanceColor.mul( colorNode );
			
		}

		const vInstanceIndexOpacity = varyingProperty( 'float', 'vInstanceIndexOpacity' ); 

		// COLOR

		diffuseColor.assign( colorNode );

		// OPACITY

		const opacityNode = this.opacityNode ? float( this.opacityNode ) : materialOpacity;
		diffuseColor.a.assign( diffuseColor.a.mul( opacityNode ).mul(vInstanceIndexOpacity) );

		// ALPHA TEST

		if ( this.alphaTestNode !== null || this.alphaTest > 0 ) {

			const alphaTestNode = this.alphaTestNode !== null ? float( this.alphaTestNode ) : materialAlphaTest;

			diffuseColor.a.lessThanEqual( alphaTestNode ).discard();

		}

		if ( this.transparent === false && this.blending === NormalBlending && this.alphaToCoverage === false ) {

			diffuseColor.a.assign( 1.0 );

		}

	}

InstanceNode.setup

setup( /*builder*/ ) {

		let instanceMatrixNode = this.instanceMatrixNode;
		let instanceColorNode = this.instanceColorNode;
		let instanceIndexNode;
		
		const instanceMesh = this.instanceMesh;
		

		if ( instanceMatrixNode === null ) {

			const instanceAttribute = instanceMesh.instanceMatrix;

			// Both WebGPU and WebGL backends have UBO max limited to 64kb. Matrix count number bigger than 1000 ( 16 * 4 * 1000 = 64kb ) will fallback to attribute.

			if ( instanceMesh.count <= 1000 ) {

				instanceMatrixNode = buffer( instanceAttribute.array, 'mat4', instanceMesh.count ).element( instanceIndex );
				console.log('instanceMatrixNode:',instanceMatrixNode)

			} else {

				const buffer = new InstancedInterleavedBuffer( instanceAttribute.array, 16, 1 );

				this.buffer = buffer;

				const bufferFn = instanceAttribute.usage === DynamicDrawUsage ? instancedDynamicBufferAttribute : instancedBufferAttribute;

				const instanceBuffers = [
					// F.Signature -> bufferAttribute( array, type, stride, offset )
					bufferFn( buffer, 'vec4', 16, 0 ),
					bufferFn( buffer, 'vec4', 16, 4 ),
					bufferFn( buffer, 'vec4', 16, 8 ),
					bufferFn( buffer, 'vec4', 16, 12 )
				];

				instanceMatrixNode = mat4( ...instanceBuffers );

			}

			this.instanceMatrixNode = instanceMatrixNode;

			if( instanceMesh.instanceIndex ){

				const insertInstanceIndex = instanceMesh.instanceIndex;
				
				// instanceIndexNode = buffer(insertInstanceIndex.array, "float", instanceMesh.count).element(instanceIndex);
				// console.log("插入实例索引:",instanceIndexNode)
			}
		}

		const instanceColorAttribute = instanceMesh.instanceColor;

		if ( instanceColorAttribute && instanceColorNode === null ) {

			const buffer = new InstancedBufferAttribute( instanceColorAttribute.array, 3 );

			const bufferFn = instanceColorAttribute.usage === DynamicDrawUsage ? instancedDynamicBufferAttribute : instancedBufferAttribute;

			this.bufferColor = buffer;

			instanceColorNode = vec3( bufferFn( buffer, 'vec3', 3, 0 ) );

			this.instanceColorNode = instanceColorNode;

		}

		// POSITION

		const instancePosition = instanceMatrixNode.mul( positionLocal ).xyz;

		// NORMAL

		const m = mat3( instanceMatrixNode );

		const transformedNormal = normalLocal.div( vec3( m[ 0 ].dot( m[ 0 ] ), m[ 1 ].dot( m[ 1 ] ), m[ 2 ].dot( m[ 2 ] ) ) );

		const instanceNormal = m.mul( transformedNormal ).xyz;

		// ASSIGNS

		positionLocal.assign( instancePosition );
		normalLocal.assign( instanceNormal );

		// COLOR

		if ( this.instanceColorNode !== null ) {

			varyingProperty( 'vec3', 'vInstanceColor' ).assign( this.instanceColorNode );
			
		}
		
		if(instanceMesh.instanceIndex){

			const indexBuffer = new InstancedBufferAttribute( instanceMesh.instanceIndex.array, 1 );
			
			const _index = instancedBufferAttribute( indexBuffer ) 

			// 当前的索引
			varyingProperty( 'float', 'vInstanceIndexOpacity' ).assign( 1 );
			// 当前index是uniform selectInstanceIndex 的实例
			If(_index.equal(instanceMesh.selectInstanceIndex),() => {
				varyingProperty( 'float', 'vInstanceIndexOpacity' ).assign( instanceMesh.selectInstanceIndexOpacity );
			})

		}


	}