正常的项目中,大家的需求都是不一样的,又通常会碰上需求中途改变的情况,我们之前做的简易版相机控制器很难满足此类项目对相机的操作需求。而且,造轮子的前提是当前的框架以及插件已经无法满足自身的需求时,才会考虑造轮子。要不然,项目的进度会被拖得很慢,甚至有可能因此而错过红利期。
好在 Three.js 官方和同道中的朋友们给我们提供了很多相关的插件,我们可以根据需求引入相关的插件来实现需求,本文我们就来看一下官方案例中提供的相机控制器。
请点击查看这里
,在这里不多说。
DeviceOrientationControls 的内容配置较少,我们先看一下案例。
使用手机打开网址:
点击这里
,然后手机朝下然后移动,你会发现能够通过手机的转向来控制相机的朝向,是不是很神奇。接下来我们看看如何引入到项目中。
首先,将插件文件引入到项目中:
1
<script src ="../js/DeviceOrientationControls.js" > </script >
然后,通过相机对象实例化相机:
1
control = new THREE.DeviceOrientationControls(camera)
最后,在每一帧渲染里面更新相机的位置:
1 2 3 4
function render () { control .update(); renderer .render (scene, camera ); }
这样我们就完成了对 DeviceOrientationControls 控制器的添加。
DeviceOrientationControls 控制器相关的配置也很少,只有一个 Enabled 属性,设置为 True,则控制器会更新相机的位置,反之,设置 False 将无法更新相机位置。
还有一个方法就是销毁当前控制器的方法:
1
controls.dispose(); // 销毁当前控制器
最后,附上源码:
请点击这里
。
案例的右上角有四个点击事件:
添加模型:将在场景内随机生成一组立方体,每次都不相同。
导出模型:将场景内这一组立方体导出到本地 JSON 文件。
导入模型:可以选择将符合 JSON 文件作解析并导入到场景内。
加载模型:将加载服务器上面的一个 JSON 文件。
案例代码地址:
请点击这里
。
点击这里
。
我们可以在这里下载一些免费的 glTF 格式的模型。
我在官网上找了个不错的模型做了一个案例,
点击这里查看
。
模型加载的速度会有些慢,大家可以等待下便能够看到这个小汽车。
接下来我们便讲解一下加载 glTF 模型的流程。
首先,将 GLTFLoader 加载器插件引入到页面,插件在官方包的
/examples/js/loaders/
文件夹中,一些文件的导入插件都在这个文件夹内,大家有兴趣可以研究一下:
1
<script src ="../js/loaders/GLTFLoader.js" > </script >
然后创建一个加载器:
1
var loader = new THREE .GLTFLoader();
使用加载器加载模型,并调节一下模型大小在场景内展示:
1 2 3 4
loader.load ('../js/models/gltf/scene .gltf', function (gltf) { gltf.scene .scale .set(.1 ,.1 ,.1 ); scene .add(gltf.scene ); });
有时候我们可能不明白,我加载了一个模型,哪一部分是需要导入场景的模型呢?
这里我们可以先将解析的出来的模型对象打印一下,然后通过查看对象属性来了解导入场景内的对象,就比如 glTF 模型转换出来的对象的 scene 属性就是需要导入场景的对象,而 JSON 格式的模型是直接可以导入的对象。
模型加载案例源码:
请点击这里
。
请点击这里查看
。
接下来,我们看下它的实现过程。
首先我们需要导入 FBXLoader 插件,并且还需要额外增加一个解析二进制文件的插件
inflate.min.js
,不导入该文件的话,除了一些字符串存储的 FBX 格式,别的格式都会报错:
1 2
<script src ="../js/loaders/inflate.min.js" > </script > <script src ="../js/loaders/FBXLoader.js" > </script >
创建 FBX 加载器:
1
var loader = new THREE .FBXLoader();
修改模型大小,并设置每个模型网格可以投射阴影:
1 2 3 4 5 6 7 8 9 10
loader.load('../js/models/fbx/file.fbx' , function (fbx) { fbx.scale.set(.1 ,.1 ,.1 ); fbx.traverse(function (item ) { if (item instanceof THREE.Mesh){ item .castShadow = true ; item .receiveShadow = true ; } }); scene.add(fbx); });
这样就实现了 FBX 模型的导入。
案例源码地址:
请点击这里
。
请点击这里
。
我们看下实现导入的过程。
首先,我们需要将 OBJLoader 插件和 MTLLoader 插件引入页面:
1 2
<script src ="../js/loaders/OBJLoader.js" > </script > <script src ="../js/loaders/MTLLoader.js" > </script >
实例化 MTLLoader :
1 2 3 4
var mtlLoader = new THREE.MTLLoader() ; mtlLoader.setPath('../ js / models / obj / ') ;
如果有需要,我们还可以设置纹理文件夹地址:
1 2
mtlLoader.setTexturePath('../ js / models / obj / ') ;
加载 MTL 文件,并在文件加载成功后,创建 OBJLoader 并设置对象应用当前的材质:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
mtlLoader.load('female02.mtl' , function (material ) { var objLoader = new THREE.OBJLoader(); objLoader.setMaterials(material); objLoader.setPath('../js/models/obj/' ); objLoader.load('female02.obj' , function (object ) { object .traverse(function (item ) { if (item instanceof THREE.Mesh){ item.castShadow = true ; item.receiveShadow = true ; } }); object .scale.set(.3 ,.3 ,.3 ); scene.add(object ); }) });
我们再去加载 OBJ 文件,加载成功的文件就是可以导入到场景内的 3D 对象。
案例源码查看地址:
请点击这里
。
请点击这里
。
我们看下实现步骤。
首先引入 ColladaLoader 插件:
1
<script src ="../js/loaders/ColladaLoader.js" > </script >
接着实例化 ColladaLoader 对象:
1
var loader = new THREE .ColladaLoader();
最后加载文件并调整文件大小,添加到场景内:
1 2 3 4 5 6 7 8 9 10 11 12 13
loader.load('../js/models/collada/elf.dae' , function (collada) { collada.scene.traverse(function (item) { if (item instanceof THREE.Mesh){ item.castShadow = true ; item.receiveShadow = true ; } }); collada.scene.scale.set(5 ,5 ,5 ); scene.add(collada.scene); });
案例源码查看:
请点击这里
。
点击这里
。
在这个案例的右上角,我们能发现两个可切换的拖拽条。这两个拖拽条对应的是两个变形目标数组,拖拽范围是0-1,即当前的变形目标对本体的影响程度。拖拽它们,可发现界面中的立方体也会跟随之变动,从而影响当前的立方体。接下来我讲解一下,该案例的实现过程。
首先,创建模型的几何体,并为几何体 morphTargets 赋值两个变形目标。morphTargets 是一个数组,我们可以为其增加多个变形目标。在给 morphTargets 添加变形目标时,需要为其定义一个名称和相关的顶点,这个顶点数据必须和默认的模型的顶点数据保持一致,设置完后,我们需要调用 geometry 的
computeMorphNormals()
进行更新,代码如下:
1 2 3 4 5 6 7 8 9 10
var cubeGeometry = new THREE .BoxGeometry(4 , 4 , 4 );var cubeTarget1 = new THREE .BoxGeometry(2 , 10 , 2 );var cubeTarget2 = new THREE .BoxGeometry(8 , 2 , 8 ); cubeGeometry.morphTargets[0 ] = {name: 'target1', vertices: cubeTarget1.vertices}; cubeGeometry.morphTargets[1] = {name: ' target2', vertices: cubeTarget2.vertices}; cubeGeometry.computeMorphNormals();
然后,为当前模型设置材质,变形目标作为参数之一,可以使其变形。
1
var cubeMaterial = new THREE.MeshLambertMaterial({morphTargets : true , color : 0x00ffff}) ;
接着,将创建好的网格模型添加到场景中。这时可以在 mesh 对象中找到 morphTargetInfluences 配置项,它也是一个数组,和 geometry 的 morphTargets 相对应,主要用来设置当前变形目标对本体的影响度,默认值为0-1,0为不影响本体,1为完全影响本体:
1 2 3 4 5 6 7 8
gui = { influence1:0.01 , influence2:0.01 , update : function () { cube .morphTargetInfluences[0 ] = gui.influence1; cube .morphTargetInfluences[1 ] = gui.influence2; } };
至此,我们就手动实现了一个变形动画。在这个过程中,我们发现,变形动画是由于不断修改变形目标对本体的影响度而产生的。我们可以通过这个原理实现其他变形动画。
案例代码查看地址:
请点击这里
。
点击这里
这是官方提供的一个案例。我对其做了些简单修改,以显示出当前一个柱形图形的骨骼。实现起来比较复杂,我们需要先理解它是怎么实现的。
首先, 我们创建了一个圆柱几何体,通过圆柱的几何体每个顶点的 y 轴坐标位置来设置绑定的骨骼的下标和影响的程度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
//遍历几何体所有的顶点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 属性分别用来设置绑定的骨骼下标和权重(骨骼影响程度)。
相应的,我们需要一组相关联的骨骼。骨骼具有嵌套关系,才得以实现一个骨架。圆柱体比较简单,我们直接创建一条骨骼垂直嵌套的骨骼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
bones = [] var prevBone = new THREE.Bone() bones.push(prevBone) prevBone.position.y = -sizing.halfHeight for (var i = 0 var bone = new THREE.Bone() bone.position.y = sizing.segmentHeight bones.push(bone) prevBone.add(bone) prevBone = bone }
创建纹理时,我们还需要设置当前材质属性,并开启骨骼动画对其的修改权限,将材质的 skinning 属性设置为 true:
1 2 3 4
var lineMaterial = new THREE.MeshBasicMaterial({ skinning: true , wireframe : true });
最后,我们需要创建骨骼材质,并将模型绑定骨骼:
1 2 3 4
mesh = new THREE.SkinnedMesh(geometry , [material , lineMaterial ]) ; var skeleton = new THREE.Skeleton(bones ) ; mesh.add(bones[0 ] ); mesh.bind(skeleton);
这样,我们就使用 Three.js 创建了一个简单的骨骼动画。使用
dat.gui
,便于我们修改每一个骨骼的 poisition、rotation 和 scale 并查看对当前模型的影响。
案例的源码地址:
点击这里
。
点击这里
。
接下来我们看一下这匹马是如何实现的。
在模型加载成功以后,我们首先将模型创建出来,并将材质的 morphTargets 设置为 ture,使顶点数据信息可以受变形动画影响:
1 2 3 4 5 6 7
mesh = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ vertexColors: THREE.FaceColors, morphTargets: true }));mesh .castShadow = true ;mesh .scale .set(0.1 , 0.1 , 0.1 );scene .add(mesh );
然后我们创建了一个针对于该模型的混合器:
1
mixer = new THREE.AnimationMixer(mesh)
接着使用变形目标数据创建一个动画片段:
1
var clip = THREE.AnimationClip.CreateFromMorphTargetSequence('gallop ', geometry .morphTargets , 30) ;
使用混合器和动画片段创建一个动画播放器来播放:
1 2 3
var action = mixer.clipAction(clip); // 创建动画播放器 action.setDuration(1 ); // 设置当前动画一秒为一个周期 action.play(); // 设置当前动画播放
最后,我们还需要在重新绘制循环中更新混合器,进行动作更新:
1 2 3 4 5 6 7 8 9 10 11 12
function render () { control .update(); var time = clock.getDelta(); if (mixer) { mixer.update(time); } renderer .render (scene, camera ); }
点击这里
。
gltf 格式的模型导入进来后,我们可以直接通过 animations 数组创建播放器:
1 2
mixer = new THREE.AnimationMixer(obj ) ; action = mixer.clipAction(gltf .animations [0]) ;
直接调用播放器的播放事件让动画播放:
最后,我们还需要在循环渲染中更新混合器,并将每一帧渲染的间隔时间传入:
1 2 3 4 5 6 7 8
function render () { control.update(); var time = clock .getDelta(); if (mixer) { mixer.update(time ); } renderer.render(scene, camera); }
点击这里
。
案例代码查看地址:
点击这里
。
本案例的开发思路是:首先获取目标模型的初始位置,然后实例化 Tween,接着设置目标位置,启动 Tween,在 TWEEN.onUpdate() 回调中改变目标模型的位置,从而实现目标模型从初始位置平滑移动到目标位置的动画。
实现代码如下:
1 2 3 4 5 6 7 8 9
var position = {x:-40 , y:0 , z:-30 }; tween = new TWEEN.Tween (position); tween.to ({x:40 , y:30 , z:30 }, 2000 ); tween.onUpdate (function (pos) { cube.position.set (pos.x, pos.y, pos.z); });
上面代码,首先创建一个 position 对象,存储了当前立方体的位置数据。然后,通过当前的对象创建了一个补间 tween。紧接着,设置每一个属性的目标位置,并告诉 Tween 在 2000 毫秒(动画时长)内移动到目标位置。最后,设置 Tween 对象每次更新的回调,即在每次数据更新以后,将立方体位置更新。
Tween 对象不会直接执行,需要我们调用
start()
方法激活,即
tween.start()
。
1 2 3 4 5 6
gui = { start:function () { tween.start(); } };
想要完成整个过程,我们还需要在每帧里面调用
TWEEN.update
,来触发 Tween 对象更新位置:
1 2 3 4 5 6 7 8 9
function render () { TWEEN.update(); control .update(); renderer .render (scene, camera ); }
点击这里
案例代码查看地址:
点击这里
调用方法为:
或者,采用一个无限的链式,即 tweenA 与 tweenB 无限循环,便可以写成:
1 2
tweenA.chain(tweenB) tweenB.chain(tweenA)
在其他情况下,您可能需要将多个补间链接到另一个补间,以使它们(链接的补间)同时开始动画:
1
tweenA .chain (tweenB,tweenC);
警告:调用
tweenA.chain(tweenB)
实际上修改的是 tweenA,tweenB 总在 tweenA 完成时启动。
chain()
的返回值只是 tweenA,不是一个新的 Tween。
链接多个补间时,比如
tweenA.chain(tweenB, tweenC)
表示 tweenA 动画结束后,tweenB 和 tweenC 动画同时开始,如果因 tweenB 和 tweenC 修改的属性相同,而存在冲突时,经测试写在后面的属性将是最终动画位置。
注意,一般不要让两个同时开始的补间存在属性冲突。
.repeat()
如果想让一个补间永远重复,可以无限链接自己,但更好的方法是使用
repeat()
方法。它接受一个参数,描述第一个补间完成后需要重复多少次,如下代码示例:
1 2
tween.repeat(10 ); // 循环10 次 tween.repeat(Infinity); // 无限循环
我们可以将案例中
simple.html
文件里面的调用改成无限循环,代码如下:
1
tween = new TWEEN.Tween(cube .position).to ({x:40 , y:30 , z:30 }, 2000 ).repeat(Infinity );
.yoyo()
该方法只有在补间使用
repeat()
方法时才会被调用。我们调用
yoyo()
以后,位置的切换就变成了从头到尾,从尾到头这样的循环过程。
单个补间无限从头到尾的循环,可以写成这样:
1
tween = new TWEEN.Tween(cube .position).to ({x:40 , y:30 , z:30 }, 2000 ).repeat(Infinity ).yoyo(true );
进一步了解
yoyo()
方法的使用,可查看下面案例。
案例 Demo 查看地址:
点击这里
。
案例代码查看地址:
点击这里
。
.delay()
这个方法用于控制激活前的延时,即触发
start()
事件后,需要延时到设置的 delay 时间,才会真正激活,使用方法见下面代码所示:
1 2
tween.delay(1000 ) tween.start()
1
new THREE.Raycaster( origin , direction , near , far ) ;
点击这里
。
案例源码地址:
点击这里
。
点击这里
。
案例源码地址:
点击这里
点击这里
。
在上面案例中,在不选中 combined 的前提下 ,选择 redraw 20000 个模型的话,一般只有十几帧的帧率。但如果选中了 combined,会发现渲染的帧率可达到满帧(60帧),性能得到了巨大提升。
点击这里
查看,这里不再过多解释原理。
接下来我们查看一下 Three.js 封装的四维变换矩阵为我们提供了哪些方法。
1 2 3 4 5 6 7 8 9 10 11 12
var m = new THREE .Matrix4(); m.set ( 11 , 12 , 13 , 14 , 21 , 22 , 23 , 24 , 31 , 32 , 33 , 34 , 41 , 42 , 43 , 44 );var v = new THREE .Vector3(); v.applyMatrix4(m);
上面代码创建了一个默认的四维矩阵,三维矢量乘以默认的四维矩阵将不会产生变化。
我们可以使用
.identity()
方法来重置变换矩阵:
1 2
var m = new THREE .Matrix4(); m.identity();
我们可以通过
.lookAt()
传入一个三个矢量生成一个旋转矩阵。这三个矢量分别代表眼的位置、查看的物体位置和向上的方向:
1 2
var m = new THREE.Matrix4() ; m.lookAt(eye , center , up ) ;
我们也可以通过旋转弧度来生成旋转变换矩阵:
1 2 3 4 5 6 7 8 9 10 11 12
var m = new THREE.Matrix4(); m.makeRotationX(Math.PI /2 ); m.makeRotationAxis(new THREE.Vector3(1 , 0 , 0 ), Math.PI /2 ); m.makeRotationY(Math.PI ); m.makeRotationAxis(new THREE.Vector3(0 , 1 , 0 ), Math.PI ); m.makeRotationZ(Math.PI /4 ); m.makeRotationAxis(new THREE.Vector3(0 , 0 , 1 ), Math.PI /4 );
通过设置缩放来生成缩放变换矩阵:
1 2 3
var m = new THREE.Matrix4 ();m .makeScale(1 , 2 , 5 ); //生成沿x轴不变,y轴放大两倍,z轴放大五倍的缩放变换矩阵
通过设置位置平移来生成变换矩阵:
1 2 3
var m = new THREE.Matrix4 ();m .makeTranslation(10 , -20 , 100 ); //生成沿x正轴偏移10 ,y负轴偏移20 ,z正轴偏移100 的平移变换矩阵