根据顶点世界位置Y坐标修改Metal fragment shading

Modify Metal fragment shading based on vertex world position Y coordinate

我正在尝试使用带有 SCNTechnique 的金属片段着色器来根据顶点 Y 世界位置修改片段颜色。

目前我的理解

SCNTechnique 可以配置一系列渲染过程。渲染通道允许注入顶点和片段着色器。这些着色器是用 Metal 编写的。 Metal Shading Language Specification 描述了这两个支持的 inputs/outputs。 为每个正在渲染的顶点调用顶点着色器。我们可以将附加信息从顶点着色器传递到片段着色器(如 3D 中的位置 space,参见 MSLS section 5.2)。 片段着色器最接近像素,如果该像素有多个 "qualify" 的三角形,则可能称为单个 "pixel" 的倍数时间。 (通常)片段着色后,如果片段未通过深度或模板测试,则片段可能会被丢弃。

我的尝试

这就是我的尝试。 (希望能指出我理解的不足之处)。

struct VertexOut {
    float4 position [[position]];
};

vertex VertexOut innerVertexShader(VertexIn in [[stage_in]]) {
    VertexOut out;
    out.position = in.position;
    return out;
};

fragment half4 innerFragmentShader(VertexOut in [[stage_in]],
                                   half4 color [[color(0)]]) {
    half4 output;
    output = color;           // test to see if getting rendered color works
    output.g = in.position.y; // test to see if getting y works
    return output;
}

这些着色器在 SCNTechnique 字典中被引用。

[
    "passes": [
        "innerPass: [
            "draw": "DRAW_NODE",
            "node": "inner",
            "metalVertexShader": "innerVertexShader",
            "metalFragmentShader": "innerFragmentShader"
        ]
    ],
    "sequence": ["innerPass"],
    "symbols": [:],
    "targets": [:],
]

// ...

let technique = SCNTechnique(dictionary: techniqueDictionary)

这会执行以下操作:技术被正确实例化并附加到场景(因为它会影响渲染)。但它似乎没有将相机变换或节点位置变换应用于顶点。而是将每个节点呈现为从 (0,0,1) 在位置 (0,0,0) 处查看。颜色不对。如果我从 SCNTechnique 中删除着色器,每个渲染器都会像我期望的那样渲染。

我如何利用常规的 SceneKit 行为(相机变换等),并且仅根据片段的 y 世界位置修改颜色输出?我希望这需要在片段级别上发生,使用在顶点着色器中以某种方式获得的世界位置。我搜索了 "Metal basic vertex shader" 之类的东西,但一无所获。 我见过着色器 like this,但我相信我应该能够依靠 SceneKit 渲染来处理光照、PBR 材质、相机变换等内容。在这一点上,每当我搜索一些 Metal 主题时,我都会觉得,我最终访问了相同的网站,但这些网站尚未成功将我的理解提升到一个新的水平。因此,也欢迎任何 new/additional 资源。

背景

在过去的两个月里,我一直在开发自己的游戏项目,该项目使用 SceneKit 作为主要图形框架。我已转向 SCNTechnique 和 Metal 着色器来获得自定义效果。最后两个特别让我头疼,都是因为缺少样本 code/documentation/runtime 反馈。 因此,我考虑过搬到 Unity/Unreal 甚至完全取消这个项目。但因为我很固执,也因为我真的不想将我的 Swift 代码移植到 C#/C++,所以我还没有放弃 SceneKit。

在过去几天研究这个主题后,我对顶点和片段着色以及 SceneKit 如何处理这些事情的理解有了很大的发展。

正如@mnuages 在评论中指出的那样,对于此用例,着色器修改器是可行的方法。他们利用默认的 SceneKit 着色(如 OP 所要求的)并允许着色器代码注入。

附加信息

为了弥补 SceneKit 文档的一些局限性,我会为研究该主题的其他人详细说明。

有关着色器修改器如何绑定到 SceneKit 默认 vertex/fragment 着色器的更多信息,请参阅 or SceneKit's default shaders。第二个 link 演示了在利用着色器修改器而不是编写自己的着色器时免费获得的 SceneKit 渲染逻辑的范围。

This page 帮助我理解了从顶点到片段的向量变换的不同阶段(模型space ➡️ 世界space ➡️ 相机space ➡️ 投影space).

替代方法(自定义着色器)

如果您想使用完全自定义的着色器进行单通道处理,这是一个简单的示例。它将世界 y 位置从顶点着色器传递到片段着色器。

// Shaders.metal file in your Xcode project

#include <metal_stdlib>
using namespace metal;
#include <SceneKit/scn_metal>

typedef struct {
    float4x4 modelTransform;
    float4x4 modelViewTransform;
} commonprofile_node;

struct VertexIn {
    float3 position [[attribute(SCNVertexSemanticPosition)]];
};

struct VertexOut {
    float4 fragmentPosition [[position]];
    float height;
};

vertex VertexOut myVertex(
                          VertexIn in [[stage_in]],
                          constant SCNSceneBuffer& scn_frame [[buffer(0)]],
                          constant commonprofile_node& scn_node [[buffer(1)]]
                          ) {
    VertexOut out;

    float4 position = float4(in.position, 1.f);
    out.fragmentPosition = scn_frame.viewProjectionTransform * scn_node.modelTransform * position;

    // store world position for fragment shading
    out.height = (scn_node.modelTransform * position).y;
    return out;
}

fragment half4 myFragment(VertexOut in [[stage_in]]) {
    return half4(in.height);
}
let dictionary: [String: Any] = [
    "passes" : [
        "y" : [
            "draw" : "DRAW_SCENE",
            "inputs" : [:],
            "outputs" : [
                "color" : "COLOR"
            ],
            "metalVertexShader": "myVertex",
            "metalFragmentShader": "myFragment",
        ]
    ],
    "sequence" : ["y"],
    "symbols" : [:]
]

let technique = SCNTechnique(dictionary: dictionary)
scnView.technique = technique

您可以将此渲染通道与其他通道组合(参见 SCNTechnique)。