个人博客地址: https://cxx001.gitee.io
前面我们所用的模型大都是静态的,没有动画,没有生命。这节我们将赋予它们生命。
动画本质是通过改变物体的旋转、缩放、位置、材质、顶点、面以及其它你所能想到的属性来实现的。这些其实在前面章节示例里或多或少已经使用了。
一、选择对象
在写示例前我们先了解一个重要操作,如何通过鼠标选中场景中的对象。
当我们在屏幕上点击鼠标时,会发生如下事情:
-
首先,基于屏幕上点击位置会创建一个THREE.Vecor3向量。
-
接着,使用vector.unproject方法将屏幕上的点击位置转换成Threejs场景中的坐标。
-
然后,创建THREE.Raycaster。使用它可以向场景中发射光线。
-
最后,我们使用raycaster.intersectObjects方法来判断指定的对象中哪些被该光线照射到了。其结果包含了所有被照射到的对象的信息,这些信息包括:
distance: 50.66666666666 // 从摄像机到被点击对象的距离 face: THREE.Face3 // 那个面被点击了 faceIndex: 6 // 被点击面的下标 object: THREE.Mesh // 被点击的网格对象 point: THREE.Vector3 // 网格对象上哪个点被点击了
<!-- chapter-09-01.html -->
<!DOCTYPE html>
<html>
<head>
<title>Selecting objects</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<script type="text/javascript" src="../libs/Projector.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var renderer = new THREE.WebGLRenderer();
renderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
renderer.setSize(window.innerWidth, window.innerHeight);
var planeGeometry = new THREE.PlaneGeometry(60, 20, 1, 1);
var planeMaterial = new THREE.MeshLambertMaterial({color: 0xffffff});
var plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -0.5 * Math.PI;
plane.position.x = 15;
plane.position.y = 0;
plane.position.z = 0;
scene.add(plane);
var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
var cubeMaterial = new THREE.MeshLambertMaterial({color: 0xff0000});
var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.position.x = -9;
cube.position.y = 3;
cube.position.z = 0;
scene.add(cube);
var sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
var sphereMaterial = new THREE.MeshLambertMaterial({color: 0x7777ff});
var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.x = 20;
sphere.position.y = 0;
sphere.position.z = 2;
scene.add(sphere);
var cylinderGeometry = new THREE.CylinderGeometry(2, 2, 20);
var cylinderMaterial = new THREE.MeshLambertMaterial({color: 0x77ff77});
var cylinder = new THREE.Mesh(cylinderGeometry, cylinderMaterial);
cylinder.position.set(0, 0, 1);
scene.add(cylinder);
camera.position.x = -30;
camera.position.y = 40;
camera.position.z = 30;
camera.lookAt(scene.position);
var ambientLight = new THREE.AmbientLight(0x0c0c0c);
scene.add(ambientLight);
var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(-40, 60, -10);
scene.add(spotLight);
document.getElementById("WebGL-output").appendChild(renderer.domElement);
var tube;
var controls = new function () {
this.showRay = false;
};
var gui = new dat.GUI();
gui.add(controls, 'showRay').onChange(function (e) {
if (tube) scene.remove(tube)
});
function render() {
stats.update();
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
// 监控鼠标按下和鼠标移动事件
document.addEventListener('mousedown', onDocumentMouseDown, false);
document.addEventListener('mousemove', onDocumentMouseMove, false);
function onDocumentMouseDown(event) {
// 1.鼠标点击位置
var vector = new THREE.Vector3(( event.clientX / window.innerWidth ) * 2 - 1, -( event.clientY / window.innerHeight ) * 2 + 1, 0.5);
// 2.把鼠标点击位置转换为threejs场景中的坐标
vector = vector.unproject(camera);
// 3.从摄像机位置向点击位置发射一条光线
var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
// 4. 获取指定数组里对象哪些被光线照射到了
var intersects = raycaster.intersectObjects([sphere, cylinder, cube]);
if (intersects.length > 0) {
console.log(intersects[0]);
intersects[0].object.material.transparent = true;
intersects[0].object.material.opacity = 0.5;
}
}
function onDocumentMouseMove(event) {
// 是否显示HREE.Raycaster射线
if (controls.showRay) {
var vector = new THREE.Vector3(( event.clientX / window.innerWidth ) * 2 - 1, -( event.clientY / window.innerHeight ) * 2 + 1, 0.5);
vector = vector.unproject(camera);
var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
var intersects = raycaster.intersectObjects([sphere, cylinder, cube]);
if (intersects.length > 0) {
var points = [];
points.push(new THREE.Vector3(-30, 39.8, 30));
points.push(intersects[0].point);
var mat = new THREE.MeshBasicMaterial({color: 0xff0000, transparent: true, opacity: 0.6});
var tubeGeometry = new THREE.TubeGeometry(new THREE.SplineCurve3(points), 60, 0.001);
if (tube) scene.remove(tube);
tube = new THREE.Mesh(tubeGeometry, mat);
scene.add(tube);
}
}
}
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>
下面回归正题,我们重新回到动画上来。
二、使用Tween.js实现动画
Tween.js是一个轻量级的javascript库 https://github.com/tweenjs/tween.js,通过这个库可以很容易实现某个属性在两个值之间进行过渡,我们称这种为补间动画。官方提供了很多示例,自行去查看。
<!-- chapter-09-02.html -->
<!DOCTYPE html>
<html>
<head>
<title>Animation tween </title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/ColladaLoader.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<script type="text/javascript" src="../libs/tween.min.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xcccccc, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;
camera.position.x = 150;
camera.position.y = 150;
camera.position.z = 150;
camera.lookAt(new THREE.Vector3(0, 20, 0));
var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(150, 150, 150);
spotLight.intensity = 2;
scene.add(spotLight);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
var mesh;
var loader = new THREE.ColladaLoader();
loader.load("../assets/models/dae/Truck_dae.dae", function (result) {
mesh = result.scene.children[0].children[0].clone();
//mesh.scale.set(4, 4, 4);
scene.add(mesh);
// 补间动画要在模型加载完后才能开始
tween.start();
});
// 使用补间动画, 在y轴上下缓动移动
// http://sole.github.io/tween.js/examples/03_graphs.html
var posSrc = {pos: 10};
var tween = new TWEEN.Tween(posSrc).to({pos: 0}, 1000);
tween.easing(TWEEN.Easing.Sinusoidal.InOut);
var tweenBack = new TWEEN.Tween(posSrc).to({pos: 10}, 1000);
tweenBack.easing(TWEEN.Easing.Sinusoidal.InOut);
// 衔接两个动作,循环执行
tween.chain(tweenBack);
tweenBack.chain(tween);
// 执行后坐标更新
var onUpdate = function () {
var pos = this.pos;
mesh.position.y = pos;
};
tween.onUpdate(onUpdate);
tweenBack.onUpdate(onUpdate);
function render() {
stats.update();
// 更新补间动画
TWEEN.update();
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>
三、使用摄像机实现动画
前面我们已经接触过了,可以通过移动摄像机的位置让整个场景动起来,我们是通过手动改变摄像机位置实现的。Threejs还提供了其它更新摄像机的控件,下面我们依次看看。
最常用的:
其它的本节不会介绍,使用和上面一样,了解下:
1. 轨迹球控制器
下面是使用这个控制器,通过鼠标左键拖动、滚轮、右键拖动分别实现场景的旋转、缩放、平移的示例。
<!-- chapter-09-03.html -->
<!DOCTYPE html>
<html>
<head>
<title>Trackball controls </title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/OBJLoader.js"></script>
<script type="text/javascript" src="../libs/MTLLoader.js"></script>
<script type="text/javascript" src="../libs/OBJMTLLoader.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<script type="text/javascript" src="../libs/chroma.js"></script>
<script type="text/javascript" src="../libs/TrackballControls.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0x000, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;
camera.position.x = 100;
camera.position.y = 100;
camera.position.z = 300;
camera.lookAt(new THREE.Vector3(0, 0, 0));
var ambientLight = new THREE.AmbientLight(0x383838);
scene.add(ambientLight);
var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(300, 300, 300);
spotLight.intensity = 1;
scene.add(spotLight);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
// 创建轨迹球控制器,并绑定摄像机
var trackballControls = new THREE.TrackballControls(camera);
// 想要全面了解这些属性可以参考rackballControls.js源文件
trackballControls.rotateSpeed = 1.0;
trackballControls.zoomSpeed = 1.0;
trackballControls.panSpeed = 1.0;
//trackballControls.noZoom=false;
//trackballControls.noPan=false;
trackballControls.staticMoving = true;
//trackballControls.dynamicDampingFactor=0.3;
// 加载OBJ/MTL模型
var mesh;
var loader = new THREE.OBJMTLLoader();
var texture = THREE.ImageUtils.loadTexture('../assets/textures/Metro01.JPG');
//texture.wrapS = texture.wrapT = THREE.MirroredRepeatWrapping;
loader.load('../assets/models/city.obj', '../assets/models/city.mtl', function (object) {
var scale = chroma.scale(['red', 'green', 'blue']);
setRandomColors(object, scale);
mesh = object;
scene.add(mesh);
});
//texture.repeat.set( 1 , 1);
function setRandomColors(object, scale) {
var children = object.children;
if (children && children.length > 0) {
children.forEach(function (e) {
setRandomColors(e, scale)
});
} else {
if (object instanceof THREE.Mesh) {
object.material.color = new THREE.Color(scale(Math.random()).hex());
if (object.material.name.indexOf("building") == 0) {
object.material.emissive = new THREE.Color(0x444444);
object.material.transparent = true;
object.material.opacity = 0.8;
}
}
}
}
var clock = new THREE.Clock();
function render() {
stats.update();
var delta = clock.getDelta();
trackballControls.update(delta); // delta距离上次调用时间间隔
requestAnimationFrame(render);
webGLRenderer.render(scene, camera)
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>
2. 飞行控制器
可行像飞行视角一样在场景中飞行。
// 创建飞行控制器
var flyControls = new THREE.FlyControls(camera);
flyControls.movementSpeed = 25;
flyControls.domElement = document.querySelector("#WebGL-output");
flyControls.rollSpeed = Math.PI / 24;
flyControls.autoForward = true;
flyControls.dragToLook = false;
// 更新
var clock = new THREE.Clock();
function render() {
var delta = clock.getDelta();
flyControls.update(delta);
webGLRenderer.clear();
requestAnimationFrame(render);
webGLRenderer.render(scene, camera)
}
3. 翻滚控制器
翻滚场景(Q/E 左/右翻滚)。
// 创建翻滚控制器
var rollControls = new THREE.RollControls(camera);
rollControls.movementSpeed = 25;
rollControls.lookSpeed = 3;
// 更新
var clock = new THREE.Clock();
function render() {
var delta = clock.getDelta();
rollControls.update(delta);
webGLRenderer.clear();
requestAnimationFrame(render);
webGLRenderer.render(scene, camera)
}
4. 第一视角控制器
类似第一视角射击游戏那样控制摄像机。鼠标用于控制视角,键盘用于控制角色移动。
// 创建第一视角控制器
var camControls = new THREE.FirstPersonControls(camera);
camControls.lookSpeed = 0.4;
camControls.movementSpeed = 20;
camControls.noFly = true;
camControls.lookVertical = true;
camControls.constrainVertical = true;
camControls.verticalMin = 1.0;
camControls.verticalMax = 2.0;
camControls.lon = -150;
camControls.lat = 120;
// 更新
var clock = new THREE.Clock();
function render() {
var delta = clock.getDelta();
rollControls.update(delta);
webGLRenderer.clear();
requestAnimationFrame(render);
webGLRenderer.render(scene, camera)
}
5. 轨道控制器
和轨迹控制器类似,只是轨道控制器强制摄像头 up
方向,而轨迹控制器允许相机倒置旋转。可以用于控制场景中的对象围绕场景中心旋转和平移。
<!-- chapter-09-04.html -->
<!DOCTYPE html>
<html>
<head>
<title>Orbit controls</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<script type="text/javascript" src="../libs/OrbitControls.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0x000, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;
// 创建球体网格对象
var sphere = createMesh(new THREE.SphereGeometry(20, 40, 40));
scene.add(sphere);
camera.position.x = -20;
camera.position.y = 30;
camera.position.z = 40;
camera.lookAt(new THREE.Vector3(00, 0, 0));
var ambiLight = new THREE.AmbientLight(0x111111);
scene.add(ambiLight);
var spotLight = new THREE.DirectionalLight(0xffffff);
spotLight.position.set(-20, 30, 40);
spotLight.intensity = 1.5;
scene.add(spotLight);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
// 使用外部纹理材质创建球体网格
function createMesh(geom) {
var planetTexture = THREE.ImageUtils.loadTexture("../assets/textures/planets/mars_1k_color.jpg");
var normalTexture = THREE.ImageUtils.loadTexture("../assets/textures/planets/mars_1k_normal.jpg");
// 加载外部图片纹理创建Phong材质
var planetMaterial = new THREE.MeshPhongMaterial({map: planetTexture, bumpMap: normalTexture});
var mesh = THREE.SceneUtils.createMultiMaterialObject(geom, [planetMaterial]);
return mesh;
}
// 创建轨道控制器
var orbitControls = new THREE.OrbitControls(camera);
orbitControls.autoRotate = true;
var clock = new THREE.Clock();
function render() {
stats.update();
var delta = clock.getDelta();
orbitControls.update(delta);
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>
四、高级动画
1. 关键帧动画
就是从动画开始到动画结束之间所有的顶点数据都会被存储下来,你需要做的就是定义好关键位置,并重复该过程。这种主要一个缺点就是对于大型网格和大型动画,模型文件会变得非常大。
Threejs库提供的使用关键字动画对象:THREE.MorphAnimMesh
。
<!-- chapter-09-05.html -->
<!DOCTYPE html>
<html>
<head>
<title>Working with morph targets</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 2000);
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;
camera.position.x = 250;
camera.position.y = 250;
camera.position.z = 350;
camera.lookAt(new THREE.Vector3(100, 50, 0));
var spotLight = new THREE.DirectionalLight(0xffffff);
spotLight.position.set(300, 200, 300);
spotLight.intensity = 1;
scene.add(spotLight);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
var controls = new function () {
this.keyframe = 0; // 关键帧位置
};
var gui = new dat.GUI();
gui.add(controls, "keyframe", 0, 15).step(1).onChange(function (e) {
showFrame(e);
});
var meshAnim;
var frames = [];
var currentMesh;
var loader = new THREE.JSONLoader();
// 1. 加载帧动画模型
loader.load('../assets/models/horse.js', function (geometry, mat) {
// 创建左边静态马网格
var mat = new THREE.MeshLambertMaterial(
{
morphTargets: true, // 设置为true网格才会动
vertexColors: THREE.FaceColors
});
var mesh = new THREE.Mesh(geometry, mat);
mesh.position.x = -100;
frames.push(mesh);
currentMesh = mesh;
morphColorsToFaceColors(geometry); // 修改各面颜色
// 创建动画所有关键帧网格
var mat2 = new THREE.MeshLambertMaterial(
{color: 0xffffff, vertexColors: THREE.FaceColors});
// 遍历动画所有帧,并创建为网格对象添加到frames数值中
mesh.geometry.morphTargets.forEach(function (e) {
var geom = new THREE.Geometry();
geom.vertices = e.vertices;
geom.faces = geometry.faces;
var morpMesh = new THREE.Mesh(geom, mat2);
frames.push(morpMesh);
morpMesh.position.x = -100;
});
// 确保运动时光照、阴影和颜色的准确性
geometry.computeVertexNormals();
geometry.computeFaceNormals();
geometry.computeMorphNormals();
// 2. THREE.MorphAnimMesh创建帧动画网格,右边运动马
meshAnim = new THREE.MorphAnimMesh(geometry, mat);
meshAnim.duration = 1000;
meshAnim.position.x = 200;
meshAnim.position.z = 0;
scene.add(meshAnim);
// 用第一帧创建右边的静态马
showFrame(0);
}, '../assets/models');
// 手的改变帧播放
function showFrame(idx) {
scene.remove(currentMesh);
scene.add(frames[idx]);
currentMesh = frames[idx];
console.log(currentMesh);
}
function morphColorsToFaceColors(geometry) {
if (geometry.morphColors && geometry.morphColors.length) {
var colorMap = geometry.morphColors[0];
for (var i = 0; i < colorMap.colors.length; i++) {
geometry.faces[i].color = colorMap.colors[i];
geometry.faces[i].color.offsetHSL(0, 0.3, 0);
}
}
}
var clock = new THREE.Clock();
function render() {
stats.update();
var delta = clock.getDelta();
webGLRenderer.clear();
if (meshAnim) {
// 3. 帧动画更新
meshAnim.updateAnimation(delta * 1000);
meshAnim.rotation.y += 0.01;
}
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>
2. 骨骼动画
就是定义好网格的骨骼,并将顶点绑定到特定的骨骼上。当骨骼运动时,相连骨骼也会随着一起变换,同时骨骼上绑定的顶点也会随之移动。
Threejs库提供的创建骨骼网格的对象:THREE.SkinnedMesh
。下面是一个手动变换骨骼对象的示例:
<!-- chapter-09-06.html -->
<!DOCTYPE html>
<html>
<head>
<title>Load blender model </title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<script type="text/javascript" src="../libs/tween.min.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;
camera.position.x = 0;
camera.position.y = 0;
camera.position.z = 4;
camera.lookAt(new THREE.Vector3(0, 0, 0));
var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(0, 50, 30);
spotLight.intensity = 2;
scene.add(spotLight);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
var mesh;
var loader = new THREE.JSONLoader();
// 1. 加载Blender导出的骨骼模型
loader.load('../assets/models/hand-1.js', function (geometry, mat) {
var mat = new THREE.MeshLambertMaterial({color: 0xF0C8C9, skinning: true}); // skinning要设置为true,否则骨骼不会动
// 2. 创建骨骼模型网格
mesh = new THREE.SkinnedMesh(geometry, mat);
mesh.rotation.x = 0.5 * Math.PI;
mesh.rotation.z = 0.7 * Math.PI;
scene.add(mesh);
// 开始运行
tween.start();
}, '../assets/models');
// 3. 手动改变骨骼变换
var onUpdate = function () {
var pos = this.pos;
console.log(mesh.skeleton);
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);
};
var tween = new TWEEN.Tween({pos: -1})
.to({pos: 0}, 3000)
.easing(TWEEN.Easing.Cubic.InOut) // 缓动
.yoyo(true) // 设置为true,使得动画运动完后反着运行
.repeat(Infinity) // 重复运行
.onUpdate(onUpdate);
var clock = new THREE.Clock();
function render() {
stats.update();
TWEEN.update();
var delta = clock.getDelta();
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>
五、使用外部模型创建动画
1. 使用Blender创建骨骼动画
Blender使用threejs导出器,把.blend
动画导出为.json
,然后加载并播放动画。
播放外部骨骼动画使用:THREE.Animation
。
<!-- chapter-09-07.html -->
<!DOCTYPE html>
<html>
<head>
<title>Animation from blender</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;
camera.position.x = 0;
camera.position.y = 0;
camera.position.z = 4;
camera.lookAt(new THREE.Vector3(0, 0, 0));
var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(0, 50, 30);
spotLight.intensity = 2;
scene.add(spotLight);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
var mesh;
var helper;
var controls = new function () {
this.showHelper = false; // 是否显示骨骼骨架
};
var gui = new dat.GUI();
gui.add(controls, 'showHelper').onChange(function (state) {
helper.visible = state;
});
var loader = new THREE.JSONLoader();
// 加载骨骼动画模型
loader.load('../assets/models/hand-2.js', function (model, mat) {
var mat = new THREE.MeshLambertMaterial({color: 0xF0C8C9, skinning: true});
mesh = new THREE.SkinnedMesh(model, mat);
mesh.rotation.x = 0.5 * Math.PI;
mesh.rotation.z = 0.7 * Math.PI;
scene.add(mesh);
// 创建骨骼动画对象,并开始播放
var animation = new THREE.Animation(mesh, model.animation);
animation.play();
// 创建骨骼骨架
helper = new THREE.SkeletonHelper(mesh);
helper.material.linewidth = 2;
helper.visible = false;
scene.add(helper);
}, '../assets/models');
var clock = new THREE.Clock();
function render() {
stats.update();
var delta = clock.getDelta();
if (mesh) {
// 更新骨骼动画
helper.update();
THREE.AnimationHandler.update(delta);
}
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>
2. 从Collada模型加载动画
使用方式和上面一样,只是加载模型文件格式变了。
<!-- chapter-09-08.html -->
<!DOCTYPE html>
<html>
<head>
<title>Animation from collada</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<script type="text/javascript" src="../libs/ColladaLoader.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;
camera.position.x = 400;
camera.position.y = 50;
camera.position.z = 150;
camera.lookAt(new THREE.Vector3(0, 0, 0));
var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(300, 500, 100);
spotLight.intensity = 3;
scene.add(spotLight);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
var meshAnim;
// 加载.dae模型
var loader = new THREE.ColladaLoader();
loader.load('../assets/models/monster.dae', function (collada) {
// 模型第一帧网格添加进场景
var child = collada.skins[0];
scene.add(child);
child.scale.set(0.15, 0.15, 0.15);
child.rotation.x = -0.5 * Math.PI;
child.position.x = -100;
child.position.y = -60;
// 创建动画并开始播放
var animation = new THREE.Animation(child, child.geometry.animation);
animation.play();
});
var clock = new THREE.Clock();
function render() {
stats.update();
var delta = clock.getDelta();
// 动画更新
THREE.AnimationHandler.update(delta);
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>
3. 其它
整的来说动画就两类:帧动画和骨骼动画,使用方法都差不多。其它动画文件格式加载和使用可以查看官方示例。