Bump Mapping

一、Bump Mapping的作用

它能能极大提高图像真实感,橘子上的皱纹、带浮雕的微标以及带凹痕的砖块都是使用Bump贴图的极好例子,下图展示了砖块的凹凸贴图,材料来源于http://www.ozone3d.net/tutorials/bump_mapping.php。

(1)直接纹理贴图,未使用Bump Mapping:

 

(2)使用Bump Mapping,凹凸细节非常明显,真实感提高。

Bump Mapping_第1张图片

 

二、Bump贴图的原理。

为什么第二张图会显得凹凸有序呢,明暗分明呢,豪无疑问这是光照的功劳。Bump贴图实质上是改变(或者替换)球面上各顶点的原有法向量,可以这么想像,将原来的法向量微量的旋转调整,新得到的这个法向量与光照向量的夹角将改变(也即改变了法向量与光线向量点积dot的值),从而改变灯光在这一点的强度,这样的结果将使该点的亮度发生变化(更亮或更暗)。这也就是其称为Bump Mapping(凹凸贴图)的原因,它并没有改变要着色的表面,它只是“欺骗”了光照计算。此时就肯定有个疑问了:

//***我怎么知道球上某点是该变亮还是变暗呢?这个点变亮(或变暗)多少呢?说白了也就是这个新的法向量该怎么得到呢?***//

 

(1)我们知道光源的计算都是通过点积dot来计算的。为了使结果有意义,通常也是将其放在视觉坐标系中来计算光照,光源的位置和方向都是在视觉坐标系中定义的,同样顶点法线和顶点坐标也是转换到这个坐标系里来(glNormalMatrix实质上是glModeViewMatrix的左上角3*3矩阵的逆矩阵)。但是视觉坐标系并不一定是最佳的选择,在将光线方向与表面法线转到视觉坐标系后,我们得将它们规范化,然后把它们作为varying 变量传给fragment shader。在进行了插值之后,为了得到准确的结果,需要再次规范化光线方向矢量。并且无论使用哪一种方法来扰动法线(程序计算或法向)都需要再进行一次规范化,如果没有规范,这个光照会显得非常不自然(非单位向量计算dot必然出问题),就性能而言,在每一个片元上执行这么多操作的代价可能是非常高昂的。

注:解释一下这里为什么要将光照计算放到fragment shader里面,而不是如平常那样放在vertex shader中。在执行凹凸光照计算的时候我们得知道在哪儿放置这样的凹凸块,这个位置我们是通过顶点的纹理坐标来计算的(如果有法向量贴图的话那么就可以直接应用上了,但同样需要在fragment中获得纹理)。而纹理坐标及其像素颜色的获取都是在fragment shader中通过如 Texture2D()等内置函数获得的。

(2)我们来看另一个坐标空间,它名为“表面本地坐标空间”(SURFACE-LOCAL COOROINATE SPACE),实质上它是由顶点表面的法向量、切向量和副向量(前两个向量的叉乘)组成的一个正交坐标系,所以当把正处理的顶点转换到该坐标系后,顶点坐标将变为(0, 0, 0),而点上未受扰动的表面法线为(0, 0, 1)。这是个非常方便的坐标系,可以方便地执行凹凸计算,但是为了执行我们的光照计算,需要确保光线向量、视线向量和扰动向量(如果用的是法向量贴图那么就不城要转换到该坐标系中了,它默认就是在表面本地坐标系中)都是在同一个坐标系中定义的。也就是说我们得将光线方向和视眼方向转到表面本地坐标系中来,这个矩阵该如何得到?

      能把一个顶点从(x,y,z)转换为(0, 0, 0)的矩阵有无数个,我们应该选用哪一种转换呢?是否可以随意选择变换矩阵?答案当然是否定的,位于同一平面上的各个顶点若它们的切线不相同将会造成严重的光照失真(见下图),因为如果在同一个平面上(一般是三角形)上的各点切线不同,在各点之间光线的插值过渡就将显得很不滑,于是就将导致严重的光照失真。

 Bump Mapping_第2张图片

这也是该坐标系有时称为"切线空间"的原因。、

 

(3)关于顶点切线的计算可以参见:http://www.blacksmith-studios.dk/projects/downloads/tangent_matrix_derivation.php

讲得非常详细了。该文章最关键的一点是要理解这句话:

  • The vector from v1 to v2 has the same direction in world space as the vector from c1 to c2 in texture space.
  • 就是说v1到v2的顶点坐标向量,与v1到v2的纹理坐标向量是相同的。(这样想不知道对不对:将纹理坐标放入正切坐标系中,因为该坐标系X、Y轴的方向是由纹理坐标系中的UV方向确定的,所以这两个点的U1-U2的矢量与V1-U2方向的向量其实上对应的就是X、Y轴上的向量,那么通过T、B变换,就将这两个向量转成了视觉坐标系中的顶点的向量)

     参考代码:

    void ComputeTangentBasis( const Vec3& P1, const Vec3& P2, const Vec3& P3, const Vec2& UV1, const Vec2& UV2, const Vec2& UV3, Vec3 &tangent, Vec3 &bitangent ) { Vec3 Edge1 = P2 - P1; Vec3 Edge2 = P3 - P1; Vec2 Edge1uv = UV2 - UV1; Vec2 Edge2uv = UV3 - UV1; float cp = Edge1uv.y * Edge2uv.x - Edge1uv.x * Edge2uv.y; if ( cp != 0.0f ) { float mul = 1.0f / cp; tangent = (Edge1 * -Edge2uv.y + Edge2 * Edge1uv.y) * mul; bitangent = (Edge1 * -Edge2uv.x + Edge2 * Edge1uv.x) * mul; tangent.Normalize(); bitangent.Normalize(); } }

     

    (4)例:使用NormalMap实现Bump贴图

    Vertex Shader:

    attribute vec3 tangent; varying vec3 lightVec; varying vec3 halfVec; varying vec3 eyeVec; void main() { gl_TexCoord[0] = gl_MultiTexCoord0; // Building the matrix Eye Space -> Tangent Space vec3 n = normalize (gl_NormalMatrix * gl_Normal); vec3 t = normalize (gl_NormalMatrix * tangent); vec3 b = cross (n, t); vec3 vertexPosition = vec3(gl_ModelViewMatrix * gl_Vertex); vec3 lightDir = normalize(gl_LightSource[0].position.xyz - vertexPosition); // transform light and half angle vectors by tangent basis vec3 v; v.x = dot (lightDir, t); v.y = dot (lightDir, b); v.z = dot (lightDir, n); lightVec = normalize (v); v.x = dot (vertexPosition, t); v.y = dot (vertexPosition, b); v.z = dot (vertexPosition, n); eyeVec = normalize (v); vertexPosition = normalize(vertexPosition); /* Normalize the halfVector to pass it to the fragment shader */ vec3 halfVector = normalize((vertexPosition + lightDir) / 2.0); v.x = dot (halfVector, t); v.y = dot (halfVector, b); v.z = dot (halfVector, n); halfVec = normalize (v); gl_Position = ftransform(); }

     

     

    Fragment Shader:

    uniform sampler2D diffuseTexture; uniform sampler2D normalTexture; // New bumpmapping varying vec3 lightVec; varying vec3 halfVec; varying vec3 eyeVec; void main() { // lookup normal from normal map, move from [0,1] to [-1, 1] range, normalize vec3 normal = 2.0 * texture2D (normalTexture, gl_TexCoord[0].st).rgb - 1.0; normal = normalize (normal); // compute diffuse lighting float lamberFactor= max (dot (lightVec, normal), 0.0) ; vec4 diffuseMaterial = 0.0; vec4 diffuseLight = 0.0; // compute specular lighting vec4 specularMaterial ; vec4 specularLight ; float shininess ; // compute ambient vec4 ambientLight = gl_LightSource[0].ambient; if (lamberFactor > 0.0) { diffuseMaterial = texture2D (diffuseTexture, gl_TexCoord[0].st); diffuseLight = gl_LightSource[0].diffuse; // In doom3, specular value comes from a texture specularMaterial = vec4(1.0) ; specularLight = gl_LightSource[0].specular; shininess = pow (max (dot (halfVec, normal), 0.0), 2.0) ; gl_FragColor = diffuseMaterial * diffuseLight * lamberFactor ; gl_FragColor += specularMaterial * specularLight * shininess ; } gl_FragColor += ambientLight; }

    使用的法向量贴图如下:

    Bump Mapping_第3张图片

    注:该法向量图片为什么大部色呈淡蓝色?因为法向量纹理的格式为RGB,也即其像素中的red代表切线空间中的x轴方向,green代表切线空间中的y轴方向,blue代表切线空间中的z轴方向。我们知道未被扰动的法向量坐标为(0, 0, 1)也即纯蓝,当扰动量不大的时候,比如:(0.2, 0.3, 0.93 ) (注意这也是单位向量),它的主体颜色依然为蓝,这就出现了上图的情况。

    你可能感兴趣的:(Bump Mapping)