将 Metal 缓冲区传递给 SceneKit 着色器

Posted

技术标签:

【中文标题】将 Metal 缓冲区传递给 SceneKit 着色器【英文标题】:Passing Metal buffer to SceneKit shader 【发布时间】:2016-02-06 03:10:10 【问题描述】:

我想使用 Metal 计算着色器来计算一些位置,然后将这些位置输入到 Metal 着色器中。听起来很简单,但我无法将 MTLBuffer 数据导入基于 Metal 的 SCNProgram。

计算内核如下,在这个人为的示例中,它采用三个 3D 向量(在两个缓冲区中)。

kernel void doSimple(const device float3 *inVector [[ buffer(0) ]],
                    device float3 *outVector [[ buffer(1) ]],
                    uint id [[ thread_position_in_grid ]]) 

    float yDisplacement = 0;
    . . . .

    outVector[id] = float3(
        inVector[id].x, 
        inVector[id].y + yDisplacement, 
        inVector[id].z);

这个内核函数在我的SCNSceneRendererDelegate- renderer:willRenderScene:atTime: 方法中每一帧都运行。有两个缓冲区,它们在每一帧之后切换。

按如下方式创建缓冲区;

func setupBuffers() 
    positions = [vector_float3(0,0,0), vector_float3(1,0,0), vector_float3(2,0,0)]

    let bufferSize = sizeof(vector_float3) * positions.count
    //copy same data into two different buffers for initialisation
    buffer1 = device.newBufferWithBytes(&positions, length: bufferSize, options: .OptionCPUCacheModeDefault)
    buffer2 = device.newBufferWithBytes(&positions, length: bufferSize, options: .OptionCPUCacheModeDefault)

计算着色器使用以下命令运行(在willRenderScene func 中);

    let computeCommandBuffer = commandQueue.commandBuffer()
    let computeCommandEncoder = computeCommandBuffer.computeCommandEncoder()

    computeCommandEncoder.setComputePipelineState(pipelineState)
    computeCommandEncoder.setBuffer(buffer1, offset: 0, atIndex: 0)
    computeCommandEncoder.setBuffer(buffer2, offset: 0, atIndex: 1)

    computeCommandEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerGroup)
    computeCommandEncoder.endEncoding()
    computeCommandBuffer.commit()
    computeCommandBuffer.waitUntilCompleted()

    let bufferSize = positions.count*sizeof(vector_float3)
    var data = NSData(bytesNoCopy: buffer2.contents(), length: bufferSize, freeWhenDone: false)
    var resultArray = [vector_float3](count: positions.count, repeatedValue: vector_float3(0,0,0))
    data.getBytes(&resultArray, length:bufferSize)

    for outPos in resultArray 
        print(outPos.x, ", ", outPos.y, ", ", outPos.z)
    

这可行,我可以看到我的计算着色器正在更新数组中每个向量的 y 坐标。

这个场景由三个均匀分布的球体组成。顶点着色器简单地获取在计算着色器中计算的位置并将其添加到每个顶点位置(无论如何都是 y 分量)。我给每个球体一个索引,顶点着色器使用这个索引从我的计算数组中拉出适当的位置。

Metal 顶点函数如下所示,它由SCNProgram 引用并设置为每个球体的材质。

vertex SimpleVertex simpleVertex(SimpleVertexInput in [[ stage_in ]],
                             constant SCNSceneBuffer& scn_frame [[buffer(0)]],
                             constant MyNodeBuffer& scn_node [[buffer(1)]],
                             constant MyPositions &myPos [[buffer(2)]],
                             constant uint &index [[buffer(3)]]
                             )


    SimpleVertex vert;
    float3 posOffset = myPos.positions[index];
    float3 pos = float3(in.position.x, 
                        in.position.y + posOffset.y, 
                        in.position.z);

    vert.position = scn_node.modelViewProjectionTransform * float4(pos,1.0);

    return vert;

MyPositions 是一个包含 float3 数组的简单结构体。

struct MyPositions

    float3 positions[3];
