Three.js骨骼动画(SkinnedMesh)

优质
小牛编辑
131浏览
2023-12-01

所谓骨骼动画,以人体为例简单地说,人体的骨骼运动,骨骼运动会带动肌肉和人体皮肤的空间移动和表面变化,下面将会提到的蒙皮概念你可以类比人体的皮肤。

Threejs骨骼动画需要通过骨骼网格模型类SkinnedMesh来实现,一般来说骨骼动画模型都是3D美术创建,然后程序员通过threejs引擎加载解析,为了让大家更深入理解骨骼动画,下面就通过threejs程序编写一个简易的骨骼动画。

相关类

直接使用Threejs编写一个骨骼动画还是比较复杂的,你首先应该了解骨头关节Bone、骨骼网格模型SkinnedMesh、骨架对象Skeleton这三个骨骼相关的类,除此之外还需要了解几何体Geometry和骨骼动画相关的顶点数据。

Bone

通过Bone类可以实例化一个骨关节对象,然后通过多个骨关节对象可以构成一个骨骼层级系统,Bone基类是Object3D,可以通过add方法给一个骨关节对象Bone添加一个子骨关节Bone

var Bone1 = new THREE.Bone(); //关节1,用来作为根关节
var Bone2 = new THREE.Bone(); //关节2
var Bone3 = new THREE.Bone(); //关节3
// 设置关节父子关系   多个骨头关节构成一个树结构
Bone1.add(Bone2);
Bone2.add(Bone3);
// 设置关节之间的相对位置
//根关节Bone1默认位置是(0,0,0)
Bone2.position.y = 60; //Bone2相对父对象Bone1位置
Bone3.position.y = 40; //Bone3相对父对象Bone2位置

骨架Skeleton

Threejs通过Skeleton类可以把所有骨关节对象Bone包含进来。

// 所有Bone对象插入到Skeleton中,全部设置为.bones属性的元素
var skeleton = new THREE.Skeleton([Bone1, Bone2, Bone3]); //创建骨骼系统
// 查看.bones属性中所有骨关节Bone
console.log(skeleton.bones);
// 返回所有关节的世界坐标
skeleton.bones.forEach(elem => {
  console.log(elem.getWorldPosition(new THREE.Vector3()));
});

Geometry(.skinWeights.skinIndices属性)

前面课程讲解过几何体Geometry的多种顶点数据。几何体Geometry的属性.skinWeights.skinIndices主要作用是用来设置几何体的顶点位置是如何受骨关节运动影响的。比如几何体Geometry的顶点位置数据是你皮肤上的一个个点位,如果你的骨关节运动了,你的皮肤外形会跟着变化,就相当于Geometry的顶点坐标需要跟着骨关节变化,这时候需要注意,关节外面包裹的一层皮肤,不同区域变形程度不同,那也就是说如果骨关节Bone变化了,几何体Geometry顶点要像皮肤一样不同区域的顶点变化程度不同。这也正是.skinWeights.skinIndices属性出现的原因,.skinWeights的字面意思就是设置骨骼蒙皮的权重。

骨骼网格模型SkinnedMesh

SkinnedMesh类的字面意思就是骨骼网格模型,骨骼网格模型SkinnedMesh的基类是普通网格模型Mesh,SkinnedMeshMesh一样都是网格模型,只是一个有骨骼动画功能,一个没有骨骼动画功能。

骨骼网格模型SkinnedMesh绑定骨骼系统。

//骨骼关联网格模型
SkinnedMesh.add(Bone1); //根骨头关节添加到网格模型
SkinnedMesh.bind(skeleton); //网格模型绑定到骨骼系统

程序创建一个骨骼动画

下面的的代码通过SkinnedMesh构造函数创建一个骨骼动画,如果你想深入理解骨骼动画可以研究一下下面的代码,下面代码还是比较复杂的,涉及到的知识点比较多,如果不想深入研究,可以大致看下有个印象,直接学习下一节,实际开发的时候,只需要会加载解析骨骼动画就可以。

/**
 * 创建骨骼网格模型SkinnedMesh
 */
// 创建一个圆柱几何体,高度120,顶点坐标y分量范围[-60,60]
var geometry = new THREE.CylinderGeometry(5, 10, 120, 50, 300);
geometry.translate(0, 60, 0); //平移后,y分量范围[0,120]
console.log("name", geometry.vertices); //控制台查看顶点坐标
//
/**
 * 设置几何体对象Geometry的蒙皮索引skinIndices、权重skinWeights属性
 * 实现一个模拟腿部骨骼运动的效果
 */
