浏览量:6,949

环境贴图

本文主要首先介绍下什么是环境贴图,然后分别介绍球面贴图,立方体贴图,和双抛物面贴图,比较三种贴图方式的优缺点,立方体贴图的代码的下载地址为:https://github.com/twinklingstar20/twinklingstar_cn_environment_mapping(从NVIDIA网站上下载下来的)。

一. 环境贴图是什么

环境贴图(Environment Mapping,EM)也称为反射贴图(Reflection Mapping),把反射对象当作一个虚拟眼睛,生成一张虚拟的纹理图,然后把该纹理图映射到反射对象上,得到的图像就是该场景的一个影像。举个例子,比如镜子中的图像,就是对真实场景的一个影像。环境贴图这项技术最早由Blinn和Newell提出的【4】,主要实现的功能是:使物体表面能显示出真实场景的影像。如下图所示,小球表面就有周围环境的影像。

2014-3-11 22-46-47

图1. 环境贴图

实现环境贴图,很常见到的主要有三种技术:球面贴图(Spherical Environment Mapping),双抛物面贴图(Dual Paraboloid Mapping)和立方体环境贴图。

环境贴图基本的思想如图2所示,假设场景离对象很远而且对象不会产生自我反射,在对象上某一点的影像就能通过反射向量r来确定。

2014-3-11 22-47-02

图2. 环境贴图基本思想

那么环境贴图主要分5个步骤:

(1)       创建一个2D环境纹理

(2)       计算反射对象上每个像素点的法向量

(3)       基于人眼位置和平面的法向量,计算反射向量

(4)       使用反射向量,计算该像素点在2D纹理上的值

(5)       使用得到的纹理值来绘制像素

基于人眼位置和平面的法向量,来计算反射向量,也很简单,如下面的图所示:

2014-3-11 22-47-17

图3. 计算反射向量

接下来分别介绍3种具体的环境贴图的算法和实现,并且分析各种方法的优缺点。

二. 球面贴图

如图1所示,就是一个经典的球面贴图的样例。假设观察者在无穷远处,则所有的入射光线都是平面,如图4所示。

2014-3-11 22-47-28

图4. 球面贴图的几何图示,红色表示法向量,蓝色表示反射射线,黄色表示入射光线

现在考虑如何将表面的颜色值与纹理上的纹理值对应起来。如图5所示,从观察者到顶点的坐标值用U来表示,归一化向量U,得到U’。既然是在视点坐标系统下进行的计算,则眼睛被放置在原点,向量U的值就等于顶点在视点空间下的坐标。当前的向量N也被变换到视点空间下,并且进行规一化,得到N’,反射向理就可以通过下面的等式计算得到:

\(R = U' - 2(N' \cdot U')N'\)(1)

2014-3-11 22-47-40

图5. 在视点空间下,计算反射向量

计算得到反射方向R后,球面的法线是反射方向R和观察方向之间的角平分向量,这里的观察方向在球面空间中是(0, 0, 1),参见图6,法线向量n是观察向量和反射向量之和,就是(Rx, Ry, Rz+1),这个向量进行归一化就得到单位法线:

\(p = \sqrt {R_x^2 + R_x^2 + {{({R_z} + 1)}^2}} \)

\(n = \left( {\frac{{{R_x}}}{p},\frac{{{R_y}}}{p},\frac{{{R_z} + 1}}{p}} \right)\)

2014-3-11 22-47-51

图6. 在球面纹理图空间中,如果给定固定不变的观察方向和反射向量,那么球面纹理图的法线n就位于两者的角平分线上。

由于(Rx/p, Ry/p)的坐标值取值范围是[-1, 1],可以将坐标值除以2,再加上0.5,即定义

\(p = \sqrt {R_x^2 + R_x^2 + {{({R_z} + 1)}^2}} \)

则纹理坐标就可以通过下面的等式计算:

\(s = \frac{{{R_x}}}{{2p}} + \frac{1}{2}\)

\(t = \frac{{{R_y}}}{{2p}} + \frac{1}{2}\)

在OpenGL中,提供了对球面贴图的支持,实现起来也特别的简单,主要的过程如下所示,NEHE中也提供了示例代码:

(1)绑定纹理对象

(2)设置球面贴图纹理坐标生成参数

glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);

(3)启动自动纹理生成和纹理支持

glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_2D);

(4) 绘制模型

球面贴图在很多硬件中都得到支持,但是它依旧存在问题:

(1)       可以造成图像的变形(Image Warping)和奇异点(Singularities)。

把一张长方形的图片,映射到一个圆上,需要拉伸和压缩,就无法避免产生变形,特别是球体的边缘,由于奇异点,可能产生人工痕迹(Artifact),引用【8】的一句话:“Sphere mapping is subject to unsightly "speckle" artifacts at the glancing edges of sphere mapped objects”,如图7所示。

2014-3-11 22-48-02

图7. 在球面贴图对象的边缘,产生亮黑色的错误效果

(2)       视点依赖(View Dependent),无法实时渲染

