多年来,在论坛和各个网站上不断能看到拿OpenGL和D3D进行比较的帖子和文章。他们经常制造很多谜思,使得初学者和一些从业人员对OpenGL和D3D产生了各种各样的流言。
真的么?
本文试图:
本文不希望:
下面先从几个比较基本的方面来探讨如何跨越两个API的鸿沟。
OpenGL和D3D的架构基本上是这个样子的:
在架构上其实两者没有什么区别,只是D3D的runtime是在OS里,对于不同硬件来说都是一样的。而OpenGL的runtime直接是和驱动合为一体的。但这并不会造成性能有所差别,破解了流言1。
OpenGL 的原生shading language是GLSL,D3D的是HLSL。两者语法相似,但细节上天差地别。好在,NVIDIA的Cg在很大程度上类似于HLSL,而且可以编译 出GLSL来。所以,Cg编译器成了跨越这两种shader的桥梁。当然,要真正实用起来,还需要不少工作。比如Cg的Geometry Shader和HLSL 10+的有些许不同,需要通过#ifdef来分开。另外,Cg编译器生成的GLSL需要一些调整才能在ATI的驱动上工作,所以还需要多一次转换。好在这 些事情都可以通过程序自动完成,而且速度很快。具体可以参考KlayGE的OGLShaderObject::ConvertToGLSL。实际上,KlayGE的所有shader都只写了一份(语法用HLSL 11的),在不同API上可以自动编译成原生的shader使用(有OpenGL和OpenGL ES的编译器,曾经还有D3D9的)。这样就没有重写的繁琐,也没有增加runtime开销,破解了流言2。
当然,这里还有另一种更好的选择,把HLSL编译器生成的bytecode转换成GLSL。UE3等引擎用了MojoShader来完成这件事情。优点是不需要多次编译,缺点是不支持SM4+。
初学者经常 说,OpenGL用右手坐标系,而D3D用左手;裁剪空间里OpenGL的z是[-1, 1],而D3D是[0, 1];不可调和。实际上,直接把左手的顶点和矩阵给OpenGL也是没有问题的。毕竟如果在VS里执行的都是mul(v, matrix),得到的会是同样的结果。可能会造成麻烦的反而是viewport的z。假设一个经过clip之后的顶点坐标为(x, y, z, w),那么在OpenGL上,该顶点经过viewport变换的z是(z/w + 1) / 2,而在D3D上则是z/w而已。这对于depth test不影响,但depth buffer里的值就不同了。所以需要对project matrix做一些调整,才能让他们写到depth buffer中的数值相同。具体来说,如果要让OpenGL流水线接受D3D的project matrix,就需要乘上
相当于把project space的顶点z都作了z = z * 2 – 1的操作,所以经过viewport变换就一致了。D3D到OpenGL的矩阵也可以依此类推。所以,在坐标系上,很容易就能使两者接受同样的输入,同时也没有增加runtime开销。
本篇讲的都是可以在不改变API的情况下,通过输入数据来消除OpenGL和D3D之区别。下一篇将讲解如何利用现代OpenGL提供的扩展和新功能,消除一些无法在上层解决的问题,继续破解各种流言。
(二):现代OpenGL
上一篇提出了跨越OpenGL和D3D的基本问题,介绍了一些能在不改变API的情况下,通过输入数据来消除OpenGL和D3D之区别。本篇的重点是如何利用现代OpenGL提供的扩展和新功能,消除一些无法在上层解决的问题。
D3D9 最常用的顶点颜色格式是BGRA格式(也就是D3DCOLOR),而OpenGL默认用的是RGBA格式。D3D9用BGRA纯粹是因为历史原因,早期硬 件不支持UBYTE4的格式,只能用D3DCOLOR,然后再shader里调用D3DCOLORtoUBYTE4。现在的GPU都支持 UBYTE4,D3D10+也是可以直接使用RGBA,所以这已经不是问题了。
如果需要兼容已经生成BGRA格式数据,现代OpenGL提供了GL_EXT_vertex_array_bgra这个扩展,也可以使用BGRA作为顶点颜色输入格式:
该扩展进入了OpenGL 3.2的核心。
Flat shading在渲染中用的机会远远少于Gouraud shading。很多人只知道Flat shading是选择一个顶点的属性作为primitive上每个像素的属性,而不会注意到D3D和OpenGL在“哪个顶点”上的选择有所区别。D3D 用line或triangle第一个顶点的属性。而OpenGL在line、triangle或quad的时候最后一个顶点的属性(但在polygon的 时候用的是第一个)。
现在,OpenGL出现了GL_EXT_provoking_vertex这个扩展,可以选择使用哪个顶点的属性来驱动(这就是provoking的意思)一个primitive。它很容易使用:
该扩展也进入了OpenGL 3.2的核心。
D3D9的状态切换是通过SetRenderState这样的函数来完成的,而OpenGL则是完全基于状态机的结构,比如要设置model view矩阵,同时要求不影响状态,就需要:
如果这里不这样繁琐,就很可能在十万八千里的地方出问题。相信每个用OpenGL的人都曾遇到过,尤其是多人合作的时候。一个状态的错误都可能导致灾难。
现在,救星来了。GL_EXT_direct_state_access扩展(简称DSA)的出现大大地改变了这点。该扩展提供了直接访问状态的能力,比如前面的设置model view矩阵,只需要:
简单多了吧。DSA把绝大部分OpenGL核心和各个扩展提供的状态都增加了一个直接访问的版本,相当方便。理论上,性能还能有所提高。可惜的是,DSA至今还没进入OpenGL的核心,虽然在NV和ATI的卡上都可以使用。
上一篇提到了坐标系的区别,另一个类似的区别出现在窗口朝向上。D3D用了左上角作为原点,而OpenGL用了左下角。D3D9用了像素左上角作为原点,而OpenGL和D3D10+用了像素中心。在像素和纹理需要1:1对应的时候,该问题就需要严重关注了。详见Directly Mapping Texels to Pixels。
窗口原点的不同造成的结果就是,两个API做render to texture之后,产生的texture在y方向是相反的。这本身可以通过调整project matrix来调整。简而言之,就是:
由于y方向反了,还需要调用glFrontFace(GL_CW)来把正面方向反一下,否则cull会出错。窗口原点就这样通过上层代码来解决了。
但这只能调整窗口原点,OpenGL下像素坐标仍是以左下角作为原点,而像素坐标在post process里很常用。因此,OpenGL提供了GL_ARB_fragment_coord_conventions这个扩展,专门用来指定像素坐标的原点和偏移。在GLSL的声明里添加个属性:
这样就可以把像素坐标调整过来。
综上所述,通过现代OpenGL核心和扩展的支持,填平了一些原本被认为位于底层的区别,同时不会有性能损失。破解了上篇提到的流言3。下篇将剖析两个API的功能异同,以及直接相互访问的可能性。
(三):交集?并集?
上一篇讲到了如何填平OpenGL和D3D之间一些原本被认为位于底层的区别。本篇将剖析两个API在功能上的异同,以及直接相互访问的可能性。
功能
D3D9的功能早已被OpenGL 2.0所覆盖,网上可以找到很多资料,这里就不提了。本文注重的是新的GPU特性。下表列出了D3D10+的新功能,以及实现该功能所需要的OpenGL扩展或核心。
D3D 10的功能 | OpenGL所对应的 |
Geomrtry shader | GL_ARB_geometry_shader4或OpenGL 3 |
Stream output | GL_EXT_transform_feedback或OpenGL 3 |
State对象 | 无,需要在上层封装GL_EXT_direct_state_access |
Constant buffer | GL_ARB_uniform_buffer_object或OpenGL 3 |
Texture array和新的资源格式 | GL_EXT_texture_array +GL_ARB_texture_compression_rgtc +GL_ARB_texture_rg +GL_ARB_texture_rgb10_a2ui +GL_EXT_texture_integer或OpenGL 3 |
texture和sampler解偶 | GL_ARB_sampler_objects或OpenGL 3 |
在shader里进行整数和位操作 | GL_ARB_shader_bit_encoding或OpenGL 3 |
Multisampled alpha-to-coverage | GL_NV_multisample_coverage或OpenGL 3 |
D3D 10.1的功能 | OpenGL所对应的 |
读取multisample depth/stencil纹理 | GL_ARB_texture_multisample或OpenGL 3 |
Cubemap array | GL_ARB_texture_cube_map_array或OpenGL 4 |
gather4 | GL_ARB_texture_gather或OpenGL 4 |
D3D 11的功能 | OpenGL所对应的 |
Compute Shader | GL_ARB_cl_event + OpenCL |
Dynamic Shader Linkage | GL_ARB_gpu_shader5或OpenGL 4 |
Multithreading | 无 |
Tessellation | GL_ARB_tessellation_shader或OpenGL 4 |
这些都是DX SDK文档里提到的功能,其他一些比较小的功能,也可以很容易找到OpenGL的对应。从上表可以看出,几乎所有D3D的功能都可以直接用相应的OpenGL功能代替,同时没有性能损失。需要重点讨论的是一些例外:
State对象
D3D 10新增了State对象,可以极大地减少由于改变渲染状态所需的系统调用次数。OpenGL中目前还没有State对象,所以只能在上层自行封装。虽然 有些 性能损失,但接口可以和D3D统一起来。ARB针对OpenGL的State对象进行过旷日持久的讨论,还最终各大厂商没有达成一致。不过这是个趋势,相 信不久的将来就会出个相关的扩展。到时候这个区别就能被完美地填平。
Compute Shader
D3D 11引入了compute shader,在D3D中直接提供了GPGPU的能力。OpenGL没有因此增加一种shader,而是增强和同门师弟OpenCL的互操作能力。 OpenGL和OpenCL能直接共享texture和buffer等,起到了和compute shader等价的功能。与GLSL和HLSL的关系一样,这里存在着shader语言不同的问题,而且没有Cg可以作为桥梁,目前只能写两份代码。
Multithreading
D3D 11的multithreading能力允许多个context都调用D3D11 API,每个context保存下来的API调用流可以在主context执行依次执行。OpenGL目前也没有引入该机制,需要在上层自行实现。话说回 来了,目前的所有显卡 驱动都没有实现multithreading,所以所有多context都是由D3D runtime软件实现的,没有达到应有的提速效果。自己实现一个command list也能达到那样的性能。仍然希望某一天multithreading能成为OpenGL的功能 之一,简化上层的工作。
总结
所以说,OpenGL和D3D功能的交集几乎就是它们的并集,并不会因为需要兼容两者而失去很多功能。从功能上说,OpenGL和D3D之间的分歧甚至小于OpenGL和OpenGL ES。破解了第一篇说的流言4。
互操作
神奇扩展WGL_NV_DX_interop的 出现,使得OpenGL可以正式与D3D进行互操作。(严格来说,互操作能力源自它的前身WGL_NVX_DX_interop,但鉴于他是个NVX实验 性质的扩展,最好小心点用。)该扩展的目的是,在D3D中建立资源,而在 OpenGL中访问它。目前可以支持的是D3D9的纹理、render target和VB的读写。D3D10+的支持将在未来加入。两个API之间所需的同步也是自动完成的。
使用WGL_NV_DX_interop进行相互渲染的范例如下:
// 跟平常一样建立D3D设备和资源
d3d->CreateDevice(..., &d3dDevice);
d3dDevice->CreateRenderTarget(width, height, D3DFMT_A8R8G8B8,D3DMULTISAMPLE_4_SAMPLES, 0,FALSE, &dxColorBuffer, NULL);
d3dDevice->CreateDepthStencilSurface(width, height, D3DFMT_D24S8,D3DMULTISAMPLE_4_SAMPLES, 0,FALSE, &dxDepthBuffer, NULL);
// 把D3D设备注册给OpenGL
HANDLE gl_handleD3D = wglDXOpenDeviceNV(d3dDevice);
// 把D3Drender target注册成OpenGL纹理对象
GLuint names[2];
HANDLE handles[2];
handles[0] = wglDXRegisterObjectNV(gl_handleD3D, dxColorBuffer,names[0], GL_TEXTURE_2D_MULTISAMPLE, WGL_ACCESS_READ_WRITE_NV);
handles[1] = wglDXRegisterObjectNV(gl_handleD3D, dxDepthBuffer,names[0], GL_TEXTURE_2D_MULTISAMPLE, WGL_ACCESS_READ_WRITE_NV);
// 现在纹理就可以当成普通的OpenGL纹理来用了
// D3D和OpenGL渲到同一个render target
direct3d_render_pass(); // 和平常一样进行D3D渲染
// 锁定render target,交给OpenGL
wglDXLockObjectsNV(handleD3D, 2, handles);
opengl_render_pass(); // 和平常一样进行OpenGL渲染
wglDXUnlockObjectsNV(handleD3D, 2, handles);
direct3d_swap_buffers(); // D3D present
这样两个API可以和谐共处了,但这个扩展目前仅限于NV的卡。
本篇讨论了两个API在功能上的交集和并集,以及互操作的方法。下一篇是本系列的结局,将讨论一些平台相关的问题,并进行系统性的总结。
(四):完结篇,平台和未来
上篇文章讨论了两个API在功能上的交集,以及互操作的方法。本篇作为系列的结局,将讨论一些平台相关的问题。
平台
长久以来,一直可以听到一种说法,D3D只能在Windows上用,而OpenGL可以用在所有平台。那么,我们就来看看在各个平台上,几种3D API的可用性。
桌面平台
Windows
Windows 平台在这方面相当全面,D3D11、D3D10、D3D9、OpenGL、OpenGL ES都支持(需要注意的是,只有Vista+支持D3D10和D3D11)。由于OpenGL 4.1可以建立OpenGL ES的context,NV和AMD的驱动都提供了原生的OpenGL ES。这也为浏览器中WebGL的实现提供了方便。
Mac OS X
Mac OS X所支持的OpenGL比较老旧,也不支持D3D和OpenGL ES。
Linux
Linux的主打API是OpenGL,最近也加入了OpenGL ES支持。但是实际上,Linux也是有D3D 1x的!Mesa 的Gallium框架现在有D3D 10和11的state tracker,能把D3D 1x的API调用转到驱动层(其实和Windows上是一样的流程)。这不是个模拟器,而是原生的D3D 1x。虽然目前这个D3D1x for Linux还比较初级,只能执行DX SDK的那些例子,还没法在产品里使用,但这已经是一个很大的突破了。如果继续这么发展下去,在Linux上D3D 1x总有一天可以和OpenGL一样自由使用。
游戏机
游戏机上的一切都很专用,没必要像PC上那样多功能。所以他们支持的API也很单一。
Xbox 360
D3D9的改进版,没有OpenGL。因此OpenGL的死硬粉丝John Carmack也让idTech 5支持了D3D。
PS3
基于OpenGL ES和Cg的LibGCM和PSGL。和OpenGL有所区别。
Wii
Wii用了独立的专用API。接口上参考了OpenGL,但功能上差别很大。
浏览器
既然写到这里了,就干脆连浏览器也一起讨论了吧。浏览器支持3D API只是前不久才开始的事情。WebGL开启了这一大门,并迅速地被各大浏览器和开发者所支持。但是IE并没有原生支持WebGL,需要安装第三方插件。
最近不少人问我,有没有WebDX?有,就集成在Silverlight中。Silverlight 5支持GPU加速,而且内建了3D的能力。Silverlight也能做到和WebGL一样的能力,而且不必使用javascript来编程,直接用支持.NET的语言就可以了。
所以,在平台方面,其实D3D既不是Windows专用的,OpenGL也不是什么平台都有。不要指望着用一种API统治所有平台。
总结
从 本系列可以看出,OpenGL和D3D的差异实际上远远小于原先所认为的。绝大部分地方都没有区别,少数地方还需要时间来缩小其差异。我相信要跨越 OpenGL和D3D的鸿沟并非难事,只要付出少许努力就能完成,而这些努力都是可以复用的。一旦上层代码脱离了具体API,在维护和移植方面都会受益无 穷。
在shading language语言方面,不同API的分歧较大。我的想法是做一个D3D1x bytecode到GLSL的编译器。这样就可以用HLSL来编程、用D3D的编译器进行编译,然后直接用于OpenGL。甚至用同样的方法可以把 compute shader的bytecode编译成OpenCL kernel。以后我会有文章来专门讨论这件事情。