Metal / SceneKit Fragment Shaders - 如何避免在其他几何体上渲染?

Posted

技术标签:

【中文标题】Metal / SceneKit Fragment Shaders - 如何避免在其他几何体上渲染?【英文标题】:Metal / SceneKit Fragment Shaders - How to avoid rendering ontop of other geometry? 【发布时间】:2021-03-14 14:58:19 【问题描述】:

鉴于这个基本的 SceneKit 场景,立方体、球体和金字塔彼此相邻,其中球体与金字塔相交:

鉴于以下,简化的金属着色器/SCNTechnique,它只渲染具有特定类别位掩码的任何节点纯红色:

技术定义:

<dict>
    <key>passes</key>
    <dict>
        <key>pass_fill_drawMask</key>
        <dict>
            <key>draw</key>
            <string>DRAW_SCENE</string>
            <key>program</key>
            <string>doesntexist</string>
            <key>metalVertexShader</key>
            <string>pass_fill_drawMask_vertex</string>
            <key>metalFragmentShader</key>
            <string>pass_fill_drawMask_fragment</string>
            <key>includeCategoryMask</key>
            <string>0</string>
            <key>colorStates</key>
            <dict>
                <key>clear</key>
                <true/>
            </dict>
            <key>inputs</key>
            <dict>
                <key>aPos</key>
                <string>vertexSymbol</string>
            </dict>
            <key>outputs</key>
            <dict>
                <key>color</key>
                <string>MASK</string>
            </dict>
        </dict>
        <key>pass_fill_render</key>
        <dict>
            <key>draw</key>
            <string>DRAW_QUAD</string>
            <key>program</key>
            <string>doesntexist</string>
            <key>metalVertexShader</key>
            <string>pass_fill_render_vertex</string>
            <key>metalFragmentShader</key>
            <string>pass_fill_render_fragment</string>
            <key>inputs</key>
            <dict>
                <key>aPos</key>
                <string>vertexSymbol</string>
                <key>colorSampler</key>
                <string>COLOR</string>
                <key>maskSampler</key>
                <string>MASK</string>
                <key>resolution</key>
                <string>resolution</string>
            </dict>
            <key>outputs</key>
            <dict>
                <key>color</key>
                <string>COLOR</string>
            </dict>
        </dict>
    </dict>
    <key>sequence</key>
    <array>
        <string>pass_fill_drawMask</string>
        <string>pass_fill_render</string>
    </array>
    <key>symbols</key>
    <dict>
        <key>resolution</key>
        <dict>
            <key>type</key>
            <string>float</string>
        </dict>
        <key>vertexSymbol</key>
        <dict>
            <key>semantic</key>
            <string>vertex</string>
        </dict>
    </dict>
    <key>targets</key>
    <dict>
        <key>MASK</key>
        <dict>
            <key>type</key>
            <string>color</string>
            <key>format</key>
            <string>rgb</string>
            <key>size</key>
            <string>1024x1024</string>
            <key>scaleFactor</key>
            <integer>1</integer>
        </dict>
    </dict>
</dict>

着色器:

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

struct Node 
    float4x4 modelTransform;
    float4x4 modelViewTransform;
    float4x4 normalTransform;
    float4x4 modelViewProjectionTransform;
;

struct VertexIn 
    float4 position [[attribute(SCNVertexSemanticPosition)]];
    float4 normal [[attribute(SCNVertexSemanticNormal)]];
;

struct VertexOut 
    float4 position [[position]];
    float2 uv;
;

typedef struct 
    float resolution;
 Uniforms;

constexpr sampler s = sampler(coord::normalized,
                              r_address::clamp_to_edge,
                              t_address::clamp_to_edge,
                              filter::linear);

////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// PASS 1 - Render solid pixels for the input geometry to a target image for later use
////////////////////////////////////////////////////////////////////////////////////////////////////////////

vertex VertexOut pass_fill_drawMask_vertex(VertexIn in [[stage_in]],
                                           constant Node& scn_node [[buffer(0)]]) 
    VertexOut out;
    out.position = scn_node.modelViewProjectionTransform * float4(in.position.xyz, 1.0);
    out.uv = float2((in.position.x + 1.0) * 0.5, 1.0 - (in.position.y + 1.0) * 0.5);
    return out;
;

fragment half4 pass_fill_drawMask_fragment(VertexOut in [[stage_in]]) 
    return half4(1.0, 1.0, 1.0, 1.0);
;

////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// PASS 2 - Render any opaque pixels from the target image a solid color
////////////////////////////////////////////////////////////////////////////////////////////////////////////

vertex VertexOut pass_fill_render_vertex(VertexIn in [[stage_in]],
                                    texture2d<float, access::sample> colorSampler,
                                    texture2d<float, access::sample> maskSampler) 
    VertexOut out;
    out.position = in.position;
    out.uv = float2((in.position.x + 1.0) * 0.5,1.0 - (in.position.y + 1.0) * 0.5);
    return out;
;

fragment half4 pass_fill_render_fragment(VertexOut in [[stage_in]],
                                         texture2d<float, access::sample> colorSampler [[texture(0)]],
                                         texture2d<float, access::sample> maskSampler [[texture(1)]],
                                         constant SCNSceneBuffer& scn_frame [[buffer(0)]],
                                         constant Uniforms& uniforms [[buffer(1)]]) 
    
    float2 ratio = float2(colorSampler.get_width() / uniforms.resolution, colorSampler.get_height() / uniforms.resolution);
    ratio = float2(ratio.x > 1 ? 1 : ratio.x, ratio.y > 1 ? 1 : ratio.y);
    
    float4 maskColor = maskSampler.sample(s, in.uv * ratio);
    
    if (maskColor.a > 0) 
        // This pixel belongs to the geometry, render it red
        return half4(1.0, 0.0, 0.0, 1.0);
     else 
        // This pixel does not belong to the geometry, render it the normal scene color
        float4 fragmentColor = colorSampler.sample(s, in.uv);
        return half4(fragmentColor);
    
    
;

快速实现:

static func fillTechnique(resolution: CGFloat, nodeCategoryBitmask: UInt32) -> SCNTechnique? 

    guard
        let fileUrl = Bundle.main.url(forResource: "FillTechnique", withExtension: "plist"),
        let data = try? Data(contentsOf: fileUrl),
        var result = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] else 
        return nil
    

    result[keyPath: "passes.pass_fill_drawMask.includeCategoryMask"] = Int(nodeCategoryBitmask)
    print(result)
    guard let technique = SCNTechnique(dictionary: result) else 
        fatalError("Unable to create outline technique")
    
    
    technique.setObject(resolution, forKeyedSubscript: "resolution" as NSCopying)

    return technique


当应用于原始场景时,球体网格已将其类别位掩码设置为与技术定义中应用于 includeCategoryBitmask 的值相同的值,我们得到以下输出:

我想做的是考虑传递给着色器的片段的深度,这样红色球体就不会呈现在从相机视图中遮挡它的几何体的“顶部”。

我知道 SceneKit 为 SCNTechnique Metal 着色器提供了 COLOR 和 DEPTH 输入,即使没有该输入,我们也可以通过将给定顶点的 z 位置乘以节点的 modelTransform 和场景的 viewProjectionTransform 来计算给定片段的深度,还提供了通过 SceneKit。为此,我对上述内容进行了修改,执行 3 次:

    渲染包含 includeCategoryBitmask 中所有节点的深度信息的图像 渲染包含 excludeCategoryBitmask 中所有节点的深度信息的图像(所以我们现在有 2 个深度图像代表我们想要“风格化”的节点和我们不想要的节点) 检查上面两张图片中片段的深度,如果第一张图片的深度大于第二张图片的深度,我们可以渲染一个纯红色的片段,否则我们可以渲染正常场景的片段颜色。

下面是这个实现:

技术定义:

<dict>
    <key>passes</key>
    <dict>
        <key>pass_fill_drawMask</key>
        <dict>
            <key>draw</key>
            <string>DRAW_SCENE</string>
            <key>program</key>
            <string>doesntexist</string>
            <key>metalVertexShader</key>
            <string>pass_fill_drawMask_vertex</string>
            <key>metalFragmentShader</key>
            <string>pass_fill_drawMask_fragment</string>
            <key>includeCategoryMask</key>
            <string>2</string>
            <key>colorStates</key>
            <dict>
                <key>clear</key>
                <true/>
            </dict>
            <key>inputs</key>
            <dict>
                <key>fillColorR</key>
                <string>fillColorR</string>
                <key>fillColorG</key>
                <string>fillColorG</string>
                <key>fillColorB</key>
                <string>fillColorB</string>
                <key>aPos</key>
                <string>vertexSymbol</string>
                <key>colorSampler</key>
                <string>COLOR</string>
            </dict>
            <key>outputs</key>
            <dict>
                <key>color</key>
                <string>MASK</string>
            </dict>
        </dict>
        <key>pass_fill_render</key>
        <dict>
            <key>draw</key>
            <string>DRAW_QUAD</string>
            <key>program</key>
            <string>doesntexist</string>
            <key>metalVertexShader</key>
            <string>pass_fill_render_vertex</string>
            <key>metalFragmentShader</key>
            <string>pass_fill_render_fragment</string>
            <key>inputs</key>
            <dict>
                <key>aPos</key>
                <string>vertexSymbol</string>
                <key>colorSampler</key>
                <string>COLOR</string>
                <key>maskSampler</key>
                <string>MASK</string>
                <key>resolution</key>
                <string>resolution</string>
            </dict>
            <key>outputs</key>
            <dict>
                <key>color</key>
                <string>COLOR</string>
            </dict>
        </dict>
    </dict>
    <key>sequence</key>
    <array>
        <string>pass_fill_drawMask</string>
        <string>pass_fill_render</string>
    </array>
    <key>symbols</key>
    <dict>
        <key>fillColorR</key>
        <dict>
            <key>type</key>
            <string>float</string>
        </dict>
        <key>fillColorG</key>
        <dict>
            <key>type</key>
            <string>float</string>
        </dict>
        <key>fillColorB</key>
        <dict>
            <key>type</key>
            <string>float</string>
        </dict>
        <key>resolution</key>
        <dict>
            <key>type</key>
            <string>float</string>
        </dict>
        <key>vertexSymbol</key>
        <dict>
            <key>semantic</key>
            <string>vertex</string>
        </dict>
    </dict>
    <key>targets</key>
    <dict>
        <key>MASK</key>
        <dict>
            <key>type</key>
            <string>color</string>
            <key>format</key>
            <string>rgb</string>
            <key>size</key>
            <string>1024x1024</string>
            <key>scaleFactor</key>
            <integer>1</integer>
        </dict>
    </dict>
</dict>

着色器:

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

struct Node 
    float4x4 modelTransform;
    float4x4 modelViewTransform;
    float4x4 normalTransform;
    float4x4 modelViewProjectionTransform;
;

struct VertexIn 
    float4 position [[attribute(SCNVertexSemanticPosition)]];
    float4 normal [[attribute(SCNVertexSemanticNormal)]];
;

struct VertexOut 
    float4 position [[position]];
    float2 uv;
;

typedef struct 
    float resolution;
 Uniforms;

constexpr sampler s = sampler(coord::normalized,
                              r_address::clamp_to_edge,
                              t_address::clamp_to_edge,
                              filter::linear);

////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// PASS 1 - Render depth for included geometries
////////////////////////////////////////////////////////////////////////////////////////////////////////////

vertex VertexOut pass_depth_incl_vertex(VertexIn in [[stage_in]],
                                        constant SCNSceneBuffer& scn_frame [[buffer(0)]],
                                        constant Node& scn_node [[buffer(1)]]) 
    VertexOut out;
    out.position = scn_node.modelViewProjectionTransform * float4(in.position.xyz, 1.0);
    
    // Store the screen depth in the position's z axis
    float4 depth = scn_frame.viewProjectionTransform * scn_node.modelTransform * in.position;
    out.position.z = depth.z;
    
    out.uv = float2((in.position.x + 1.0) * 0.5,1.0 - (in.position.y + 1.0) * 0.5);
    return out;
;

fragment half4 pass_depth_incl_fragment(VertexOut in [[stage_in]]) 
    return half4(in.position.z, in.position.z, in.position.z, 1.0);
;

////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// PASS 2 - Render depth for excluded geometries
////////////////////////////////////////////////////////////////////////////////////////////////////////////

vertex VertexOut pass_depth_excl_vertex(VertexIn in [[stage_in]],
                                        constant SCNSceneBuffer& scn_frame [[buffer(0)]],
                                        constant Node& scn_node [[buffer(1)]]) 
    VertexOut out;
    out.position = scn_node.modelViewProjectionTransform * float4(in.position.xyz, 1.0);
    
    // Store the screen depth in the position's z axis
    float4 depth = scn_frame.viewProjectionTransform * scn_node.modelTransform * in.position;
    out.position.z = depth.z;
    
    out.uv = float2((in.position.x + 1.0) * 0.5,1.0 - (in.position.y + 1.0) * 0.5);
    return out;
;

fragment half4 pass_depth_excl_fragment(VertexOut in [[stage_in]]) 
    return half4(in.position.z, in.position.z, in.position.z, 1.0);
;

////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// PASS 3 - Render fragments for pixels passing depth test
////////////////////////////////////////////////////////////////////////////////////////////////////////////

vertex VertexOut pass_depth_check_vertex(VertexIn in [[stage_in]]) 
    VertexOut out;
    out.position = in.position;
    out.uv = float2(
                    (in.position.x + 1.0) * 0.5,
                    1.0 - (in.position.y + 1.0) * 0.5
                    );
    return out;
;

