今后的几篇郭先生主要说说three.js骨骼动画。three.js骨骼动画十分有意思,但是对于初学者来说,学起来要稍微困难一些,官方文档比较少,网上除了用圆柱体的例子就是引用外部模型的,想要熟练使用骨骼动画就需要不断地探索和练习。这篇是初探three.js骨骼动画,也不深入讲解,先说说它的实现和原理,然后一点一点解读官网案例,骨骼动画官网案例
骨骼动画主要有以下三个部分构成:
(1) 几何体--在新版本中这个几何体要求必须是一个BufferGeometry而非Geometry,而骨骼动画需要的几何体还有两个十分重要的属性,
(2) 其材质必须支持蒙皮,并且已经启用了蒙皮,既skinning = true;
(3) 创建骨骼和骨架
骨骼(Bone)其实就是一个Object3D对象,可以把骨架看成是人体骨架,假如脊柱的根节点,那么大腿就是下一级节点,小腿就是更下一级的节点,如果大腿转动,那么小腿在世界坐标系必然会动,而小腿动,不一定影响大腿。
现在我们假如有一个几何体(这个几何体加上带蒙皮的材质就是我们的腿的网格),想让这个几何体跟着这个骨骼运动,那么这个动画就是骨骼动画,现在我们假设bones0为大腿上端点,bones1为大小腿关节点,bones2为小腿下端点,这里如果我们把腿看成是圆柱体(官方案例就是这样做的),将极大的降低了难度,让heightSegments为2(就是分两段)也就生成了沿高度分布的3层点
我们将最上层点对应的skinIndices设置成0,skinWeights设置成1。中间层点对应的skinIndices设置成1,skinWeights设置成1。最下层点对应的skinIndices设置成2,skinWeights设置成1。这样几何体的顶点就和骨骼的端点建立了联系。
//这是生成蒙皮网格的主方法
initBones() {
//下面是一些会用到的参数
var segmentHeight = 8; //每段的高度
var segmentCount = 4; //段数
var height = segmentHeight * segmentCount; //总高度
var halfHeight = height * 0.5; //一般高度
var sizing = {
segmentHeight: segmentHeight,
segmentCount: segmentCount,
height: height,
halfHeight: halfHeight
};
var geometry = this.createGeometry( sizing ); //这是生成几何体的方法,主要是根据顶点生成对应的skinIndex和skinWeight属性
var bones = this.createBones( sizing ); //这是生成骨骼的方法
mesh = this.createMesh( geometry, bones ); //这是生成蒙皮网格的方法
mesh.scale.multiplyScalar( 1 );
scene.add( mesh );
this.render();
document.getElementById("loading").style.display = "none";
},
createGeometry(sizing) {
//创建一个圆柱体
var geometry = new THREE.CylinderBufferGeometry(
5, // 上面半径
5, // 下面半径
sizing.height, // 总高度
8, // 圆形面分段数
sizing.segmentCount * 3, // 沿高度的分段数4*3
true // 无上下面
);
var position = geometry.attributes.position; //圆柱体顶点位置集合
var vertex = new THREE.Vector3(); //创建一个三维向量用于保存顶点坐标
var skinIndices = []; //顶点索引聚合
var skinWeights = []; //顶点权重聚合
for ( var i = 0; i < position.count; i ++ ) { //遍历顶点
vertex.fromBufferAttribute( position, i ); //依次取出每个点
var y = ( vertex.y + sizing.halfHeight ); //y保存相对于圆柱体底面的高度值。
//这两行比较重要
var skinIndex = Math.floor( y / sizing.segmentHeight ); //高度除以总高度在向下取整,得到当前的skinIndex
var skinWeight = ( y % sizing.segmentHeight ) / sizing.segmentHeight; //当前的y值占该段的百分比
skinIndices.push( skinIndex, skinIndex + 1, 0, 0 ); //该点关联bone[skinIndex]和bone[skinIndex+1]
skinWeights.push( 1 - skinWeight, skinWeight, 0, 0 ); //关联bone[skinIndex]的比重为1 - skinWeight,关联bone[skinIndex+1]的比重为skinWeight。
//举个例子,第一个y值刚好为0。那么skinIndex为0,skinWeight也为0。所以呢该点相关的骨骼索引为0和1,权重分别是1和0,也就是该点只与bone[0]有关。
//再比如y值为4,那么skinIndex为0,skinWeight也为0.5,所以呢该点相关的骨骼索引为0和1,权重分别是0.5和0.5,也就是该点与bone[0]和bone[1]都相关。其实也很容易理解,因为4恰好在该分段的中间,所以决定于两个骨骼点的状态。
}
geometry.setAttribute( 'skinIndex', new THREE.Uint16BufferAttribute( skinIndices, 4 ) ); //几何体中添加skinIndex属性
geometry.setAttribute( 'skinWeight', new THREE.Float32BufferAttribute( skinWeights, 4 ) ); //几何体中添加skinWeight属性
return geometry;
},
createBones(sizing) {
bones = []; //骨骼数组
var prevBone = new THREE.Bone(); //根骨骼节点
bones.push( prevBone ); //数组中添加根骨骼节点
prevBone.position.y = - sizing.halfHeight; //为根骨骼添加位置
for ( var i = 0; i < sizing.segmentCount; i ++ ) { //遍历分段
var bone = new THREE.Bone(); //创建骨骼节点
bone.position.y = sizing.segmentHeight; //为骨骼节点添加本地位置 虽然本地设置的位置都是一样的,但是由于这些骨骼都是父子关系,所以在世界坐标系上位置不同
bones.push( bone ); //数组中继续添加骨骼
prevBone.add( bone ); //根骨骼添加当前骨骼
prevBone = bone; //再将当前骨骼赋值给根骨骼
}
return bones;
},
createMesh(geometry, bones) {
//创建一个带蒙皮的材质
var material = new THREE.MeshPhongMaterial( {
skinning: true, //重点
color: 0x156289,
emissive: 0x072534,
side: THREE.DoubleSide,
flatShading: true
} );
var mesh = new THREE.SkinnedMesh( geometry, material ); //创建蒙皮网格
var skeleton = new THREE.Skeleton( bones ); //创建骨架
mesh.add( bones[ 0 ] ); //网格添加根骨骼节点(此例bones[0]为根节点)
mesh.bind( skeleton ); //网格绑定骨架
skeletonHelper = new THREE.SkeletonHelper( mesh ); //创建骨骼显示助手
skeletonHelper.material.linewidth = 2;
scene.add( skeletonHelper );
return mesh;
},
最后就是使用gui进行界面控制,这里只说一下蒙皮网格有一个pose()方法,使用后骨架还原为初始状态。官方的骨骼动画解析就到此为止,后面还会继续说说骨骼动画。
转载请注明地址:郭先生的博客