跳到主要内容

创建网格

在代码中创建 Mesh 有三种方式,按从简单到灵活的顺序如下:

  1. 使用 SphereGeometry 等几何类,生成常见形状。
  2. 使用自己提供的顶点数据,例如位置、法线和纹理坐标。
  3. 使用自己创建的顶点和索引缓冲区。

要渲染网格,为其创建网格实例,再交给渲染组件或添加到图层,具体参见网格实例。

几何类​

几何类在 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, z32 位浮点数
setNormals()x, y, z32 位浮点数
setUvs()纹理坐标集 0 到 7 的 u, v32 位浮点数
setColors()r, g, b, a32 位浮点数,适用于 HDR 颜色
setColors32()0 到 255 范围的 r, g, b, a8 位值,在着色器中读作 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()。