Appearance
自定义几何体
什么是 BufferGeometry
Threejs 中提供了多种几何体,但是有些时候我们可能需要自定义几何体,比如绘制一个心形,或者一个不规则的形状。这时就需要使用更加底层的BufferGeometry
。
BufferGeometry
是 Three.js 中用于存储几何体数据的高效数据结构,本质上是一系列 BufferAttributes
的名称。BufferAttributes
主要有以下几种类型:
- position(位置):顶点的 3D 坐标 (x, y, z)
- normal(法线):用于光照计算的法线向量
- uv(UV 坐标):用于纹理映射的 2D 坐标 (u, v)
- index(索引):定义面的顶点索引
- color(颜色):顶点颜色(可选)
以上属性是并行存储在各自的数组中的,代表每个属性的第N个数据集属于同一个顶点。如下图就是其中一个顶点的所有信息:
这里没有提到index,后面会再做说明。
立方体的顶点组成
在这张图中,不同的面都需要一个不同的法线。法线是面朝向的信息。因此每一个顶点都需要有各自不同方向上的法线,如图中坐标轴所示。
同理,一个角在不同的面需要不同的UVs。UVs是用来指定纹理区域中,画在相应顶点位置三角形的纹理坐标。你可以看到,绿色的面需要顶点的UV对应于F纹理的右上角,蓝色的面需要的UV对应于F纹理的左上角,红色的面需要的UV对应于F纹理的左下角。
从这里可以发现:一个 顶点 (Vertex) 在图形学里,并不是只有一个 3D 空间坐标 (x,y,z)
,它其实是一个 属性集合:
- 位置
position
- 法线
normal
- 纹理坐标
uv
- 颜色
color
- …等等
只要其中任意一个属性不同,这个点就必须被当作“不同的顶点”。
以立方体为例,一个立方体看似只有 8 个空间顶点,但实际上:
- 每个角点连接了 3 个面
- 不同的面有不同的 法线(面朝向不同)
- 不同的面需要不同的 UV 坐标(从纹理图中取不同的部分)
所以,一个“顶点”在实际几何数据里必须复制 3 次,分别带上不同的法线和 UV,对于立方体,实际上需要 36个顶点(每个面2个三角形,每个三角形3个顶点,6个面=36个顶点)。
WebGL绘制图形的方式
WebGL中只能绘制三种图形:点、线段和三角形。但是,我们所看到的任何图形,都可以由小的三角形组成。实际上,可以使用这些最基本的图形来绘制出任何东西。
这里我们主要讨论绘制三角形的方式,其他的暂不讨论。由图上可知,绘制三角形有三种方式:
基本图形 | 绘制方式 | 描述 |
---|---|---|
三角形 | TRIANGLES | 一系列单独的三角形,绘制在(v0, v1, v2)、(v3, v4, v5)...处,如果点的个数不是整数倍,最后剩下的一或两个点将被忽略。 |
三角带 | TRIANGLE_STRIP | 一系列条带状的三角形,前3个点构成了第1个三角形,从第2个点开始的三个点构成了第2个三角形(该三角形与前一个三角形共享一条边),以此类推。这些三角形被绘制在(v0, v1, v2)、(v2, v1, v3)、(v2, v3, v4)...处(注意点的顺序)。 |
三角扇 | TRIANGLE_FAN | 一系列三角形组成的类似于扇形的图形。前三个点构成了第1个三角形,接下来的一个点和前一个三角形的最后一条边组成接下来的一个三角形,这些三角形被绘制在(v0, v1, v2),(v0, v2, v3),(v0, v3, v4)... 处。 |
在 实际 3D 开发里,几乎所有引擎(Three.js, Babylon.js, Unity, Unreal)内部都用 TRIANGLES,因为它是 GPU 原生支持的通用格式。后面说到的绘制方式也是用TRIANGLES实现,所以:
总结
绘制一个面需要两个三角形,绘制一个三角形需要三个点。
实现绘制立方体
根据上述的理论基础,现在可以写一个立方体的绘制过程。
position
每个面2个三角形 × 6面 = 12三角形 × 3顶点 = 36个顶点。
坐标方向依次为:红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.
javascript
const positions = new Float32Array([
// 前面 (z=1)
-1,-1, 1, 1,-1, 1, 1, 1, 1,
-1,-1, 1, 1, 1, 1, -1, 1, 1,
// 后面 (z=-1)
-1,-1,-1, -1, 1,-1, 1, 1,-1,
-1,-1,-1, 1, 1,-1, 1,-1,-1,
// 上面 (y=1)
-1, 1,-1, -1, 1, 1, 1, 1, 1,
-1, 1,-1, 1, 1, 1, 1, 1,-1,
// 下面 (y=-1)
-1,-1,-1, 1,-1,-1, 1,-1, 1,
-1,-1,-1, 1,-1, 1, -1,-1, 1,
// 右面 (x=1)
1,-1,-1, 1, 1,-1, 1, 1, 1,
1,-1,-1, 1, 1, 1, 1,-1, 1,
// 左面 (x=-1)
-1,-1,-1, -1,-1, 1, -1, 1, 1,
-1,-1,-1, -1, 1, 1, -1, 1,-1,
]);
normals
每个面法线固定(6个方向)。
每个黄色线条代表了每个点在每个面上的法线方向。
javascript
const normals = new Float32Array([
// 前 (0,0,1)
0,0,1, 0,0,1, 0,0,1,
0,0,1, 0,0,1, 0,0,1,
// 后 (0,0,-1)
0,0,-1, 0,0,-1, 0,0,-1,
0,0,-1, 0,0,-1, 0,0,-1,
// 上 (0,1,0)
0,1,0, 0,1,0, 0,1,0,
0,1,0, 0,1,0, 0,1,0,
// 下 (0,-1,0)
0,-1,0, 0,-1,0, 0,-1,0,
0,-1,0, 0,-1,0, 0,-1,0,
// 右 (1,0,0)
1,0,0, 1,0,0, 1,0,0,
1,0,0, 1,0,0, 1,0,0,
// 左 (-1,0,0)
-1,0,0, -1,0,0, -1,0,0,
-1,0,0, -1,0,0, -1,0,0,
]);
uv
每个三角形的UV。这里先介绍以下UV坐标系。
UV坐标系
U和V分别表示纹理坐标的水平和垂直方向,使用UV来表达纹理的坐标系。 UV坐标的取值范围是0~1
,纹理贴图左下角对应的UV坐标是(0,0),右上角对应的坐标(1,1)。
javascript
const uv = new Float32Array([
// 前
0,0, 1,0, 1,1,
0,0, 1,1, 0,1,
// 后
0,0, 1,1, 1,0,
0,0, 0,1, 1,1,
// 上
0,0, 1,0, 1,1,
0,0, 1,1, 0,1,
// 下
0,0, 1,0, 1,1,
0,0, 1,1, 0,1,
// 右
0,0, 1,0, 1,1,
0,0, 1,1, 0,1,
// 左
0,0, 1,0, 1,1,
0,0, 1,1, 0,1,
]);
有了上面的三种坐标,就可以对其组装为一个几何体:
javascript
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
const material = new THREE.MeshStandardMaterial({
color: 0x44aa88,
metalness: 0.1,
roughness: 0.7
});
const cube = new THREE.Mesh(geometry, material);
// 添加到场景中
scene.add(cube);
这样,就在场景中创建了一个几何体:
通过顶点索引优化性能
看回我们的position数据,每个面由2个三角形组成,每个三角形3个顶点,总共6个,但是其中2个是完全一样的,这在顶点数多的几何体中会出现大量的重复点坐标。我们可以移除重复的顶点,然后用索引代表他们。
position
这次position是去除重复点后的数组,每个面4个顶点(位置),总共6个面 → 24个顶点。
javascript
const positions = new Float32Array([
// 前面
-1, -1, 1,
1, -1, 1,
1, 1, 1,
-1, 1, 1,
// 后面
-1, -1, -1,
-1, 1, -1,
1, 1, -1,
1, -1, -1,
// 上面
-1, 1, -1,
-1, 1, 1,
1, 1, 1,
1, 1, -1,
// 下面
-1, -1, -1,
1, -1, -1,
1, -1, 1,
-1, -1, 1,
// 右面
1, -1, -1,
1, 1, -1,
1, 1, 1,
1, -1, 1,
// 左面
-1, -1, -1,
-1, -1, 1,
-1, 1, 1,
-1, 1, -1,
]);
index
用点位的索引告诉geometry可以用哪些点进行绘制
javascript
// 顶点索引(每个面两个三角形)
const indices = [
0, 1, 2, 0, 2, 3, // 前
4, 5, 6, 4, 6, 7, // 后
8, 9, 10, 8, 10, 11, // 上
12, 13, 14, 12, 14, 15, // 下
16, 17, 18, 16, 18, 19, // 右
20, 21, 22, 20, 22, 23, // 左
];
normal和uv保持不变,组装geometry的代码如下,需要调用geometry.setIndex(indices)
javascript
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
geometry.setIndex(indices);
至此,我们得到了优化过顶点数的立方体。