1-基本概念
BufferGeometry 是所有three.js 内置几何体的基类,通过此基类可以自定义几何体。
BufferGeometry 具备以下重要属性:
- position 顶点位置
- index 顶点索引
- normal 法线
- uv 坐标
- color 顶点颜色
从上图可以看出,在顶点索引为4的地方,position 、normal 、uv 、color 是基于顶点一一对应的。
接下来咱们自己自定义一个几何体。
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 变量名,那就可以随便写了,只要符合基本的命名规范就行。
几何体形状如下:
自定义几何体的基本方法就是这样,接下来我们还可以使用顶点索引自定义几何体。
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([
// front
0, 1, 2, 2, 1, 3,
// right
4, 5, 6, 6, 5, 7,
// back
8, 9, 10, 10, 9, 11,
// left
12, 13, 14, 14, 13, 15,
// top
16, 17, 18, 18, 17, 19,
// bottom
20, 21, 22, 22, 21, 23,
]);
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 cube = new Mesh(geometry, material);
const helper = new VertexNormalsHelper(cube);
scene.add(mesh, helper);
效果如下:
在用computeVertexNormals() 方法计算法线的时候,若在光滑的物体表面发现了明显的接缝线,那这可能就是由两排位置相同、法线不同的顶点引起的。
这需要我们自己根据模型特征调整法线,将这两排顶点的法线调成一样。
5-顶点数据的更新
当我们想要修改几何体的顶点数据的时候,可以直接通过BufferGeometry 对象下的BufferAttribute 修改。
举个例子:我要在一秒后,把上面自定义的几何体打开一个口子。
代码如下:
setTimeout(() => {
positionAttr.setXYZ(18, 1, 2, 1);
positionAttr.needsUpdate = true;
}, 1000);
接下来在渲染器执行render() 方法时,便会将最新的顶点数据传递给顶点着色器里的attribute 变量。
关于几何体的基础知识,咱们就说到这,接下来咱们做点好玩的东西,巩固咱们之前的所学。
案例-自定义波浪球
下面这个绚丽的波浪球就是我们接下来要做的,我之后还会让它蠕动来。其中会制涉及的知识点有自定义几何体的封装、几何体顶点数据的读写、正弦函数动画等。
1.封装一个波浪球对象。
import {
BufferGeometry,
Vector3,
Object3D,
BufferAttribute,
InterleavedBufferAttribute,
Euler,
} 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) {
// 经度[-Math.PI,Math.PI]
const long = (x / widthSegments - 0.5) * 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)
// changePos(r0 * r1 * decay + 1)
// changePos(r1 * 0.1 + 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) {
// 旋转经、纬度辅助对象
longHelper.rotation.y = long
latHelper.rotation.x = lat
// 返回longHelper的世界坐标位
return pointHelper.getWorldPosition(temp).toArray()
}
}
// 欧拉
function getPoint(lat: number, long: number) {
const euler = new Euler(lat, long, 0, "YXZ")
const p = new Vector3(0, 0, 1)
p.applyEuler(euler)
return p.toArray()
}
2.将波浪球实例化
const geometry = new WaveBall(48, 48);
geometry.wave();
3.之后我们也可以在连续渲染方法里将时间传递到geometry.wave()里,作为波浪的偏移值。
stage.beforeRender = (time = 0) => {
geometry.wave(time * 0.002)
}