作者:i_dovelemon
日期:2014 / 9 / 7
来源:CSDN博客
主题:Bump Mapping, Tangent Space, Normal Map, Height Map
引言
我们知道,在真实世界中,任何物体的表面都不是非常光滑的,这种粗糙的表面效果,在光源的照射下会有明暗的感觉。那么,如何在3D图形中模拟这种效果了?是通过建模的方法,为模型的表面建造出粗糙的表面?这种方法很难做出真实的粗糙感出来,毕竟表面的粗糙程度是很细微的效果,很难用建模的方式建模出来。所以,只能用其他的方法来代替了。在3D图形学中,有一种称为Bump Mapping的技术,这个技术可以实现模型的表面粗糙感,而不需要额外的为模型增加顶点数据。
Bump Mapping
想要营造这种明暗效果,很显然,我们需要通过光照计算来模拟。要进行光照计算,自然需要光源和法线。光源,我们可以很简单的使用颜色值,光源位置来定义就可以了。而对于法线,因为我们不是增加顶点,改变模型的方式,所以,仅仅有顶点的法线,然后经过插值很难做出粗糙的感觉。我们需要的是,在这个模型渲染到最后的表面的时候,每一个像素的法线都需要不同,都需要根据不同的明度和暗度来营造。
而每一个像素的法线,如何计算了?想象一下,我们可以为模型的表面贴上一个凹凸不平的贴图,但是这种贴图在光源的效果下,并不会有粗糙材质在光源照耀下的明暗感觉。为了能够让这个贴图有那种凹凸明暗的感觉,我们就需要为每一个像素弄出一个法线出来。这样,随着光源不断的移动,我们对每一个像素进行光照计算,这种,每一个像素的明暗效果就可以出现了。所以剩下的问题就是,如何设计贴图每一个像素的法线,从而来营造出我们想要的凹凸粗糙感了?
在3D图形学中,有一种图,称为Height Map(高度图)的东西。如下所示为一张高度图:
从上面的图中可以看出,高低不平,黑色的地方表示很低,白色的地方表示很高。这种高度图,实际上是一种灰度图,也就是说,它的颜色值中RGB的各个分量实际上都是一样的。
好了,有了这种凹凸的效果,接下来,我们的任务就是根据这张高度图来构建每一个像素法线。由于我们不能在Shader中实时的构建,那样效率太差,所以我们需要预先根据这种图来构建所有纹理像素的法线,由于法线是向量,所以也可以使用三个分量来表示,所以,我们完全可以将法线保存在RGB值中,只不过要将法线各个分量的值缩放到0-255范围之类就可以了。
下面的函数用于将BMP格式的高度图计算为法线,并且存在在另外一个相同尺寸BMP位图中,这张图就称为Normal Map:
<span style="font-family:Microsoft YaHei;">/** * Suppose the texture is 32 Bit format heightmap and needs to be * converted to be a normal map instead. */ HRESULT ZFXD3DSkinManager::convertToNormalmap(ZFXTEXTURE *pTexture) { HRESULT hr= ZFX_OK; D3DLOCKED_RECT d3dRect; D3DSURFACE_DESC desc; // get a dummy pointer LPDIRECT3DTEXTURE9 pTex = ((LPDIRECT3DTEXTURE9)pTexture->pData); pTex->GetLevelDesc(0, &desc); if (FAILED(pTex->LockRect(0, &d3dRect, NULL, 0))) { log("error: cannot lock texture to copy pixels \"%s\"", pTexture->chName); return ZFX_BUFFERLOCK; } // get pointer to pixel data DWORD* pPixel = (DWORD*)d3dRect.pBits; // build normals at each pixel for (DWORD j=0; j<desc.Height; j++) { for (DWORD i=0; i<desc.Width; i++) { DWORD color00 = pPixel[0]; DWORD color10 = pPixel[1]; DWORD color01 = pPixel[d3dRect.Pitch/sizeof(DWORD)]; float fHeight00 = (float)((color00&0x00ff0000)>>16)/255.0f; float fHeight10 = (float)((color10&0x00ff0000)>>16)/255.0f; float fHeight01 = (float)((color01&0x00ff0000)>>16)/255.0f; ZFXVector vcPoint00( i+0.0f, j+0.0f, fHeight00 ); ZFXVector vcPoint10( i+1.0f, j+0.0f, fHeight10 ); ZFXVector vcPoint01( i+0.0f, j+1.0f, fHeight01 ); ZFXVector vc10 = vcPoint10 - vcPoint00; ZFXVector vc01 = vcPoint01 - vcPoint00; ZFXVector vcNormal; vcNormal.cross(vc10, vc01); vcNormal.normalize(); // store normal as RGBA in normalmap *pPixel++ = VectortoRGBA( &vcNormal, fHeight00 ); } } pTex->UnlockRect(0); LPDIRECT3DSURFACE9 pSurface=NULL; pTex->GetSurfaceLevel(0, &pSurface); D3DXSaveSurfaceToFile("normal.bmp", D3DXIFF_BMP, pSurface, NULL, NULL); return ZFX_OK; } // ConvertToNormalmap</span>
从高度图中计算法线的方法是取出相邻的是三个点来构建这三个点的法线。比如,当前像素点为A,那么第二个点为A右边相邻的一个像素点B,接下来的第三个点就是A点下面的那个相邻的点C。由于是灰度图,所以颜色值的三个分量都是相同的,所以我选择R分量来构建出与A像素点相对应的向量点的Z值,并且使用A点像素在位图中的位置xy,作为构建向量的x,y。通过同样的方法,我们依次建立三个点的向量,然后计算出最终的法线。我们对法线进行归一化操作,然后通过VectortoRGBA将法线转化为RGB值,保存在新的BMP图像中。
下面是VectortoRGBA的函数:
<span style="font-family:Microsoft YaHei;">// encode vector data as RGBA color value for normal map DWORD VectortoRGBA(ZFXVector *vc, float fHeight) { DWORD r = (DWORD)( 127.0f * vc->x + 128.0f ); DWORD g = (DWORD)( 127.0f * vc->y + 128.0f ); DWORD b = (DWORD)( 127.0f * vc->z + 128.0f ); DWORD a = (DWORD)( 255.0f * fHeight ); return( (a<<24L) + (r<<16L) + (g<<8L) + (b<<0L) ); } // VectortoRGBA</span>
这个函数还将Normal map保存在normal.bmp中,下面是上面的高度图对应的normal.bmp:
总结
这篇文章,暂时简单的讲述了如何根据高度图来构建法线图。在接下来的文章中,将会向大家讲述如何根据这个法线图来进行光照计算,从而达到凹凸明暗的效果。并且在接下来将会讲述在3D图形学中很重要的一个概念:Tangent Space。