您的位置:首页 > Web前端 > JavaScript

80 - tree.js 笔记 -通过 AnimationMixer混合器解析骨骼动画

2018-11-16 16:02 736 查看

1、骨骼动画原理

骨骼包括骨架和骨头,在

three.js
中,骨骼模型是
SkinnedMesh
就是具有骨架
Skeleton
和骨头
bones
的网格
Mesh
,骨骼网格可以控制几何体
Geometry
的顶点生成骨骼动画。
因此,要实现骨骼动画就需要先创建骨架和骨头,这一般可以通过3D建模软件自动生成,因此
three.js
只需要负责解析即可,但是也需要了解骨架和骨头所组成的数据结构。

1.1、创建一个手臂

1.1.1、Bone骨头对象

THREE.Bone
是骨头关节,是骨骼
Skeleton
的一部分,
THREE.Bone
继承自
Object3D
对象,因此可以完全等同于
THREE.Object3D
对象,仅仅是语义上的区别,通过
THREE.Bone
对象可以构建一个树结构,用来描述骨骼的各个关节的联系。一个父关节可以有多个子关节,父关节
vertex
的变化,可以带动子关节的变化。
如下面的代码创建各个骨关节,然后创建一个骨骼系统:

var bones = []

// 肩膀
var shoulder = new THREE.Bone();

// 胳膊
var elbow = new THREE.Bone();

// 手臂
var hand = new THREE.Bone()

shoulder.add(elbow)
elbow.add(hand)

bones.push(shoulder, elbow, hand);

// 设置相对位置
elbow.position.y = 60;//胳膊相对于肩膀的位置
hand.position.y = 40;//手臂相对于胳膊的位置

var armSkeleton = new THREE.Skeleton(bones);

1.1.2、设置皮肤索引
skinIndices
皮肤权重
skinWeight

首先,需要创建一个几何体

Geometry
然后对几何体的顶点进行皮肤索引和皮肤权重设置。
1、创建一个圆柱几何体

var geometry = new THREE.CylinderBufferGeometry(3, 7, 120, 50, 10);

2、设置皮肤索引和权重
通过遍历几何体顶点为每一个顶点设置皮肤索引和权重,

Geometry
具有两个方法
skinWeights
skinIndices
,
skinWeights是一个权重值数组,对应于几何体中顶点的顺序,因此第一个
skinWeight
对应于几何体中的第一个顶点由于每个顶点可以被
4
个骨骼
Bone
修改,因此,使用
THREE.Vector4()
来表示顶点皮肤的权重,也就是第四个参数
w

skinIndices同样对应于几何体的顶点,每个顶点最多可以有四个与之关联的骨骼,
skinIndices
同样是一个
THREE.Vector4
,对应骨架对象
Skeleton
的属性
bones

//根据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));

}

}

1.1.3、创建
SkinnedMesh
对象

首先,需要创建材质,

var material = new THREE.MeshPhongMaterial({
color: 0xffff00,
transparent: true,
opacity: 0.5,
skinning: true,
});

这里的

skinning
属性需要设置为
true
表示支持使用皮肤
创建骨骼网格模型

var mesh = new THREE.SkinnedMesh(geometry, material);
mesh.translateY(-60);

并将骨架绑定到

SkinnedMesh

var rootBone = armSkeleton.bones[0];
mesh.add(rootBone);

//将骨架绑定到网格
mesh.bind(armSkeleton);

//移动骨骼并操纵模型
armSkeleton.bones[0].rotation.x = -0.1;
armSkeleton.bones[1].rotation.x = 0.2;

为了使骨骼显示的比较清晰,可以添加辅助线条来体现骨骼

// 骨骼辅助线显示
var helper = new THREE.SkeletonHelper(mesh);
scene.add(helper);

效果如图所示

1:点击查看示例

2、外部模型骨骼动画

如果直接通过

three.js
创建骨骼动画,工作量将会非常巨大,因此可以借助一些三维建模软件制作骨骼动画,然后导出以供
three.js
直接加载解析即可。

