Three.js用SkinedMesh创建蒙皮 (1)

怼烎@ 2023-05-30 09:09 151阅读 0赞

学习交流欢迎加群:789723098,博主会将一些demo整理共享

蒙皮(skin)是3D动画中常用的一种为模型添加骨骼的技术,由于骨骼与模型是相互独立的,为了让骨骼驱动模型性运动,把模型绑定到骨骼上的技术称之为蒙皮。蒙皮被广泛应用于3D动画制作或者3D游戏中的角色对象,很多模型制作软件都有带这一功能,例如3Dmax和blender,而由它们创建出来的动画模型利用unity这样的游戏引擎,可以用于各种各样的客户端游戏和动画,这已经有一套相当成熟的体系,而对于Web端来说,复现3D建模软件创建的模型动画也是可行的,可以利用相关的图形引擎(例如Three.js和babylon.js)的模型对应的Loader加载,并将动画信息解析出来,然后执行动画即可。那还有没有一种可能就是,在web端直接使用js编程实现蒙皮,让模型动起来?答案是可以的,Three.js提供了这样一种功能,THREE.SkinedMesh,用于创建蒙皮网格几何,然后通过骨架绑定使几何对象实现动画的效果,这里先来看一下利SkinedMesh做出来的demo的效果:

20191108212635312.gif

