webgpu_skinning_instancing_individual.html 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <title>three.js webgpu - skinning individual instancing</title>
  5. <meta charset="utf-8">
  6. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  7. <meta property="og:title" content="three.js webgpu - skinning instancing individual">
  8. <meta property="og:type" content="website">
  9. <meta property="og:url" content="https://threejs.org/examples/webgpu_skinning_instancing_individual.html">
  10. <meta property="og:image" content="https://threejs.org/examples/screenshots/webgpu_skinning_instancing_individual.jpg">
  11. <link type="text/css" rel="stylesheet" href="example.css">
  12. </head>
  13. <body>
  14. <div id="info">
  15. <a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>
  16. <div class="title-wrapper">
  17. <a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>Skinning Individual Instancing</span>
  18. </div>
  19. <small>
  20. Per-instance poses are computed once and reused by every render pass.
  21. </small>
  22. </div>
  23. <script type="importmap">
  24. {
  25. "imports": {
  26. "three": "../build/three.webgpu.js",
  27. "three/webgpu": "../build/three.webgpu.js",
  28. "three/tsl": "../build/three.tsl.js",
  29. "three/addons/": "./jsm/"
  30. }
  31. }
  32. </script>
  33. <script type="module">
  34. import * as THREE from 'three/webgpu';
  35. import { Fn, add, attributeArray, color, instanceIndex, screenUV, storage, transformNormal, transformNormalToView, uint, uniform, vec4, vertexIndex } from 'three/tsl';
  36. import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  37. import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
  38. import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
  39. import { Inspector } from 'three/addons/inspector/Inspector.js';
  40. import WebGPU from 'three/addons/capabilities/WebGPU.js';
  41. let camera, scene, renderer, controls;
  42. let mixer, timer, animatedObject, proportionBones, referenceMesh, footBones, groundFoot, dummy;
  43. let instanceMatrices, instanceMatricesNode, bellyWeightsNode;
  44. let computeSkinning;
  45. const skeletonStates = new Map();
  46. const timeOffsets = [];
  47. const variations = [];
  48. const leftFoot = new THREE.Vector3();
  49. const rightFoot = new THREE.Vector3();
  50. init();
  51. function createSourceVertexAttribute( geometry ) {
  52. const position = geometry.getAttribute( 'position' );
  53. const normal = geometry.getAttribute( 'normal' );
  54. const bellyPosition = position.clone();
  55. const data = new Float32Array( position.count * 12 );
  56. for ( let i = 0; i < position.count; i ++ ) {
  57. const amount = Math.max( 0, 1 - Math.abs( position.getY( i ) - 0.94 ) / 0.3 ) * 0.8;
  58. bellyPosition.setXYZ(
  59. i,
  60. position.getX( i ) * amount,
  61. 0,
  62. position.getZ( i ) * amount
  63. );
  64. const offset = i * 12;
  65. data[ offset + 0 ] = position.getX( i );
  66. data[ offset + 1 ] = position.getY( i );
  67. data[ offset + 2 ] = position.getZ( i );
  68. data[ offset + 4 ] = normal.getX( i );
  69. data[ offset + 5 ] = normal.getY( i );
  70. data[ offset + 6 ] = normal.getZ( i );
  71. data[ offset + 8 ] = bellyPosition.getX( i );
  72. data[ offset + 9 ] = bellyPosition.getY( i );
  73. data[ offset + 10 ] = bellyPosition.getZ( i );
  74. }
  75. return new THREE.StorageBufferAttribute( data, 4 );
  76. }
  77. function getSkeletonState( skeleton, instanceCount ) {
  78. let state = skeletonStates.get( skeleton );
  79. if ( state === undefined ) {
  80. const boneCount = skeleton.bones.length;
  81. const boneMatrices = new THREE.StorageBufferAttribute( instanceCount * boneCount, 16 );
  82. state = {
  83. skeleton,
  84. boneCount,
  85. boneMatrices,
  86. boneMatricesNode: storage( boneMatrices, 'mat4', boneMatrices.count ).toReadOnly()
  87. };
  88. skeletonStates.set( skeleton, state );
  89. }
  90. return state;
  91. }
  92. function createComputedMesh( source, instanceCount ) {
  93. const geometry = source.geometry.clone();
  94. const material = source.material.clone();
  95. const vertexCount = geometry.getAttribute( 'position' ).count;
  96. const skeletonState = getSkeletonState( source.skeleton, instanceCount );
  97. const sourceVertices = storage( createSourceVertexAttribute( geometry ), 'vec4', vertexCount * 3 ).toReadOnly();
  98. const skinIndices = storage( new THREE.StorageBufferAttribute( new Uint32Array( geometry.getAttribute( 'skinIndex' ).array ), 4 ), 'uvec4', vertexCount ).toReadOnly();
  99. const skinWeights = storage( new THREE.StorageBufferAttribute( geometry.getAttribute( 'skinWeight' ).array, 4 ), 'vec4', vertexCount ).toReadOnly();
  100. const bindMatrix = uniform( source.bindMatrix, 'mat4' );
  101. const bindMatrixInverse = uniform( source.bindMatrixInverse, 'mat4' );
  102. const vertices = attributeArray( instanceCount * vertexCount * 2, 'vec4' );
  103. computeSkinning = Fn( () => {
  104. const sourceVertex = instanceIndex.mod( uint( vertexCount ) );
  105. const meshInstance = instanceIndex.div( uint( vertexCount ) );
  106. const sourceOffset = sourceVertex.mul( uint( 3 ) );
  107. const targetOffset = instanceIndex.mul( uint( 2 ) );
  108. const boneOffset = meshInstance.mul( uint( skeletonState.boneCount ) );
  109. const skinIndex = skinIndices.element( sourceVertex );
  110. const skinWeight = skinWeights.element( sourceVertex );
  111. const morphPosition = sourceVertices.element( sourceOffset ).xyz.add( sourceVertices.element( sourceOffset.add( uint( 2 ) ) ).xyz.mul( bellyWeightsNode.element( meshInstance ) ) );
  112. const skinVertex = bindMatrix.mul( morphPosition );
  113. const boneMatX = skeletonState.boneMatricesNode.element( boneOffset.add( skinIndex.x ) );
  114. const boneMatY = skeletonState.boneMatricesNode.element( boneOffset.add( skinIndex.y ) );
  115. const boneMatZ = skeletonState.boneMatricesNode.element( boneOffset.add( skinIndex.z ) );
  116. const boneMatW = skeletonState.boneMatricesNode.element( boneOffset.add( skinIndex.w ) );
  117. const skinMatrix = add(
  118. skinWeight.x.mul( boneMatX ),
  119. skinWeight.y.mul( boneMatY ),
  120. skinWeight.z.mul( boneMatZ ),
  121. skinWeight.w.mul( boneMatW )
  122. );
  123. const skinPosition = bindMatrixInverse.mul( add(
  124. boneMatX.mul( skinWeight.x ).mul( skinVertex ),
  125. boneMatY.mul( skinWeight.y ).mul( skinVertex ),
  126. boneMatZ.mul( skinWeight.z ).mul( skinVertex ),
  127. boneMatW.mul( skinWeight.w ).mul( skinVertex )
  128. ) ).xyz;
  129. const skinNormal = bindMatrixInverse.mul( skinMatrix ).mul( bindMatrix ).transformDirection( sourceVertices.element( sourceOffset.add( uint( 1 ) ) ).xyz ).xyz;
  130. const instanceMatrix = instanceMatricesNode.element( meshInstance );
  131. vertices.element( targetOffset ).assign( vec4( instanceMatrix.mul( skinPosition ).xyz, 1 ) );
  132. vertices.element( targetOffset.add( uint( 1 ) ) ).assign( vec4( transformNormal( skinNormal, instanceMatrix ), 0 ) );
  133. } )().compute( instanceCount * vertexCount ).setName( 'Compute Instanced Skinning' );
  134. const meshVertex = instanceIndex.mul( uint( vertexCount ) ).add( vertexIndex ).mul( uint( 2 ) );
  135. material.positionNode = vertices.element( meshVertex ).xyz;
  136. material.normalNode = transformNormalToView( vertices.element( meshVertex.add( uint( 1 ) ) ).xyz ).toVarying();
  137. const mesh = new THREE.Mesh( geometry, material );
  138. mesh.count = instanceCount;
  139. mesh.castShadow = true;
  140. mesh.frustumCulled = false;
  141. source.parent.add( mesh );
  142. source.visible = false;
  143. }
  144. async function init() {
  145. if ( WebGPU.isAvailable() === false ) {
  146. document.body.appendChild( WebGPU.getErrorMessage() );
  147. throw new Error( 'No WebGPU support' );
  148. }
  149. camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.01, 60 );
  150. camera.position.set( 4.6, 3.3, 6.2 );
  151. scene = new THREE.Scene();
  152. scene.backgroundNode = screenUV.y.mix( color( 0x8f989c ), color( 0xe7eaeb ) );
  153. timer = new THREE.Timer();
  154. timer.connect( document );
  155. scene.add( new THREE.HemisphereLight( 0xffffff, 0x939b9e, 1.2 ) );
  156. const directionalLight = new THREE.DirectionalLight( 0xfffbf4, 2.8 );
  157. directionalLight.position.set( - 4, 10, 8 );
  158. directionalLight.castShadow = true;
  159. directionalLight.shadow.mapSize.set( 2048, 2048 );
  160. directionalLight.shadow.camera.left = - 5;
  161. directionalLight.shadow.camera.right = 5;
  162. directionalLight.shadow.camera.top = 5;
  163. directionalLight.shadow.camera.bottom = - 5;
  164. directionalLight.shadow.camera.near = 0.1;
  165. directionalLight.shadow.camera.far = 30;
  166. directionalLight.shadow.camera.updateProjectionMatrix();
  167. scene.add( directionalLight );
  168. const rimLight = new THREE.DirectionalLight( 0xe8f4ff, 0.85 );
  169. rimLight.position.set( 7, 5, - 5 );
  170. scene.add( rimLight );
  171. const ground = new THREE.Mesh(
  172. new THREE.PlaneGeometry( 400, 400 ),
  173. new THREE.ShadowMaterial( { color: 0x5d6568, opacity: 0.3 } )
  174. );
  175. ground.rotation.x = - Math.PI / 2;
  176. ground.receiveShadow = true;
  177. scene.add( ground );
  178. new GLTFLoader().load( 'models/gltf/Michelle.glb', function ( gltf ) {
  179. const object = gltf.scene;
  180. const instanceCount = 30;
  181. const duration = gltf.animations[ 0 ].duration;
  182. const skinnedMeshes = [];
  183. dummy = new THREE.Object3D();
  184. animatedObject = object;
  185. mixer = new THREE.AnimationMixer( object );
  186. mixer.clipAction( gltf.animations[ 0 ] ).play();
  187. object.traverse( ( child ) => {
  188. if ( child.isSkinnedMesh === true ) skinnedMeshes.push( child );
  189. } );
  190. referenceMesh = skinnedMeshes[ 0 ];
  191. const skeleton = referenceMesh.skeleton;
  192. const getBone = ( name ) => skeleton.bones.find( ( bone ) => bone.name.endsWith( name ) );
  193. const proportionTargets = [
  194. getBone( 'Head' ), getBone( 'Spine' ), getBone( 'Spine2' ),
  195. getBone( 'LeftArm' ), getBone( 'RightArm' ),
  196. getBone( 'LeftForeArm' ), getBone( 'RightForeArm' ),
  197. getBone( 'LeftUpLeg' ), getBone( 'RightUpLeg' ),
  198. getBone( 'LeftLeg' ), getBone( 'RightLeg' )
  199. ];
  200. proportionBones = {
  201. head: proportionTargets[ 0 ],
  202. belly: proportionTargets[ 1 ],
  203. chest: proportionTargets[ 2 ],
  204. arms: proportionTargets.slice( 3, 7 ),
  205. legs: proportionTargets.slice( 7 ),
  206. baseScales: proportionTargets.map( ( bone ) => bone.scale.clone() )
  207. };
  208. footBones = [ getBone( 'LeftFoot' ), getBone( 'RightFoot' ) ];
  209. const bodyTypes = [
  210. { width: 0.92, height: 1.09, depth: 0.92, belly: 0, headScale: 0.96, bellyWidth: 0.98, chestWidth: 0.98, armLength: 1.03, legLength: 1.04 },
  211. { width: 1.0, height: 1.05, depth: 0.99, belly: 0, headScale: 0.98, bellyWidth: 1.0, chestWidth: 1.03, armLength: 1.02, legLength: 1.02 },
  212. { width: 1.0, height: 1.0, depth: 1.0, belly: 0, headScale: 1.0, bellyWidth: 1.0, chestWidth: 1.0, armLength: 1.0, legLength: 1.0 },
  213. { width: 1.08, height: 0.9, depth: 1.1, belly: 0.75, headScale: 1.06, bellyWidth: 1.12, chestWidth: 1.0, armLength: 0.9, legLength: 0.9 },
  214. { width: 1.2, height: 0.8, depth: 1.22, belly: 1.6, headScale: 1.12, bellyWidth: 1.24, chestWidth: 1.04, armLength: 0.72, legLength: 0.7 }
  215. ];
  216. const bellyWeights = new THREE.StorageBufferAttribute( instanceCount, 1 );
  217. instanceMatrices = new THREE.StorageBufferAttribute( instanceCount, 16 );
  218. bellyWeightsNode = storage( bellyWeights, 'float', instanceCount ).toReadOnly();
  219. instanceMatricesNode = storage( instanceMatrices, 'mat4', instanceCount ).toReadOnly();
  220. for ( let i = 0; i < instanceCount; i ++ ) {
  221. const bodyType = bodyTypes[ i % bodyTypes.length ];
  222. const isChild = i % 2 === 0;
  223. const size = isChild ? ( i % 4 === 0 ? 0.68 : 0.78 ) : 1;
  224. const innerRing = i < 10;
  225. const ringIndex = innerRing ? i : i - 10;
  226. const ringCount = innerRing ? 10 : 20;
  227. const angle = ringIndex / ringCount * Math.PI * 2;
  228. const radius = innerRing ? 175 : 340;
  229. timeOffsets.push( duration * i / instanceCount );
  230. variations.push( {
  231. ... bodyType,
  232. size,
  233. x: Math.sin( angle ) * radius,
  234. y: Math.cos( angle ) * radius
  235. } );
  236. bellyWeights.array[ i ] = bodyType.belly;
  237. }
  238. bellyWeights.needsUpdate = true;
  239. animatedObject.updateMatrixWorld( true );
  240. skeleton.update();
  241. groundFoot = getFootCoordinate();
  242. for ( const child of skinnedMeshes ) createComputedMesh( child, instanceCount );
  243. scene.add( object );
  244. } );
  245. renderer = new THREE.WebGPURenderer( { antialias: true } );
  246. renderer.setPixelRatio( window.devicePixelRatio );
  247. renderer.setSize( window.innerWidth, window.innerHeight );
  248. renderer.setAnimationLoop( animate );
  249. renderer.shadowMap.enabled = true;
  250. renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  251. renderer.toneMapping = THREE.NeutralToneMapping;
  252. renderer.toneMappingExposure = 0.96;
  253. renderer.inspector = new Inspector();
  254. document.body.appendChild( renderer.domElement );
  255. await renderer.init();
  256. scene.environment = new THREE.PMREMGenerator( renderer ).fromScene( new RoomEnvironment(), 0.04 ).texture;
  257. controls = new OrbitControls( camera, renderer.domElement );
  258. controls.target.set( 0, 0.75, 0 );
  259. controls.enableDamping = true;
  260. controls.minDistance = 3;
  261. controls.maxDistance = 25;
  262. controls.maxPolarAngle = Math.PI / 2;
  263. controls.update();
  264. window.addEventListener( 'resize', onWindowResize );
  265. }
  266. function onWindowResize() {
  267. camera.aspect = window.innerWidth / window.innerHeight;
  268. camera.updateProjectionMatrix();
  269. renderer.setSize( window.innerWidth, window.innerHeight );
  270. }
  271. function applyProportions( variation ) {
  272. const { head, belly, chest, arms, legs, baseScales } = proportionBones;
  273. const bones = [ head, belly, chest, ... arms, ... legs ];
  274. for ( let i = 0; i < bones.length; i ++ ) bones[ i ].scale.copy( baseScales[ i ] );
  275. head.scale.multiplyScalar( variation.headScale );
  276. belly.scale.x *= variation.bellyWidth;
  277. belly.scale.z *= variation.bellyWidth;
  278. chest.scale.x *= variation.chestWidth;
  279. chest.scale.z *= variation.chestWidth;
  280. for ( const bone of arms ) bone.scale.y *= variation.armLength;
  281. for ( const bone of legs ) bone.scale.y *= variation.legLength;
  282. }
  283. function getFootCoordinate() {
  284. footBones[ 0 ].getWorldPosition( leftFoot );
  285. footBones[ 1 ].getWorldPosition( rightFoot );
  286. referenceMesh.worldToLocal( leftFoot );
  287. referenceMesh.worldToLocal( rightFoot );
  288. return Math.max( leftFoot.z, rightFoot.z );
  289. }
  290. function updateInstanceMatrix( index, variation ) {
  291. const foot = getFootCoordinate();
  292. dummy.position.set( variation.x, variation.y, groundFoot - foot * variation.height * variation.size );
  293. dummy.scale.set( variation.width, variation.depth, variation.height ).multiplyScalar( variation.size );
  294. dummy.updateMatrix();
  295. dummy.matrix.toArray( instanceMatrices.array, index * 16 );
  296. }
  297. function animate() {
  298. timer.update();
  299. controls.update();
  300. if ( mixer ) {
  301. const elapsed = timer.getElapsed();
  302. for ( let i = 0; i < timeOffsets.length; i ++ ) {
  303. mixer.setTime( elapsed + timeOffsets[ i ] );
  304. applyProportions( variations[ i ] );
  305. animatedObject.updateMatrixWorld( true );
  306. for ( const state of skeletonStates.values() ) {
  307. state.skeleton.update();
  308. state.boneMatrices.array.set( state.skeleton.boneMatrices, i * state.boneCount * 16 );
  309. }
  310. updateInstanceMatrix( i, variations[ i ] );
  311. }
  312. for ( const state of skeletonStates.values() ) state.boneMatrices.needsUpdate = true;
  313. instanceMatrices.needsUpdate = true;
  314. renderer.compute( computeSkinning );
  315. }
  316. renderer.render( scene, camera );
  317. }
  318. </script>
  319. </body>
  320. </html>
粤ICP备19079148号