写在最前
往期回顾:
- # threejs系列: 相机与投影 📷
- # threejs系列: 光源与光照🪀
- # threejs系列: 自定义几何体🎃
- # threejs系列: 几何变换(上)🍺
- # threejs系列: 几何变换(下)🏄♂️
- # threejs系列: 矩阵推导🎰
- threejs系列: 物体详解🐧
- threejs系列: 材质与贴图详解🦥
- threejs系列: 曲线详解🥓
- threejs系列: 动画详解🧠
- .......
物体
在 threejs 中主要提供点、线、精灵、网格等类给开发者快速的构建物体,接下来我们快速过一下这些内容。
Object3D
再讲物体之前,我们先认识物体的祖先类,也是threejs中最重要的类,为什么这么说呢?可以看看哪些东西是继承Object3D
- Scene
- 物体
- Camera
- Light
- Audio
- 等等.....
threejs中的北斗之尊了属于是
之前文章提到的物体几何变换的API都是它提供的,除此之外它还提供了很多强大的功能,如:
- 控制物体是否可见
js
mesh.visible = false;
- 阴影相关
js
mesh.castShadow = true; // 是否在阴影贴图中
mesh.receiveShadow = true; // 是否接收阴影
阴影相关会专门写一篇内容,争取在明天发出。
3.物体是否受到摄像机视锥体设置的最大远近距离影响:
js
mesh.frustumCulled = true; // 默认 true;
- 矩阵相关
物体拥有matrix属性,保存物体的矩阵信息,当物体使用Object3D提供的几何变换API时,物体会自动计算并赋值matrix值。如果想自行修改matrix属性的话,则需要将matrixAutoUpdate = 设置为false;
updateMatrix 方法重置掉自行修改的matrix,重新通过scale\rotation\position方法计算并赋值matrix属性。
除非你非常熟悉 threejs 的矩阵内部运行原理,否则请不要修改matrixAutoUpdate属性
层级
我们知道一个物体由多个部件构成的, 部件又可能由多个小部件组成。threejs也是如此,物体可以直接通过.add方法添加子级,如:
js
let mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshPhongMaterial({ color: 0xFFFFFF }))
scene.add(mesh);
let mesh2 = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshPhongMaterial({ color: 0xFFFFFF }))
mesh.add(mesh2);
console.log(mesh.children)
可通过children查看子级。
如果你希望父级是一个没有具体意义的对象,就像在 Vue 中的 template。
则可以通过 Group 类来添加,如:
js
let mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshPhongMaterial({ color: 0xFFFFFF }));
let group = new THREE.Group();
group.add(mesh);
console.log(group.children)
本地矩阵与世界矩阵
有了层级后,几何变换也变得不太一样了,当父级放大2倍,子级也会放大两倍。
csharp
group.scale.set(2, 2, 2)
我们来看一下子级的矩阵信息 matrix
奇怪怎么还是单位矩阵并没有发生改变?因为这只是本地矩阵,只保存自己的变换信息,发生改变的是世界矩阵matrixWorld。
js
console.log(mesh.matrixWord);
可以看到世界矩阵发生了改变。
本地矩阵
本地矩阵包含了对象的旋转、平移和缩放变换,本地矩阵的乘积表示了对象在本地坐标系中的变换。
世界矩阵
世界矩阵是对象本地矩阵和其所有祖宗对象本地矩阵的乘积,它表示了对象在世界坐标系中的变换。
世界坐标与本地坐标
对象的本地坐标是相对于其父对象的坐标,对象的世界坐标是相对于世界坐标原点的坐标。
获取对象的世界坐标
可以通过对象的 getWorldPosition() 方法来获取对象的世界坐标。
节点查找
随着层级越来越复杂,查找物体就变得困难了,正如 Dom 一样, Object3D 提供了 getObjectById、getObjectByName 等诸多方法用于查询。
也提供了 uuid 进行标识,也提供了 userData 用于存储自定义信息,还有 name 进行命名区分。
你可以使用 traverse 方法进行遍历,如需不展示的内容不进行遍历的话,threejs也封装了 traverseVisible 让你使用。
所以在 threejs 中,查找物体非常容易。
Mesh 网格
网格是什么?
在 3D 计算机图形和实体建模中,网格是定义多面体对象形状的顶点、边和面的集合,由网格创建的对象需要包含顶点(Vertex)、边(edge)、面(faces)、多边形(polygons)和曲面(sufaces)元素信息。
创建
在前面的文章中,我们多次使用到了Mesh类,Mesh是常用的网格,不管是在创建一个简单的正方体还是复杂的圆环缓冲都得用它创建。
js
new THREE.Mesh(BufferGeometry, Material);
需要传入几何体和材质的实例,几何体在前面的文章中有详细的讲解,而材质(Material)是描述物体外观和光学特性的属性集合,它包括物体的颜色、反射属性(如漫反射、高光反射)、透明度、折射率等。几何体更像是前端的HTML,而材质像是CSS,他们组合从而构成一个完美的物体。
InstancedMesh 实例化网格
每次 three.js 将一个物体交给 WebGL 进行渲染,就称为一次 draw call。
draw call 是指向图形渲染管线提交一次绘制请求的操作,减少draw call次数可以有效的提高渲染效率。
对于多个具有相同材质的 mesh,可以只创建一个材质实例,然后将其应用于所有 mesh,这样可以减少 draw call 的数量,这就是 InstancedMesh 实例化网格的由来。
当你需要渲染大量的相同的网格时,使用InstancedMesh是最好的选择,例如《我的世界》游戏中有大量相同的石块,这时候你就需要用到这个了。
我们来创建 800 * 800 个的Mesh
js
const row = 800;
const col = 800;
let group = new THREE.Group();
for (let i = 0; i < row; i++) {
for (let j = 0; j < col; j++) {
let mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshPhongMaterial({ color: 0xFFFFFF }));
mesh.position.set(i, j, 0);
group.add(mesh)
}
}
scene.add(group)
因为加载物体太多的缘故,此时我的浏览器已经无响应重启了,就如前端的长列表一样。
再看看用 InstancedMesh,InstancedMesh设置变换需要用它自己独特的方法,设置颜色需要用setColorAt,设置变换需要用 setMatrixAt
js
const row = 800;
const col = 800;
let mesh = new THREE.InstancedMesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshPhongMaterial({ color: 0xFFFFFF }), row * col);
let index = 0;
for (let i = 0; i < row; i++) {
for (let j = 0; j < col; j++) {
index++;
let matrix = new THREE.Matrix4();
matrix.setPosition(i - row / 2, j - col / 2, 0);
mesh.setColorAt(index, new THREE.Color(index / (row * col), 0, 0))
mesh.setMatrixAt(index, matrix);
}
}
scene.add(mesh)
此时会发现虽然fps虽然有一点点下降,但是还是能够使用的,并且基本上不会有同时出现160000的业务场景。
InstanceMesh算是 threejs 性能优化方案的其中一种。
Points 与 Line
Points 通常搭配 PointMaterial 点材质使用, Line 通常搭配 LineBasicMaterial(实线) 或 LineDashMaterial(虚线)使用,用法非常简单就不在这里细讲了,过!