GL 移植到 Metal 的小细节

945 阅读4分钟
原文链接: zhuanlan.zhihu.com

假如弄清楚了编程管线、纹理、Buffer 这些基础概念,找准 API 的对应关系,将 GL 程序改写成 Metal 程序实际不难。只是有几个小细节需要注意,在此记录一下。

z 轴裁剪

GL 的 z 轴裁剪范围是 [-1, 1],但是 Metal 的 z 轴裁剪范围是 [0, 1]。假如超出了 z 轴范围,顶点就会被裁剪掉。

因为 z 轴范围不同,有时就会导致相同的顶点数据,相同的矩阵,GL 可以显示出来,而 Metal 就被裁剪掉一半。这种情况下,可以调整投影矩阵,也可以改写 Metal Shader,调整顶点 z 轴。比如

struct MtlShaderVaryings {
    float4 gl_Position[[position]];
    float2 vTexCoord;
};

vertex MtlShaderVaryings vertex_main(MtlShaderAttributes mtl_a[[stage_in]]) {
    MtlShaderVaryings result;
    xxxx
    // 调整顶点 z 轴
    result.gl_Position.z = (result.gl_Position.z + result.gl_Position.w) / 2.0f;
    return result;
}

调整投影矩阵,参考这里

mod 函数

GLSL 中 mod 函数定义为

return x - y * floor(x/y)

但 Metal Shader 中,fmod 函数定义为

x – y * trunc(x/y)

两者是不同的,不可以直接将 GLSL 的 mod 改写成 Metal Shader 的 fmod。比如 shadertoy 上的这个Industry II的例子,假如将 mod 直接对应成 fmod,背景中那个行走的机器人就显示不出来。

这时可以额外模拟一个函数

template <typename T1, typename T2>
inline auto emu_mod(T1 x, T2 y) -> decltype(x - y * floor(x/y)) {
    return x - y * floor(x/y);
}

参考这里

shader 中传递数组

在 GLSL 中,支持将数组从 vertex shader 传递到 fragment shader, 指下面这种语法。

varying vec3 thing[4];

但假如在 Metal 中,定义下面的语法。

struct FragmentIn {
    float4 position [[position]];
    float3 thing[4];
};

