简介
变形动画十分简洁。Three.js
知道所有目标顶点的位置,实现变形动画所要做的就是将每一个顶点从一个位置变换到另一个位置。而骨骼和蒙皮则要复杂一些。当你使用骨骼创建动画时,你移动一下骨骼,Three.js
需要决定如何响应地移动相应的皮肤(一系列顶点)。
实现案例
案例查看地址:http://www.wjceo.com/blog/threejs/2018-04-19/147.html
这次案例是导入了一只手的模型,并且模型上面带有骨骼点。我们要实现的是通过控制这些个骨骼点bones
,来实现这只手模型有一个握的动作。
- 首先,正常导入模型。模型文件包含了顶点、面和骨骼的定义。在回调中,我们获得了模型的几何体geometry
,我们通过模型的geometry
来实例出一个SkinnedMesh
对象。并且将模型所使用的material
的skinning
属性设置为true
,否则骨头不会运动。
loader.load("/lib/assets/models/hand-1.js", function (geometry) {
mesh = new THREE.SkinnedMesh(geometry, new THREE.MeshLambertMaterial({
color:0xf4b397,
skinning: true
}));
mesh.rotation.x = 0.5 * Math.PI;
mesh.rotation.z = 0.7 * Math.PI;
mesh.scale.set(10, 10, 10);
scene.add(mesh);
});
- 然后,我们使用
TWEEN
来创建补间动画,动态修改某些骨骼点的方向,来实现模拟手握的动作。
tween = new TWEEN.Tween({pos: -1})
.to({pos: 0}, 3000)
.easing(TWEEN.Easing.Cubic.InOut) //动画曲线
.yoyo(true) //实现来回动画
.repeat(Infinity); //一直循环
tween.onUpdate(function () {
var pos = this.pos;
// 旋转手指的方向
mesh.skeleton.bones[5].rotation.set(0, 0, pos);
mesh.skeleton.bones[6].rotation.set(0, 0, pos);
mesh.skeleton.bones[10].rotation.set(0, 0, pos);
mesh.skeleton.bones[11].rotation.set(0, 0, pos);
mesh.skeleton.bones[15].rotation.set(0, 0, pos);
mesh.skeleton.bones[16].rotation.set(0, 0, pos);
mesh.skeleton.bones[20].rotation.set(0, 0, pos);
mesh.skeleton.bones[21].rotation.set(0, 0, pos);
// 旋转手腕
mesh.skeleton.bones[1].rotation.set(pos, 0, 0);
});
- 最后,为了方便我们理解骨骼点的位置,添加辅助
SkeletonHelper
来显示位置:
skeletonHelper = new THREE.SkeletonHelper( mesh );
skeletonHelper.visible = false;
scene.add( skeletonHelper );
在这个案例当中,我们只改变了骨骼的旋转方向,除此之外,我们还可以修改骨骼的位置和缩放比例。我们将在下一节中演示。
案例代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>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/91/three.min.js"></script>
<script src="/lib/js/controls/OrbitControls.js"></script>
<script src="https://cdn.bootcss.com/tween.js/r14/Tween.min.js"></script>
<script src="https://cdn.bootcss.com/stats.js/r17/Stats.min.js"></script>
<script src="https://cdn.bootcss.com/dat-gui/0.7.1/dat.gui.min.js"></script>
<script src="/lib/js/Detector.js"></script>
<script> var renderer, camera, scene, gui, light, stats, controls, mesh, skeletonHelper, tween, animation = true; var clock = new THREE.Clock(); 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 = { animation: true, helper: false }; var datGui = new dat.GUI(); //将设置属性添加到gui当中,gui.add(对象,属性,最小值,最大值) datGui.add(gui, "animation").onChange(function (e) { animation = e; }); datGui.add(gui, "helper").onChange(function (e) { skeletonHelper.visible = e; }); } 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); scene.add(helper); //加载模型 var loader = new THREE.JSONLoader(); loader.load("/lib/assets/models/hand-1.js", function (geometry) { mesh = new THREE.SkinnedMesh(geometry, new THREE.MeshLambertMaterial({ color:0xf4b397, skinning: true })); mesh.rotation.x = 0.5 * Math.PI; mesh.rotation.z = 0.7 * Math.PI; mesh.scale.set(10, 10, 10); skeletonHelper = new THREE.SkeletonHelper( mesh ); skeletonHelper.visible = false; scene.add( skeletonHelper ); console.log(mesh); scene.add(mesh); tween.start(); }); } function initTween() { tween = new TWEEN.Tween({pos: -1}) .to({pos: 0}, 3000) .easing(TWEEN.Easing.Cubic.InOut) .yoyo(true) .repeat(Infinity); //一直循环 tween.onUpdate(function () { var pos = this.pos; // 旋转手指的方向 mesh.skeleton.bones[5].rotation.set(0, 0, pos); mesh.skeleton.bones[6].rotation.set(0, 0, pos); mesh.skeleton.bones[10].rotation.set(0, 0, pos); mesh.skeleton.bones[11].rotation.set(0, 0, pos); mesh.skeleton.bones[15].rotation.set(0, 0, pos); mesh.skeleton.bones[16].rotation.set(0, 0, pos); mesh.skeleton.bones[20].rotation.set(0, 0, pos); mesh.skeleton.bones[21].rotation.set(0, 0, pos); // 旋转手腕 mesh.skeleton.bones[1].rotation.set(pos, 0, 0); }); } //初始化性能插件 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() { animation && TWEEN.update(); 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() { //兼容性判断 if (!Detector.webgl) Detector.addGetWebGLMessage(); initGui(); initRender(); initScene(); initCamera(); initLight(); initModel(); initControls(); initStats(); initTween(); animate(); window.onresize = onWindowResize; } </script>
</html>