//遍历几何体顶点,为每一个顶点设置蒙皮索引、权重属性
//根据y来分段,0~60一段、60~100一段、100~120一段
for (var i = 0; i < geometry.vertices.length; i++) {
  var vertex = geometry.vertices[i]; //第i个顶点
  if (vertex.y <= 60) {
    // 设置每个顶点蒙皮索引属性  受根关节Bone1影响
    geometry.skinIndices.push(new THREE.Vector4(0, 0, 0, 0));
    // 设置每个顶点蒙皮权重属性
    // 影响该顶点关节Bone1对应权重是1-vertex.y/60
    geometry.skinWeights.push(new THREE.Vector4(1 - vertex.y / 60, 0, 0, 0));
  } else if (60 < vertex.y && vertex.y <= 60 + 40) {
    // Vector4(1, 0, 0, 0)表示对应顶点受关节Bone2影响
    geometry.skinIndices.push(new THREE.Vector4(1, 0, 0, 0));
    // 影响该顶点关节Bone2对应权重是1-(vertex.y-60)/40
    geometry.skinWeights.push(new THREE.Vector4(1 - (vertex.y - 60) / 40, 0, 0, 0));
  } else if (60 + 40 < vertex.y && vertex.y <= 60 + 40 + 20) {
    // Vector4(2, 0, 0, 0)表示对应顶点受关节Bone3影响
    geometry.skinIndices.push(new THREE.Vector4(2, 0, 0, 0));
    // 影响该顶点关节Bone3对应权重是1-(vertex.y-100)/20
    geometry.skinWeights.push(new THREE.Vector4(1 - (vertex.y - 100) / 20, 0, 0, 0));
  }
}
// 材质对象
var material = new THREE.MeshPhongMaterial({
  skinning: true, //允许蒙皮动画
  // wireframe:true,
});
// 创建骨骼网格模型
var SkinnedMesh = new THREE.SkinnedMesh(geometry, material);
SkinnedMesh.position.set(50, 120, 50); //设置网格模型位置
SkinnedMesh.rotateX(Math.PI); //旋转网格模型
scene.add(SkinnedMesh); //网格模型添加到场景中
/**
 * 骨骼系统
 */
var Bone1 = new THREE.Bone(); //关节1,用来作为根关节
var Bone2 = new THREE.Bone(); //关节2
var Bone3 = new THREE.Bone(); //关节3
// 设置关节父子关系   多个骨头关节构成一个树结构
Bone1.add(Bone2);
Bone2.add(Bone3);
// 设置关节之间的相对位置
//根关节Bone1默认位置是(0,0,0)
Bone2.position.y = 60; //Bone2相对父对象Bone1位置
Bone3.position.y = 40; //Bone3相对父对象Bone2位置
// 所有Bone对象插入到Skeleton中,全部设置为.bones属性的元素
var skeleton = new THREE.Skeleton([Bone1, Bone2, Bone3]); //创建骨骼系统
// console.log(skeleton.bones);
// 返回所有关节的世界坐标
// skeleton.bones.forEach(elem => {
//   console.log(elem.getWorldPosition(new THREE.Vector3()));
// });
//骨骼关联网格模型
SkinnedMesh.add(Bone1); //根骨头关节添加到网格模型
SkinnedMesh.bind(skeleton); //网格模型绑定到骨骼系统
console.log(SkinnedMesh);
/**
 * 骨骼辅助显示
 */
var skeletonHelper = new THREE.SkeletonHelper(SkinnedMesh);
scene.add(skeletonHelper);
// 转动关节带动骨骼网格模型出现弯曲效果  好像腿弯曲一样
skeleton.bones[1].rotation.x = 0.5;
skeleton.bones[2].rotation.x = 0.5;

程序实现骨骼动画

通过骨骼骨骼系统代码实现骨骼动画效果。

