跨越OpenGL和D3D的鸿沟(二):现代OpenGL
转载请注明出处为KlayGE游戏引擎,本文地址为http://www.klayge.org/2011/07/17/%e8%b7%a8%e8%b6%8aopengl%e5%92%8cd3d%e7%9a%84%e9%b8%bf%e6%b2%9f%ef%bc%88%e4%ba%8c%ef%bc%89%ef%bc%9a%e7%8e%b0%e4%bb%a3opengl/
上一篇提出了跨越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的功能异同,以及直接相互访问的可能性。