;

我使用每个球体材质的setValue 方法将数据传递给顶点着色器没有问题,如下所示(也在willRenderScene 方法中完成)。一切都按预期工作(三个球体向上移动)。

    var i0:UInt32 = 0
    let index0 = NSData(bytes: &i0, length: sizeof(UInt32))
    sphere1Mat.setValue(index0, forKey: "index")
    sphere1Mat.setValue(data, forKey: "myPos")

但是这需要将数据从 GPU 复制到 CPU 再到 GPU,这确实是我宁愿避免的事情。所以我的问题是...... 如何将 MTLBuffer 传递给 SCNProgram?

willRenderScene 中尝试过以下操作,但除了EXEC_BAD... 什么都没有

let renderCommandEncoder = renderer.currentRenderCommandEncoder!
renderCommandEncoder.setVertexBuffer(buffer2, offset: 0, atIndex: 2)
renderCommandEncoder.endEncoding()

填写example is over on GitHub。

感谢阅读,一直在努力解决这个问题。解决方法是使用 MTLTexture 代替 MTLBuffer,因为我已经能够通过漫反射垫道具将它们传递到 SCNProgram。

【问题讨论】:

仍然有兴趣知道这个问题的答案,如果可能的话......目前我已经通过创建一个由MTLBuffer 支持的SCNGeometrySource 来存储我的顶点数据来解决它.渲染后的顶点数据然后由计算着色器直接修改。没有自定义顶点着色器,因此无需传入MTLBuffer 【参考方案1】:

只需一步一步切换缓冲区的绑定。

第 1 步 computeCommandEncoder.setBuffer(buffer1, offset: 0, atIndex: 0) computeCommandEncoder.setBuffer(buffer2, offset: 0, atIndex: 1)

第二步 computeCommandEncoder.setBuffer(buffer1, offset: 0, atIndex: 1) computeCommandEncoder.setBuffer(buffer2, offset: 0, atIndex: 0)

第三步 computeCommandEncoder.setBuffer(buffer1, offset: 0, atIndex: 0) computeCommandEncoder.setBuffer(buffer2, offset: 0, atIndex: 1)

等等……

输出缓冲区成为新的输入缓冲区,反之亦然...

【讨论】:

这就是我为大多数计算功能所做的。但在这种情况下,计算函数工作正常,问题是从计算着色器获取输出并将其传递给渲染函数。使用 SceneKit,我们拥有支持自定义着色器的 SCNProgram 抽象,不幸的是,它似乎缺少传递 MTLBuffer 之类的方法。如果我在没有 SceneKit 的情况下使用 Metal,那将完全没有问题。谢谢 你在源代码中写到://不起作用 let renderCommandEncoder = renderer.currentRenderCommandEncoder! renderCommandEncoder.setVertexBuffer(buffer2, offset: 0, atIndex: 2) renderCommandEncoder.endEncoding() 你是否也在 renderEncoder 中设置了另外两个缓冲区(0 和 1)?或者您是否只为计算编码器执行此操作?我无法尝试这个,因为我没有合适的 ios 硬件。我使用 OS X。 这值得一提,因为我正在经历类似的问题。我不想失去 SceneKit 的实用性,但也有一些金属计算着色器的功能,我会传递其他缓冲区。这是不可能的ATM吗?我正在寻求的一种选择是使用计算着色器直接准备每个片段的颜色值,并通过绑定或键/值注入传递它

以上是关于将 Metal 缓冲区传递给 SceneKit 着色器的主要内容,如果未能解决你的问题,请参考以下文章

SceneKit 中的金属生成网格

将 UInt8 组件类型的纹理传递给 Metal 计算着色器

iOS SceneKit 啥是 inversedTransform? (对于假人)

Metal 2:MetalKit (MTKView) 和 SceneKit 互操作性

在 Metal 中,将顶点和片段缓冲区设置为相同的 MTLBuffer 是不是仅将其复制到 GPU 一次?

使用 SceneKit 通过 Metal 计算管道渲染几何图形