假设场景中的一个人物在移动,视点发生了变换,那么再使用预定义的纹理图片进行球面贴图,就会产生错误,如图8所示。如果每一帧都生成一张纹理图片,用于球面贴图,那么纹理图片生成过程的开销将会非常的大。

2014-3-11 22-48-13

图8. 左图,正确的球面贴图;当视点发生了变化,如果还是采用原先的纹理图片,就会产生错误

(3)       生成用于球面贴图的纹理图片是非常困难的

设计者必需创建一张纹理图片,来表示场景,使它在球面贴图的变形发生后,产生的结果不令人反感,这个过程是很耗费时间和精力的。我想很多软件开发者,不会选择这种技术来实现环境贴图。

三. 立方体贴图

立方体贴图最早由Ned Greene在1986年【6】提出的,像Renderman里面就支持了该种贴图,现在OpenGL中也提供了该种功能的支持。立方体贴图使用了6张纹理图像,设想环境围绕着你,你可以拍摄上下左右前后,各一张,立方体的每个面就一张图片,如图9所示。把6张纹理图片贴到立方体的6个面上,再把立方体打开,得到效果如图10所示。假设环境距离立方体的中心有无穷远,则认为立方体是无穷小的,因此可以使用反射向量来检索纹理元。

2014-3-11 22-48-22

图9. 拍摄的一组立方体纹理图片

2014-3-11 22-48-33

图10. 打开的立方体贴图

OpenGL中提供了TEXT_texture_cube_map扩展【9】,就是用于支持立方体纹理贴图。这里就解释下OpenGL中,对纹理的索引策略。

与球面贴图类似,在视点空间下,根据等式(1)计算多边形顶点的反射向量,得到三个分量(rx, ry, rz)。这种方法,可以计算多边形顶点的反射向量,但是顶点与顶点之间的区域都是未定义的,可以通过对两个顶点的反射向量进行线性插值,但计算顶点之间的像素的反射向量,以像素的宽度为递增量,如图11所示。立方体贴图与球面贴图一个很大的区别再于,球面贴图是基于顶点的;而立方体贴图是基于像素的,所以它必需计算每个像素的反射向量。

2014-3-11 22-48-46

图11. 多边形顶点间反射向量的线性插值

立方体贴图使用3维纹理坐标表示反射向量,(rx, ry, rz)可以表示成(s, t, r),一旦一个像素的反射向量被计算出来,接着就是检索在立方体上的纹理元(texel)。首先判断反射向量rx、ry、rz中哪一维的数字是最大,就表示选择的哪个面的纹理,根据【9】提供了一个表,中下表所示。(表中Target选项的内容解释会在后面介绍)。

2014-3-11 22-48-55

表1.

选择了面的纹理后,需要计算(s, t)值,可以通过下面的等式来计算

\(s = \frac{{\frac{{sc}}{{\left| {ma} \right|}} + 1}}{2}\)

\(t = \frac{{\frac{{tc}}{{\left| {ma} \right|}} + 1}}{2}\)

举个例子,某个像素计算得到的反射向量是(0.25, 0.25, 0.5),基于最大值原则,0.5最大,所以会采用+z方向上的图片纹理,\( \pm x, \pm y, \pm z\)在立方体上的表示如下图所示。根据表1和等式,计算得到s、t为(s, t)=(0.75, 0.25)。接下来介绍OpenGL中实现立方体贴图的方法。

2014-3-11 22-49-05

图12. 立方体上的\( \pm x, \pm y, \pm z\)方向表示

在OpenGL中支持立方体纹理贴图,可以使用下面的语句激活立方体贴图

glEnable(GL_TEXTURE_CUBE_MAP_EXT);
glDisable(GL_TEXTURE_CUBE_MAP_EXT);

由于立方体贴图需要指定6张图像纹理,所以可以使用glTexImage2D来指定立方体面的纹理,如果需要创建mipmap纹理,可以使用gluBuild2DMipmaps。代码示例如下所示:

GLubyte face[6][64][64][3];
for (i=0; i<6; i++) { 
      glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X_EXT + i,
      0,                           //level
      GL_RGB8,                     //internal format
      64,                          //width
      64,                          //height
      0,                           //border
      GL_RGB,                      //format
      GL_UNSIGNED_BYTE,            //type
      &face[i][0][0][0]);          // pixel data
}

如里使用gluBuild2DMipmaps,可以使用下面的代码片段替代上面的glTexImage2D:

gluBuild2DMipmaps(GL_TEXTURE_CUBE_MAP_NEGATIVE_X_EXT, GL_RGB8, 64, 64, GL_RGB, GL_UNSIGNED_BYTE, &face[1][0][0][0]);

接着就是计算反射向量(s, t, r),通过表1提供的计算方法,得到最终对立方体纹理的映射

glTexGenfv(GL_S, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP_EXT); 
glTexGenfv(GL_T, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP_EXT); 
glTexGenfv(GL_R, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP_EXT); 
glEnable(GL_TEXTURE_GEN_S); 
glEnable(GL_TEXTURE_GEN_T); 
glEnable(GL_TEXTURE_GEN_R);

