Buffer Mesh

上次修改时间:2021-05-29 00:11:75

BufferMesh 可以自由操作顶点缓冲和索引缓冲数据,以及一些与几何体绘制相关的指令。具备高效、灵活、简洁等特点。开发者如果想高效灵活的实现自定义几何体就可以使用该类。

/**
 * @title Buffer Mesh
 * @category Mesh
 */
import { AmbientLight, BlinnPhongMaterial, Buffer, BufferBindFlag, BufferMesh, BufferUsage, Camera, Color, DirectLight, Engine, IndexFormat, Mesh, MeshRenderer, Vector3, VertexElement, VertexElementFormat, WebGLEngine } from "oasis-engine";

// Create engine and get root entity.
const engine = new WebGLEngine("canvas");
const canvas = engine.canvas;
canvas.resizeByClientSize();
const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root");

// Create light.
const lightEntity = rootEntity.createChild("DirectLight");
const ambient = lightEntity.addComponent(AmbientLight);
const directLight = lightEntity.addComponent(DirectLight);
ambient.color = new Color(0.2, 0.2, 0.2);
directLight.color = new Color(0.3, 0.4, 0.4);

// Create camera.
const cameraEntity = rootEntity.createChild("Camera");
cameraEntity.transform.setPosition(0, 6, 10);
cameraEntity.transform.lookAt(new Vector3(0, 0, 0));
cameraEntity.addComponent(Camera);

// Create custom cube.
// Use createCustomMesh() to create custom cube mesh.
const cubeEntity = rootEntity.createChild("Cube");
const cubeRenderer = cubeEntity.addComponent(MeshRenderer);
const cubeGeometry = createCustomMesh(engine, 1.0);
const material = new BlinnPhongMaterial(engine);
cubeEntity.transform.rotate(0, 60, 0);
cubeRenderer.mesh = cubeGeometry;
cubeRenderer.setMaterial(material);

// Run engine.
engine.run();

/**
 * Create cube geometry with custom BufferGeometry.
 * @param engine - Engine
 * @param size - Cube size
 * @returns Cube mesh
 */
function createCustomMesh(engine: Engine, size: number): Mesh {
  const geometry = new BufferMesh(engine, "CustomCubeGeometry");

  // prettier-ignore
  // Create vertices data.
  const vertices: Float32Array = new Float32Array([
    	// Up
    	-size, size, -size, 0, 1, 0, size, size, -size, 0, 1, 0, size, size, size, 0, 1, 0, -size, size, size, 0, 1, 0,
    	// Down
    	-size, -size, -size, 0, -1, 0, size, -size, -size, 0, -1, 0, size, -size, size, 0, -1, 0, -size, -size, size, 0, -1, 0,
    	// Left
    	-size, size, -size, -1, 0, 0, -size, size, size, -1, 0, 0, -size, -size, size, -1, 0, 0, -size, -size, -size, -1, 0, 0,
    	// Right
    	size, size, -size, 1, 0, 0, size, size, size, 1, 0, 0, size, -size, size, 1, 0, 0, size, -size, -size, 1, 0, 0,
    	// Front
    	-size, size, size, 0, 0, 1, size, size, size, 0, 0, 1, size, -size, size, 0, 0, 1, -size, -size, size, 0, 0, 1,
    	// Back
    	-size, size, -size, 0, 0, -1, size, size, -size, 0, 0, -1, size, -size, -size, 0, 0, -1, -size, -size, -size, 0, 0, -1]);

  // prettier-ignore
  // Create indices data.
  const indices: Uint16Array = new Uint16Array([
    	// Up
    	0, 2, 1, 2, 0, 3,
    	// Down
    	4, 6, 7, 6, 4, 5,
    	// Left
    	8, 10, 9, 10, 8, 11,
    	// Right
    	12, 14, 15, 14, 12, 13,
    	// Front
    	16, 18, 17, 18, 16, 19,
    	// Back
    	20, 22, 23, 22, 20, 21]);

  // Create gpu vertex buffer and index buffer.
  const vertexBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, vertices, BufferUsage.Static);
  const indexBuffer = new Buffer(engine, BufferBindFlag.IndexBuffer, indices, BufferUsage.Static);

  // Bind buffer
  geometry.setVertexBufferBinding(vertexBuffer, 24);
  geometry.setIndexBufferBinding(indexBuffer, IndexFormat.UInt16);

  // Add vertexElement
  geometry.setVertexElements([
    new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0),
    new VertexElement("NORMAL", 12, VertexElementFormat.Vector3, 0)
  ]);

  // Add one sub geometry.
  geometry.addSubMesh(0, indices.length);
  return geometry;
}

