创建网格
在代码中创建 Mesh 有三种方式,按从简单到灵活的顺序如下:
要渲染网格,为其创建网格实例,再交给渲染组件或添加到图层,具体参见网格实例。
几何类
几何类在 CPU 上生成形状的顶点数据。Mesh.fromGeometry() 将其上传到新网格:
import { Mesh, SphereGeometry } from 'playcanvas';
const mesh = Mesh.fromGeometry(app.graphicsDevice, new SphereGeometry({
radius: 2,
latitudeBands: 32,
longitudeBands: 32
}));
| 类 | 形状 | 主要选项 |
|---|---|---|
BoxGeometry | 盒体 | halfExtents, widthSegments, lengthSegments, heightSegments |
PlaneGeometry | 朝上的平面 | halfExtents, widthSegments, lengthSegments |
SphereGeometry | 球体 | radius, latitudeBands, longitudeBands |
CapsuleGeometry | 胶囊体 | radius, height, heightSegments, sides |
CylinderGeometry | 圆柱体 | radius, height, heightSegments, capSegments |
ConeGeometry | 圆锥或截头圆锥 | baseRadius, peakRadius, height, heightSegments, capSegments |
TorusGeometry | 圆环或部分圆环 | tubeRadius, ringRadius, sectorAngle, segments, sides |
CircleGeometry | 平面圆盘 | radius, sectors, rings, ringExponent |
DomeGeometry | 底部压平的球体,例如天空穹顶 | latitudeBands, longitudeBands |
它们均生成位置、法线、纹理坐标和索引。多数还接受 calculateTangents: true,添加材质使用法线贴图所需的切线。
Geometry 是一个普通的数组容器,因此可以在创建网格前修改数据。下面将细分平面位移为起伏地形,并重新计算法线:
import { Mesh, PlaneGeometry, Vec2 } from 'playcanvas';
const geometry = new PlaneGeometry({
halfExtents: new Vec2(10, 10),
widthSegments: 64,
lengthSegments: 64
});
const positions = geometry.positions;
for (let i = 0; i < positions.length; i += 3) {
positions[i + 1] = Math.sin(positions[i] * 0.5) * Math.cos(positions[i + 2] * 0.5);
}
geometry.calculateNormals();
const mesh = Mesh.fromGeometry(app.graphicsDevice, geometry);
也可以向空的 Geometry 填入自己的 positions、normals、uvs、colors 和 indices,再传给 Mesh.fromGeometry()。
设置顶点数据
要用自己的数据构建网格,在新网格上设置各顶点流,再调用 update(),创建顶点和索引缓冲区并上传数据。下面的网格是带纹理坐标的单个四边形:
import { Mesh, calculateNormals } from 'playcanvas';
const positions = new Float32Array([
-1, 0, 1,
1, 0, 1,
1, 0, -1,
-1, 0, -1
]);
const uvs = new Float32Array([
0, 0,
1, 0,
1, 1,
0, 1
]);
const indices = [
0, 1, 2,
0, 2, 3
];
const mesh = new Mesh(app.graphicsDevice);
mesh.setPositions(positions);
mesh.setNormals(calculateNormals(positions, indices));
mesh.setUvs(0, uvs);
mesh.setIndices(indices);
mesh.update();
每个方法设置一个顶点流:
| 方法 | 每个顶点的数据 | 存储形式 |
|---|---|---|
setPositions() | x, y, z | 32 位浮点数 |
setNormals() | x, y, z | 32 位浮点数 |
setUvs() | 纹理坐标集 0 到 7 的 u, v | 32 位浮点数 |
setColors() | r, g, b, a | 32 位浮点数,适用于 HDR 颜色 |
setColors32() | 0 到 255 范围的 r, g, b, a | 8 位值,在着色器中读作 0 到 1 |
setVertexStream() | 任意语义,例如 SEMANTIC_TANGENT | 自行选择的数据类型 |
setIndices() | 各图元的顶点索引 | 16 位,超过 65535 个顶点的网格使用 32 位 |
数据可以是 JavaScript 数组或类型化数组。对于大型网格和每帧变化的数据,类型化数组更快;对于小网格,普通数组可避免创建类型化数组的开销。网格的所有顶点流必须具有相同的顶点数。索引是可选的:没有索引时按顶点顺序绘制。
calculateNormals() 和 calculateTangents() 根据三角网格的位置、纹理坐标和索引生成法线和切线。
要使用顶点颜色绘制,在材质上启用它,例如对 StandardMaterial 设置 material.diffuseVertexColor = true。
图元类型
默认情况下,update() 将网格绘制为三角形,每个三角形使用三个顶点或索引。传入其他图元类型可改为绘制点或线:
import { PRIMITIVE_LINES } from 'playcanvas';
mesh.update(PRIMITIVE_LINES);
类型包括 PRIMITIVE_POINTS、PRIMITIVE_LINES、PRIMITIVE_LINELOOP、PRIMITIVE_LINESTRIP、PRIMITIVE_TRIANGLES、PRIMITIVE_TRISTRIP 和 PRIMITIVE_TRIFAN。这种方式绘制的线宽为一个像素。粗线参见宽线。
包围盒
update() 还会根据位置计算网格局部空间的包围盒,供引擎进行视锥体剔除和排序。要跳过这项工作,将第二个参数设为 false,并自行设置 mesh.aabb:
import { BoundingBox, PRIMITIVE_TRIANGLES, Vec3 } from 'playcanvas';
mesh.aabb = new BoundingBox(new Vec3(0, 0, 0), new Vec3(10, 2, 10));
mesh.update(PRIMITIVE_TRIANGLES, false);
仅对具有三个分量的位置计算包围盒。对于其他位置格式,或着色器将顶点移出顶点数据范围时,应自行设置 aabb。
更新网格
网格可随时更新,包括逐帧更新。设置发生变化的顶点流,再次调用 update()。未设置的流保留之前的数据。下面创建一个高度每帧变化的网格,而纹理坐标和索引只设置一次:
const mesh = new Mesh(app.graphicsDevice);
mesh.setPositions(positions);
mesh.setNormals(calculateNormals(positions, indices));
mesh.setUvs(0, uvs);
mesh.setIndices(indices);
mesh.update();
app.on('update', (dt) => {
animateHeights(positions, dt);
// only the positions and normals change
mesh.setPositions(positions);
mesh.setNormals(calculateNormals(positions, indices));
mesh.update();
});
更新网格时请遵守以下规则:
- **创建网格实例前,先设置所有顶点流。**网格实例根据赋给它时网格所具有的顶点流配置着色器,因此应先调用
update()。后续更改顶点流,参见更改顶点格式。 - **为会增大的网格预留空间。**更新所需顶点或索引超过缓冲区容量时,会按新大小重新创建缓冲区,且仅包含上次
update()之后设置的顶点流,因此这次更新必须设置所有流。为避免重新分配,可在设置数据前将最大数量传给clear(),例如mesh.clear(false, false, maxVertices, maxIndices)。前两个参数是可选的缓冲区使用提示。 - **更新包围盒有开销。**对于每帧变化的大型网格,可考虑设置一个覆盖所有姿态的包围盒,参见包围盒。
更改顶点格式
要改变网格包含的顶点流,例如添加顶点颜色或纹理坐标集,请调用 clear()。它会丢弃顶点格式,下次 update() 根据设置的数据创建新的顶点和索引缓冲区,因此必须重新设置全部流和索引。网格实例在网格赋给它时,根据当时的顶点流配置着色器,所以即使已经设置过,也需要将网格重新赋给每个网格实例。引擎调试构建会对网格赋值后其顶点流发生变化的实例发出警告。
mesh.clear();
mesh.setPositions(positions);
mesh.setNormals(normals);
mesh.setColors32(colors);
mesh.setIndices(indices);
mesh.update();
// set up the shaders of the mesh instance for the new vertex streams
meshInstance.mesh = mesh;
修改加载的网格
相同方法也适用于加载的网格。通过 getPositions() 等读取方法取得当前数据,修改后再设置回去。请注意,加载的网格由从其资源实例化的所有实体共享,参见共享网格。
const mesh = entity.render.meshInstances[0].mesh;
const positions = [];
mesh.getPositions(positions);
// lift every vertex
for (let i = 1; i < positions.length; i += 3) {
positions[i] += 0.5;
}
mesh.setPositions(positions);
mesh.update();
顶点和索引缓冲区
要完全控制顶点布局,可以自行创建 VertexBuffer 和 IndexBuffer,再赋给网格。这样可以直接上传已经具有最终布局的数据,例如二进制文件中的交错数据,无需按顶点流复制。
VertexFormat 描述布局。默认交错排列属性,每个属性从 4 字节的整数倍位置开始。下面绘制一个三角形,每个顶点包含位置和 8 位颜色,总计 16 字节:
import {
BoundingBox,
Mesh,
PRIMITIVE_TRIANGLES,
SEMANTIC_COLOR,
SEMANTIC_POSITION,
TYPE_FLOAT32,
TYPE_UINT8,
Vec3,
VertexBuffer,
VertexFormat
} from 'playcanvas';
const device = app.graphicsDevice;
const format = new VertexFormat(device, [
{ semantic: SEMANTIC_POSITION, components: 3, type: TYPE_FLOAT32 },
{ semantic: SEMANTIC_COLOR, components: 4, type: TYPE_UINT8, normalize: true }
]);
const vertices = [
{ position: [-1, 0, 0], color: [255, 0, 0, 255] },
{ position: [1, 0, 0], color: [0, 255, 0, 255] },
{ position: [0, 1.5, 0], color: [0, 0, 255, 255] }
];
// format.size is the size of one vertex in bytes
const data = new ArrayBuffer(format.size * vertices.length);
const floats = new Float32Array(data);
const bytes = new Uint8Array(data);
vertices.forEach((vertex, i) => {
floats.set(vertex.position, i * format.size / 4);
bytes.set(vertex.color, i * format.size + 12);
});
const mesh = new Mesh(device);
mesh.vertexBuffer = new VertexBuffer(device, format, vertices.length, { data });
mesh.primitive[0] = {
type: PRIMITIVE_TRIANGLES,
base: 0,
baseVertex: 0,
count: vertices.length,
indexed: false
};
mesh.aabb = new BoundingBox(new Vec3(0, 0.75, 0), new Vec3(1, 0.75, 0));
自行赋值缓冲区时,由于不调用 update(),还必须设置描述绘制方式的图元和包围盒。对于索引网格,将索引缓冲区赋给 mesh.indexBuffer[0],并将 indexed 设为 true。销毁网格会同时销毁赋给它的缓冲区。
在 WebGPU 上,网格缓冲区还可以由计算着色器读写。使用 new Mesh(device, { storageVertex: true, storageIndex: true }) 创建网格,或将相同选项传给 Mesh.fromGeometry()。