模型加载器
数据导入导出
Threejs文档Object3D等类,提供了一个方法 .toJSON() 通过这个方法可以导出Threejs三维模型的各类数据,该方法的功能就是把Threejs的几何体、材质、光源等对象转化为JSON格式导出。
导出数据
导出一个json文件的过程:
- 从three.js对象提取数据
.toJSON() - 将JSON对象转为字符串
JSON.stringify() - HTML5的文件保存模块
- 导出几何体信息
var geometry = new THREE.BoxGeometry(100, 100, 100);
console.log(geometry);
console.log(geometry.toJSON()); // JSON对象
// JSON对象转化为字符串
console.log(JSON.stringify(geometry.toJSON()));
// JSON.stringify()方法内部会自动调用参数的toJSON()方法
console.log(JSON.stringify(geometry));
- 导出材质信息
var material = new THREE.MeshLambertMaterial({
color: 0x0000ff,
});
console.log(material);
console.log(material.toJSON());
console.log(JSON.stringify(material));
- 导出场景信息,包含模型、光源等等
console.log(scene.toJSON());
导入(加载解析)
加载解析过程:
-
加载json文件
-
字符串转为JSON对象
JSON.parse() -
调用threejs相应的API创建对象
- MaterialLoader 材质
- BufferGeometryLoader 几何体
var loader = new THREE.BufferGeometryLoader(); // 缓冲几何体数据加载器 loader.load('bufferGeometry.json',function (geometry) { console.log(geometry); var material = new THREE.MeshLambertMaterial({ color: 0x0000ff, }); var mesh = new THREE.Mesh(geometry, material); scene.add(mesh); })- AnimationLoader 帧动画
- TextureLoader 纹理
- JSONLoader
- ObjectLoader
- 网格模型对象
- 模型组
- 场景
/** * 网格模型Mesh加载,包含几何体Geometry和材质Material */ var loader = new THREE.ObjectLoader(); loader.load('model.json',function (obj) { console.log(obj); obj.scale.set(100,100,100) scene.add(obj) })
模型加载器
- stl:不包含材质Material信息,只包含几何体顶点数据的信息。
- obj:导出
.obj文件与材质文件.mtl。-
.obj包含几何体顶点相关数据,.mtl包含模型的材质信息。 - 只加载obj文件:没有材质文件,系统自动设置Phong网格材质。
-
- fbx:除了包含几何、材质信息,可以存储骨骼动画等数据。
stl
.stl格式的三维模型不包含材质Material信息,只包含几何体顶点数据的信息,你可以简单地把stl文件理解为几何体对象Geometry。
stl文件数据结构:
solid box //文件名字
//三角面1
facet normal 0 0 -1 //三角形面法向量
outer loop
vertex 50 50 -50 //顶点位置
vertex 50 -50 -50 //顶点位置
vertex -50 50 -50 //顶点位置
endloop
endfacet
//三角面2
...
endsolid
通过STLLoader.js加载.stl文件:
var loader = new THREE.STLLoader();
loader.load('立方体.stl',function (geometry) {
// 加载完成后会返回一个几何体对象BufferGeometry
console.log(geometry);
var material = new THREE.MeshLambertMaterial({
color: 0x0000ff,
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
})
obj
使用三维软件导出.obj模型文件的时候,会同时导出一个材质文件.mtl, .obj和.stl文件包含的信息一样都是几何体顶点相关数据,材质文件.mtl包含的是模型的材质信息,比如颜色、贴图路径等。
obj文件可以包含多个网格模型对象,不一定就是一个,这些网格模型对象全部是并列关系,无法通过父子关系构建一个树结构层级模型。
- 只加载obj文件:没有材质文件,系统自动设置Phong网格材质
var loader = new THREE.OBJLoader();
// 没有材质文件,系统自动设置Phong网格材质
loader.load('./立方体/box.obj',function (obj) {
// 控制台查看返回结构:包含一个网格模型Mesh的组Group
console.log(obj);
// 查看加载器生成的材质对象:MeshPhongMaterial
console.log(obj.children[0].material);
scene.add(obj);
})
- 同时加载obj和mtl文件
var OBJLoader = new THREE.OBJLoader();//obj加载器
var MTLLoader = new THREE.MTLLoader();//材质文件加载器
MTLLoader.load('./立方体/box.mtl', function(materials) {
// 返回一个包含材质的对象MaterialCreator
console.log(materials);
//obj的模型会和MaterialCreator包含的材质对应起来
OBJLoader.setMaterials(materials);
OBJLoader.load('./立方体/box.obj', function(obj) {
console.log(obj);
obj.scale.set(10, 10, 10); //放大obj组对象
scene.add(obj);//返回的组对象插入场景中
})
})
材质文件mtl贴图路径
// 一个包含纹理贴图路径的.mtl文件
newmtl material_1
Ns 32
d 1
Tr 0
Tf 1 1 1
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.9880 0.9880 0.9880
Ks 0.1200 0.1200 0.1200
map_Kd ./贴图/Earth.png
map_ks ./贴图/EarthSpec.png
norm ./贴图/EarthNormal.png
mtl和threejs贴图对应关系:
| mtl贴图 | Threejs贴图 |
|---|---|
| map_kd | map 颜色贴图 |
| map_ks | specularMap 高光贴图 |
| norm | normalMap 法线贴图 |
| map_bump/bump | bumpMap 凹凸贴图 |
3dmax导出的obj和mtl模型文件有时候需要修改一下个别位置字符,比如
.obj中.mtl文件的名称可能是乱码mtllib �����.mtl,.mtl文件中贴图的路径要设置正确,比如导出的是绝对路径,要改为相对路径。
fbx
fbx除了包含几何、材质信息,可以存储骨骼动画等数据。
解析之前可以先在浏览器控制台查看动画相关的数据是如何存储的。你可以看到obj.animations属性的数组包含两个剪辑对象AnimationClip,obj.animations[0]对应剪辑对象AnimationClip包含多组关键帧KeyframeTrack数据,obj.animations[1]对应的剪辑对象AnimationClip没有关键帧数据,也就是说没有关键帧动画。
var mixer=null;//声明一个混合器变量
var loader = new THREE.FBXLoader();//创建一个FBX加载器
loader.load("SambaDancing.fbx", function(obj) {
scene.add(obj)
obj.translateY(-80);
// obj作为参数创建一个混合器,解析播放obj及其子对象包含的动画数据
mixer = new THREE.AnimationMixer(obj);
// 查看动画数据
console.log(obj.animations)
// obj.animations[0]:获得剪辑对象clip
var AnimationAction=mixer.clipAction(obj.animations[0]);
// AnimationAction.timeScale = 1; //默认1,可以调节播放速度
// AnimationAction.loop = THREE.LoopOnce; //不循环播放
// AnimationAction.clampWhenFinished=true;//暂停在最后一帧播放的状态
AnimationAction.play();//播放动画
})
...
// 创建一个时钟对象Clock
var clock = new THREE.Clock();
// 渲染函数
function render() {
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
// 异步
if (mixer !== null) {
//clock.getDelta()方法获得两帧的时间间隔
// 更新混合器相关的时间
mixer.update(clock.getDelta());
}
}
render();
动画
帧动画
Threejs提供了一系列用户编辑和播放关键帧动画的API:
- 编辑
- 关键帧
KeyframeTrack- 位置、颜色等属性随着时间变化
- 离散时间点对应离散属性值
- 剪辑
AnimationClip- 多个关键帧构成一个剪辑clip对象
- 关键帧
- 播放
- 混合器
AnimationMixer- 一个对象及其子对象的动画播放器
- 操作
AnimationAction- 设置播放方式、开始/暂停播放。。。
- 混合器
一般不会手动编辑,都是外部模型编辑好了动画直接播放。
1. 编辑关键帧并解析播放
/* 创建两个网格模型并设置一个父对象group */
var group = new THREE.Group(); //作为网格模型的父对象
// 网格模型1
var geometry1 = new THREE.BoxGeometry(40, 6, 6); //长方体
var material1 = new THREE.MeshLambertMaterial({
color: 0x0000ff
});
var mesh1 = new THREE.Mesh(geometry1, material1);
mesh1.name = "Box"; //网格模型命名
group.add(mesh1); //网格模型添加到组中
// 网格模型2
var geometry2 = new THREE.SphereGeometry(10, 25, 25); //球体
var material2 = new THREE.MeshLambertMaterial({
color: 0xff00ff
});
var mesh2 = new THREE.Mesh(geometry2, material2);
mesh2.name = "Sphere"; //网格模型命名
group.add(mesh2); //网格模型添加到组中
scene.add(group); //组添加到场景中中
/* 编辑group子对象网格模型mesh1和mesh2的帧动画数据 */
// 创建名为Box对象的关键帧数据
var times = [0, 10]; //关键帧时间数组,离散的时间点序列
var values = [0, 0, 0, 150, 0, 0]; // 与时间点对应的值组成的数组
// 创建位置关键帧对象:0时刻对应位置0, 0, 0 10时刻对应位置150, 0, 0
var posTrack = new THREE.KeyframeTrack('Box.position', times, values);
// 创建颜色关键帧对象:10时刻对应颜色1, 0, 0 20时刻对应颜色0, 0, 1
var colorKF = new THREE.KeyframeTrack('Box.material.color', [10, 20], [1, 0, 0, 0, 0, 1]);
// 创建名为Sphere对象的关键帧数据 从0~20时间段,尺寸scale缩放3倍
var scaleTrack = new THREE.KeyframeTrack('Sphere.scale', [0, 20], [1, 1, 1, 3, 3, 3]);
// 剪辑
// duration决定了默认的播放时间,一般取所有帧动画的最大时间
// duration偏小,帧动画数据无法播放完,偏大,播放完帧动画会继续空播放
var duration = 20; // 播放结束时间,而不是播放时长
// 多个帧动画作为元素创建一个剪辑clip对象,命名"default",持续时间20
var clip = new THREE.AnimationClip("default", duration, [posTrack, colorKF, scaleTrack]);
/* 播放编辑好的关键帧数据 */
// group作为混合器的参数,可以播放group中所有子对象的帧动画
var mixer = new THREE.AnimationMixer(group);
// 剪辑clip作为参数,通过混合器clipAction方法返回一个操作对象AnimationAction
var AnimationAction = mixer.clipAction(clip);
//通过操作Action设置播放方式
AnimationAction.timeScale = 20;//默认1,可以调节播放速度
// AnimationAction.loop = THREE.LoopOnce; //不循环播放
AnimationAction.play();//开始播放
...
// 创建一个时钟对象Clock
var clock = new THREE.Clock();
// 渲染函数
function render() {
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
//clock.getDelta()方法获得两帧的时间间隔
// 更新混合器相关的时间
mixer.update(clock.getDelta());
}
render();
2. 解析外部模型的帧动画
var loader = new THREE.ObjectLoader();
var mixer = null; //声明一个混合器变量
// 加载文件返回一个对象obj
loader.load("model.json", function(obj) {
obj.scale.set(15, 15, 15);
scene.add(obj);
// obj作为混合器的参数,可以播放obj包含的帧动画数据
mixer = new THREE.AnimationMixer(obj);
// obj.animations[0]:获得剪辑clip对象
// 剪辑clip作为参数,通过混合器clipAction方法返回一个操作对象AnimationAction
var AnimationAction = mixer.clipAction(obj.animations[0]);
// AnimationAction.loop = THREE.LoopOnce; //不循环播放
// AnimationAction.clampWhenFinished=true;//暂停在最后一帧播放的状态
AnimationAction.play();
});
...
// 创建一个时钟对象Clock
var clock = new THREE.Clock();
// 渲染函数
function render() {
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
// 模型文件的加载是异步的,需要判断混合器是否存在再进行更新
if(mixer!==null){
//clock.getDelta()方法获得两帧的时间间隔
// 更新混合器相关的时间
mixer.update(clock.getDelta());
}
}
render();
3. 播放设置
- 播放/暂停
.paused
// 暂停继续播放函数
function pause() {
if (AnimationAction.paused) {
// 如果是播放状态,设置为暂停状态
AnimationAction.paused = false;
} else {
// 如果是暂停状态,设置为播放状态
AnimationAction.paused = true;
}
}
- 播放clip特定时间段
// 设置播放区间10~18
AnimationAction.time = 10; //操作对象设置开始播放时间
clip.duration = 18; // 剪辑对象设置播放结束时间
AnimationAction.play(); //开始播放
- 定位在某个时间点
// 开始结束时间设置为一样,相当于播放时间为0,直接跳转到时间点对应的状态
AnimationAction.time = 10; //操作对象设置开始播放时间
clip.duration = AnimationAction.time;//剪辑对象设置播放结束时间
AnimationAction.play(); //开始播放
骨骼动画
1. 骨骼动画原理
直接使用Threejs编写一个骨骼动画还是比较复杂的,你首先应该了解骨头关节Bone、骨骼网格模型SkinnedMesh、骨架对象Skeleton这三个骨骼相关的类,除此之外还需要了解几何体Geometry和骨骼动画相关的顶点数据。
-
骨关节Bone
- 树结构:一个父关节可以有多个子关节
- 设置子关节的位置是相对父关节的位置
var Bone1 = new THREE.Bone(); //关节1 var Bone2 = new THREE.Bone(); //关节2 // 通过add方法给一个骨关节对象添加一个子骨关节 // 多个骨头关节构成一个树结构 Bone1.add(Bone2); // 设置关节父子关系:关节1作为根关节 // Bone2是Bone1的子关节,通过Bone1的`children`属性可以访问Bone2 console.log(Bone1.children) // [Bone] // 设置关节之间的**相对位置** // 根关节Bone1默认位置是(0,0,0) Bone2.position.y = 60; // Bone2相对父对象Bone1位置 -
骨架对象Skeleton
THREE.Skeleton([Bone1, Bone2, Bone3]).bones: Array 包含所有骨头关节对象
// 所有Bone对象插入到Skeleton中 var skeleton = new THREE.Skeleton([Bone1, Bone2, Bone3]); //创建骨骼系统 // 查看骨架相关的所有骨关节 console.log(skeleton.bones); // 返回所有关节的世界坐标 skeleton.bones.forEach(elem => { console.log(elem.getWorldPosition(new THREE.Vector3())); }); -
几何体Geometry
.skinWeights: Array[Vector4] 顶点蒙皮权重- 每个顶点最多可以有4个骨骼对象Bone影响它
- 顶点可以被4个骨骼Bone修改,因此Vector4用于表示该顶点的蒙皮权重
- 向量vector分量的值通常应在0和1之间
.skinIndices: Array[Vector4] 顶点蒙皮索引- 索引对应骨架对象的.bones属性的元素索引
几何体
Geometry的属性.skinWeights和.skinIndices主要作用是用来设置几何体的顶点位置是如何受骨关节运动影响的。 -
骨骼网格模型SkinnedMesh
- 骨头Bone构成骨架Skeleton
SkinnedMesh.add(Bone1);添加关节。通过SkinnedMesh的.children属性可以访问骨关节Bone
- 网格模型绑定骨架
SkinnedMesh.bind(skeleton)绑定骨架。通过SkinnedMesh的skeleton属性可以访问骨架Skeleton
SkinnedMesh.add(Bone1); //根骨头关节添加到网格模型 SkinnedMesh.bind(skeleton); //网格模型绑定到骨骼系统 - 骨头Bone构成骨架Skeleton
手动创建一个骨骼网格模型
/**
* 创建骨骼网格模型SkinnedMesh
*/
// 创建一个圆柱几何体,高度120,顶点坐标y分量范围[-60,60]
var geometry = new THREE.CylinderGeometry(5, 10, 120, 50, 300);
geometry.translate(0, 60, 0); //平移后,y分量范围[0,120]
/**
* 设置几何体对象Geometry的蒙皮索引skinIndices、权重skinWeights属性
* 实现一个模拟腿部骨骼运动的效果
*/
//遍历几何体顶点,为每一个顶点设置蒙皮索引、权重属性
//根据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));
}
}
// 材质对象
var material = new THREE.MeshPhongMaterial({
skinning: true, //允许蒙皮动画
});
// 创建骨骼网格模型
var SkinnedMesh = new THREE.SkinnedMesh(geometry, material);
SkinnedMesh.position.set(50, 120, 50); //设置网格模型位置
SkinnedMesh.rotateX(Math.PI); //旋转网格模型
scene.add(SkinnedMesh); //网格模型添加到场景中
/**
* 骨骼系统
*/
var Bone1 = new THREE.Bone(); //关节1,用来作为根关节
var Bone2 = new THREE.Bone(); //关节2
var Bone3 = new THREE.Bone(); //关节3
// 设置关节父子关系 多个骨头关节构成一个树结构
Bone1.add(Bone2);
Bone2.add(Bone3);
// 设置关节之间的相对位置
//根关节Bone1默认位置是(0,0,0)
Bone2.position.y = 60; //Bone2相对父对象Bone1位置
Bone3.position.y = 40; //Bone3相对父对象Bone2位置
console.log(Bone1.children)
// 所有Bone对象插入到Skeleton中,全部设置为.bones属性的元素
var skeleton = new THREE.Skeleton([Bone1, Bone2, Bone3]); //创建骨骼系统
console.log(skeleton.bones);
// 返回所有关节的世界坐标
// skeleton.bones.forEach(elem => {
// console.log(elem.getWorldPosition(new THREE.Vector3()));
// });
//骨骼关联网格模型
SkinnedMesh.add(Bone1); //根骨头关节添加到网格模型
SkinnedMesh.bind(skeleton); //网格模型绑定到骨骼系统
console.log(SkinnedMesh);
// 区分显示一下各骨关节
skeleton.bones[1].rotation.x = 0.5;
skeleton.bones[2].rotation.x = 0.5;
// 骨骼辅助显示
var skeletonHelper = new THREE.SkeletonHelper(SkinnedMesh);
scene.add(skeletonHelper);
2. 加载外部模型骨骼动画
/**
* 加载解析骨骼模型动画
*/
var loader = new THREE.ObjectLoader(); //创建一个加载器
var mixer = null; //声明一个混合器变量
loader.load("./marine_anims_core.json", function(obj) {
console.log(obj)
scene.add(obj); //添加到场景中
/* 查看骨骼数据 */
//从返回对象获得骨骼网格模型
var SkinnedMesh = obj.children[0];
// 查看骨头关节Bone
// console.log(SkinnedMesh.skeleton.bones);
// 遍历骨骼模型中的骨关节Bone,并获得世界坐标
// SkinnedMesh.traverse(function(elem) {
// if (elem.type === 'Bone') {
// console.log(elem.getWorldPosition(new THREE.Vector3()));
// }
// });
//骨骼网格模型作为参数创建一个混合器
mixer = new THREE.AnimationMixer(SkinnedMesh);
// 查看骨骼网格模型的帧动画数据
console.log(SkinnedMesh.geometry.animations)
// 解析跑步状态对应剪辑对象clip中的关键帧数据
var AnimationAction = mixer.clipAction(SkinnedMesh.geometry.animations[1]);
// 解析步行状态对应剪辑对象clip中的关键帧数据
// var AnimationAction = mixer.clipAction(SkinnedMesh.geometry.animations[3]);
AnimationAction.play();
// 骨骼辅助显示
// var skeletonHelper = new THREE.SkeletonHelper(SkinnedMesh);
// scene.add(skeletonHelper);
})
...
// 创建一个时钟对象Clock
var clock = new THREE.Clock();
// 渲染函数
function render() {
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
if (mixer !== null) {
//clock.getDelta()方法获得两帧的时间间隔
// 更新混合器相关的时间
mixer.update(clock.getDelta());
}
}
render();
变形动画
关于变形动画,你可以理解为多组顶点数据,从一个状态变化到另一个状态,比如人的面部表情,哭的表情用一系列的顶点表示,笑的表情用一系列的顶点表示,从哭的表情过渡到笑的表情,就是表情对应的两组顶点之间的过渡,几何体的顶点的位置坐标发生变化,从一个状态过渡到另一个状态自然就产生了变形动画。
- 通过几何体Geometry的变形目标属性
.morphTargets设置好变形动画
/**
* 创建网格模型,并给模型的几何体设置多个变形目标
*/
// 创建一个几何体
var geometry = new THREE.BoxGeometry(50, 50, 50); //立方体几何对象
// 为geometry提供变形目标的数据
var box1 = new THREE.BoxGeometry(100, 5, 100); //立方体(为变形目标1提供数据)
var box2 = new THREE.BoxGeometry(5, 200, 5); //立方体(为变形目标2提供数据)
// 设置变形目标的数据
geometry.morphTargets[0] = {name: 'target1',vertices: box1.vertices};
geometry.morphTargets[1] = {name: 'target2',vertices: box2.vertices};
var material = new THREE.MeshLambertMaterial({
morphTargets: true, //允许变形
color: 0x0000ff
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
- 控制变形目标影响权重
.morphTargetInfluences(范围一般0~1)的属性值生成关键帧数据,实现关键帧动画
/**
* 设置关键帧数据
*/
//启用变形目标并设置变形目标影响权重,范围一般0~1
// mesh.morphTargetInfluences[0] = 0.5;
// mesh.morphTargetInfluences[1] = 1;
// 设置变形目标1对应权重随着时间的变化
// 创建位置关键帧对象:0时刻对应权重0 10时刻对应权重1 20时刻对应权重0
var Track1 = new THREE.KeyframeTrack('.morphTargetInfluences[0]', [0,10,20], [0,1, 0]);
// 设置变形目标2对应权重随着时间的变化
var Track2 = new THREE.KeyframeTrack('.morphTargetInfluences[1]', [20,30, 40], [0, 1,0]);
// 创建一个剪辑clip对象,命名"default",持续时间40
var clip = new THREE.AnimationClip("default", 40, [Track1,Track2]);
- 播放关键帧动画即可实现变形动画
/**
* 播放编辑好的关键帧数据
*/
var mixer = new THREE.AnimationMixer(mesh); //创建混合器
var AnimationAction = mixer.clipAction(clip); //返回动画操作对象
AnimationAction.timeScale = 5; //默认1,可以调节播放速度
// AnimationAction.loop = THREE.LoopOnce; //不循环播放
// AnimationAction.clampWhenFinished=true;//暂停在最后一帧播放的状态
AnimationAction.play(); //开始播放
...
var clock = new THREE.Clock();
// 渲染函数
function render() {
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
//clock.getDelta()方法获得两帧的时间间隔
// 更新混合器相关的时间
mixer.update(clock.getDelta());
}
render();
解析外部模型变形目标数据
var loader = new THREE.JSONLoader(); //创建加载器
var mixer = null; //声明一个混合器变量
loader.load("./鸟/flamingo.json", function(geometry) {
// console.log(geometry);
var material = new THREE.MeshPhongMaterial({
morphTargets: true, // 材质对象开启渲染目标
vertexColors: THREE.FaceColors,
});
// 通过平均面法线来计算顶点法线,效果更光滑
geometry.computeVertexNormals();
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh); //插入到场景中
// 创建一个混合器,播放网格模型模型的变形动画
mixer = new THREE.AnimationMixer(mesh);
// geometry.animations[0]:获得剪辑对象clip
var AnimationAction=mixer.clipAction(geometry.animations[0]);
// AnimationAction.timeScale = 0.5; //默认1,可以调节播放速度
// AnimationAction.loop = THREE.LoopOnce; //不循环播放
// AnimationAction.clampWhenFinished=true;//暂停在最后一帧播放的状态
AnimationAction.play();//播放动画
})
音频
本质上是对原生Web Audio API的封装。
- 监听者
AudioListener - 音频
Audio- 可用于不考虑位置的背景音乐
- 位置音频
PositionalAudio- 音频源位置发生变化,听到的声音有所变化,比如音量大小
- 音频分析器
AudioAnalyser - 音频加载器
AudioLoader
// 创建一个音频加载器
var audioLoader = new THREE.AudioLoader();
// 加载音频文件,返回一个音频缓冲区对象作为回调函数参数
audioLoader.load('./中国人.mp3', function(AudioBuffer) {
// 音频缓冲区对象关联到音频对象audio
audio.setBuffer(AudioBuffer);
audio.setLoop(true); //是否循环
audio.setVolume(0.9); //音量
// 播放缓冲区中的音频数据
audio.play(); //play播放、stop停止、pause暂停
});
1. 音频与场景关联(位置音频)
...
//创建相机对象
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
//设置相机位置,相当于设置监听者位置
camera.position.set(0, 0, 200);
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
// 用来定位音源的网格模型
var audioMesh = new THREE.Mesh(geometry, material);
// 设置网格模型的位置,相当于设置音源的位置
audioMesh.position.set(0, 0, 300);
scene.add(audioMesh);
...
render()
...
// 创建一个虚拟的监听者
var listener = new THREE.AudioListener();
// 监听者绑定到相机对象
camera.add(listener);
// 创建一个位置音频对象,监听者作为参数,音频和监听者关联。
var PosAudio = new THREE.PositionalAudio(listener);
// 音源绑定到一个网格模型上
audioMesh.add(PosAudio);
// 创建一个音频加载器
var audioLoader = new THREE.AudioLoader();
// 加载音频文件,返回一个音频缓冲区对象作为回调函数参数
audioLoader.load('./中国人.mp3', function(AudioBuffer) {
// 音频缓冲区对象关联到音频对象audio
PosAudio.setBuffer(AudioBuffer);
PosAudio.setVolume(0.9); //音量
PosAudio.setRefDistance(200); //参数值越大,声音越大
PosAudio.play(); //播放
});
2. 音乐可视化
获取频率数据,然后通过频率数据控制网格模型的长度方向伸缩变化:
/**
* 创建多个网格模型组成的组对象
*/
var group = new THREE.Group();
let N = 128; //控制音频分析器返回频率数据数量
for (let i = 0; i < N / 2; i++) {
var box = new THREE.BoxGeometry(10, 100, 10);
var material = new THREE.MeshPhongMaterial({
color: 0x0000ff
});
var mesh = new THREE.Mesh(box, material);
// 长方体间隔20,整体居中
mesh.position.set(20 * i - N / 2 * 10, 0, 0)
group.add(mesh)
}
scene.add(group)
...
var analyser = null; // 声明一个分析器变量
// 渲染函数
function render() {
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
if (analyser) {
// 获得频率数据N个
var arr = analyser.getFrequencyData();
// console.log(arr);
// 遍历组对象,每个网格子对象设置一个对应的频率数据
group.children.forEach((elem, index) => {
elem.scale.y = arr[index] / 80
elem.material.color.r = arr[index] / 200;
});
}
}
render();
var listener = new THREE.AudioListener() //监听者
var audio = new THREE.Audio(listener); //非位置音频对象
var audioLoader = new THREE.AudioLoader(); //音频加载器
// 加载音频文件
audioLoader.load('中国人.mp3', function(AudioBuffer) {
audio.setBuffer(AudioBuffer); // 音频缓冲区对象关联到音频对象audio
audio.setLoop(true); //是否循环
audio.setVolume(0.5); //音量
audio.play(); //播放
// 音频分析器和音频绑定,可以实时采集音频时域数据进行快速傅里叶变换
analyser = new THREE.AudioAnalyser(audio,2*N);
});
控制器
- 轨道控制器
OrbitControls- 给浏览器定义了一个鼠标、键盘事件,自动检测鼠标键盘的变化,如果变化了就会自动更新相机的数据
轨道控制器 OrbitControls
OrbitControls.js控件支持鼠标左中右键操作和键盘方向键操作。
执行构造函数THREE.OrbitControls()浏览器会同时干两件事,一是给浏览器定义了一个鼠标、键盘事件,自动检测鼠标键盘的变化,如果变化了就会自动更新相机的数据, 执行该构造函数同时会返回一个对象,可以给该对象添加一个监听事件,只要鼠标或键盘发生了变化,就会触发渲染函数。
场景操作:
- 缩放:滚动—鼠标中键
- 平移:拖动—鼠标右键
- 旋转:拖动—鼠标左键
- 添加监听事件,更新渲染效果:
renderer.render(scene, camera);
// 创建控件对象 相机对象camera作为参数 控件可以监听鼠标的变化,改变相机对象的属性
var controls = new THREE.OrbitControls(camera,renderer.domElement);
// 监听鼠标事件,触发渲染函数,更新canvas画布渲染效果
controls.addEventListener('change', render);
- 使用requestAnimationFrame更新渲染
如果threejs代码中通过requestAnimationFrame()实现渲染器渲染方法render()的周期性调用,当通过OrbitControls操作改变相机状态的时候,没必要再通过controls.addEventListener('change', render)监听鼠标事件调用渲染函数,因为requestAnimationFrame()就会不停的调用渲染函数。
function render() {
renderer.render(scene,camera);
requestAnimationFrame(render); // OrbitControls改变相机对象的属性,再次渲染
}
render();
// 创建控件对象 相机对象camera作为参数 控件可以监听鼠标的变化,改变相机对象的属性
var controls = new THREE.OrbitControls(camera,renderer.domElement);