fragment float4 fragment_main(FragmentIn frag_in[[stage_in]], ....

会编译报错。Metal 中 stage_in 修饰的结构中不能包含数组,而需要将数组展开。参考这里

struct FragmentIn {
    float4 position [[position]];
    float2 thing0;
    float3 thing1;
    float3 thing2;
    float3 thing3;
};

在移植 shader 的时候,假如展开,有时很难跟 GLSL 直接对应。可以添加一个间接层。比如 GLSL 中的

varying vec2 thing[4];

void main()
{
    thing[0] = aTextureCoord.xy;
    thing[1] = aTextureCoord.xy + vec2(-texelWidthOffset, -texelHeightOffset);
    ....
}

就可以转换为

struct FragmentIn {
    float4 gl_Position [[position]];
    float2 thing0;
    float3 thing1;
    float3 thing2;
    float3 thing3;
};

fragment float4 fragment_main(FragmentIn frag_in[[stage_in]], .... {
    thread float2* thing = &frag_in.thing0;
    thing[0] = aTextureCoord.xy;
    thing[1] = aTextureCoord.xy + float2(-texelWidthOffset, -texelHeightOffset);
}

添加了这个间接层,GLSL 转换到 Metal Shader 容易用工具来完成。

处理 YUV 数据

摄像头录制出来的经常是 yuv 数据。在 iOS 中,录制出来的 yuv 放到 CVPixelBufferRef 中,分成两个 plane。Y 数据独占一个 plane,planeIndex = 0, UV 数据共用一个 plane,planeIndex = 1。

因而使用 GLES 去显示 yuv 数据,会对应成两个纹理。一个是 Y 纹理,一个是 UV 纹理。而为了兼容 GLES 2.x, 通常不会使用 GL_REDGL_RG 像素格式,而会使用 GL_LUMINANCE 去获取 Y 数据,GL_LUMINANCE_ALPHA 去获取 UV 数据。

对应的 GLSL 就类似下面这样子

varying lowp vec2 vTexCoord;
uniform sampler2D uTextureY;
uniform sampler2D uTextureUV;

void main() {
    vec3 yuv;
    vec3 rgb;
    yuv.x = texture2D(uTextureY, vTexCoord).r;
    yuv.yz = texture2D(uTextureUV, vTexCoord).ra - vec2(0.5, 0.5);
    
    // Using BT.709 which is the standard for HDTV
    rgb = mat3(      1,       1,      1,
                     0, -.18732, 1.8556,
               1.57481, -.46813,      0) * yuv;

    gl_FragColor = vec4(rgb, 1);
}

因为使用 GL_LUMINANCE_ALPHA 去获取 UV 数据,GLSL 中会使用 ra 通道。

当使用 Metal 去显示 YUV 数据时,并没有跟 GL_LUMINANCEGL_LUMINANCE_ALPHA 直接对应的纹理格式。而会使用 MTLPixelFormatR8Unorm 去获取 Y 数据,使用 MTLPixelFormatRG8Unorm 去获取 UV 数据。

因此当使用 Metal 去显示 YUV 数据时,获取 UV 数据的通道并非是 ra 通道,而是 rg 通道。

当将 GLSL 转换到 MSL 时,需要注意这个 ra 通道问题,不然就跟 Metal 的纹理格式不兼容,显示不出来。

渲染到 Texture

假如是离屏渲染,将结果渲染到纹理(Texture)中,GL 和 Metal 的渲染结果会上下颠倒。在 Metal 中,可以再次使用纹理时,将纹理坐标翻转一下。另外一种修改方式是 Metal 离屏渲染时翻转一下视口,

static inline MTLViewport transToMtlViewport(CGRect rt, BOOL upsideDown) {
    MTLViewport result;
    if (upsideDown) {
        result.originX = rt.origin.x;
        result.originY = rt.origin.y + rt.height;
        result.width = rt.size.width;
        result.height = -rt.size.height;
        result.znear = 0;
        result.zfar = 1;
    } else {
        result.originX = rt.origin.x;
        result.originY = rt.origin.y;
        result.width = rt.size.width;
        result.height = rt.size.height;
        result.znear = 0;
        result.zfar = 1;
    }
    return result;
}

翻转视口,会导致渲染出的结果也翻转。这种翻转视口的方式可能有隐患,参考这里

备注

glsl-optimizer 这个库用于优化 GLSL,它包含了 Metal 转换后端,支持将优化后的 GLSL 转换成 MSL(Metal Shader)。但它在转换时候忽略了上面提到的小细节,有些问题。

  1. glsl-optimizer 将 mod 直接转换成 MSL 的 fmod。正如上述,这两者实际是不等价的。
  2. glsl-optimizer 在转换 varying vec3 thing[4]; 这种语法时,没有将数组展开,导致转换后的 MSL 编译不过。
  3. glsl-optimizer 分别独立转换 vertex shader 和 fragment shader, 假如 varying 定义变量顺序不一致,转换后的 MSL 就会出问题。比如原来的 GLSL 中:
// vertex shader
varying vec4 color0;
varying vec4 color1;


// framgent shader
varying vec4 color1;
varying vec4 color0;

这种情况下,GLSL 的所有 varying 变量会被收集,转换成 MSL 的结构。但因为顺序不一致,vertex shader 中的结构跟 fragment shader 的内存布局不一致。将结构从 vertex shader 传递到 fragment shader, color0 和 color1 的值就刚好调转了。

MoltenGLcocos2d-x (metal-support 那个分支)的 shader 转换器都使用了 glsl-optimizer,因而也有上述问题。