Metal使用计算着色器模拟几何着色器

9
我正在尝试在Metal中实现体素锥追踪,算法中的一个步骤是使用几何着色器对几何体进行体素化。由于Metal没有几何着色器,因此我正在尝试使用计算着色器来模拟它们。我将顶点缓冲传递到计算着色器中,执行几何着色器通常要执行的操作,并将结果写入输出缓冲区。我还向间接缓冲区添加了一个绘制命令。我使用输出缓冲区作为我的顶点着色器的顶点缓冲区。这个方法很好用,但是我的顶点需要两倍的内存,一个用于顶点缓冲区,一个用于输出缓冲区。有没有办法直接将计算着色器的输出传递给顶点着色器而不将其存储在中间缓冲区中?我不需要保存计算着色器的输出缓冲区的内容。我只需要将结果传递给顶点着色器。这种情况是否可能?谢谢
编辑:
本质上,我正在尝试模拟来自GLSL的以下着色器:
#version 450

layout(triangles) in;
layout(triangle_strip, max_vertices = 3) out;

layout(location = 0) in vec3 in_position[];
layout(location = 1) in vec3 in_normal[];
layout(location = 2) in vec2 in_uv[];

layout(location = 0) out vec3 out_position;
layout(location = 1) out vec3 out_normal;
layout(location = 2) out vec2 out_uv;

void main()
{
    vec3 p = abs(cross(in_position[1] - in_position[0], in_position[2] - in_position[0]));

    for (uint i = 0; i < 3; ++i)
    {
        out_position = in_position[i];
        out_normal = in_normal[i];
        out_uv = in_uv[i];

        if (p.z > p.x && p.z > p.y)
        {
            gl_Position = vec4(out_position.x, out_position.y, 0, 1);
        }
        else if (p.x > p.y && p.x > p.z)
        {
            gl_Position = vec4(out_position.y, out_position.z, 0, 1);
        }
        else
        {
            gl_Position = vec4(out_position.x, out_position.z, 0, 1);
        }

        EmitVertex();
    }

    EndPrimitive();
}

对于每个三角形,我需要输出一个三角形,其顶点位于这些新位置。三角形的顶点来自一个顶点缓冲区,并使用索引缓冲区进行绘制。我计划添加代码以进行保守光栅化(只需略微增加三角形的大小),但这里没有显示。目前,在Metal计算着色器中,我使用索引缓冲区获取顶点,在上面的几何着色器中执行相同的代码,并将新顶点输出到另一个缓冲区中,然后我使用该缓冲区进行绘制。

即使Metal内置了这样的功能,它也可能只是在内部使用缓冲区。如果您使用私有存储模式创建输出缓冲区,则它将完全存在于GPU上,并且永远不会被传输。就资源使用而言,它可能与Metal内部的操作非常接近。 - Ken Thomases
我明白了。只是使用这种技术进行索引绘制也几乎没有意义,因为我最终仍然会复制顶点。在使用索引时有更好的方法吗? - theonewhoknocks
这取决于你的几何着色器正在做什么。你能分享一些相关内容吗?代码是最好的,但也可以包括其他方面,例如:输出基元数量是否可以从输入基元数量轻松计算?你是否需要处理不止一种类型的输入基元类型,例如三角形列表与三角形带? - Ken Thomases
2个回答