fragment half4 pass_depth_check_fragment(VertexOut in [[stage_in]],
                                         texture2d<float, access::sample> inclSampler [[texture(0)]],
                                         texture2d<float, access::sample> exclSampler [[texture(1)]],
                                         texture2d<float, access::sample> colorSampler [[texture(2)]],
                                         constant SCNSceneBuffer& scn_frame [[buffer(0)]],
                                         constant Uniforms& uniforms [[buffer(1)]]) 

    float2 ratio = float2(colorSampler.get_width() / uniforms.resolution, colorSampler.get_height() / uniforms.resolution);
    ratio = float2(ratio.x > 1 ? 1 : ratio.x, ratio.y > 1 ? 1 : ratio.y);

    float4 inclColor = inclSampler.sample(s, in.uv * ratio);
    float4 exclColor = exclSampler.sample(s, in.uv * ratio);
    
    float inclDepth = inclColor.r;
    float exclDepth = exclColor.r;
    
    bool isBackground = inclColor.a == 0 && exclColor.a == 0;
    
    if (inclDepth >= exclDepth && !isBackground) 
        return half4(1.0, 0.0, 0.0, 1.0);
     else 
        float4 color = colorSampler.sample(s, in.uv);
        return half4(color);
    
    
;

快速实现:

static func depthCheckTechnique(resolution: CGFloat, nodeCategoryBitmask: UInt32) -> SCNTechnique? 

    guard
        let fileUrl = Bundle.main.url(forResource: "DepthCheckTechnique", withExtension: "plist"),
        let data = try? Data(contentsOf: fileUrl),
        var result = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] else 
        return nil
    

    result[keyPath: "passes.pass_depth_incl.includeCategoryMask"] = Int(nodeCategoryBitmask)
    result[keyPath: "passes.pass_depth_excl.excludeCategoryBitmask"] = Int(nodeCategoryBitmask)
    result[keyPath: "targets.TARG_DEPTH_INCL.size"] = "\(resolution)x\(resolution)"
    result[keyPath: "targets.TARG_DEPTH_EXCL.size"] = "\(resolution)x\(resolution)"
    
    guard let technique = SCNTechnique(dictionary: result) else 
        fatalError("Unable to create outline technique")
    

    technique.setObject(resolution, forKeyedSubscript: "resolution" as NSCopying)

    return technique


以上应用于与之前相同的场景,接近预期的结果:

但是,有几个问题:

    球体与三角形相交的边缘呈锯齿状,并且随着相机围绕场景运行而抖动 我不相信这种方法会考虑半透明或渲染顺序等因素,例如,如果我在场景中有一个节点具有更高的渲染顺序,因此它出现在其他节点之上,或者一个节点具有稍微透明的材料,我认为这种方法不能很好地处理这些情况。 这感觉就像我在重新发明***一样。就像我说的,我知道 SceneKit 为我们提供了一个实际的深度输入来读取,但我不确定如何正确使用它来实现我想要的。

这是使用 SCNTechniques 创建着色器的最佳方法吗?这些着色器会被我们想要影响的片段“前面”的片段正确遮挡?我是否试图将 SCNTechnique 用于它不是为它设计的东西?我对着色器开发还很陌生。

谢谢!

亚当

【问题讨论】:

所需结果与当前结果的对比有助于更好地理解问题。 SCNTechnique 也可能不是实现此效果的最佳方式。您是否需要坚持使用SCNTechnique(例如了解它的工作原理)还是可以接受任何其他解决方案? @mnuages 谢谢,我用更多信息重做了这个问题。如果可能的话,我更愿意用 SCNTechniques 来解决这个问题,这样我才能看到这个系统可以推进多远,但如果这不是 SCNTechniques 的设计目的,那么我愿意接受其他建议。 我遇到了同样的问题。 SCNTechnique 总是在所有其他几何图形之上渲染...您找到解决方案了吗? 【参考方案1】:

您看到的交点不准确问题是由将深度值编码到颜色目标的单个通道中引起的。颜色目标的每个输出都存储到 32 位中,但由于您只使用一个通道,因此您的全精度浮点数仅被打包到这 32 位的四分之一(8 位)中。

您应该渲染到两个深度目标,而不是颜色目标,这样在最终混合通道中采样时可以获得更高的精度深度。

根据您要实现的目标,使用 SCNProgram 可能会更好。

【讨论】:

以上是关于Metal / SceneKit Fragment Shaders - 如何避免在其他几何体上渲染?的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

高质量渲染——RealityKit vs SceneKit vs Metal

将纯 Metal-API 与 SceneKit 或 SpriteKit 一起使用

iOS 14 解决方法中的 SceneKit Metal 着色器编译性能错误