2.1、加载外部模型

var loader = new THREE.JSONLoader();
loader.load('bf109e/bf109e.json', function (geometry, materials) {

// 设置材质皮肤属性为true
materials.forEach(elem => {
elem.skinning = true;
});
// 创建骨骼模型
skinnedMesh = new THREE.SkinnedMesh(geometry, materials);
// 加入场景
scene.add(skinnedMesh );
}

一个飞机的模型

2.2、通过帧动画让飞机转动

在控制台打印一下

skinnedMesh
找到
geometry
属性下的
animations
数组
可以看到,
animations
数组中有一个剪辑动画,因此可以创建帧动画。如果对帧动画不了解可以查看上一篇文章帧动画介绍
1、首先,需要创建一个
AnimationMixer
混合器

// 将模型作为参数创建一个混合器
mixer = new THREE.AnimationMixer(skinnedMesh);

// 解析飞行状态对应剪辑对象
var action = mixer.clipAction(skinnedMesh.geometry.animations[0]);
action.timeScale = 10;
action.play();

2、循环执行

if (mixer !== undefined ) {
mixer.update(clock.getDelta());
}

效果

2.点击查看示例

示例代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="../../../three.png">
<title>加载外部模型谷歌动画</title>
<style>
body {
margin: 0;
overflow: hidden; /* 溢出隐藏 */
}
</style>
<script src="../../libs/build/three-r93.js"></script>
<script src="../../libs/examples/js/controls/OrbitControls.js"></script>
</head>
<body>
<script>

var scene, camera, renderer, controls, mixer;
var clock = new THREE.Clock();
var group = new THREE.Group();

/* 场景 */
function initScene() {

scene = new THREE.Scene();

scene.background = new THREE.CubeTextureLoader().setPath('skybox/').load([ 'posx.jpg', 'negx.jpg', 'posy.jpg', 'negy.jpg', 'posz.jpg', 'negz.jpg' ]);

}

/* 相机 */
function initCamera() {

camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);
camera.position.set(1, 0, 1.5);
camera.lookAt(new THREE.Vector3(0, 0, 0));

}

/* 渲染器 */
function initRender() {

renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);

document.body.appendChild(renderer.domElement);

}

/* 灯光 */
function initLight() {

scene.add(new THREE.AmbientLight(0xffffff));

var spotLight1 = new THREE.SpotLight(0xffffff);
spotLight1.position.set(-400, -400, -400);

var spotLight2 = new THREE.SpotLight(0xffffff);
spotLight2.position.set(400, 400, 400);

scene.add(spotLight1);
scene.add(spotLight2);

}

/* 控制器 */
function initControls() {

controls = new THREE.OrbitControls(camera, renderer.domElement);

}
var skinnedMesh;
function initContent() {

var loader = new THREE.JSONLoader();

loader.load('bf109e/bf109e.json', function (geometry, materials) {

materials.forEach(elem => {
elem.skinning = true;
});

skinnedMesh = new THREE.SkinnedMesh(geometry, materials);

group.add(skinnedMesh);

scene.add(group);
console.log(skinnedMesh);

// 将模型作为参数创建一个混合器
mixer = new THREE.AnimationMixer(skinnedMesh);

// 解析飞行状态对应剪辑对象
var action = mixer.clipAction(skinnedMesh.geometry.animations[0]);
action.timeScale = 10;
action.play();
console.log(skinnedMesh);

});

}

/* 窗口变动触发 */
function onWindowResize() {

camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);

}

/* 数据更新 */
function update() {

controls.update();

if (mixer !== undefined ) {
mixer.update(clock.getDelta());
}
}

/* 初始化 */
function init() {

initScene();
initCamera();
initRender();
initLight();
initControls();
initContent();
/* 监听事件 */
window.addEventListener('resize', onWindowResize, false);

}

/* 循环渲染 */
function animate() {

requestAnimationFrame(animate);
renderer.render(scene, camera);
update();

}

/* 初始加载 */
(function () {

init();
animate();

})();

</script>
</body>
</html>

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: