快手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 | ✅ | ❌ | ❌ | ✅ |
用户空间坐标
不同的 3D 框架有各种坐标系约定,如 Unity 约定使用左手系,Y 轴向上;Unreal 也使用左手系,但是 Z 轴向上;起源引擎则使用右手系,Z 轴向上。
NDC 坐标
各图形 API 在进行坐标裁剪时,X 坐标和 Y 坐标的裁剪范围都是 [-1, 1],但 Z 坐标裁剪范围有所区别,OpenGL(ES) 的 Z 坐标裁剪范围和 X,Y 一样是 [-1, 1],而其它更现代的图形 API,包括 DirectX,Metal,Vulkan,都将 Z 坐标裁剪到 [0, 1]。
gl_Position.z = gl_Position.z * 2.0 - gl_Position.w;
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 坐标值全都是相反的。
三、纹理坐标和帧缓冲坐标
常见的说法是 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 内存中像素转换到纹理采样坐标系示意图
目前常用的 PNG,JPEG 图像格式在存储和加载时都从左上角开始,也就是说以图像左上角的像素为第一个像素,这样在 OpenGL 中正常加载纹理,并且让纹理通过和 NDC 坐标方向一致的方式显示,就会渲染出倒置的图像,所以在 OpenGL 中要通过各种方式处理这一问题,比如加载图片时就将图片倒置,或者采样纹理时处理 V 坐标等。另外 BMP 格式图片的像素数据是从左下角开始存储的,和 OpenGL 假设一致。
NDC 空间 Y 轴正方向 | 帧缓冲坐标定义 Y 轴正方向 | |
OpenGL | 向上 | 向上 |
D3D12/Metal | 向上 | 向下 |
Vulkan | 向下 | 向下 |
此过程在顶点着色器之后执行,若三角形面片朝向与设置相符,丢弃该面片。常见的应用场景是剔除 3D 模型的背面三角形。
在 NDC 空间绘制同样的内容,Y 轴朝向不同会导致面片方向相反,所以在 OpenGL 和 Metal/DirectX 中常需使用不同的背面剔除设置,如 Metal 默认顺时针方向为正面,而 OpenGL 默认为逆时针。
坐标系兼容
此处的场景是,我们需要设计一个支持多后端的跨平台图形引擎,能够使用 OpenGL(ES),DirectX,Metal,Vulkan,作为渲染后端,希望使用一套后端无关的坐标系定义,能尽量贴近图形 API 原有的逻辑,降低兼容的成本。为了达到目的,引擎可以适当修改着色器代码,翻转纹理,而这些操作对使用者而言应当是无感知的。
有一点需要注意,引擎需要有渲染到纹理的能力,此时结果纹理在不同图形 API 中应当保持统一,而如上文所言,Metal/DirectX 和 OpenGL 渲染出的纹理互为倒像。同时翻转纹理的操作比较浪费性能,我们希望能尽量少做。
我们先列出各图形 API 直接渲染一张纹理的过程,使用的着色器代码如下,使用 GLSL:
顶点着色器:
layout(location = 0) in vec2 aPosition;
layout(location = 0) out vec2 vPosition;
void main() {
gl_Position = vec4(aPosition, 0.0, 1.0);
vPosition = aPosition;
}
片元着色器:
precision 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);
}
图中“NDC 空间”和“帧缓冲”都是按照预期的屏幕显示来绘制的,也就是如果渲染目标是屏幕,那么它们的渲染效果就是这样,而绘制结果在坐标系中的位置需要看图中的坐标轴。例如图中 OpenGL 和 Vulkan 的 NDC 空间的内容其实是一样的。
使用类似 OpenGL 的坐标系,我们的修改方式是:
这种坐标系定义的好处是不需要特殊处理就能够直接兼容 DirectX/Metal/Vulkan 这样的现代图形 API。输出的纹理保存成图片时不需要翻转,与设备坐标一致。
实际应用
而跨平台图形 WebGPU,Unreal 游戏引擎等则选择了类 DirectX 坐标系,因为现在主流 PC 配置是 Windows 操作系统加 DirectX 后端,使用类 DirectX 坐标系不需要特别处理。并且 OpenGL 和 OpenGL ES 标准不再更新,拥抱 Metal/Vulkan/D3D12 这样更接近底层的现代图形 API 是未来趋势所在。
https://zhuanlan.zhihu.com/p/339295068
https://www.saschawillems.de/blog/2019/03/29/flipping-the-vulkan-viewport/
你可能还想看
等你来
Y-tech
快手Y-tech部门致力于通过计算机视觉、计算机图形学、深度学习、AR/VR/HCI等多领域的交叉,帮助每个人更好地表达、创作以及互动。Y-tech在北京、深圳、Palo Alto均有研发团队。如果你对我们做的事情感兴趣,欢迎联系并加入我们!
Y-tech长期招聘(全职或实习生):计算机视觉、计算机图形学、UE/Unity开发、技术美术、AI推理引擎、AI工程架构、美颜/特效技术、平台/工具开发、技术产品经理等方向的优秀人才。联系方式:
y-techservice@kuaishou.com
Y-tech///