Metal 中的多个模型。如何?
Multiple models in Metal. How?
这绝对是初学者的问题。
背景:我不是真正的游戏开发者,但我正在尝试学习低级 3D 编程的基础知识,因为这是一个有趣且有趣的话题。我选择了 Apple 的 Metal 作为图形框架。我知道 SceneKit 和其他更高级别的框架,但我有意尝试学习低级别的位。不幸的是,我已经超出了我的理解范围,网络上似乎很少有面向初学者的 Metal 资源。
通过阅读 Apple 文档并遵循我能找到的教程,我成功地实现了一个简单的顶点着色器和一个片段着色器,并在屏幕上绘制了一个实际的 3D 模型。现在我想再画一个模型,但我有点卡住了,因为我真的不确定什么才是最好的方法。
我……
- 对我的所有模型使用单个顶点缓冲区和索引缓冲区,并在渲染单个模型时告诉 MTLRenderCommandEncoder 偏移量?
- 每个模型都有单独的顶点缓冲区/索引缓冲区?这种方法会扩展吗?
- 还有别的吗?
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
希望对您有所帮助。
这绝对是初学者的问题。
背景:我不是真正的游戏开发者,但我正在尝试学习低级 3D 编程的基础知识,因为这是一个有趣且有趣的话题。我选择了 Apple 的 Metal 作为图形框架。我知道 SceneKit 和其他更高级别的框架,但我有意尝试学习低级别的位。不幸的是,我已经超出了我的理解范围,网络上似乎很少有面向初学者的 Metal 资源。
通过阅读 Apple 文档并遵循我能找到的教程,我成功地实现了一个简单的顶点着色器和一个片段着色器,并在屏幕上绘制了一个实际的 3D 模型。现在我想再画一个模型,但我有点卡住了,因为我真的不确定什么才是最好的方法。
我……
- 对我的所有模型使用单个顶点缓冲区和索引缓冲区,并在渲染单个模型时告诉 MTLRenderCommandEncoder 偏移量?
- 每个模型都有单独的顶点缓冲区/索引缓冲区?这种方法会扩展吗?
- 还有别的吗?
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
希望对您有所帮助。