Skip to content

Commit

Permalink
WebGPURenderer: InstancedMesh Supports Instance Color and Morph Target (
Browse files Browse the repository at this point in the history
#27928)

* add instancing and some fix/feat

* update example for webgpu

* add example to files.json

* fix skinning webgpu

* cleanup example and pup

* up shadow

* add default normal instead
  • Loading branch information
RenaudRohlinger authored Mar 18, 2024
1 parent 83f7580 commit b0a6ea1
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 10 deletions.
3 changes: 2 additions & 1 deletion examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,8 @@
"webgpu_mirror",
"webgpu_multisampled_renderbuffers",
"webgpu_materials_texture_anisotropy",
"webgpu_storage_buffer"
"webgpu_storage_buffer",
"webgpu_instancing_morph"
],
"webaudio": [
"webaudio_orientation",
Expand Down
27 changes: 25 additions & 2 deletions examples/jsm/nodes/accessors/InstanceNode.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Node, { addNodeClass } from '../core/Node.js';
import { varyingProperty } from '../core/PropertyNode.js';
import { instancedBufferAttribute, instancedDynamicBufferAttribute } from './BufferAttributeNode.js';
import { normalLocal } from './NormalNode.js';
import { positionLocal } from './PositionNode.js';
import { nodeProxy, vec3, mat3, mat4 } from '../shadernode/ShaderNode.js';
import { DynamicDrawUsage, InstancedInterleavedBuffer } from 'three';
import { DynamicDrawUsage, InstancedInterleavedBuffer, InstancedBufferAttribute } from 'three';

class InstanceNode extends Node {

Expand All @@ -15,15 +16,18 @@ class InstanceNode extends Node {

this.instanceMatrixNode = null;

this.instanceColorNode = null;

}

setup( /*builder*/ ) {

let instanceMatrixNode = this.instanceMatrixNode;

const instanceMesh = this.instanceMesh;

if ( instanceMatrixNode === null ) {

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

Expand All @@ -43,6 +47,17 @@ class InstanceNode extends Node {

}

const instanceColorAttribute = instanceMesh.instanceColor;

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

const buffer = new InstancedBufferAttribute( instanceColorAttribute.array, 3 );
const bufferFn = instanceColorAttribute.usage === DynamicDrawUsage ? instancedDynamicBufferAttribute : instancedBufferAttribute;

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

}

// POSITION

const instancePosition = instanceMatrixNode.mul( positionLocal ).xyz;
Expand All @@ -60,6 +75,14 @@ class InstanceNode extends Node {
positionLocal.assign( instancePosition );
normalLocal.assign( instanceNormal );

// COLOR

if ( this.instanceColorNode !== null ) {

varyingProperty( 'vec3', 'vInstanceColor' ).assign( this.instanceColorNode );

}

}

}
Expand Down
16 changes: 13 additions & 3 deletions examples/jsm/nodes/accessors/MorphNode.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import Node, { addNodeClass } from '../core/Node.js';
import { NodeUpdateType } from '../core/constants.js';
import { nodeProxy, tslFn } from '../shadernode/ShaderNode.js';
import { float, nodeProxy, tslFn } from '../shadernode/ShaderNode.js';
import { uniform } from '../core/UniformNode.js';
import { reference } from './ReferenceNode.js';
import { positionLocal } from './PositionNode.js';
import { normalLocal } from './NormalNode.js';
import { textureLoad } from './TextureNode.js';
import { vertexIndex } from '../core/IndexNode.js';
import { instanceIndex, vertexIndex } from '../core/IndexNode.js';
import { ivec2, int } from '../shadernode/ShaderNode.js';
import { DataArrayTexture, Vector2, Vector4, FloatType } from 'three';
import { loop } from '../utils/LoopNode.js';
Expand Down Expand Up @@ -188,7 +188,17 @@ class MorphNode extends Node {

loop( morphTargetsCount, ( { i } ) => {

const influence = reference( 'morphTargetInfluences', 'float' ).element( i );
const influence = float( 0 ).toVar();

if ( this.mesh.isInstancedMesh === true && this.mesh.morphTexture !== null ) {

influence.assign( textureLoad( this.mesh.morphTexture, ivec2( int( i ).add( 1 ), int( instanceIndex ) ) ).r );

} else {

influence.assign( reference( 'morphTargetInfluences', 'float' ).element( i ).toVar() );

}

if ( hasMorphPosition === true ) {

Expand Down
14 changes: 12 additions & 2 deletions examples/jsm/nodes/accessors/NormalNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { property } from '../core/PropertyNode.js';
import { normalize } from '../math/MathNode.js';
import { cameraViewMatrix } from './CameraNode.js';
import { modelNormalMatrix } from './ModelNode.js';
import { nodeImmutable } from '../shadernode/ShaderNode.js';
import { nodeImmutable, vec3 } from '../shadernode/ShaderNode.js';

class NormalNode extends Node {

Expand Down Expand Up @@ -37,7 +37,17 @@ class NormalNode extends Node {

if ( scope === NormalNode.GEOMETRY ) {

outputNode = attribute( 'normal', 'vec3' );
const geometryAttribute = builder.hasGeometryAttribute( 'normal' );

if ( geometryAttribute === false ) {

outputNode = vec3( 0, 1, 0 );

} else {

outputNode = attribute( 'normal', 'vec3' );

}

} else if ( scope === NormalNode.LOCAL ) {

Expand Down
14 changes: 12 additions & 2 deletions examples/jsm/nodes/materials/NodeMaterial.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Material, ShaderMaterial, NoColorSpace, LinearSRGBColorSpace } from 'three';
import { getNodeChildren, getCacheKey } from '../core/NodeUtils.js';
import { attribute } from '../core/AttributeNode.js';
import { output, diffuseColor } from '../core/PropertyNode.js';
import { output, diffuseColor, varyingProperty } from '../core/PropertyNode.js';
import { materialAlphaTest, materialColor, materialOpacity, materialEmissive, materialNormal } from '../accessors/MaterialNode.js';
import { modelViewProjection } from '../accessors/ModelViewProjectionNode.js';
import { transformedNormalView } from '../accessors/NormalNode.js';
Expand Down Expand Up @@ -224,7 +224,7 @@ class NodeMaterial extends ShaderMaterial {

}

setupDiffuseColor( { geometry } ) {
setupDiffuseColor( { object, geometry } ) {

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

Expand All @@ -236,6 +236,16 @@ class NodeMaterial extends ShaderMaterial {

}

// Instanced colors

if ( object.instanceColor ) {

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

colorNode = instanceColor.mul( colorNode );

}

// COLOR

diffuseColor.assign( colorNode );
Expand Down
Binary file added examples/screenshots/webgpu_instancing_morph.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
195 changes: 195 additions & 0 deletions examples/webgpu_instancing_morph.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgpu - instancing - Morph Target Animations</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>
<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/",
"three/nodes": "./jsm/nodes/Nodes.js"
}
}
</script>

<script type="module">

import * as THREE from 'three';

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

import Stats from 'three/addons/libs/stats.module.js';

import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
import { MeshStandardNodeMaterial } from 'three/nodes';

let camera, scene, renderer, stats, mesh, mixer, dummy;

const offset = 5000;

const timeOffsets = new Float32Array( 1024 );

for ( let i = 0; i < 1024; i ++ ) {

timeOffsets[ i ] = Math.random() * 3;

}

const clock = new THREE.Clock( true );

init();

function init() {

camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 100, 10000 );

scene = new THREE.Scene();

scene.background = new THREE.Color( 0x99DDFF );

scene.fog = new THREE.Fog( 0x99DDFF, 5000, 10000 );


//

stats = new Stats();
document.body.appendChild( stats.dom );

const light = new THREE.DirectionalLight( 0xffffff, 1 );

light.position.set( 200, 1000, 50 );

light.shadow.mapSize.width = 2048;
light.shadow.mapSize.height = 2048;
light.castShadow = true;

light.shadow.camera.left = - 5000;
light.shadow.camera.right = 5000;
light.shadow.camera.top = 5000;
light.shadow.camera.bottom = - 5000;
light.shadow.camera.far = 2000;

light.shadow.bias = - 0.01;

light.shadow.camera.updateProjectionMatrix();

scene.add( light );

const hemi = new THREE.HemisphereLight( 0x99DDFF, 0x669933, 1 / 3 );

scene.add( hemi );

const ground = new THREE.Mesh(
new THREE.PlaneGeometry( 1000000, 1000000 ),
new THREE.MeshStandardMaterial( { color: 0x669933 } )
);

ground.rotation.x = - Math.PI / 2;

ground.receiveShadow = true;

scene.add( ground );

const loader = new GLTFLoader();

loader.load( 'models/gltf/Horse.glb', function ( glb ) {

dummy = glb.scene.children[ 0 ];

mesh = new THREE.InstancedMesh( dummy.geometry, new MeshStandardNodeMaterial( {
flatShading: true,
} ), 1024 );

mesh.castShadow = true;

for ( let x = 0, i = 0; x < 32; x ++ ) {

for ( let y = 0; y < 32; y ++ ) {

dummy.position.set( offset - 300 * x + 200 * Math.random(), 0, offset - 300 * y );

dummy.updateMatrix();

mesh.setMatrixAt( i, dummy.matrix );

mesh.setColorAt( i, new THREE.Color( `hsl(${Math.random() * 360}, 50%, 66%)` ) );

i ++;

}


}

scene.add( mesh );

mixer = new THREE.AnimationMixer( glb.scene );

const action = mixer.clipAction( glb.animations[ 0 ] );

action.play();

} );


// renderer

renderer = new WebGPURenderer( { antialias: true } );

renderer.setAnimationLoop( animate );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
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() {

const time = clock.getElapsedTime();

const r = 3000;
camera.position.set( Math.sin( time / 10 ) * r, 1500 + 1000 * Math.cos( time / 5 ), Math.cos( time / 10 ) * r );
camera.lookAt( 0, 0, 0 );

if ( mesh ) {

for ( let i = 0; i < 1024; i ++ ) {

mixer.setTime( time + timeOffsets[ i ] );

mesh.setMorphAt( i, dummy );

}

mesh.morphTexture.needsUpdate = true;

}

renderer.render( scene, camera );

stats.update();

}

</script>

</body>
</html>
1 change: 1 addition & 0 deletions test/e2e/puppeteer.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ const exceptionList = [
'webgpu_tsl_transpiler',
'webgpu_portal',
'webgpu_custom_fog',
'webgpu_instancing_morph',

// WebGPU idleTime and parseTime too low
'webgpu_compute_particles',
Expand Down

0 comments on commit b0a6ea1

Please sign in to comment.