Фрагментные шейдеры 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 прохода:
- Отображает изображение, содержащее информацию о глубине всех узлов в 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 для чего-то, для чего он не предназначен? Я новичок в разработке шейдеров.
Благодаря!
- Адам
1 ответ
Проблема с неточными пересечениями, которую вы видите, вызвана кодированием значения глубины в один канал цветовой цели. Каждый вывод для цветовой цели сохраняется в 32-битном формате, но, поскольку вы используете только один канал, ваш поплавок с полной точностью упаковывается только в 1 четверть (8 бит) из этих 32-битных.
Вместо цветовых целей вы должны выполнять рендеринг с двумя целевыми значениями глубины, таким образом вы получите гораздо более высокую точность глубины при сэмплировании на последнем проходе микширования.
В зависимости от того, чего вы пытаетесь достичь, вам может быть лучше использовать программу SCN.