原理图

我们先概览一下 BufferMesh  的原理图

image.png

BufferMesh 有三大核心元素分别是:

名称解释
VertexBufferBinding顶点缓冲绑定,用于将顶点缓冲和顶点跨度(字节)打包。
VertexElement顶点元素,用于描述顶点语义、顶点偏移、顶点格式和顶点缓冲绑定索引等信息。
IndexBufferBinding索引缓冲绑定(可选),用于将索引缓冲和索引格式打包。

其中 IndexBufferBinding 为可选,也就是说必要核心元素只有两个,分别通过 setVertexBufferBindings() 接口和 setVertexElements() 接口设置。 最后一项就是通过 addSubMesh 添加子 SubMesh,并设置顶点或索引绘制数量, SubMesh 包含三个属性分别是起始绘制偏移、绘制数量、图元拓扑,并且开发者可以自行添加多个 SubMesh,每个子几何体均可对应独立的材质。

常用案例

这里列举几个 MeshRendererBufferMesh 的常用使用场景,因为这个类的功能偏底层和灵活,所以这里给出了比较详细的代码。

交错顶点缓冲

常用方式,比如自定义 Mesh、Particle 等实现,具有显存紧凑,每帧 CPU 数据上传至 GPU 次数少等优势。这个案例的主要特点是多个 VertexElement 对应一个 VertexBuffer (Buffer),仅使用一个 VertexBuffer 就可以将不同顶点元素与 Shader 关联。

// add MeshRenderer component
const renderer = entity.addComponent(MeshRenderer);

// create mesh
const mesh = new BufferMesh(engine);

// create vertices.
const vertices = new ArrayBuffer(vertexByteCount);

// create vertexBuffer and upload vertices.
const vertexBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, vertices);

// bind vertexBuffer with stride, stride is every vertex byte length,so the value is 16.
mesh.setVertexBufferBinding(vertexBuffer, 16);

// add vertexElement to tell GPU how to read vertex from vertexBuffer.
mesh.setVertexElements([new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0),
                            new VertexElement("COLOR", 12, VertexElementFormat.NormalizedUByte4, 0)]);

// add one subMesh and set how many vertex you want to render.
mesh.addSubMesh(0, vertexCount);

// set mesh
renderer.mesh = mesh;

独立顶点缓冲

动态顶点 buffer 和静态顶点 buffer 混用时具有优势,比如 position 为静态,但 color 为动态,独立顶点缓冲可以仅更新颜色数据至 GPU。这个案例的主要特点是一个 VertexElement 对应一个 VertexBuffer ,可以分别调用 Buffer 对象的 setData 方法独立更新数据。

// add MeshRenderer component
const renderer = entity.addComponent(MeshRenderer);

// create mesh
const mesh = new BufferMesh(engine);

// create vertices.
const positions = new Float32Array(vertexCount);
const colors = new Uint8Array(vertexCount);

// create vertexBuffer and upload vertices.
const positionBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, positions);
const colorBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, colors);