5
这取决于你的几何着色器需要做什么,以下是一种非常推测性的可能性。我认为你可以通过使用顶点着色器而无需单独使用计算着色器来“反向”完成它,但这会在 GPU 上产生冗余工作。你将执行一个绘制操作,就好像你手头有一个包含所有几何着色器输出基元的输出顶点的缓冲区一样。然而,你实际上并没有这个缓冲区,而是构建了一个顶点着色器,在其中计算这些数据。
因此,在应用程序代码中,计算输出基元的数量以及对于给定输入基元数量将产生的输出顶点数量。使用该数量进行输出基元类型的绘制。
您不会提供带有输出顶点数据的缓冲区作为此绘制的输入。
您将提供原始索引缓冲区和原始顶点缓冲区作为该绘制的输入。着色器将根据顶点 ID 计算出其所属的输出基元以及该基元的哪个顶点(例如,对于三角形,分别为 vid / 3 和 vid % 3)。从输出基元 ID,它将计算出在原始几何着色器中生成它的输入基元。
着色器将从索引缓冲区查找该输入基元的索引,然后从顶点缓冲区查找顶点数据。(例如,这将对三角形列表与三角形带之间的区别进行敏感。)它将对该数据应用任何几何着色器前顶点着色。然后,它将执行对已识别的输出基元的已识别顶点有贡献的几何计算。一旦它计算出了输出顶点数据,您可以应用任何后几何着色器顶点着色(?)所需的效果。结果就是它将返回的内容。
如果几何着色器可以为每个输入基元生成可变数量的输出基元,那么至少您有一个最大数量。因此,您可以绘制最大潜在输出基元计数的最大潜在顶点计数。顶点着色器可以执行必要的计算,以确定实际上是否会生成该基元。如果没有,顶点着色器可以安排整个基元被剪裁掉,无论是通过将其定位在视锥体外部还是使用输出顶点数据的 [[clip_distance]] 属性。
这避免了在缓冲区中存储生成的基元。但是,它会导致 GPU 重复执行某些几何着色器和几何着色器前顶点着色器的计算。当然,它将被并行化,但可能仍比您现在所做的要慢。此外,它可能会破坏一些围绕获取索引和顶点数据的优化,这些优化可能在更正常的顶点着色器中是可能的。
以下是您的几何着色器的示例转换:
#include <metal_stdlib>
using namespace metal;

struct VertexIn {
    // maybe need packed types here depending on your vertex buffer layout
    // can't use [[attribute(n)]] for these because Metal isn't doing the vertex lookup for us
    float3 position;
    float3 normal;
    float2 uv;
};

struct VertexOut {
    float3 position;
    float3 normal;
    float2 uv;
    float4 new_position [[position]];
};


vertex VertexOut foo(uint vid [[vertex_id]],
                     device const uint *indexes [[buffer(0)]],
                     device const VertexIn *vertexes [[buffer(1)]])
{
    VertexOut out;

    const uint triangle_id = vid / 3;
    const uint vertex_of_triangle = vid % 3;

    // indexes is for a triangle strip even though this shader is invoked for a triangle list.
    const uint index[3] = { indexes[triangle_id], index[triangle_id + 1], index[triangle_id + 2] };
    const VertexIn v[3] = { vertexes[index[0]], vertexes[index[1]], vertexes[index[2]] };

    float3 p = abs(cross(v[1].position - v[0].position, v[2].position - v[0].position));

    out.position = v[vertex_of_triangle].position;
    out.normal = v[vertex_of_triangle].normal;
    out.uv = v[vertex_of_triangle].uv;

    if (p.z > p.x && p.z > p.y)
    {
        out.new_position = float4(out.position.x, out.position.y, 0, 1);
    }
    else if (p.x > p.y && p.x > p.z)
    {
        out.new_position = float4(out.position.y, out.position.z, 0, 1);
    }
    else
    {
        out.new_position = float4(out.position.x, out.position.z, 0, 1);
    }

    return out;
}

谢谢您的帮助。我已经更新了问题,包括我正在尝试模拟的几何着色器。我的输出图元是三角形带,我基本上只需要当前“正在处理”的三角形的另外两个顶点来计算新位置。所以我会使用顶点ID来访问顶点/索引缓冲区,以获取三角形的另外两个顶点? - theonewhoknocks
你将无法使用drawIndexedPrimitives。传递给着色器的顶点ID不足以计算三角形ID和其他两个顶点的索引。它是从索引缓冲区查找索引后的值,而不是该索引的元素编号。例如,如果您的索引缓冲区包含1,2,3,1,4,5,则有两个共享一个顶点的三角形。现在,您的着色器被调用并传递了顶点ID为1。这是哪个三角形的呢? - Ken Thomases
我明白了。有没有一种简单的方法来获取三角形ID?类似于GLSL中的gl_PrimitiveID这样的东西? - theonewhoknocks
1
哦,没关系。我应该可以只使用drawPrimitives,并将顶点计数作为索引计数。 - theonewhoknocks
是的。或者对于一般情况,您将使用所需输出基元数量的计数乘以每个基元的顶点数量。 - Ken Thomases
显示剩余2条评论

0

很遗憾,在Metal中没有办法做到这一点(以及其他事情),而不涉及不必要的复杂性。该API缺乏在Vulkan、OpenGL和DirectX中常见的关键功能...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接