假如弄清楚了編程管線、紋理、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 的值就剛好調轉了。

MoltenGL 和 cocos2d-x (metal-support 那個分支)的 shader 轉換器都使用了 glsl-optimizer,因而也有上述問題。


推薦閱讀:
相关文章