【GPU编程】《The Cg Tutorial》学习之环境贴图(Environment Mapping)
本例实现了一个基于GPU加速的OpenGL+Cg实现的环境贴图例子。
环境贴图(Environment Mapping)也叫做反射贴图,这项技术由Blinn和Newell于1976年提出,可以用于模拟光滑表面对周围场景的映射效果。生活中有很多这样的例子,比如一个光滑的球体,我们可以在它表面看到周围的环境。图一是维基百科给出的例子:
图一 环境贴图实例
环境贴图基于这样一种假设:物体周围的场景一切都位于无穷远处,这样能够编码到一个被称为环境贴图的全向图像里。而且,模型只能反射环境,不能反射本身。这样也不要期望多个反射,比如两个模型相互反射的情况。
一、立方体纹理
环境贴图最常用的纹理是立方体纹理。它包括六幅正方形的纹理图像,即组成了我们用来生成环境贴图的全向图像。图二是一个环境贴图的例子。
图二 一个环境贴图例子
图三 立方体环境贴图示意图
访问立方体纹理需要一个三维的纹理坐标,它代表了一个三维的向量,这个向量从立方体的中心出发,向外射出,立方体的一个面相交。
需要注意的是:访问立方体所得到的纹理颜色只与这个向量的方向有关,而与大小没有关系。正因为环境映射依赖于方向而不依赖于位置,它在平的反射表面上表现得很差。例如镜子,在镜子上反射主要依赖于位置,相反,环境映射在曲面上表现得很好。
产生立方体纹理:需要得到两个方向的图像,可以把照相机摆在物体中心的位置,然后向六个方向以90度视角拍照。必须要是90度,这样6幅图像才能刚好组合成一个全向图像。
从磁盘加载图像生成纹理可参考:GLAUX纹理生成
二、环境贴图的算法步骤如下:
1、根据视线方向和法向量计算反射向量;
2、使用反射向量检索环境贴图得到纹理信息;
3、将纹理信息融合到当前像素中。
反射向量计算
图四绘制了反射向量的计算示意图。
图四 反射向量计算
计算公式:
R = I - 2 * N * (N * I)
I是入射光线,N是法向量。
Cg语言提供了计算反射光线的库函数:
reflect(I,N)
注意:N必须被单位化,且向量必须是3维的。
计算得到的反射光线向量长度和入射光线向量长度相等。
在本次实现中,我把反射向量的计算放在顶点程序中,虽然这样效果不如把它放在片断程序中效果好,但是用片段程序计算反射向量并不被一些基本片断profile支持。
顶点Cg程序vertex.cg:
void main_v(float4 position : POSITION, float2 texCoord : TEXCOORD0,//decal texture float3 normal : NORMAL, out float4 oPosition : POSITION, out float2 oTexCoord : TEXCOORD0,//out decal texture out float3 R : TEXCOORD1,//reflective vector uniform float3 eyePositionW,//eye position in world space uniform float4x4 modelViewProj, uniform float4x4 modelToWorld ) { modelViewProj = glstate.matrix.mvp; oPosition = mul(modelViewProj,position); oTexCoord = texCoord; float3 positionW = mul(modelToWorld,position).xyz; float3 N = mul((float3x3)modelToWorld,normal); N = normalize(N); float3 I = positionW - eyePositionW;//incident vector R = reflect(I,N); }环境贴图通常是基于世界空间来确定方向的,因此我们在世界坐标系中进行计算反射向量。要实现这一点,必须把其余的顶点数据变化到世界空间。我们也需要提供一个uniform的模型变换矩阵把顶点和房法向量变换到世界空间。模型变换矩阵的计算可能稍微有点复杂。他需要根据模型变换包括旋转平移进行计算。
在这个实例中,在绘制模型猴头之前,进行的模型变换是:先旋转、再平移。
我们先依据旋转的参数计算出旋转矩阵rotateMatrix,计算函数:
void makeRotateMatrix(float angle, float ax,float ay,float az, float m[16]) { float radians, sine, cosine, ab, bc, ca, tx, ty, tz; float axis[3]; float mag; axis[0] = ax; axis[1] = ay; axis[2] = az; mag = sqrt(axis[0]*axis[0] + axis[1]*axis[1] + axis[2]*axis[2]); if (mag) { axis[0] /= mag; axis[1] /= mag; axis[2] /= mag; } radians = angle * myPi / 180.0; sine = sin(radians); cosine = cos(radians); ab = axis[0] * axis[1] * (1 - cosine); bc = axis[1] * axis[2] * (1 - cosine); ca = axis[2] * axis[0] * (1 - cosine); tx = axis[0] * axis[0]; ty = axis[1] * axis[1]; tz = axis[2] * axis[2]; m[0] = tx + cosine * (1 - tx); m[1] = ab + axis[2] * sine; m[2] = ca - axis[1] * sine; m[3] = 0.0f; m[4] = ab - axis[2] * sine; m[5] = ty + cosine * (1 - ty); m[6] = bc + axis[0] * sine; m[7] = 0.0f; m[8] = ca + axis[1] * sine; m[9] = bc - axis[0] * sine; m[10] = tz + cosine * (1 - tz); m[11] = 0; m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1; }其数学推导:图五 旋转变换矩阵计算
然后进行平移变换矩阵translateMatrix的计算:
//compute translation transformation matrix static void makeTranslateMatrix(float x, float y, float z, float m[16]) { m[0] = 1; m[1] = 0; m[2] = 0; m[3] = x; m[4] = 0; m[5] = 1; m[6] = 0; m[7] = y; m[8] = 0; m[9] = 0; m[10] = 1; m[11] = z; m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1; }其数学依据:图六 平移变换矩阵计算
然后得到模型变换矩阵modelMatrix = tranlateMatrix * rotateMatrix(注意顺序:先旋转变换,再平移变换)。
片断Cg程序fragment.cg:
void main_f(float2 texCoord : TEXCOORD0, float3 R : TEXCOORD1, out float4 color : COLOR, uniform float reflectivity, uniform sampler2D decalMap, uniform samplerCUBE environmentMap) { // Fetch reflected environment color float4 reflectedColor = texCUBE(environmentMap, R); // Fetch the decal base color float4 decalColor = tex2D(decalMap, texCoord); color = lerp(decalColor, reflectedColor, reflectivity); }注意:我们最后需要将环境贴图所得颜色与模型本身颜色进行混合,这样做的原因是更加贴近现实,因为没有完全反射的物体。
模型本身的颜色是通过访问一个二维纹理得到的,这个二维纹理也是从磁盘加载一幅图像生成。
程序运行效果:
我所使用的用来构建环境贴图的6幅图:
用于模型本身纹理映射的图:
折射效果的实现
折射的物理依据:
我们一般假设光线只进行一次这是,即忽略光线从介质射出时发生的折射,这样是为了简化计算,而且人眼也不会感觉到和现实有明显的区别。
折射光线计算
Cg提供了计算折射光线的函数refract:
float3 refract (float3 I, float3 N, float etaRatio) { float cosI = dot(-I, N); float cosT2 = 1.0f - etaRatio * etaRatio * (1.0f – cosI * cosI); float3 T = etaRatio * I + ((etaRatio * cosI - sqrt(abs(cosT2))) * N); return T * (float3)(cosT2 > 0); }这样,和上述反射环境贴图的区别就是:用折射光线向量去访问立方体环境贴图。顶点Cg程序:veretx.cg:
void main_v(float4 position : POSITION, float2 texCoord : TEXCOORD0,//decal texture float3 normal : NORMAL, out float4 oPosition : POSITION, out float2 oTexCoord : TEXCOORD0,//out decal texture out float3 T : TEXCOORD1,//refractive vector uniform float etaRatio,//the ratio of the indices of refraction uniform float3 eyePositionW,//eye position in world space uniform float4x4 modelViewProj, uniform float4x4 modelToWorld ) { modelViewProj = glstate.matrix.mvp; oPosition = mul(modelViewProj,position); oTexCoord = texCoord; float3 positionW = mul(modelToWorld,position).xyz; float3 N = mul((float3x3)modelToWorld,normal); N = normalize(N); float3 I = normalize(positionW - eyePositionW);//incident vector T = refract(I,N,etaRatio); }片断Cg程序fragment.cg: void main_f(float2 texCoord : TEXCOORD0, float3 T : TEXCOORD1, out float4 color : COLOR, uniform float transmittance, uniform sampler2D decalMap, uniform samplerCUBE environmentMap) { //fetch reflected environment color float4 refractedColor = texCUBE(environmentMap,T); //fetch the decal base coloe float4 decalColor = tex2D(decalMap,texCoord); //color = refractedColor; color = lerp(refractedColor,decalColor,transmittance); }
运行效果: