cover_image

图形 API 坐标系统的跨平台兼容

快手Y-tech团队 快手大模型
2023年08月25日 11:50
图片


快手Y-tech

最新技术干货分享


摘要


快手 Y-tech 的魔法表情底层使用一种跨平台的图形引擎 KGFX,能够以不同图形硬件加速 API 为后端进行绘制,类似 BGFX 和 WebGPU。KGFX 面临的问题之一是,不同的平台需要在底层适配不同的图形 API。如下图所示,不同的平台支持各种图形 API 的种类不同,实际上支持的力度也不同,微软虽然支持 OpenGL 和 Vulkan,但对自家的 DirectX 支持更完善;苹果更是已经退出 Khronos 组织,不再更新系统 OpenGL 驱动版本,也没有原生支持 Vulkan。


OpenGL (ES)

DirectX

Metal

Vulkan

Windows

MacOS

Linux

iOS

Android

注:不同平台对各种图形 API 的支持情况,✅ = 原生支持,❌ = 不能原生支持
而各图形 API 之间的坐标系约定略有区别,本文将描述这些区别并给出统合这些区别的解决方案。
本文中的 “DirectX” 指 DirectX 10 及以上的版本。


用户空间坐标


不同的 3D 框架有各种坐标系约定,如 Unity 约定使用左手系,Y 轴向上;Unreal 也使用左手系,但是 Z 轴向上;起源引擎则使用右手系,Z 轴向上。

在使用非固定可编程管线的现代图形 API 中,对这些三维坐标系约定没有限制,不同坐标系约定的区别只在于顶点着色器中使用变换矩阵不同。
不同图形 API 之间的关键区别在于 NDC 空间和帧缓冲空间的坐标约定,在 API 中这些约定是不可修改的(除了下文要提到的 VK_KHR_Maintenance1 扩展等例外),这也是设计兼容各种图形 API 后端的跨平台图形引擎的难点之一。


NDC 坐标

可编程管线中,顶点着色器的输出中有一组输出具有特殊地位,那就是当前顶点在 NDC 空间中的位置 (x, y, z, w), 其它的输出变量只是经过插值再送入片元着色器,而顶点位置则参与了光栅化过程:进行透视除法得到 (x', y', z') = (x/w, y/w, z/w),根据透视投影原理,这可以实现“近大远小”效果,符合人眼观察的现象;然后将 (x', y', z') 在 NDC 立方体范围外的部分进行裁剪,最后再进行插值送入片元着色器。
一、Z 轴裁剪范围

    各图形 API 在进行坐标裁剪时,X 坐标和 Y 坐标的裁剪范围都是 [-1, 1],但 Z 坐标裁剪范围有所区别,OpenGL(ES) 的 Z 坐标裁剪范围和 X,Y 一样是 [-1, 1],而其它更现代的图形 API,包括 DirectX,Metal,Vulkan,都将 Z 坐标裁剪到 [0, 1]。

这是因为当使用浮点数格式的深度缓冲时,z 值分布在 [0, 1] 范围内能得到更高的精度,OpenGL 实际上也把 z 值转换到 [0, 1] 再存入深度缓冲,并在 OpenGL 4.5 将 GL_ARB_clip_control 扩展加入核心 API 以允许直接使用 Z 轴在 [0, 1] 范围的裁剪空间。但由于历史包袱,OpenGL 仍然维持 [-1, 1] 的默认设置,而 OpenGL ES 并未引入 GL_ARB_clip_control 扩展。
要统一这种差异,可以按照 Z 坐标裁剪到 [0, 1] 生成坐标变换矩阵,并在 OpenGL 的顶点着色器最后增加来将 z 值变换到 [-1, 1]。
gl_Position.z = gl_Position.z * 2.0 - gl_Position.w;
二、Y 轴方向
    虽然 NDC 空间并没有实际呈现在屏幕上,但其与最终呈现结果之间的变换已经固定了,也就是说只要知道了 NDC 空间的渲染结果,我们就能知道对应的屏幕空间或者纹理坐标的渲染结果,细节可见下节“纹理坐标和帧缓冲坐标”。因此可以根据最后渲染到屏幕上的结果来讨论 NDC 坐标系 X 轴和 Y 轴的方向。
各图形 API 的 NDC 空间都以向左为 X 轴正方向,向屏幕内为 Z 轴正方向。

OpenGL,Metal,D3D12 的 NDC 空间都向上为 Y 轴正方向,因此想画出一个正立的三角形,可以使用 (-0.5, -0.5),(0.5, -0.5),(0.0, 0.5) 三个顶点坐标,分别是左下角,右下角,上方的三个顶点。

图片

图1 在非 Vulkan 的图形 API 的 NDC 空间中绘制正立三角形

而 Vulkan 的 NDC 空间以向下为 Y 轴正方向。这样想画出和上图一样的三角形,需要的顶点坐标是 (-0.5, 0.5),(0.5, 0.5),(0.0, -0.5),Y 坐标值全都是相反的。

图片

