Metal 中的多个模型。如何?

Multiple models in Metal. How?

这绝对是初学者的问题。

背景:我不是真正的游戏开发者,但我正在尝试学习低级 3D 编程的基础知识,因为这是一个有趣且有趣的话题。我选择了 Apple 的 Metal 作为图形框架。我知道 SceneKit 和其他更高级别的框架,但我有意尝试学习低级别的位。不幸的是,我已经超出了我的理解范围,网络上似乎很少有面向初学者的 Metal 资源。

通过阅读 Apple 文档并遵循我能找到的教程,我成功地实现了一个简单的顶点着色器和一个片段着色器,并在屏幕上绘制了一个实际的 3D 模型。现在我想再画一个模型,但我有点卡住了,因为我真的不确定什么才是最好的方法。

我……

TL;DR: 在 Metal(或任何其他 3D 框架)中存储多个模型的顶点数据的推荐方法是什么?

没有推荐的方法。当您在像 Metal 这样低的级别上工作时,有很多可能性,您选择哪种可能性在很大程度上取决于情况以及您 want/need 要优化的性能特征。如果您只是玩介绍性项目,那么这些决定中的大部分都是无关紧要的,因为在您扩展到 "real" 个项目之前,性能问题不会产生影响。

通常,游戏引擎为每个模型使用一个缓冲区(或一组 vertex/index 缓冲区),尤其是当每个模型需要不同的渲染状态(例如着色器、绑定纹理)时。这意味着当场景中引入新模型或不再需要旧模型时,可以将必要的资源加载到 GPU 内存中/从中移除(通过创建/销毁 MTL 对象)。

从同一缓冲区(的不同部分)进行多次抽取的主要用例是当您改变缓冲区时。例如,在帧 n 上,您正在使用缓冲区的前 1KB 进行绘制,同时您正在计算/流入新的顶点数据并将其写入缓冲区的第二个 1KB...然后对于帧 n + 1,您可以切换缓冲区的哪些部分用于什么。

为了给 rickster 的回答增加一点,我会将您的模型封装在一个 class 中,每个模型包含一个缓冲区(或者两个,如果您计算索引缓冲区),并传递一个可选参数您要创建的该模型的实例数。

然后,保留一个额外的缓冲区,用于存储要为每个实例引入的任何变体。通常,它只是转换和不同的 material。例如,

struct PerInstanceUniforms {
  var transform : Transform
  var material : Material
}

在我的例子中,material 包含 UV 变换,但所有实例的纹理必须相同。

然后你的模型 class 看起来像这样,

class Model {
  fileprivate var indexBuffer : MTLBuffer!
  fileprivate var vertexBuffer : MTLBuffer!
  var perInstanceUniforms : [PerInstanceUniforms]
  let uniformBuffer : MTLBuffer!

  // ... constructors, etc.

  func draw(_ encoder: MTLRenderCommandEncoder) {
    encoder.setVertexBuffer(vertexBuffer, offset: 0, at: 0)
    RenderManager.sharedInstance.setUniformBuffer(encoder, atIndex: 1)
    encoder.setVertexBuffer(self.uniformBuffer, offset: 0, at: 2)
    encoder.drawIndexedPrimitives(type: .triangle, indexCount: numIndices, indexType: .uint16, indexBuffer: indexBuffer, indexBufferOffset: 0, instanceCount: self.numInstances)
  }

  // this gets called when we need to update the buffers used by the GPU
  func updateBuffers(_ syncBufferIndex: Int) {
    let uniformB = uniformBuffer.contents()
    let uniformData = uniformB.advanced(by: MemoryLayout<PerInstanceUniforms>.size * perInstanceUniforms.count * syncBufferIndex).assumingMemoryBound(to: Float.self)
    memcpy(uniformData, &perInstanceUniforms, MemoryLayout<PerInstanceUniforms>.size * perInstanceUniforms.count)
  }
}

带有实例的顶点着色器看起来像这样,

vertex VertexInOut passGeometry(uint vid [[ vertex_id ]],
                            uint iid [[ instance_id ]],
                            constant TexturedVertex* vdata [[ buffer(0) ]],
                            constant Uniforms& uniforms  [[ buffer(1) ]],
                            constant Transform* perInstanceUniforms [[ buffer(2) ]])
{
  VertexInOut outVertex;
  Transform t = perInstanceUniforms[iid];
  float4x4 m = uniforms.projectionMatrix * uniforms.viewMatrix;
  TexturedVertex v = vdata[vid];
  outVertex.position = m * float4(t * v.position, 1.0);
  outVertex.uv = float2(0,0);
  outVertex.color = float4(0.5 * v.normal + 0.5, 1);
  return outVertex;
}

这是我写的一个使用实例化的例子,带有性能分析:http://tech.metail.com/performance-quaternions-gpu/

您可以在此处找到完整的参考代码:https://github.com/endavid/VidEngine

希望对您有所帮助。