Фрагментные шейдеры Metal / SceneKit - как избежать рендеринга поверх другой геометрии?

Учитывая эту базовую сцену SceneKit с кубом, сферой и пирамидой, расположенными рядом друг с другом, где сфера пересекает пирамиду:

И, учитывая нижеприведенный упрощенный шейдер Metal / 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, и даже без этого ввода мы можем вычислить глубину заданного фрагмента, умножив позицию z заданной вершины на modelTransform узла и viewProjectionTransform сцены, также предоставляемые SceneKit. С этой целью я придумал модификацию вышеупомянутого, которая выполняет 3 прохода:

  1. Отображает изображение, содержащее информацию о глубине всех узлов в includeCategoryBitmask
  2. Визуализирует изображение, содержащее информацию о глубине всех узлов в excludeCategoryBitmask (так что теперь у нас есть 2 изображения глубины, представляющие узел (узлы), которые мы хотим "стилизовать", и узел (узлы), которые мы не делаем)
  3. Проверяет глубину фрагмента на двух изображениях выше, и, если глубина первого изображения больше, чем глубина второго изображения, мы можем визуализировать сплошной красный фрагмент, в противном случае мы можем визуализировать цвет фрагмента нормальной сцены.

Ниже представлена ​​эта реализация:

Определение техники:

       <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

}

Вышеупомянутое, примененное к той же сцене, что и раньше, приближается к желаемому результату:

Однако есть несколько проблем:

  1. Край, где сфера встречается с треугольником, зазубрен и дрожит, когда камера вращается вокруг сцены.
  2. Я не верю, что этот метод учитывает такие вещи, как полупрозрачность или порядок рендеринга, например, если у меня есть узел в сцене с более высоким порядком рендеринга, чтобы он появлялся поверх других узлов, или узел со слегка прозрачным материалом, Я не думаю, что этот подход должным образом справится с такими ситуациями.
  3. Такое чувство, что я могу заново изобретать колесо. Как я уже сказал, я знаю, что SceneKit предоставляет нам фактическую глубину для чтения, но я не уверен, как правильно использовать его для достижения того, что я хочу.

Является ли это лучшим подходом к SCNTechniques для создания шейдеров, которые должным образом перекрываются фрагментами "перед" фрагментами, на которые мы хотим воздействовать? Пытаюсь ли я использовать SCNTechnique для чего-то, для чего он не предназначен? Я новичок в разработке шейдеров.

Благодаря!

  • Адам

1 ответ

Проблема с неточными пересечениями, которую вы видите, вызвана кодированием значения глубины в один канал цветовой цели. Каждый вывод для цветовой цели сохраняется в 32-битном формате, но, поскольку вы используете только один канал, ваш поплавок с полной точностью упаковывается только в 1 четверть (8 бит) из этих 32-битных.

Вместо цветовых целей вы должны выполнять рендеринг с двумя целевыми значениями глубины, таким образом вы получите гораздо более высокую точность глубины при сэмплировании на последнем проходе микширования.

В зависимости от того, чего вы пытаетесь достичь, вам может быть лучше использовать программу SCN.

Другие вопросы по тегам