图2 在 Vulkan 的 NDC 空间中绘制正立三角形,注意 Y 轴正方向朝下
可见,在 NDC 空间画坐标相同的内容,用 Metal/OpenGL/DirectX 呈现在屏幕上是一致的,用 Vulkan 则是其它 API 的倒像。

三、纹理坐标和帧缓冲坐标

    常见的说法是 OpenGL 中纹理坐标以左下角为原点,其它图形 API 以纹理左上角为原点。这样的说法容易产生误导。实际上,包括 OpenGL 在内,所有图形 API 采样纹理的坐标都用的是“数据坐标”。

图形 API 生成纹理对象时需要一段连续数据(位于内存或显存)来作为纹理内容,这段连续数据表示一个个像素,先行后列,纹理中的 uv 坐标都是以行方向为 u 坐标,列方向为 v 坐标。也就是说,假设加载纹理时输入的数据是一个数组 Pixel data[N],宽为 width,高为 height,满足 N = width * height,那么在 shader 中采样对应纹理单元 tex,GLSL 中就是 texelFetch(tex, ivec2(x, y)),这样得到结果是 data[width * y + x] ,OpenGL 和其它图形 API 皆符合这一规律。

图片

图3 内存中像素转换到纹理采样坐标系示意图

那“OpenGL 中纹理坐标以左下角为原点,其它图形 API 以纹理左上角为原点”这样的说法是什么意思呢,这种说法其实在描述渲染结果最终输出到屏幕时,所使用的屏幕坐标系定义。OpenGL 渲染到屏幕时,认为屏幕渲染区域左下角为原点,向上为 Y 轴正方向,而 DirectX 等认为屏幕渲染区域的左上角为原点,向下为 Y 轴正方向。OpenGL 的屏幕坐标和数学上常用的坐标系定义一致,而其它图形 API 的屏幕坐标系定义和帧缓冲的内存表示更一致,更有利于优化。
用 OpenGL 和 DirectX 渲染同样的 NDC 内容,呈现到屏幕上的结果相同,但纹理互为倒像,原因就是 OpenGL 和 DirectX 是按照互相颠倒的纹理坐标定义向纹理写入渲染结果的。而另一方面,在帧缓冲上绘制同样的内容,再读取,在 Metal 和 Vulkan 上读出的内容一样,在 OpenGL 上是倒着的。

目前常用的 PNG,JPEG 图像格式在存储和加载时都从左上角开始,也就是说以图像左上角的像素为第一个像素,这样在 OpenGL 中正常加载纹理,并且让纹理通过和 NDC 坐标方向一致的方式显示,就会渲染出倒置的图像,所以在 OpenGL 中要通过各种方式处理这一问题,比如加载图片时就将图片倒置,或者采样纹理时处理 V 坐标等。另外 BMP 格式图片的像素数据是从左下角开始存储的,和 OpenGL 假设一致。


NDC 空间 Y 轴正方向

帧缓冲坐标定义 Y 轴正方向

OpenGL

向上

向上

D3D12/Metal

向上

向下

Vulkan

向下

向下

四、背面剔除

      此过程在顶点着色器之后执行,若三角形面片朝向与设置相符,丢弃该面片。常见的应用场景是剔除 3D 模型的背面三角形。

在 NDC 空间绘制同样的内容,Y 轴朝向不同会导致面片方向相反,所以在 OpenGL 和 Metal/DirectX 中常需使用不同的背面剔除设置,如 Metal 默认顺时针方向为正面,而 OpenGL 默认为逆时针。


坐标系兼容

了解以上背景知识之后,我们开始设计一套能兼容各个图形 API 的坐标系定义。

此处的场景是,我们需要设计一个支持多后端的跨平台图形引擎,能够使用 OpenGL(ES),DirectX,Metal,Vulkan,作为渲染后端,希望使用一套后端无关的坐标系定义,能尽量贴近图形 API 原有的逻辑,降低兼容的成本。为了达到目的,引擎可以适当修改着色器代码,翻转纹理,而这些操作对使用者而言应当是无感知的。

有一点需要注意,引擎需要有渲染到纹理的能力,此时结果纹理在不同图形 API 中应当保持统一,而如上文所言,Metal/DirectX 和 OpenGL 渲染出的纹理互为倒像。同时翻转纹理的操作比较浪费性能,我们希望能尽量少做。
我们先列出各图形 API 直接渲染一张纹理的过程,使用的着色器代码如下,使用 GLSL:

顶点着色器:

#version 310 eslayout(location = 0) in vec2 aPosition;layout(location = 0) out vec2 vPosition;void main() {    gl_Position = vec4(aPosition, 0.0, 1.0);    vPosition = aPosition;}

片元着色器:

#version 310 esprecision highp float;layout(location = 0) in vec2 vPosition;layout(location = 0) out vec4 fragColor;layout(location = 0) uniform sampler2D texture0;void main() {    fragColor = texture(texture0, vPosition);}
传入顶点数据是 { { 0.0, 0.0 }, {1.0, 0.0}, {0.0, 1.0}, {1.0, 1.0} },以 “triangle strip” 模式绘制。这样就是在 NDC 空间的第一象限,以与 NDC 空间坐标相同的方向绘制了纹理。