var n = 0;
var T = 50;
var step = 0.01;
// 渲染函数
function render() {
  renderer.render(scene, camera);
  requestAnimationFrame(render);
  n += 1;
  if (n < T) {
    // 改变骨关节角度
    skeleton.bones[0].rotation.x = skeleton.bones[0].rotation.x - step;
    skeleton.bones[1].rotation.x = skeleton.bones[1].rotation.x + step;
    skeleton.bones[2].rotation.x = skeleton.bones[2].rotation.x + 2 * step;
  }
  if (n < 2 * T && n > T) {
    skeleton.bones[0].rotation.x = skeleton.bones[0].rotation.x + step;
    skeleton.bones[1].rotation.x = skeleton.bones[1].rotation.x - step;
    skeleton.bones[2].rotation.x = skeleton.bones[2].rotation.x - 2 * step;
  }
  if (n === 2 * T) {
    n = 0;
  }
}
render();

解析外部骨骼动画模型

开发的时候,3D美术如果导出一个包含骨骼动画数据的三维模型,你可以通过下面的代码进行加载解析,查看骨骼动画的运动效果。

骨骼动画除了需要创建一个骨骼动画模型SkinnedMesh外,还需要通过帧动画存储相关的关节动画数据。

var mixer = null; //声明一个混合器变量
loader.load("./marine_anims_core.json", function(obj) {
  console.log(obj)
  scene.add(obj); //添加到场景中
  //从返回对象获得骨骼网格模型
  var SkinnedMesh = obj.children[0];
  //骨骼网格模型作为参数创建一个混合器
  mixer = new THREE.AnimationMixer(SkinnedMesh);
  // 查看骨骼网格模型的帧动画数据
  // console.log(SkinnedMesh.geometry.animations)
  // 解析跑步状态对应剪辑对象clip中的关键帧数据
  var AnimationAction = mixer.clipAction(SkinnedMesh.geometry.animations[1]);
  // 解析步行状态对应剪辑对象clip中的关键帧数据
  // var AnimationAction = mixer.clipAction(SkinnedMesh.geometry.animations[3]);
  AnimationAction.play();
  // 骨骼辅助显示
  // var skeletonHelper = new THREE.SkeletonHelper(SkinnedMesh);
  // scene.add(skeletonHelper);
})
// 创建一个时钟对象Clock
var clock = new THREE.Clock();
// 渲染函数
function render() {
  renderer.render(scene, camera);
  requestAnimationFrame(render);
  if (mixer !== null) {
    //clock.getDelta()方法获得两帧的时间间隔
    // 更新混合器相关的时间
    mixer.update(clock.getDelta());
  }
}
render();

皮肤顶点权重属性.skinWeights

.skinWeights表示的是几何体顶点权重数据,当使用骨骼动画网格模型SkinnedMesh的时候, 每个顶点最多可以有4个骨关节Bone影响它. skinWeights属性是一个权重值数组,对应于几何体中顶点的顺序。 例如,第一个skinWeight将对应于几何体中的第一个顶点. 由于每个顶点可以被4个骨关节Bone修改,因此使用四维向量对象Vector4表示一个顶点的权重.

四维向量Vector4每个分量的值通常应在0和1之间。当设置为0时,骨关节Bone变换将不起作用;设置为0.5时,将产生50%的影响;设置为100%时,会产生100%的影响。 如果只有一个骨关节Bone与顶点关联,那么你只需要考虑设置四维向量Vector4的第一个分量,其余分量的可以忽略并设置为0.

顶点索引属性.skinIndices

顶点索引属性.skinIndices就像skinWeights属性一样,skinIndices的值对应几何体的顶点. 每个顶点最多可以有4个与之关联的骨关节Bone。

骨骼动画顶点数据

骨骼动画的相关的一些顶点数据,主要是描述几何体表面的顶点数据是如何受骨骼系统关节运动影响的。加载外部模型你可以访问骨骼动画网格模型的几何体对象查看骨骼动画相关顶点数据。网格模型的几何体类型可能是Geometry,也可能是BufferGeometry,一般是缓冲类型的几何体BufferGeometry比较常见。

Geometry的骨骼动画顶点数据,直接查看.skinWeights.skinIndices属性

console.log('骨骼动画皮肤顶点权重数据',SkinnedMesh.geometry.skinWeights);
console.log('骨骼动画皮肤顶点索引数据',SkinnedMesh.geometry.skinIndices);

BufferGeometry的骨骼动画顶点数据,如果你熟悉BufferGeometry的结构,应该都知道该几何体通过.attributes属性存储各种顶点数据。

console.log('骨骼动画皮肤顶点权重数据',SkinnedMesh.geometry.attributes.skinWeights);
console.log('骨骼动画皮肤顶点索引数据',SkinnedMesh.geometry.attributes.skinIndices);