跨越OpenGL和D3D的鸿沟[转]

跨越OpenGL和D3D的鸿沟[转]
注:本文转自 KlayGE游戏引擎 .

(一):开篇

多年来,在论坛和各个网站上不断能看到拿OpenGL和D3D进行比较的帖子和文章。他们经常制造很多谜思,使得初学者和一些从业人员对OpenGL和D3D产生了各种各样的流言。

  1. 有人说,OpenGL直接调到驱动,性能高于D3D。
  2. 有人说,Shader都得写两套,很麻烦。
  3. 有人说,OpenGL和D3D在底层有很多区别,而且不可设置。
  4. 有人说,图形引擎如果要兼容两者,就只能取其功能的交集,最后还不如任何一种API。

真的么?

本文试图:

  • 找出现代OpenGL和D3D的共通之处
  • 归纳如何让API对上层代码尽量透明

本文不希望:

  • 讲解函数间的对应关系
  • 如何在OpenGL和D3D之间作选择
  • 贬低一方,抬高另一方

下面先从几个比较基本的方面来探讨如何跨越两个API的鸿沟。

架构

OpenGL和D3D的架构基本上是这个样子的:

跨越OpenGL和D3D的鸿沟[转]_第1张图片

在架构上其实两者没有什么区别,只是D3D的runtime是在OS里,对于不同硬件来说都是一样的。而OpenGL的runtime直接是和驱动合为一体的。但这并不会造成性能有所差别,破解了流言1。

Shader

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作为顶点颜色输入格式:

glColorPointer (GL_BGRA, GL_UNSIGNED_BYTE, stride, pointer ) ;
glSecondaryColorPointer (GL_BGRA, GL_UNSIGNED_BYTE, stride, pointer ) ;
glVertexAttribPointer (GL_BGRA, GL_UNSIGNED_BYTE, stride, pointer ) ; 

 

该扩展进入了OpenGL 3.2的核心。

Flat shading

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原生的方式
glProvokingVertex (GL_LAST_VERTEX_CONVENTION ) ;
// D3D的方式
glProvokingVertex (GL_FIRST_VERTEX_CONVENTION ) ;

 

 

 

该扩展也进入了OpenGL 3.2的核心。

状态切换

D3D9的状态切换是通过SetRenderState这样的函数来完成的,而OpenGL则是完全基于状态机的结构,比如要设置model view矩阵,同时要求不影响状态,就需要:

void set_model_view_matrix (GLfloat  const matrix [ 16 ] )
{
   GLenum saved_mode ;
   glGetIntegerv (GL_MATRIX_MODE,  &amp ;saved_mode ) ;
   glMatrixMode (GL_MODELVIEW ) ;
   glLoadMatrixf (matrix ) ;
   glMatrixMode (saved_mode ) ;
} 

 

如果这里不这样繁琐,就很可能在十万八千里的地方出问题。相信每个用OpenGL的人都曾遇到过,尤其是多人合作的时候。一个状态的错误都可能导致灾难。

现在,救星来了。GL_EXT_direct_state_access扩展(简称DSA)的出现大大地改变了这点。该扩展提供了直接访问状态的能力,比如前面的设置model view矩阵,只需要:

void set_model_view_matrix (GLfloat  const matrix [ 16 ] )
{
   glMatrixLoadfEXT (GL_MODELVIEW, matrix ) ;
}

 

 

 

简单多了吧。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来调整。简而言之,就是:

glMatrixLoadIdentityEXT (GL_PROJECTION ) ;
// y方向取反
glMatrixScalefEXT (GL_PROJECTION,  1- 10 ) ; 
// 调整到D3D9的话还需要偏移0.5个像素
glMatrixTranslatefEXT (GL_PROJECTION,   0.5f  / win_width,  0.5f  / win_height,  0 ) ;  

 

由于y方向反了,还需要调用glFrontFace(GL_CW)来把正面方向反一下,否则cull会出错。窗口原点就这样通过上层代码来解决了。

但这只能调整窗口原点,OpenGL下像素坐标仍是以左下角作为原点,而像素坐标在post process里很常用。因此,OpenGL提供了GL_ARB_fragment_coord_conventions这个扩展,专门用来指定像素坐标的原点和偏移。在GLSL的声明里添加个属性:

// OpenGL原生的方式
in vec4 gl_FragCoord ;
// D3D9的方式
layout (origin_upper_left, pixel_center_integer ) in vec4 gl_FragCoord ;
// D3D10+的方式
layout (origin_upper_left ) in vec4 gl_FragCoord ;

 

 

 

这样就可以把像素坐标调整过来。

综上所述,通过现代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。以后我会有文章来专门讨论这件事情。

你可能感兴趣的:(跨越OpenGL和D3D的鸿沟[转])