图片

图4 三端坐标系对比

图中“NDC 空间”和“帧缓冲”都是按照预期的屏幕显示来绘制的,也就是如果渲染目标是屏幕,那么它们的渲染效果就是这样,而绘制结果在坐标系中的位置需要看图中的坐标轴。例如图中 OpenGL 和 Vulkan 的 NDC 空间的内容其实是一样的。

“结果纹理”则帧缓冲内容再作为纹理采样时的内容,统一以左上角为原点。这也是将帧缓存内容读取到内存再保存为图片文件看到的结果。
由图可见各个图形 API 的结果纹理不一致,为了解决这一点,要么都和 OpenGL 的结果纹理保持一致,要么都和 Metal 的结果纹理保持一致,这就引出了两种解决方案。
一、类 OpenGL 坐标系

使用类似 OpenGL 的坐标系,我们的修改方式是:


对于 Metal/DirectX
(1)在管线的顶点着色器修改输出的顶点坐标的 y 值,就是在合适的地方插入 gl_Positon.y = -gl_Positon.y,以此来翻转 NDC 空间的内容。这个过程一般被称作翻转 Y 坐标(flip y)。
(2)设置剔除模式时取反,也就是说引擎设置剔除背面,实际在后端设置剔除正面。相应地修改默认逆时针为正面。
(3)最后输出的结果纹理输出到屏幕时需要上下翻转输出

对于 Vulkan
(1)设置剔除模式时取反
(2)最后输出的结果纹理输出到屏幕时需要上下翻转输出

关于剔除模式,除了上述做法,也可在设置正面环绕方向时取反,剔除模式保持不变。修改后的绘制过程:

图片

图5 三端对齐为类 OpenGL 坐标系
使用这种坐标系的好处是能够兼容为 OpenGL 编写的代码逻辑,使用 OpenGL 后端时不需要太多修改。
二、类 DirectX 坐标系
使用类似 D3D12/Metal 的坐标系定义。需要做的修改:

对于 OpenGL
(1)修改顶点着色器,翻转 Y 坐标
(2)相应修改剔除模式设置
(3)最后的结果纹理输出到屏幕时需要上下翻转输出

对于 Vulkan
(1)修改顶点着色器,翻转 Y 坐标
(2)相应修改剔除模式设置

如果 Vulkan 支持 VK_KHR_Maintenance1 扩展或者版本在 1.1 以上,则支持通过设置负的 viewport 高度使其坐标系定义和 D3D12/Metal 一致,下图中标为“Vulkan 1.1”。

图片

图6 三端对齐为类 DirectX 坐标系

这种坐标系定义的好处是不需要特殊处理就能够直接兼容 DirectX/Metal/Vulkan 这样的现代图形 API。输出的纹理保存成图片时不需要翻转,与设备坐标一致。


实际应用


快手 KGFX 图形引擎需要兼容大量直接使用 OpenGL API 的历史代码,并且主要在移动平台上使用 OpenGL ES 后端,因此选择了类 OpenGL 坐标系。

而跨平台图形 WebGPU,Unreal 游戏引擎等则选择了类 DirectX 坐标系,因为现在主流 PC 配置是 Windows 操作系统加 DirectX 后端,使用类 DirectX 坐标系不需要特别处理。并且 OpenGL 和 OpenGL ES 标准不再更新,拥抱 Metal/Vulkan/D3D12 这样更接近底层的现代图形 API 是未来趋势所在。

除了在快手 Y-tech 部门广泛使用,未来将计划在音视频处理和 AI 部门引入 KGFX 来统一各端代码差异,在后端切换为 Metal、Vulkan 来提高性能。另外未来还有将 KGFX 图形库开源的计划,希望能具有一定影响力,惠及整个行业发展。
参考链接

https://zhuanlan.zhihu.com/p/339295068 

https://www.saschawillems.de/blog/2019/03/29/flipping-the-vulkan-viewport/



你可能还想看

图片

CVPR2023|单视图3D发型重建技术

快手数字美妆系列:钻石口红效果解析

VAT轻量级动画技术


图片

    等你来 

Y-tech

快手Y-tech部门致力于通过计算机视觉、计算机图形学、深度学习、AR/VR/HCI等多领域的交叉,帮助每个人更好地表达、创作以及互动。Y-tech在北京、深圳、Palo Alto均有研发团队。如果你对我们做的事情感兴趣,欢迎联系并加入我们!

Y-tech长期招聘(全职或实习生):计算机视觉、计算机图形学、UE/Unity开发、技术美术、AI推理引擎、AI工程架构、美颜/特效技术、平台/工具开发、技术产品经理等方向的优秀人才。联系方式:

y-techservice@kuaishou.com

Y-tech///

修改于2023年08月28日
继续滑动看下一个
快手大模型
向上滑动看下一个