根据【8】实现的代码,可以得到效果如图13所示,右击鼠标,可以选择不同的模式。如图14所示,是另外一种采用立方体贴图的效果图,每个面采用不同的颜色,绘制球面的效果图。

2014-3-11 22-49-15

图13. 立方体贴图的效果图

2014-3-11 22-49-24

图14. 立方体贴图的每个面采用不同的颜色,绘制球面,得到的效果图

开发者可以直接通过照相机拍摄得到的图片来实现环境贴图,不会产生变形和奇异点,不像球面贴图,需要预先拉伸和消耗较大的投影步骤。

它能够实现视点独立的、实时的反射渲染。对于视点的变换,球面贴图需要计算每一个帧的纹理图像,但是立方体贴图则不存在这个问题。只需要根据新的反射向量,检索预先定义好的纹理图像。

立方体算法的实现较简单,而且现在已经在很多硬件中得到了支持。

四. 双抛物面贴图

双抛物面贴图(Dual-Paraboloid Mapping)最早由Heidrich和Wolfgang提出的【5】,该方法解决了球面贴图中出现的视点依赖,在球面贴图边缘存在奇异点的问题,可以在任意两个反射方向之间进行插值,而且有更好的采样特点。它的主要的一个问题在于纹理图片的生成上,它需要通过弯曲图像或者射线追踪的方式来获得,这就得到一个结论就是:方法挺好,但是不实用。【5】的文章,作者提供了详细的数学论证,这里就简单介绍下该方法最终的实现公式。

双抛物面贴图采用的数学公式如下所示:

\[f(x,y) = \frac{1}{2} - \frac{1}{2}({x^2} + {y^2}),{x^2} + {y^2} \le 1\]

与球面贴图不同的是,它采用了两张纹理图片,一张用于“前面”的环境,另一张用于“后面”的环境。Heidrich和Seidel通过使用OpenGL的矩阵,把视点空间的反射向量r映射到双抛物面上的(s, t)值,构造的纹理矩阵如下所示:

\[\left( {\begin{array}{*{20}{c}}s\\t\\1\\1\end{array}} \right) = A \cdot P \cdot S \cdot {({M_l})^{ - 1}} \cdot \left( {\begin{array}{*{20}{c}}{{R_x}}\\{{R_y}}\\{{R_z}}\\1\end{array}} \right)\]

其中

\[A = \left( {\begin{array}{*{20}{c}}{0.5}&0&0&{0.5}\\0&{0.5}&0&{0.5}\\0&0&1&0\\0&0&0&1\end{array}} \right)\]

把在[-1, 1]范围内的2D坐标,变换到范围[0, 1]内,

\[P = \left( {\begin{array}{*{20}{c}}1&0&0&0\\0&1&0&0\\0&0&1&0\\0&0&1&0\end{array}} \right)\]

除以z坐标,3D向量变成2D向量,

\[S = \left( {\begin{array}{*{20}{c}}{ - 1}&0&0&{{d_x}}\\0&{ - 1}&0&{{d_y}}\\0&0&1&{{d_z}}\\0&0&0&1\end{array}} \right)\]

向量d表示观察朝向,它只能是\(d = {(0,0, - 1)^T}\)或者\(d = {(0,0,1)^T}\),如果映射的是前面的话就是前者,否则的话就是后者。

(Ml-1就是当前的模型视图矩阵(仿射变换),把视点空间下的反射向量变换到模型空间下的向量。

可以使用OpenGL提供的glTexGeni函数中的GL_REFLECTION_MAP_EXT参数来计算反射向量,具体的实现和分析,可以参考【3】,【5】。

五. 参考

【1】       http://www.cse.ohio-state.edu/~whmin/courses/cse5542-2013-spring/17-env.pdf

【2】       http://www.unc.edu/~zimmons/cs238/maps/environment.html

【3】       ftp://ftp.sgi.com/opengl/contrib/blythe/advanced99/notes/node174.html

【4】       Blinn, James F., and Martin E. Newell. "Texture and reflection in computer generated images." Communications of the ACM 19.10 (1976): 542-547.

【5】       Heidrich, Wolfgang, and Hans-Peter Seidel. "View-independent environment maps." Proceedings of the ACM SIGGRAPH/EUROGRAPHICS workshop on Graphics hardware. ACM, 1998.

【6】       Greene, Ned. "Environment mapping and other applications of world projections." Computer Graphics and Applications, IEEE 6.11 (1986): 21-29.

【7】       https://developer.nvidia.com/sites/default/files/akamai/gamedev/docs/CubeEnvMapping2.pdf

【8】       http://www.nvidia.com/object/cube_map_ogl_tutorial.html

【9】       http://www.berkelium.com/OpenGL/EXT/texture_cube_map.txt

【10】       Akenine-Möller, Tomas, Eric Haines, and Naty Hoffman.Real-time rendering, Second Edition

 

spacer

Leave a reply