动画一般可以定义两种:一种是变形动画,另一种是骨骼动画。
骨骼动画是需要生成一个与模型相关的骨架,骨架中的骨骼也会存在对应关系,模型的每一个需要动画的顶点需要设置影响它的骨骼以及骨骼影响顶点的程度。骨骼动画和变形动画相比会比较复杂一些,但是它又有更多的灵活性。我们可以想象一下人体的骨骼,如果使用变形动画,需要把所有的每一次的变动都存一个顶点数组,而骨骼动画,只需要设置骨骼的相关信息,就可以实现更多的动画。
首先, 我们创建了一个圆柱几何体,然后通过圆柱的几何体每一个顶点的y轴坐标来设置需要绑定的骨骼的下标和影响的程度:
//遍历几何体所有的顶点
for (var i = 0; i < geometry.vertices.length; i++) {
//根据顶点的位置计算出骨骼影响下标和权重
var vertex = geometry.vertices[i];
var y = (vertex.y + sizing.halfHeight);
var skinIndex = Math.floor(y / sizing.segmentHeight);
var skinWeight = (y % sizing.segmentHeight) / sizing.segmentHeight;
geometry.skinIndices.push(new THREE.Vector4(skinIndex, skinIndex + 1, 0, 0));
geometry.skinWeights.push(new THREE.Vector4(1 - skinWeight, skinWeight, 0, 0));
}
几何体的
skinIndices
属性和skinWeights
属性就是来设置相关的绑定下标和权重(骨骼影响程度)。Vector4 不代表任何意义,仅仅是4个float , 你需要根据你的需求 赋予这个Vetor4的含义
比如 XYZ 代表坐标 ,W 代表比例, 这样 你可以用 Vector3 pos = new Vector3(X/W,Y/W,Z/W) 来控制坐标的比例
你也可用W 代表透明度, XYZ来控制RPG
或者也可以用W 来进行bool 判断
相应的,我们需要设置一组相关的骨骼,骨骼具有嵌套关系,这样才能实现一个骨架,由于圆柱体比较简单,我们就创建一条骨骼垂直嵌套的骨骼:
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;
}
创建纹理时,我们还需要设置当前纹理需要受到骨骼的影响,将材质的skinning
属性设置为true
:
var lineMaterial = new THREE.MeshBasicMaterial({
skinning: true,
wireframe: true
});
最后,我们需要创建骨骼材质,并将模型绑定骨骼:
mesh = new THREE.SkinnedMesh(geometry, [material, lineMaterial]);
var skeleton = new THREE.Skeleton(bones); //创建骨架
mesh.add(bones[0]); //将骨骼添加到模型里面
mesh.bind(skeleton); //模型绑定骨架
效果-骨骼动画
skeleton.html
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>骨骼动画实现案例title>
<style type="text/css">
html, body {
margin: 0;
height: 100%;
}
canvas {
display: block;
}
style>
head>
<body onload="draw();">
body>
<script src="https://cdn.bootcss.com/three.js/92/three.js">script>
<script src="./control.js">script>
<script src="http://www.wjceo.com/lib/js/libs/stats.min.js">script>
<script src="https://cdn.bootcss.com/dat-gui/0.7.1/dat.gui.min.js">script>
<script>
var renderer, camera, scene, gui, light, stats, controls, mesh, skeletonHelper, bones;
var state = {
animateBones: false
};
function initRender() {
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xeeeeee);
//告诉渲染器需要阴影效果
document.body.appendChild(renderer.domElement);
}
function initCamera() {
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 40, 50);
}
function initScene() {
scene = new THREE.Scene();
}
//初始化dat.GUI简化试验流程
function initGui() {
gui = new dat.GUI();
var folder = gui.addFolder("动画配置");
folder.add(state, "animateBones").name("开启骨骼动画");
folder.add(mesh, "pose").name("重置");
var bones = mesh.skeleton.bones;
for (var i = 0; i < bones.length; i++) {
var bone = bones[i];
folder = gui.addFolder("骨骼 " + i);
folder.add(bone.position, 'x', -10 + bone.position.x, 10 + bone.position.x).name("position.x");
folder.add(bone.position, 'y', -10 + bone.position.y, 10 + bone.position.y).name("position.y");
folder.add(bone.position, 'z', -10 + bone.position.z, 10 + bone.position.z).name("position.z");
folder.add(bone.rotation, 'x', -Math.PI * 0.5, Math.PI * 0.5).name("rotation.x");
folder.add(bone.rotation, 'y', -Math.PI * 0.5, Math.PI * 0.5).name("rotation.y");
folder.add(bone.rotation, 'z', -Math.PI * 0.5, Math.PI * 0.5).name("rotation.z");
folder.add(bone.scale, 'x', 0, 2).name("scale.x");
folder.add(bone.scale, 'y', 0, 2).name("scale.y");
folder.add(bone.scale, 'z', 0, 2).name("scale.z");
}
}
function initLight() {
scene.add(new THREE.AmbientLight(0x444444));
light = new THREE.PointLight(0xffffff);
light.position.set(0, 50, 0);
//告诉平行光需要开启阴影投射
light.castShadow = true;
scene.add(light);
}
function initModel() {
//辅助工具
var helper = new THREE.AxesHelper(50); // xyz的辅助线
scene.add(helper);
//制作模型
var segmentHeight = 6; //每一节骨骼的的高度
var segmentCount = 4; //总节数
var height = segmentHeight * segmentCount; //总高度
var halfHeight = height * 0.5; //总高度一半的高度
var sizing = { // 模型信息
segmentHeight: segmentHeight,
segmentCount: segmentCount,
height: height,
halfHeight: halfHeight
};
var geometry = createGeometry(sizing); //创建几何体
var bones = createBones(sizing); //创建骨骼 !!!!
mesh = createMesh(geometry, bones); //创建网格模型
scene.add(mesh);
}
//创建几何体
function createGeometry(sizing) {
var geometry = new THREE.CylinderGeometry( // 实例化圆柱体
5, // 顶部圆柱体的半径
5, // 底部圆柱体的半径
sizing.height, // 圆柱体的高度
8, // 圆柱周围的分段面数
sizing.segmentCount * 3, // 沿圆柱体高度的面的行数
true // 圆柱体的末端是打开
);
//添加绘制第二个纹理的面
var len = geometry.faces.length;
for (var i = 0; i < len; i++) {
var face = geometry.faces[i].clone();
face.materialIndex = 1;
geometry.faces.push(face);
}
//删除掉几何体的uv映射,解决报错问题
geometry.faceVertexUvs = [];
//遍历几何体所有的顶点
for (var i = 0; i < geometry.vertices.length; i++) {
//根据顶点的位置计算出骨骼影响下标和权重
var vertex = geometry.vertices[i];
var y = (vertex.y + sizing.halfHeight);
// skinIndex代表 哪些关节影响这哪些关节
var skinIndex = Math.floor(y / sizing.segmentHeight);
// skinWeight代表index之间的影响权重
var skinWeight = (y % sizing.segmentHeight) / sizing.segmentHeight;
// Vector4不代表任何意义,它只是比Vector3多了一个参数 (x,y,z, 1),第4个参数在于你自己定义意义
geometry.skinIndices.push(new THREE.Vector4(skinIndex, skinIndex + 1, 0, 0));
geometry.skinWeights.push(new THREE.Vector4(1 - skinWeight, skinWeight, 0, 0));
}
return geometry;
}
//创建骨骼
function 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;
}
function createMesh(geometry, bones) {
var material = new THREE.MeshPhongMaterial({
skinning: true,
color: 0x156289,
emissive: 0x072534,
side: THREE.DoubleSide
});
var lineMaterial = new THREE.MeshBasicMaterial({
skinning: true,
wireframe: true
});
mesh = new THREE.SkinnedMesh(geometry, [material, lineMaterial]);
var skeleton = new THREE.Skeleton(bones); //创建骨架
mesh.add(bones[0]); //将骨骼添加到模型里面
mesh.bind(skeleton); //模型绑定骨架
//添加骨骼辅助标记
skeletonHelper = new THREE.SkeletonHelper(mesh);
skeletonHelper.material.linewidth = 2;
scene.add(skeletonHelper);
return mesh;
}
//初始化性能插件
function initStats() {
stats = new Stats();
document.body.appendChild(stats.dom);
}
function initControls() {
controls = new THREE.OrbitControls(camera, renderer.domElement);
// 如果使用animate方法时,将此函数删除
//controls.addEventListener( 'change', render );
// 使动画循环使用时阻尼或自转 意思是否有惯性
controls.enableDamping = true;
//动态阻尼系数 就是鼠标拖拽旋转灵敏度
//controls.dampingFactor = 0.25;
//是否可以缩放
controls.enableZoom = true;
//是否自动旋转
controls.autoRotate = false;
controls.autoRotateSpeed = 0.5;
//设置相机距离原点的最远距离
controls.minDistance = 1;
//设置相机距离原点的最远距离
controls.maxDistance = 2000;
//是否开启右键拖拽
controls.enablePan = true;
}
function render() {
var time = Date.now() * 0.001;
//骨骼动画
if (state.animateBones) {
for (var i = 0; i < mesh.skeleton.bones.length; i++) {
mesh.skeleton.bones[i].rotation.z = Math.sin(time) * 2 / mesh.skeleton.bones.length;
}
}
controls.update();
}
//窗口变动触发的函数
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
//更新控制器
render();
//更新性能插件
stats.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
function draw() {
initStats();
initRender();
initScene();
initCamera();
initLight();
initModel();
initControls();
initGui();
animate();
window.onresize = onWindowResize;
}
script>
html>
附
注意参数:
//创建模型
function initModel() {}
//创建几何体
function createGeometry(sizing) {}
//创建骨骼
function createBones(sizing) {} // 核心: 骨骼直接的运动关系
附
注意参数:
//创建模型
function initModel() {}
//创建几何体
function createGeometry(sizing) {}
//创建骨骼
function createBones(sizing) {} // 核心: 骨骼直接的运动关系