// bind vertexBuffer with stride,stride is every vertex byte length,so the value is 12.
mesh.setVertexBufferBindings([new VertexBufferBinding(positionBuffer, 12),
                                 	new VertexBufferBinding(colorBuffer, 4)]);

// add vertexElement to tell GPU how to read vertex from vertexBuffer.
mesh.setVertexElements([new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0),
                            new VertexElement("COLOR", 0, VertexElementFormat.NormalizedUByte4, 1)]);

// add one subMesh and set how many vertex you want to render.
mesh.addSubMesh(0, vertexCount);

// set mesh
renderer.mesh = mesh;

Instance 渲染

GPU Instance 渲染是三维引擎的常用技术,比如可以把相同几何体形状的物体一次性渲染到不同的位置,可以大幅提升渲染性能。这个案例的主要特点是使用了 VertexElement 的实例功能,其构造函数的最后一个参数表示实例步频(在缓冲中每前进一个顶点绘制的实例数量,非实例元素必须为 0),BufferMesh 的 instanceCount 表示实例数量。

// add MeshRenderer component
const renderer = entity.addComponent(MeshRenderer);

// create mesh
const mesh = new BufferMesh(engine);

// create vertices.
const vertices = new ArrayBuffer( vertexByteLength );

// create instance data.
const instances = new Float32Array( instanceDataLength );

// create vertexBuffer and upload vertex data.
const vertexBuffer = new Buffer( engine, BufferBindFlag.VertexBuffer, vertices );

// create instance buffer and upload instance data.
const instanceBuffer = new Buffer( engine, BufferBindFlag.VertexBuffer, instances );

// bind vertexBuffer with stride, stride is every vertex byte length,so the value is 16.
mesh.setVertexBufferBindings([new VertexBufferBinding( vertexBuffer, 16 ),
                                  new VertexBufferBinding( instanceBuffer, 12 )]);

// add vertexElement to tell GPU how to read vertex from vertexBuffer.
mesh.setVertexElements([new VertexElement( "POSITION", 0, VertexElementFormat.Vector3, 0 ),
                            new VertexElement( "COLOR", 12, VertexElementFormat.NormalizedUByte4, 0 ),
                            new VertexElement( "INSTANCE_OFFSET", 0, VertexElementFormat.Vector3, 1 , 1 ),
                            new VertexElement( "INSTANCE_ROTATION", 12, VertexElementFormat.Vector3, 1 , 1 )]]);

// add one sub mesh and set how many vertex you want to render, here is full vertexCount.
mesh.addSubMesh(0, vertexCount);

// set mesh
renderer.mesh = mesh;

索引缓冲

使用索引缓冲可以复用顶点缓冲内的顶点,从而达到节省显存的目的。其使用方式很简单,就是在原基础上增加了索引缓冲对象,以下代码是在第一个 交错顶点缓冲 案例的基础上修改而来的

// add MeshRenderer component
const renderer = entity.addComponent(MeshRenderer);

// create mesh
const mesh = new BufferMesh(engine);

// create vertices.
const vertices = new ArrayBuffer(vertexByteCount);

// create indices.
const indices = new Uint16Array(indexCount);

// create vertexBuffer and upload vertices.
const vertexBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, vertices);

// create indexBuffer and upload indices.
const indexBuffer = new Buffer(engine, BufferBindFlag.IndexBuffer, indices);

// bind vertexBuffer with stride, stride is every vertex byte length,so the value is 16.
mesh.setVertexBufferBinding(vertexBuffer, 16);

// bind vertexBuffer with format.
mesh.setIndexBufferBinding(indexBuffer, IndexFormat.UInt16);

// add vertexElement to tell GPU how to read vertex from vertexBuffer.
mesh.setVertexElements([new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0),
                            new VertexElement("COLOR", 12, VertexElementFormat.NormalizedUByte4, 0)]);

// add one subMesh and set how many vertex you want to render.
mesh.addSubMesh(0, vertexCount);

// set mesh
renderer.mesh = mesh;