1-1-基本概念
BufferGeometry 是所有three.js 内置几何体的基类,通过此基类可以自定义几何体。
BufferGeometry 具备以下重要属性:
- position 顶点位置
- index 顶点索引
- normal 法线
- uv 坐标
- color 顶点颜色
从上图可以看出,在顶点索引为4的地方,position 、normal 、uv 、color 是基于顶点一一对应的。
接下来咱们自己自定义一个几何体。
1-2-自定义立方体
1.在下面的vertices里,我们按照对立三角形的绘图方式,定义了立方体的6个面,共36个顶点。
const vertices = [
// front
{ pos: [-1, -1, 1], norm: [ 0, 0, 1], uv: [0, 0], },
{ pos: [ 1, -1, 1], norm: [ 0, 0, 1], uv: [1, 0], },
{ pos: [-1, 1, 1], norm: [ 0, 0, 1], uv: [0, 1], },
{ pos: [-1, 1, 1], norm: [ 0, 0, 1], uv: [0, 1], },
{ pos: [ 1, -1, 1], norm: [ 0, 0, 1], uv: [1, 0], },
{ pos: [ 1, 1, 1], norm: [ 0, 0, 1], uv: [1, 1], },
// right
{ pos: [ 1, -1, 1], norm: [ 1, 0, 0], uv: [0, 0], },
{ pos: [ 1, -1, -1], norm: [ 1, 0, 0], uv: [1, 0], },
{ pos: [ 1, 1, 1], norm: [ 1, 0, 0], uv: [0, 1], },
{ pos: [ 1, 1, 1], norm: [ 1, 0, 0], uv: [0, 1], },
{ pos: [ 1, -1, -1], norm: [ 1, 0, 0], uv: [1, 0], },
{ pos: [ 1, 1, -1], norm: [ 1, 0, 0], uv: [1, 1], },
// back
{ pos: [ 1, -1, -1], norm: [ 0, 0, -1], uv: [0, 0], },
{ pos: [-1, -1, -1], norm: [ 0, 0, -1], uv: [1, 0], },
{ pos: [ 1, 1, -1], norm: [ 0, 0, -1], uv: [0, 1], },
{ pos: [ 1, 1, -1], norm: [ 0, 0, -1], uv: [0, 1], },
{ pos: [-1, -1, -1], norm: [ 0, 0, -1], uv: [1, 0], },
{ pos: [-1, 1, -1], norm: [ 0, 0, -1], uv: [1, 1], },
// left
{ pos: [-1, -1, -1], norm: [-1, 0, 0], uv: [0, 0], },
{ pos: [-1, -1, 1], norm: [-1, 0, 0], uv: [1, 0], },
{ pos: [-1, 1, -1], norm: [-1, 0, 0], uv: [0, 1], },
{ pos: [-1, 1, -1], norm: [-1, 0, 0], uv: [0, 1], },
{ pos: [-1, -1, 1], norm: [-1, 0, 0], uv: [1, 0], },
{ pos: [-1, 1, 1], norm: [-1, 0, 0], uv: [1, 1], },
// top
{ pos: [ 1, 1, -1], norm: [ 0, 1, 0], uv: [0, 0], },
{ pos: [-1, 1, -1], norm: [ 0, 1, 0], uv: [1, 0], },
{ pos: [ 1, 1, 1], norm: [ 0, 1, 0], uv: [0, 1], },
{ pos: [ 1, 1, 1], norm: [ 0, 1, 0], uv: [0, 1], },
{ pos: [-1, 1, -1], norm: [ 0, 1, 0], uv: [1, 0], },
{ pos: [-1, 1, 1], norm: [ 0, 1, 0], uv: [1, 1], },
// bottom
{ pos: [ 1, -1, 1], norm: [ 0, -1, 0], uv: [0, 0], },
{ pos: [-1, -1, 1], norm: [ 0, -1, 0], uv: [1, 0], },
{ pos: [ 1, -1, -1], norm: [ 0, -1, 0], uv: [0, 1], },
{ pos: [ 1, -1, -1], norm: [ 0, -1, 0], uv: [0, 1], },
{ pos: [-1, -1, 1], norm: [ 0, -1, 0], uv: [1, 0], },
{ pos: [-1, -1, -1], norm: [ 0, -1, 0], uv: [1, 1], },
];
2.按照属性将这些顶点分成三组:
const positions = [];
const normals = [];
const uvs = [];
for (const vertex of vertices) {
positions.push(...vertex.pos);
normals.push(...vertex.norm);
uvs.push(...vertex.uv);
}
3.基于positions、normals 和uvs 建立BufferAttribute 对象。
const geometry = new BufferGeometry();
const positionNumComponents = 3;
const normalNumComponents = 3;
const uvNumComponents = 2;
const positionAttr=new BufferAttribute(
new Float32Array(positions),
positionNumComponents
)
const normalAttr=new BufferAttribute(
new Float32Array(normals),
normalNumComponents
)
const uvAttr=new BufferAttribute(
new Float32Array(uvs),
uvNumComponents
)
BufferAttribute 对象就是对顶点着色器中Attribute 变量的管理,通过此对象可以存储顶点点位、顶点数量、矢量长度等,并可以对其进行矩阵变换、拷贝、读写等。
4.将BufferAttribute 对象添加到geometry 几何体中。
geometry.setAttribute('position',positionAttr);
geometry.setAttribute('normal',normalAttr);
geometry.setAttribute('uv',uvAttr);
在setAttribute()中,'position'、'normal'、'uv'、' color ' 都是内置attribute 变量名,不能随便写。
当然如果我们想自定义attribute 变量名,那就可以随便写了,只要符合基本的命名规范就行。
几何体形状如下:
自定义几何体的基本方法就是这样,接下来我们还可以使用顶点索引自定义几何体。
1-3-顶点索引
1.将vertices 设置为4*6=24 个点,一个面4个点。
const vertices = [
// front
{ pos: [-1, -1, 1], norm: [ 0, 0, 1], uv: [0, 0], }, // 0
{ pos: [ 1, -1, 1], norm: [ 0, 0, 1], uv: [1, 0], }, // 1
{ pos: [-1, 1, 1], norm: [ 0, 0, 1], uv: [0, 1], }, // 2
{ pos: [ 1, 1, 1], norm: [ 0, 0, 1], uv: [1, 1], }, // 3
// right
{ pos: [ 1, -1, 1], norm: [ 1, 0, 0], uv: [0, 0], }, // 4
{ pos: [ 1, -1, -1], norm: [ 1, 0, 0], uv: [1, 0], }, // 5
{ pos: [ 1, 1, 1], norm: [ 1, 0, 0], uv: [0, 1], }, // 6
{ pos: [ 1, 1, -1], norm: [ 1, 0, 0], uv: [1, 1], }, // 7
// back
{ pos: [ 1, -1, -1], norm: [ 0, 0, -1], uv: [0, 0], }, // 8
{ pos: [-1, -1, -1], norm: [ 0, 0, -1], uv: [1, 0], }, // 9
{ pos: [ 1, 1, -1], norm: [ 0, 0, -1], uv: [0, 1], }, // 10
{ pos: [-1, 1, -1], norm: [ 0, 0, -1], uv: [1, 1], }, // 11
// left
{ pos: [-1, -1, -1], norm: [-1, 0, 0], uv: [0, 0], }, // 12
{ pos: [-1, -1, 1], norm: [-1, 0, 0], uv: [1, 0], }, // 13
{ pos: [-1, 1, -1], norm: [-1, 0, 0], uv: [0, 1], }, // 14
{ pos: [-1, 1, 1], norm: [-1, 0, 0], uv: [1, 1], }, // 15
// top
{ pos: [ 1, 1, -1], norm: [ 0, 1, 0], uv: [0, 0], }, // 16
{ pos: [-1, 1, -1], norm: [ 0, 1, 0], uv: [1, 0], }, // 17
{ pos: [ 1, 1, 1], norm: [ 0, 1, 0], uv: [0, 1], }, // 18
{ pos: [-1, 1, 1], norm: [ 0, 1, 0], uv: [1, 1], }, // 19
// bottom
{ pos: [ 1, -1, 1], norm: [ 0, -1, 0], uv: [0, 0], }, // 20
{ pos: [-1, -1, 1], norm: [ 0, -1, 0], uv: [1, 0], }, // 21
{ pos: [ 1, -1, -1], norm: [ 0, -1, 0], uv: [0, 1], }, // 22
{ pos: [-1, -1, -1], norm: [ 0, -1, 0], uv: [1, 1], }, // 23
];
2.用BufferGeometry.setIndex() 方法设置顶点索引。
geometry.setIndex([
0, 1, 2, 2, 1, 3, // front
4, 5, 6, 6, 5, 7, // right
8, 9, 10, 10, 9, 11, // back
12, 13, 14, 14, 13, 15, // left
16, 17, 18, 18, 17, 19, // top
20, 21, 22, 22, 21, 23, // bottom
]);
1-4-计算法线
BufferGeometry有一个自己根据现有顶点计算法线的方法computeVertexNormals()。
computeVertexNormals() 方法是按照逐顶点着色的方式计算法线的。
逐顶点着色的原理我们在webgl 的光里说过。
代码示例:
geometry.setAttribute("position", positionAttr);
// geometry.setAttribute("normal", normalAttr);
geometry.setAttribute("uv", uvAttr);
geometry.setIndex([0, 1, 2, 2, 1, 3, 4, 5, 6, 6, 5, 7, 8, 9, 10, 10, 9, 11, 12, 13, 14, 14, 13, 15, 16, 17, 18, 18, 17, 19, 20, 21, 22, 22, 21, 23]);
geometry.computeVertexNormals();
我们可以用VertexNormalsHelper 对象将一个Mesh对象的法线显示出来:
const mesh = new Mesh(geometry, material);
const helper = new VertexNormalsHelper(mesh);
scene.add(mesh, helper);
效果如下:
注:若几何体有接缝,即在几何体接缝的地方有两排位置相同的顶点,则用computeVertexNormals()方法计算出的这两排顶点的法线可能会不同。
1-5-顶点数据的更新
当我们想要修改几何体的顶点数据的时候,可以直接通过BufferGeometry 对象下的BufferAttribute 修改。
举个例子:我要在一秒后,把上面自定义的几何体打开一个口子。
代码如下:
setTimeout(() => {
positionAttr.setXYZ(18, 1, 2, 1);
positionAttr.needsUpdate = true;
}, 1000);
接下来在渲染器执行render() 方法时,便会将最新的顶点数据传递给顶点着色器里的attribute 变量。
关于几何体的基础知识,咱们就说到这,接下来咱们做点好玩的东西,巩固咱们之前的所学。
案例-自定义波浪球
下面这个绚丽的波浪球就是我们接下来要做的,我之后还会让它蠕动来。其中会制涉及的知识点有自定义几何体的封装、几何体顶点数据的读写、正弦函数动画等。
1.封装一个波浪球对象。
import {
MeshBasicMaterial,
MeshStandardMaterial,
Mesh,
PerspectiveCamera,
Raycaster,
Scene,
Texture,
TextureLoader,
WebGLRenderer,
Vector2,
Color,
BufferGeometry,
Material,
Vector3,
Object3D,
BufferAttribute,
InterleavedBufferAttribute,
} from "three";
const pi2 = Math.PI * 2;
export default class WaveBall extends BufferGeometry {
// 分段数
widthSegments: number;
heightSegments: number;
// 正弦参数y=Asin(ωx+φ)
a: number = 0.7;
omega: number = 12;
constructor(widthSegments: number = 18, heightSegments: number = 12) {
super();
this.widthSegments = widthSegments;
this.heightSegments = heightSegments;
this.init();
}
init() {
const { widthSegments, heightSegments } = this;
//网格线的数量
const [width, height] = [widthSegments + 1, heightSegments + 1];
//顶点数量
const count = width * height;
//顶点点位
const positions = new Float32Array(count * 3);
//顶点索引
const indices = [];
//根据经纬度计算顶点点位的方法
const setPos = SetPos(positions);
// 逐行列遍历
for (let y = 0; y < height; ++y) {
// 维度[-Math.PI/2,Math.PI/2]
const lat = (y / heightSegments - 0.5) * Math.PI;
for (let x = 0; x < width; ++x) {
// 经度[0,Math.PI*2]
const long = (x / widthSegments) * Math.PI * 2;
// 设置顶点点位
setPos(lat, long);
// 设置顶点索引
if (y && x) {
// 一个矩形格子的左上lt、右上rt、左下lb、右下rb点
const lt = (y - 1) * width + (x - 1);
const rt = (y - 1) * width + x;
const lb = y * width + (x - 1);
const rb = y * width + x;
indices.push(lb, rb, lt, lt, rb, rt);
}
}
}
this.setAttribute("position", new BufferAttribute(positions, 3));
this.setIndex(indices);
this.computeVertexNormals();
}
//波浪
wave(phi = 0) {
const { widthSegments, heightSegments, a, omega } = this;
//网格线的数量
const [width, height] = [widthSegments + 1, heightSegments + 1];
// 顶点点位
const posAttr = this.getAttribute("position");
// 修改点位的方法
const changePos = ChangePos(posAttr);
// 一圈波浪线的总弧度
const allAng = omega * pi2;
// 每个分段的弧度
const yAng = allAng / heightSegments;
const xAng = allAng / widthSegments;
// 逐行列遍历
for (let y = 0; y < height; ++y) {
// y向起伏
const r0 = Math.sin(y * yAng + phi);
// 基于y值做起伏衰减
const decay = a * 0.2 + a * (0.5 - Math.abs(y / heightSegments - 0.5));
for (let x = 0; x < width; ++x) {
// x向起伏
const r1 = Math.sin(x * xAng + phi);
// 基于半径修改顶点位置
changePos((r0 + r1) * decay + 1);
}
}
// this.computeVertexNormals();
// 顶点数据需要更新
posAttr.needsUpdate = true;
}
}
// 基于半径修改顶点位置
function ChangePos(attr: BufferAttribute | InterleavedBufferAttribute) {
let index = 0;
// 根据索引获取顶点
const getXYZ = GetXYZ(attr);
return function (r: number) {
const p = getXYZ(index);
p.setLength(r);
// 设置指定索引位的顶点的x、y、z值
attr.setXYZ(index, ...p.toArray());
index += 1;
};
}
// 根据索引获取顶点
function GetXYZ(attr: BufferAttribute | InterleavedBufferAttribute) {
return function (ind: number) {
return new Vector3(attr.getX(ind), attr.getY(ind), attr.getZ(ind));
};
}
// 基于经纬度计算顶点点位
function SetPos(positions: Float32Array) {
let posNdx = 0;
//根据经纬度获取点位
const getPoint = GetPoint();
return function (lat: number, long: number) {
const pos = getPoint(lat, long);
positions.set(pos, posNdx);
posNdx += 3;
return pos;
};
}
// 获取顶点位
function GetPoint() {
//经度辅助对象
const longHelper = new Object3D();
//维度辅助对象
const latHelper = new Object3D();
// 顶点辅助对象
const pointHelper = new Object3D();
//构建层级关系
longHelper.add(latHelper);
latHelper.add(pointHelper);
pointHelper.position.z = 1;
//暂存顶点
const temp = new Vector3();
return function (lat: number, long: number) {
// 旋转经、纬度辅助对象
latHelper.rotation.x = lat;
longHelper.rotation.y = long;
// 更新longHelper的世界坐标位
longHelper.updateMatrixWorld(true);
// 返回longHelper的世界坐标位
return pointHelper.getWorldPosition(temp).toArray();
};
}
2.将波浪球实例化
const geometry = new WaveBall(48, 48);
geometry.wave();
3.之后我们也可以在连续渲染方法里将时间传递到geometry.wave()里,作为波浪的偏移值。
geometry.wave(time * 0.002);