跨越OpenGL和D3D的鸿沟

 
转载请注明出处为KlayGE游戏引擎,本文地址为http://www.klayge.org/2011/07/15/%e8%b7%a8%e8%b6%8aopengl%e5%92%8cd3d%e7%9a%84%e9%b8%bf%e6%b2%9f%ef%bc%88%e4%b8%80%ef%bc%89%ef%bc%9a%e5%bc%80%e7%af%87/

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

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

真的么?java

本文试图:git

  • 找出现代OpenGL和D3D的共通之处
  • 概括如何让API对上层代码尽可能透明

本文不但愿:web

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

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

架构

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

 

在架构上其实二者没有什么区别,只是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。app

固然,这里还有另外一种更好的选择,把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,就须要乘上ide

至关于把project space的顶点z都做了z = z * 2 – 1的操做,因此通过viewport变换就一致了。D3D到OpenGL的矩阵也能够依此类推。因此,在坐标系上,很容易就能使二者接受一样的输入,同时也没有增长runtime开销。

本篇讲的都是能够在不改变API的状况下,经过输入数据来消除OpenGL和D3D之区别。下一篇将讲解如何利用现代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, &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);
glMatrixScalefEXT(GL_PROJECTION, 1, -1, 0); // y方向取反
glMatrixTranslatefEXT(GL_PROJECTION,  0.5f / win_width, 0.5f / win_height, 0); // 调整到D3D9的话还须要偏移0.5个像素

因为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 平台在这方面至关全面,D3D十一、D3D十、D3D九、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。之后我会有文章来专门讨论这件事情

做者:pizi0475 发表于2011-7-26 17:11:28 原文连接
阅读:148 评论:1 查看评论