利用物体材质specular属性来模拟高反光的物体是不够的。高反光的物体通常可以在表面反射出周围的物体,这样的效果需要通过环境贴图来实现。这篇教程将介绍如何利用Cg进行环境贴图。环境反射的原理很简单,一个光滑的物体表面可以根据我们观察的不同角度反射出不同位置的环境。即物体表面一点反射的颜色和该点的法线,观察视线和反射视线有关系。
Fig1中显示了它们的关系,其中I为观察视线,N为物体上一点p的法线,R为I的反射视线。P点的颜色取决于反射视线R,只要R达到周围环境一点p1,那么p1的颜色就会显示到p点上。向量R可以根据公式
R = 2(I•N)•N-I
简单计算,或者使用Cg的内置函数reflect(),该函数接受参数向量I和法线N,返回I关于N的反射向量。
- float3 R = reflect(I,N);
有了反射向量R,现在只要创建好环境贴图后,再根据R在环境贴图中查询出对应的颜色即可。环境贴图就是一个每个面都带有贴图的立方体,六个面的贴图刚好可以并接在一起,看起来就像周围环境一样。
Fig2 环境贴图
Fig2展示了一个环境贴图的六个面,在创建环境贴图的时候,只需要将这六个面贴到立方体对应面上即可。要创建环境贴图,可以简单的渲染六个正方形,然后分别贴上不同的贴图。下面是使用环境贴图渲染茶壶的例子。
OpenGL的扩展glew中提供了对环境贴图的支持,要开启对环境贴图的支持,使用下面代码即可。
- glEnable(GL_TEXTURE_CUBE_MAP);
当然要在使用环境贴图之间,要创建一个环境贴图。下面给出创建环境贴图的代码。
- void LoadCubeMapFromBMP()
- {
- AUX_RGBImageRec *x_pos_map = auxDIBImageLoad("img/pos_x.bmp");
- AUX_RGBImageRec *x_neg_map = auxDIBImageLoad("img/neg_x.bmp");
- AUX_RGBImageRec *y_pos_map = auxDIBImageLoad("img/pos_y.bmp");
- AUX_RGBImageRec *y_neg_map = auxDIBImageLoad("img/neg_y.bmp");
- AUX_RGBImageRec *z_pos_map = auxDIBImageLoad("img/pos_z.bmp");
- AUX_RGBImageRec *z_neg_map = auxDIBImageLoad("img/neg_z.bmp");
- glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, 3, x_pos_map->sizeX, x_pos_map->sizeY,
- 0, GL_RGB, GL_UNSIGNED_BYTE, x_pos_map->data);
- glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, 3, x_neg_map->sizeX, x_neg_map->sizeY,
- 0, GL_RGB, GL_UNSIGNED_BYTE, x_neg_map->data);
- glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, 3, y_pos_map->sizeX, y_pos_map->sizeY,
- 0, GL_RGB, GL_UNSIGNED_BYTE, y_pos_map->data);
- glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, 3, y_neg_map->sizeX, y_neg_map->sizeY,
- 0, GL_RGB, GL_UNSIGNED_BYTE, y_neg_map->data);
- glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, 3, z_pos_map->sizeX, z_pos_map->sizeY,
- 0, GL_RGB, GL_UNSIGNED_BYTE, z_pos_map->data);
- glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, 3, z_neg_map->sizeX, z_neg_map->sizeY,
- 0, GL_RGB, GL_UNSIGNED_BYTE, z_neg_map->data);
- glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
- }
从代码中可以看到,首先使用glaux扩展读取六个作为环境贴图的纹理数据。使用glTexImage2D创建纹理的时候使用了
- GL_TEXTURE_CUBE_MAP_POSITIVE_X
- .
- .
- .
- GL_TEXTURE_CUBE_MAP_NEGATIVE_Z
它们分别表示环境贴图的不同方向的纹理,然后glTexParameteri设置贴图参数的时候使用GL_TEXTURE_CUBE_MAP表示对环境贴图设置参数。创建好环境贴图后,就可以直接在openGL中渲染。
- glEnable(GL_TEXTURE_CUBE_MAP);
- glBindTexture(GL_TEXTURE_CUBE_MAP, ENVIRONMENT_MAP);
- glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
- glBegin(GL_QUADS);
- for (int i=0; i<4*6; i++)
- {
- glTexCoord3fv(vertex[i]);
- glVertex3fv(vertex[i]);
- }
- glEnd();
- glDisable(GL_TEXTURE_CUBE_MAP);
上面的代码就可以渲染整个环境贴图了,vertex表示立方体的顶点,它的定义如下。
- static const GLfloat vertex[4*6][3] = {
- /* Positive X face. */
- { 1, -1, -1 }, { 1, 1, -1 }, { 1, 1, 1 }, { 1, -1, 1 },
- /* Negative X face. */
- { -1, -1, -1 }, { -1, 1, -1 }, { -1, 1, 1 }, { -1, -1, 1 },
- /* Positive Y face. */
- { -1, 1, -1 }, { 1, 1, -1 }, { 1, 1, 1 }, { -1, 1, 1 },
- /* Negative Y face. */
- { -1, -1, -1 }, { 1, -1, -1 }, { 1, -1, 1 }, { -1, -1, 1 },
- /* Positive Z face. */
- { -1, -1, 1 }, { 1, -1, 1 }, { 1, 1, 1 }, { -1, 1, 1 },
- /* Negative Z face. */
- { -1, -1, -1 }, { 1, -1, -1 }, { 1, 1, -1 }, { -1, 1, -1 },
- };
立方体的六个面这里用了24个顶点来表示,这样可以方便的对每个顶点设置纹理坐标。就像下面的代码:
- glTexCoord3fv(vertex[i]);
- glVertex3fv(vertex[i]);
到现在为止,已经做好了渲染环境贴图的准备工作了,下面就是完成shader的内容。利用Cg来进行环境贴图很方便,也很简单。Cg中提供了内置函数来为我们做这些工作,先来看看vertex shader的内容。
vs05.cg
- void vs_main( float4 position : POSITION, // 顶点坐标
- float2 texCoord : TEXCOORD0, // 纹理坐标
- float3 normal : NORMAL, // 顶点法线
- out float4 oPosition : POSITION,
- out float2 oTexCoord : TEXCOORD0,
- out float3 R : TEXCOORD1, //反射向量R,保存在纹理坐标中
- uniform float3 eyePositionW,
- uniform float4x4 MVP)
- {
- oPosition = mul(MVP, position);
- oTexCoord = texCoord;
- float3 N = normalize(normal);
- float3 I = position-eyePositionW; //计算观察向量
- R = reflect(I,N); //利用Cg内置函数计算I关于N的反射向量
- }
可以看到vertex shader的内容很简单,主要就是计算反射向量R,并将它传到fragment shader。 而在fragment shader中,我们要用Cg函数texCUBE(cubemap,R),该函数接受两个参数,一个是刚才我们创建的环境贴图纹理数据,一个是反射向量。该函数会自动根据传入的反射向量,然后计算该反射向量和这个环境贴图的交点,并且插值计算出该点对应的像素颜色并返回。整个fragment shader代码如下。
05fs.cg
- void fs_main( float2 texCoord : TEXCOORD0,
- float3 R : TEXCOORD1,
- out float4 color :COLOR,
- uniform float reflectivity,
- uniform samplerCUBE cubemap)
- {
- float4 reflectedColor = texCUBE(cubemap, R);
- color = reflectedColor;
- }
fragment shader代码很简洁,就是利用函数texCUBE在环境贴图中查询向量R对应的像素颜色。这样就可得到Fig3中的效果。为了真实的模拟反光的物体,有的时候可以对反光的物体再加上纹理,因为在生活中完全反光的物体几乎是没有的,加上其他纹理可以是反光效果真实一些。
要让环境反射的颜色和纹理颜色混合,只需要向alpha混合一样即可。如果反射的颜色为Cr,纹理颜色为Ct,那么混合后的颜色C
C = (1 - factor)•Ct + factor•Cr
这里factor就是混合系数了。现在只与需要对fragment进行很少的修改就可以实现这个效果。
05fs.cg
- void fs_main( float2 texCoord : TEXCOORD0,
- float3 R : TEXCOORD1,
- out float4 color :COLOR,
- uniform float reflectivity,
- uniform sampler2D decalMAP, // 只需增加传入一个纹理
- uniform samplerCUBE cubemap)
- {
- float4 reflectedColor = texCUBE(cubemap, R); //计算反射颜色
- float4 decalColor = tex2D(decalMAP, texCoord); //计算纹理颜色
- color = lerp(decalColor, reflectedColor, reflectivity); //混合反射颜色和纹理颜色,reflectivity为混合系数
- }
这里使用了Cg另一个内置函数lerp,该函数根据传入的混合系数,进行线性插值计算。一般混合系数在0和1之间。整个环境贴图还是比较简单,也比较容易实现。但是这中环境贴图也有很多不利的地方,比如模拟环境的贴图一般是静态图片,所以环境不能发生变化。还有如果同时渲染多个物体,每个物体不能其他的物体反射到表面,想要达到这种效果要使用ray tracing(光线跟踪)或 render to texture(渲染到纹理)技术。