作者:i_dovelemon
日期: 2014 / 9 / 13
来源 : CSDN
主题 :Bump Mapping, Tangent Space, Normal Map, Height Map
引言
在上篇文章中,我讲述了如何根据高度图来创建法线图。并且承诺在后面会讲述3D几何学中的重要工具Tangent Space的相关知识。今天,就在这里,向大家讲述如何接下来的工作部分。
Tangent Space
我们知道,在前篇文章中,讲述的法线图中的法线是在纹理图的空间中也就是Tangent Space中。当我们进行光照计算的时候,我们需要将光的照射向量和像素的法线放在一个空间中,从而进行光照计算。一般来说光源的照射向量是在世界空间中的,而法线是在Tangent Space中的。如果我们在进行光照计算的时候,将每一个像素的法线从Tangent Space中变换到世界坐标空间的话,对于所有的像素都要进行这样的处理,但是如果我们对光照向量进行这样的操作的话,只需要在Vertex
Shader中执行一次就可以了。所以,我们的做法自然就是想办法将光源的照射向量转换到与像素的法线相同的Tangent Space中去。
进行坐标空间变换是3D图形学的基本知识。如果读者不了解的话,请看我的博客中的3D坐标变换。为了能够进行坐标变换,我们需要像素的法线的坐标空间Tangent Space的基坐标在模型坐标空间的表示,这样,我们就能够很容易的将模型坐标空间中的向量变换到Tangent
Space中。
那么,如何创建Tangent Space的三个基向量了?
Tangent Space中的x轴和Bump Map的的u轴方向对齐,而y轴和Bump Map的v轴方向对齐。也就是说,如果Q点表示的是三角形中的一个点,那么我们就能够使用如下的方程来表示:
Q - P0 = (u - u0) T + (v - v0)B,(公式1)
这里的T和B就是和Bump Map对齐的向量。P0是三角形的一个顶点,(u0,v0)表示的是P0顶点的纹理坐标。而后面的B表示的是bitangent,有些书本上也称之为binormal。
假设我们有一个三角形,它的三个顶点分别为P0, P1, P2,他们对应的纹理坐标分别为(u0,v0),(u1,v1),(u2,v2)。为了方便起见,我们这里将P0做为参考点。所以,我们假设:
Q1 = P1 - P0
Q2 = P2 - P0
并且
(s1, t1) = (u1 - u0, v1 - v0)
(s2, t2) = (u2 - u0, v2 - v0)
由于上面的公式1表示的是一个三角形中所有点的关系,那么对于点P1和P2同样也能够满足这个函数,也就是说:
Q1 = s1T + t1B
Q2 = s2T + t2B
我们使用矩阵来解上面的方程,得到如下的矩阵方程:
两边同时乘以(s,t)矩阵的逆矩阵,得到如下的矩阵方程:
(公式2)
好了,从上面的矩阵方程中,我们就可以得到两个向量T和B了。
在有了T,B,N向量之后,我们就可以使用如下的矩阵,将Tangent Space中的坐标变换到模型的坐标空间中去了:
上面的矩阵,是将Tangent Space中的坐标变换到模型坐标空间中去的,但是在这里我们需要的是将模型坐标空间中的坐标变换到Tangent Space坐标空间中去的。所以我们需要上面矩阵的逆矩阵。(实际上这是不精确的,因为我们在上面使用的N向量是顶点的Normal向量方向,这个向量和实际的Tangent Space中的N向量有细微的差别,但是可以忽略不计。)由于这三个向量相互垂直,那么这个矩阵就是正交矩阵,正交矩阵的逆矩阵等于它的转置矩阵,所以如下所示为最终的将模型坐标空间中的坐标变换到Tangent
Space空间中的矩阵:
计算Tangent Space
下面的两个函数用于计算顶点的Tangent Space坐标向量中的Tangent Vector。
<span style="font-family:Microsoft YaHei;">void CalcTangent(ZFXTVERTEX* v) { for(int i = 0 ; i < 8 ; i ++) { CalcTangent(&v[i*3],&v[i*3+1],&v[i*3+2]); } }// end for CalcTangent void CalcTangent(ZFXTVERTEX *t1, ZFXTVERTEX *t2, ZFXTVERTEX *t3) { ZFXVector vc, vcA, vcB ; float fu21 = t2->tu - t1->tu , fv21 = t2->tv - t1->tv , fu31 = t3->tu - t1->tu , fv31 = t3->tv - t1->tv ; //x-component of tangent vector vcA.set(t2->x - t1->x, fu21, fv21); vcB.set(t3->x - t1->x, fu31, fv31); vc.cross(vcA, vcB); if(fabs(vc.x) > 0.0001f) { t1->vcU[0] = -vc.y / vc.x ; } //y-component of tangent vector vcA.set(t2->y - t1->y, fu21, fv21); vcB.set(t3->y - t1->y, fu31, fv31); vc.cross(vcA, vcB); if(fabs(vc.x) > 0.0001f) { t1->vcU[1] = -vc.y / vc.x ; } //z-component of tangent vector vcA.set(t2->z - t1->z, fu21, fv21); vcB.set(t3->z - t1->z, fu31, fv31); vc.cross(vcA, vcB); if(fabs(vc.x) > 0.0001f) { t1->vcU[2] = -vc.y / vc.x ; } ZFXVector normalV(t1->vcU[0],t1->vcU[1],t1->vcU[2]); normalV.normalize(); t2->vcU[0] = t3->vcU[0] = t1->vcU[0] = normalV.x; t2->vcU[1] = t3->vcU[1] = t1->vcU[1] = normalV.y; t2->vcU[2] = t3->vcU[2] = t1->vcU[2] = normalV.z; }// end for CalcTangent</span>
公式完全使用的是在上面结论公式2来进行计算,只要将cross展开,你就可以明白。在函数中,我仅仅计算了T向量,而没有和上面理论那样计算T和B向量了。因为,我将要使用顶点的法线方向做为Tangent Space的N向量,所以在有了T和N之后,只要进行叉积运算就能够得到B向量了。这个决策,也反应在我定义的顶点结构中,如下所示:
<span style="font-family:Microsoft YaHei;">/** * Define the Tangent Vertex format */ typedef struct ZFXTVERTEX_TYPE { float x, y, z ; float vcN[3] ; float tu, tv ; float vcU[3] ; // Tangent vector }ZFXTVERTEX; </span>
函数很简单,只要按照公式,照搬照抄就可以,就不用多说了。
程序实例
既然我们知道了如何计算Tangent Space了,那么我们就用此种方法来构建一个Room的模型,一下是Room模型的创建代码:
<span style="font-family:Microsoft YaHei;">HRESULT CreateRoom(ZFXTVERTEX* v, WORD* index) { //Create a Wall ZFXVector normal ; //Triangle 1 v[0].x = -100 ; v[0].y = 100 ; v[0].z = 100 ; v[0].vcN[0] = 0 ; v[0].vcN[1] = 0 ; v[0].vcN[2] = 1 ; v[0].tu = 0.0f ; v[0].tv = 0.0f ; v[1].x = 100 ; v[1].y = 100 ; v[1].z = 100 ; v[1].vcN[0] = 0 ; v[1].vcN[1] = 0 ; v[1].vcN[2] = 1 ; v[1].tu = 1.0f ; v[1].tv = 0.0f ; v[2].x = 100 ; v[2].y = -100 ; v[2].z = 100 ; v[2].vcN[0] = 0 ; v[2].vcN[1] = 0 ; v[2].vcN[2] = 1 ; v[2].tu = 1.0f ; v[2].tv = 1.0f ; //Triangle 2 v[3].x = -100 ; v[3].y = 100 ; v[3].z = 100 ; v[3].vcN[0] = 0 ; v[3].vcN[1] = 0 ; v[3].vcN[2] = 1 ; v[3].tu = 0.0f ; v[3].tv = 0.0f ; v[4].x = 100 ; v[4].y = -100 ; v[4].z = 100 ; v[4].vcN[0] = 0 ; v[4].vcN[1] = 0 ; v[4].vcN[2] = 1 ; v[4].tu = 1.0f ; v[4].tv = 1.0f ; v[5].x = -100 ; v[5].y = -100 ; v[5].z = 100 ; v[5].vcN[0] = 0 ; v[5].vcN[1] = 0 ; v[5].vcN[2] = 0 ; v[5].tu = 0.0f ; v[5].tv = 1.0f ; //Triangle 3 v[6].x = -100 ; v[6].y = -100 ; v[6].z = 100 ; v[6].vcN[0] = 0 ; v[6].vcN[1] = 1 ; v[6].vcN[2] = 0 ; v[6].tu = 0.0f; v[6].tv = 0.0f ; v[7].x = 100 ; v[7].y = -100 ; v[7].z = 100 ; v[7].vcN[0] = 0 ; v[7].vcN[1] = 1 ; v[7].vcN[2] = 0 ; v[7].tu = 1.0f ; v[7].tv = 0.0f ; v[8].x = 100 ; v[8].y = -100 ; v[8].z = -100 ; v[8].vcN[0] = 0 ; v[8].vcN[1] = 1 ; v[8].vcN[2] = 0 ; v[8].tu = 1.0f ; v[8].tv = 1.0f ; //Triangle 4 v[9].x = -100 ; v[9].y = -100 ; v[9].z = 100 ; v[9].vcN[0] = 0 ; v[9].vcN[1] = 1 ; v[9].vcN[2] = 0 ; v[9].tu = 0.0f; v[9].tv = 0.0f ; v[10].x = 100 ; v[10].y = -100 ; v[10].z = -100 ; v[10].vcN[0] = 0 ; v[10].vcN[1] = 1 ; v[10].vcN[2] = 0 ; v[10].tu = 1.0f ; v[10].tv = 1.0f ; v[11].x = -100 ; v[11].y = -100 ; v[11].z = -100 ; v[11].vcN[0] = 0 ; v[11].vcN[1] = 1 ; v[11].vcN[2] = 0 ; v[11].tu = 0.0f ; v[11].tv = 1.0f ; //Triangle 5 v[12].x = -100 ; v[12].y = 100 ; v[12].z = -100 ; v[12].vcN[0] = 1 ; v[12].vcN[1] = 0 ; v[12].vcN[2] = 0; v[12].tu = 0.0f ; v[12].tv = 0.0f ; v[13].x = -100 ; v[13].y = 100 ; v[13].z = 100 ; v[13].vcN[0] = 1 ; v[13].vcN[1] = 0 ; v[13].vcN[2] = 0 ; v[13].tu = 1.0f; v[13].tv = 0.0f ; v[14].x = -100 ; v[14].y = -100 ; v[14].z = 100 ; v[14].vcN[0] = 1 ; v[14].vcN[1] = 0 ; v[14].vcN[2] = 0 ; v[14].tu = 1.0f; v[14].tv = 1.0f ; //Triangle 6 v[15].x = -100 ; v[15].y = 100 ; v[15].z = -100 ; v[15].vcN[0] = 1 ; v[15].vcN[1] = 0 ; v[15].vcN[2] = 0; v[15].tu = 0.0f ; v[15].tv = 0.0f ; v[16].x = -100 ; v[16].y = -100 ; v[16].z = 100 ; v[16].vcN[0] = 1 ; v[16].vcN[1] = 0 ; v[16].vcN[2] = 0 ; v[16].tu = 1.0f; v[16].tv = 1.0f ; v[17].x = -100 ; v[17].y = -100 ; v[17].z = -100 ; v[17].vcN[0] = 1 ; v[17].vcN[1] = 0 ; v[17].vcN[2] = 0 ; v[17].tu = 0.0f ; v[17].tv = 1.0f ; //Triangle 7 v[18].x = 100 ; v[18].y = 100 ; v[18].z = 100 ; v[18].vcN[0] = -1 ; v[18].vcN[1] = 0 ; v[18].vcN[2] = 0 ; v[18].tu = 0.0f; v[18].tv = 0.0f ; v[19].x = 100 ; v[19].y = 100; v[19].z = -100 ; v[19].vcN[0] = -1 ; v[19].vcN[1] = 0 ; v[19].vcN[2] = 0; v[19].tu = 1.0f ; v[19].tv = 0.0f ; v[20].x = 100 ; v[20].y = -100 ; v[20].z = -100 ; v[20].vcN[0] = -1 ; v[20].vcN[1] = 0 ; v[20].vcN[2] = 0 ; v[20].tu = 1.0f; v[20].tv = 1.0f; //Triangle 8 v[21].x = 100 ; v[21].y = 100 ; v[21].z = 100 ; v[21].vcN[0] = -1 ; v[21].vcN[1] = 0 ; v[21].vcN[2] = 0 ; v[21].tu = 0.0f; v[21].tv = 0.0f ; v[22].x = 100 ; v[22].y = -100 ; v[22].z = -100 ; v[22].vcN[0] = -1 ; v[22].vcN[1] = 0 ; v[22].vcN[2] = 0 ; v[22].tu = 1.0f; v[22].tv = 1.0f; v[23].x = 100 ; v[23].y = -100 ; v[23].z = 100 ; v[23].vcN[0] = -1 ; v[23].vcN[1] = 0 ; v[23].vcN[2] = 0 ; v[23].tu = 0.0f ; v[23].tv = 1.0f ; //Calculate the tangent vector CalcTangent(v); for(int i = 0 ; i < 24 ; i ++) index[i] = i ; return 0; }//end for CreateRoom</span>
在构建好了24个顶点数据之后,我们就对数组进行Tangent Space中的T向量的计算,并保存起来。
好了,计算好了之后,我们通过如下的函数来进行渲染:
<span style="font-family:Microsoft YaHei;">void RenderOmniPass(UINT bID, UINT vsID, UINT psID, UINT nLight) { ZFXMatrix mAttenu; static float n = 0.0f ; ZFXVector pos_1(100.0f, -100.0f, 0.0f); ZFXVector pos_2(-100.0f, 100.0f, 0.0f); ZFXVector pos ; if(nLight == 1) pos = pos_1 ; else pos = pos_2 ; ZFXMatrix mRotate; mRotate.identity(); mRotate.rotateY(n); n += 0.01f; pos = pos * mRotate ; //Set the help vector to c30 ZFXVector vHelp(0.5f, 0.5f, 0.5f); if(FAILED(g_pDevice->setShaderConstant(SHT_VERTEX, DT_FLOAT, 30, 1, &vHelp))) return ; //Set the light color to c0 ZFXCOLOR lightColor = {1.0f, 1.0f, 1.0f, 1.0f}; if(FAILED(g_pDevice->setShaderConstant(SHT_PIXEL, DT_FLOAT, 0, 1, &lightColor))) return ; if(FAILED(g_pDevice->activateVShader(vsID, VID_UT))) return ; if(FAILED(g_pDevice->activatePShader(psID))) return ; ZFXMatrix mW; mW.identity(); g_pDevice->setWorldTransform(&mW); //Set the light position to c25 ZFXMatrix invM; invM.inverseOf(mW); pos = pos * invM ; if(FAILED(g_pDevice->setShaderConstant(SHT_VERTEX, DT_FLOAT, 25, 1, &pos))) return ; //Set the world_transform to c31 ZFXMatrix transM ; transM.transposeOf(mW); if(FAILED(g_pDevice->setShaderConstant(SHT_VERTEX, DT_FLOAT, 31, 4, &transM))) return ; g_pDevice->useAdditiveBlending(true); if(FAILED(g_pDevice->getVertexCacheManager()->render(bID))) return ; g_pDevice->useAdditiveBlending(false); }// end for RenderOmniPass</span>
上面的函数没有什么太麻烦的地方。唯一需要注意的是,我们需要将光源变换到模型的坐标空间中去,因为我们要在模型的坐标空间中对光照进行光照计算。下面是这个实例程序的Vertex Shader和Pixel Shader:
<span style="font-family:Microsoft YaHei;">vs.1.1 dcl_position v0 dcl_normal v3 dcl_texcoord v7 dcl_tangent v8 m4x4 oPos, v0, c0 ; transform position mov r5, v8 mov r7, v3 ; calculate binormal V as ; crossproduct of U and N mul r0,r5.zxyw, -r7.yzxw; mad r6,r5.yzxw, -r7.zxyw,-r0; ; build vector to_light sub r2, c25, v0 ; transform to_light vector dp3 r8.x, r5.xyz, r2 dp3 r8.y, r6.xyz, r2 dp3 r8.z, r7.xyz, r2 ; normalize transformed to_light vector dp3 r8.w, r8, r8 rsq r8.w, r8.w mul r8, r8, r8.w ; *.5 + .5 mad oD0.xyz, r8.xyz, c30.x, c30.x mov oT0.xy, v7.xy mov oT1.xy, v7.xy </span>
<span style="font-family:Microsoft YaHei;">ps.1.1 tex t0 tex t1 dp3 r1, t1_bx2, v0_bx2 mul r0, c0, t0 mul r0, r1, r0 </span>
我这里使用的是Assembly Shader,如果看不懂的话也没有关系,有了上面关于Tangent Space的计算,读者应该能够完全使用HLSL来进行光照计算了。
最终的程序截图如下所示:
好了,今天的笔记到此结束!