基于Swift、Metal与片元着色器在iOS绘制椭圆的片元丢弃问题排查
问题根因
你遇到的所有片元都被丢弃的问题,核心是Metal特殊语义变量的坐标系在顶点/片元阶段不统一,和你之前在OpenGL环境的行为存在差异。
具体问题拆解
- 按照Metal渲染管线的规则,顶点着色器中标记为
[[position]]的输出值,代表裁剪空间坐标,经过透视除法后会变成范围在[-1, 1]的归一化设备坐标(NDC)。 - 但当这个带有
[[position]]语义的字段作为片元着色器的输入时,光栅化阶段会自动把它转换为帧缓冲空间坐标:原点在渲染目标的左上角,x轴向右递增、y轴向下递增,取值范围是[0, 渲染目标宽度] × [0, 渲染目标高度],单位为像素。
你代码里传入的tlPosition和brPosition都是NDC范围的量级(-1到1之间),但用来计算椭圆方程的ellipseIn.position.x/y,在片元阶段已经变成了像素量级的坐标(比如1000像素宽度的渲染目标下,x值范围是0到1000)。两者数值范围完全不在一个维度上,计算出来的椭圆方程结果会远大于1,触发discard_fragment(),最终所有片元都被丢弃,没有任何内容渲染出来。
你之前在OpenGL环境下代码能正常运行,本质是当时传入的tl/br值和gl_FragCoord的窗口像素坐标系是统一的,迁移到Metal时直接把tl/br换成NDC值,却没有对齐坐标系,就出现了错位。
修复方案
你的整体实现思路是完全合理的:渲染图元的包围四边形,在片元阶段通过数学方程裁剪出椭圆形状,这是图形领域非常经典的SDF渲染方案,只需要调整坐标系不匹配的问题即可:
- 在顶点输出结构体中新增一个无特殊语义的普通插值变量,专门用来传递片元对应的NDC坐标,避免被光栅化阶段转换成帧缓冲坐标:
struct EllipseOut { float4 position [[position]]; float4 color; float2 ndcCoord; // 新增:普通插值变量,存储片元对应的NDC坐标 float3 tlPosition [[flat]]; float3 brPosition [[flat]]; }; - 在顶点着色器中,直接把当前顶点的NDC坐标赋值给这个普通变量,这个变量没有特殊语义标记,光栅化时会按普通顶点属性做线性插值,在片元阶段拿到和传入的tl/br同范围的NDC值:
vertex EllipseOut ellipse_vertex(const EllipseIn ellipseIn [[stage_in]]) { EllipseOut ellipseOut; ellipseOut.position = ellipseIn.position; ellipseOut.ndcCoord = ellipseIn.position.xy; // 直接取顶点输出的NDC xy值 ellipseOut.color = ellipseIn.color; ellipseOut.tlPosition = ellipseIn.tlPosition; ellipseOut.brPosition = ellipseIn.brPosition; return ellipseOut; } - 在片元着色器中,把原本用来计算椭圆方程的x/y值,从原来的
ellipseIn.position.x/y替换成刚才新增的ellipseIn.ndcCoord.x/y,此时两边坐标系完全统一,计算出的椭圆方程结果就是正确的。
额外说明
你代码中其他部分的实现没有问题:
- 顶点描述符的内存偏移、步长计算都是正确的,和Swift侧的结构体布局完全匹配
- 三角形索引顺序是符合Metal的正面朝向要求的,不会被背面剔除
- 给tlPosition、brPosition加上
[[flat]]修饰符是正确的,这两个值在四边形的所有顶点上都是固定值,不需要插值,可以减少不必要的计算开销
内容的提问来源于stack exchange,提问作者Aleksei Kritskov
相关产品推荐
相关产品推荐

