three.js总结(二)

698 阅读18分钟

模型加载器

数据导入导出

Threejs文档Object3D等类,提供了一个方法 .toJSON() 通过这个方法可以导出Threejs三维模型的各类数据,该方法的功能就是把Threejs的几何体、材质、光源等对象转化为JSON格式导出

导出数据

导出一个json文件的过程:

  1. 从three.js对象提取数据.toJSON()
  2. 将JSON对象转为字符串JSON.stringify()
  3. 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));
image.png
  • 导出材质信息
var material = new THREE.MeshLambertMaterial({
  color: 0x0000ff,
});
console.log(material);
console.log(material.toJSON());
console.log(JSON.stringify(material));
image.png
  • 导出场景信息,包含模型、光源等等
console.log(scene.toJSON());
image.png

导入(加载解析)

加载解析过程:

  1. 加载json文件

  2. 字符串转为JSON对象JSON.parse()

  3. 调用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)
    })
    
    image.png

模型加载器

  • 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_kdmap 颜色贴图
map_ksspecularMap 高光贴图
normnormalMap 法线贴图
map_bump/bumpbumpMap 凹凸贴图

3dmax导出的obj和mtl模型文件有时候需要修改一下个别位置字符,比如.obj.mtl文件的名称可能是乱码mtllib �����.mtl.mtl文件中贴图的路径要设置正确,比如导出的是绝对路径,要改为相对路径。

fbx

fbx除了包含几何、材质信息,可以存储骨骼动画等数据

解析之前可以先在浏览器控制台查看动画相关的数据是如何存储的。你可以看到obj.animations属性的数组包含两个剪辑对象AnimationClipobj.animations[0]对应剪辑对象AnimationClip包含多组关键帧KeyframeTrack数据,obj.animations[1]对应的剪辑对象AnimationClip没有关键帧数据,也就是说没有关键帧动画。 image.png

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();
w4.png

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); //网格模型绑定到骨骼系统
    

手动创建一个骨骼网格模型

/**
 * 创建骨骼网格模型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);
image.png

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();
threejs730.gif

变形动画

关于变形动画,你可以理解为多组顶点数据,从一个状态变化到另一个状态,比如人的面部表情,哭的表情用一系列的顶点表示,笑的表情用一系列的顶点表示,从哭的表情过渡到笑的表情,就是表情对应的两组顶点之间的过渡,几何体的顶点的位置坐标发生变化,从一个状态过渡到另一个状态自然就产生了变形动画

  • 通过几何体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();
threejs740.gif

解析外部模型变形目标数据

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);
});
threejs78可视化.gif

控制器

  • 轨道控制器 OrbitControls
    • 给浏览器定义了一个鼠标、键盘事件,自动检测鼠标键盘的变化,如果变化了就会自动更新相机的数据

轨道控制器 OrbitControls

OrbitControls.js控件支持鼠标左中右键操作和键盘方向键操作。

执行构造函数THREE.OrbitControls()浏览器会同时干两件事,一是给浏览器定义了一个鼠标、键盘事件,自动检测鼠标键盘的变化,如果变化了就会自动更新相机的数据, 执行该构造函数同时会返回一个对象,可以给该对象添加一个监听事件,只要鼠标或键盘发生了变化,就会触发渲染函数。

场景操作:

  • 缩放:滚动—鼠标中键
  • 平移:拖动—鼠标右键
  • 旋转:拖动—鼠标左键
  1. 添加监听事件,更新渲染效果:
renderer.render(scene, camera);

// 创建控件对象  相机对象camera作为参数   控件可以监听鼠标的变化,改变相机对象的属性
var controls = new THREE.OrbitControls(camera,renderer.domElement);
// 监听鼠标事件,触发渲染函数,更新canvas画布渲染效果
controls.addEventListener('change', render);
  1. 使用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);