下面是这个demo的代码:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. <style>
  7. body {
  8. margin: 0;
  9. overflow: hidden;
  10. }
  11. </style>
  12. </head>
  13. <body>
  14. <script type="module">
  15. import {
  16. Bone,
  17. Color,
  18. CylinderBufferGeometry,
  19. DoubleSide,
  20. Float32BufferAttribute,
  21. MeshPhongMaterial,
  22. PerspectiveCamera,
  23. PointLight,
  24. Scene,
  25. SkinnedMesh,
  26. Skeleton,
  27. SkeletonHelper,
  28. Vector3,
  29. Uint16BufferAttribute,
  30. WebGLRenderer
  31. } from "../build/three.module.js";
  32. import { GUI } from './../examples/jsm/libs/dat.gui.module.js';
  33. import Stats from './../examples/jsm/libs/stats.module.js';
  34. import { OrbitControls } from "../examples/jsm/controls/OrbitControls.js";
  35. let gui, stats, scene, camera, renderer, orbit, lights, mesh, bones, skeletonHelper;
  36. let state = {
  37. animateBones: false,
  38. wireframe: false
  39. };
  40. // 初始化场景
  41. function initScene() {
  42. gui = new GUI();
  43. stats = new Stats();
  44. document.body.appendChild( stats.domElement );
  45. scene = new Scene();
  46. scene.background = new Color( 0x444444 );
  47. camera = new PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 200 );
  48. camera.position.z = 30;
  49. camera.position.y = 30;
  50. renderer = new WebGLRenderer( { antialias: true } );
  51. renderer.setPixelRatio( window.devicePixelRatio );
  52. renderer.setSize( window.innerWidth, window.innerHeight );
  53. document.body.appendChild( renderer.domElement );
  54. orbit = new OrbitControls( camera, renderer.domElement );
  55. orbit.enableZoom = true;
  56. orbit.enableKeys = false;
  57. lights = [];
  58. lights[ 0 ] = new PointLight( 0xffffff, 1, 0 );
  59. lights[ 1 ] = new PointLight( 0xffffff, 1, 0 );
  60. lights[ 2 ] = new PointLight( 0xffffff, 1, 0 );
  61. lights[ 0 ].position.set( 0, 200, 0 );
  62. lights[ 1 ].position.set( 100, 200, 100 );
  63. lights[ 2 ].position.set( - 100, - 200, - 100 );
  64. scene.add( lights[ 0 ] );
  65. scene.add( lights[ 1 ] );
  66. scene.add( lights[ 2 ] );
  67. window.addEventListener( 'resize', function () {
  68. camera.aspect = window.innerWidth / window.innerHeight;
  69. camera.updateProjectionMatrix();
  70. renderer.setSize( window.innerWidth, window.innerHeight );
  71. }, false );
  72. initBones();
  73. setupDatGui();
  74. }
  75. // 创建几何
  76. function createGeometry( sizing ) {
  77. let geometry = new CylinderBufferGeometry( 5, 5, sizing.height, 20, sizing.segmentCount * 6, true );
  78. let position = geometry.attributes.position;
  79. let vertex = new Vector3();
  80. let skinIndices = []; // 蒙皮索引数组
  81. let skinWeights = []; // 蒙皮权值
  82. for ( let i = 0; i < position.count; i ++ ) {
  83. vertex.fromBufferAttribute( position, i );
  84. let y = ( vertex.y + sizing.halfHeight );
  85. let skinIndex = Math.floor( y / sizing.segmentHeight );
  86. let skinWeight = ( y % sizing.segmentHeight ) / sizing.segmentHeight;
  87. skinIndices.push( skinIndex, skinIndex + 1, 0, 0 );
  88. skinWeights.push( 1 - skinWeight, skinWeight, 0, 0 );
  89. }
  90. geometry.addAttribute( 'skinIndex', new Uint16BufferAttribute( skinIndices, 4 ) );
  91. geometry.addAttribute( 'skinWeight', new Float32BufferAttribute( skinWeights, 4 ) );
  92. return geometry;
  93. }
  94. // 创建骨骼
  95. function createBones( sizing ) {
  96. bones = [];
  97. let prevBone = new Bone();
  98. bones.push( prevBone );
  99. prevBone.position.y = - sizing.halfHeight;
  100. for ( let i = 0; i < sizing.segmentCount; i ++ ) {
  101. let bone = new Bone();
  102. bone.position.y = sizing.segmentHeight;
  103. bones.push( bone );
  104. prevBone.add( bone );
  105. prevBone = bone;
  106. }
  107. return bones;
  108. }
  109. // 创建几何
  110. function createMesh( geometry, bones ) {
  111. let material = new MeshPhongMaterial( {
  112. skinning: true,
  113. color: 0x156289,
  114. emissive: 0x072534,
  115. side: DoubleSide,
  116. flatShading: true
  117. } );
  118. let mesh = new SkinnedMesh( geometry, material );
  119. let skeleton = new Skeleton( bones );
  120. mesh.add( bones[ 0 ] ); // 绑定骨骼
  121. mesh.bind( skeleton ); // 绑定骨架
  122. skeletonHelper = new SkeletonHelper( mesh );
  123. skeletonHelper.material.linewidth = 2;
  124. scene.add( skeletonHelper );
  125. return mesh;
  126. }
  127. function setupDatGui() {
  128. let folder = gui.addFolder( "General Options" );
  129. folder.add( state, "animateBones" );
  130. folder.__controllers[ 0 ].name( "Animate Bones" );
  131. folder.add( mesh, "pose" );
  132. folder.__controllers[ 1 ].name( ".pose()" );
  133. folder.add( state, "wireframe" ).onChange( ( e ) => {
  134. mesh.material.wireframe = e;
  135. } );
  136. let bones = mesh.skeleton.bones;
  137. for ( let i = 0; i < bones.length; i ++ ) {
  138. let bone = bones[ i ];
  139. folder = gui.addFolder( "Bone " + i );
  140. folder.add( bone.position, 'x', - 10 + bone.position.x, 10 + bone.position.x );
  141. folder.add( bone.position, 'y', - 10 + bone.position.y, 10 + bone.position.y );
  142. folder.add( bone.position, 'z', - 10 + bone.position.z, 10 + bone.position.z );
  143. folder.add( bone.rotation, 'x', - Math.PI * 0.5, Math.PI * 0.5 );
  144. folder.add( bone.rotation, 'y', - Math.PI * 0.5, Math.PI * 0.5 );
  145. folder.add( bone.rotation, 'z', - Math.PI * 0.5, Math.PI * 0.5 );
  146. folder.add( bone.scale, 'x', 0, 2 );
  147. folder.add( bone.scale, 'y', 0, 2 );
  148. folder.add( bone.scale, 'z', 0, 2 );
  149. folder.__controllers[ 0 ].name( "position.x" );
  150. folder.__controllers[ 1 ].name( "position.y" );
  151. folder.__controllers[ 2 ].name( "position.z" );
  152. folder.__controllers[ 3 ].name( "rotation.x" );
  153. folder.__controllers[ 4 ].name( "rotation.y" );
  154. folder.__controllers[ 5 ].name( "rotation.z" );
  155. folder.__controllers[ 6 ].name( "scale.x" );
  156. folder.__controllers[ 7 ].name( "scale.y" );
  157. folder.__controllers[ 8 ].name( "scale.z" );
  158. }
  159. }
  160. // 绑定骨骼
  161. function initBones() {
  162. let segmentHeight = 8;
  163. let segmentCount = 4;
  164. let height = segmentHeight * segmentCount;
  165. let halfHeight = height * 0.5;
  166. let sizing = {
  167. segmentHeight: segmentHeight,
  168. segmentCount: segmentCount,
  169. height: height,
  170. halfHeight: halfHeight
  171. };
  172. let geometry = createGeometry( sizing );
  173. let bones = createBones( sizing );
  174. mesh = createMesh( geometry, bones );
  175. mesh.scale.multiplyScalar( 1 );
  176. scene.add( mesh );
  177. }
  178. function render() {
  179. requestAnimationFrame( render );
  180. let time = Date.now() * 0.001;
  181. //Wiggle the bones
  182. if ( state.animateBones ) {
  183. for ( let i = 0; i < mesh.skeleton.bones.length; i ++ ) {
  184. mesh.skeleton.bones[ i ].rotation.z = Math.sin( time ) * 2 / mesh.skeleton.bones.length;
  185. }
  186. }
  187. renderer.render( scene, camera );
  188. stats.update();
  189. }
  190. initScene();
  191. render();
  192. </script>
  193. </body>
  194. </html>

发表评论

表情:
评论列表 (有 0 条评论,151人围观)

还没有评论,来说两句吧...

相关阅读

    相关 threejs

    Three.js是一个基于WebGL技术的JavaScript库,它可以简化3D图形的创建和展示。使用Three.js,你可以在网页上创建复杂的3D场景、模型和动画。 Thr

    相关 ThreeJS环境搭建

    ThreeJS介绍 ThreeJS是一款基于WebGL封装的在浏览器中运行的 3D 库,你可以用它来创造你所需要的一系列3D场景,如最近比较火的数字孪生、元宇宙可视化等等

    相关 ThreeJs创建天空盒

    ThreeJS创建天空盒比较简单,可以理解成给一个立方体贴图,所以需要给六个面分别贴图,6张图构建整个场景的图片。这六张图分别是朝前的(posz)、朝后的(negz)、朝上的(