3.5 凹凸映射

现实世界中有这样一类物体,其表面是粗糙的,具有很多微小细节。例如,具有小花纹的茶壶、常吃的橘子等。当需要渲染这些物体时,可以采用前面章节介绍过的基于三角形的立体物体构建技术去描述这些微小细节。但这样势必造成顶点数量的急剧增加,大大降低渲染速度。

本节将介绍一种基于特殊纹理映射的低成本细节模拟技术——凹凸映射,其不需要增加很多的顶点来描述细节,只是在计算光照前通过运算扰动法向量,效率很高。

3.5.1 案例效果与基本原理

介绍凹凸映射的基本知识之前首先需要了解一下本节案例Sample3_5的运行效果,对凹凸映射有一个基本的认识,具体情况如图3-10所示。

从图3-10中可以看出,本案例中的茶壶表面具有很多椭圆形的小凸起,效果非常逼真。已经掌握了前面章节基于三角形构建立体物体技术的读者可以很容易地分析出,若使用三角形来构建上述茶壶将需要数倍于普通茶壶的顶点数,这在当下的主流手机上是难以支持的。

▲图3-10 凹凸映射案例的运行效果

幸运的是,Jim Blinn创造了凹凸映射技术,其使得仅仅需要采用普通茶壶的模型就可以绘制出具有微小细节的粗糙表面,此技术的基本思想如下所列。

● 首先绘制时采用的是没有表面细节的模型,并且模型中的每个顶点都指定了不考虑表面细节情况下的法向量、切向量以及一个纹理坐标。其中切向量指的是与顶点处表面相切与法向量正交的向量,在后面的计算中有重要作用。

● 给顶点分配的纹理坐标与前面章节中的案例一样也是用来进行纹理采样的,但被采样的纹理图中记录的不再是片元的颜色,而是此片元处考虑了微小细节的法向量相对于标准法向量的扰动结果。因此,每次纹理采样结果中的RGB通道值不再代表颜色,而分别代表扰动后法向量的xyz分量。

● 当取出扰动后法向量时,可以将其与此片元处不考虑表面细节的法向量进行计算得出此片元处考虑了表面细节后的法向量。最后用计算后的法向量进行正常的光照计算即可渲染出具有表面粗糙细节的物体。

一般情况下为了计算方便,纹理图中记录扰动结果法向量时,认为扰动前的标准法向量为[0,0,1]。也就是说从纹理图中取出的扰动后法向量是以标准法向量[0,0,1]为基础的,而并不是基于对应片元处不考虑表面细节情况下的法向量。

这就涉及了折算的问题,最直观的方式就是根据片元处的法向量、切向量将纹理图中取出的扰动后法向量折算到实际坐标系中进行光照计算。但基于这种思路的计算非常复杂,故为了简化问题,本节案例采用的是逆向计算的方式,实施策略为将光源位置、摄像机位置变换到标准法向量所属的坐标系中再直接基于从纹理图中取出的扰动后法向量进行光照计算,具体步骤如下。

● 首先根据片元的法向量、切向量计算出副法向量。副法向量的计算很简单,将法向量、切向量直接进行叉积即可得到。

● 然后将切向量、副法向量、法向量组装成一个3×3的变换矩阵,此变换矩阵可以用来将光源位置、摄像机位置变换到标准法向量所属的坐标系中。

● 将光源位置乘以上述3×3的变换矩阵,得到用于光照计算的光源位置。

● 将摄像机位置乘以上述3×3的变换矩阵,得到用于光照计算的摄像机位置。

提示

有了上述计算模型后,就可以开发出实现此计算模型的渲染程序了,具体内容会在后面的小节进行介绍。

3.5.2 法向量纹理图的生成

从上一小节凹凸映射计算模型的介绍中可以看出,实施此计算模型需要有携带了扰动后法向量信息的纹理图。具体来说就是纹理图中的每个像素都代表了一个扰动后的法向量,RGB通道的值分别为扰动后法向量的xyz分量。

显然,这种纹理图是没有办法用绘图工具(如PhotoShop、画图等)来直接绘制的。笔者采用的方式是用绘图工具生成高度域灰度图,然后再用自己开发的工具将高度域灰度图转换为法向量纹理图。图3-11给出了一系列的高度域灰度图。

▲图3-11 高度域灰度图

提示

图3-11中给出了4幅高度域灰度图,其中最左侧的是本节案例所采用的,读者掌握了开发技术之后也可以采用其他高度域灰度图。

高度域灰度图中黑色的部分代表高度低的区域,而白色的部分代表凸起的区域。也就是颜色越浅,凸起越高。有了高度域灰度图后就可以将其转换为法向量纹理图以备使用了,具体转换方法如下所列。

● 当需要计算某个像素对应的扰动后法向量时,首先需要计算出两个差分向量。第一个差分向量为[1,0,H -H ],第二个差分向量为[0,1,H -H ]。H 为此像素正上方的像素灰度值所代表的高度,H 为此像素正右侧的像素灰度值所代表的高度,H 为此像素灰度值所代表的高度。

提示

H 、H 、H 并不能从高度域灰度图中直接取出,当取出一个像素的灰度值后需要除以255以折算出0~1内的高度。

● 计算出两个差分向量后将两个差分向量进行叉积即可计算出此像素处扰动后法向量的值,然后再将扰动后法向量的xyz分量各自折算到0~255的整数范围内,并作为像素的RGB值存入法向量纹理图中的对应位置即可。

了解了法向量纹理图的生成算法后,就可以基于此开发出将高度域灰度图转换为法向量纹理图的工具了。在介绍转换工具的开发之前可以首先了解一下转换后法向量纹理图的内容,具体情况如图3-12所示。

▲图3-12 生成的法向量纹理图

说明

图3-12中给出了图3-11中4幅高度域灰度图对应的法向量纹理图。要注意的是由于书中的插图是灰度印刷的因此很难看出效果,若读者运行光盘中附带的法向量纹理图自动生成工具就会发现生成的法向量纹理图其主体颜色是蓝色,在高度变化大的区域会有其他颜色。

了解了法向量纹理图的具体内容后,就可以进行转换工具的开发了。由于本转换工具是采用Java进行开发,工作在PC上,大部分搭建界面和人机交互的代码与本书内容无关,故略去不讲,需要的读者可以参考随书关盘中的源代码。这里仅简要介绍一下实现此生成算法的核心代码,具体内容如下所列。

(1)首先介绍对向量进行各种运算的工具类VectorUtil,其代码如下。

代码位置:见随书光盘中源代码/第3章/高度域灰度图转换为法向量纹理图的工具目录下的VectorUtil.java。

      1    package com.bn;
      2    public class VectorUtil{
      3          public static float[] getCrossProduct           //求两个向量叉积的方法
      4        (float x1,float y1,float z1,float x2,float y2,float z2){
      5               //求出两个矢量的叉积矢量在xyz轴上的分量A、B、C
      6            float A=y1*z2-y2*z1;  float B=z1*x2-z2*x1;  float C=x1*y2-x2*y1;
      7             return new float[]{A,B,C};                   //返回结果向量
      8          }
      9          public static float[] vectorNormal(float[] vector){      //向量规格化的方法
      10              float module=(float)Math.sqrt(vector[0]*vector[0]  //求向量的模
      11                           +vector[1]*vector[1]+vector[2]*vector[2]);
      12              return new float[]{vector[0]/module,vector[1]/module,vector[2]/module};
      13         }
      14         public static float mould(float[] vec){                  //求向量模的方法
      15              return(float)Math.sqrt(vec[0]*vec[0]+vec[1]*vec[1]+vec[2]*vec[2]);
      16         }
      17   }

说明

上述VectorUtil类中主要包含了计算向量的模、向量规格化及计算两个向量叉积的方法。如果读者对于向量相关的数学不是很熟悉,请查阅其他相关的技术资料。

(2)接着介绍的是实现将高度域灰度图转换为法向量纹理图核心算法的方法process,其代码如下。

代码位置:见随书光盘中源代码/第3章/高度域灰度图转换为法向量纹理图的工具目录下的NormalMapUtil.java。

      1    public Image process(){
      2          int width=ii.getImage().getWidth(null);//获取待处理图像的宽度与高度
      3          int height=ii.getImage().getHeight(null);
      4          //创建两个BufferedImage对象分别用来放置待处理图像与处理后的图像
      5          BufferedImage sourceBuf=new BufferedImage(width,height,BufferedImage.TYPE_
                  INT_ARGB);
      6          BufferedImage targetBuf=new BufferedImage(width,height,BufferedImage.TYPE_
                  INT_RGB);
      7          Graphics graph=sourceBuf.getGraphics();
                                            //将待处理图像绘制加载到源BufferedImage对象中
      8          graph.drawImage(ii.getImage(),0,0,Color.white,null);
      9          for(int i=0;i<height;i++){//对待处理图中像素的行循环
      10           for(int j=0;j<width;j++){//对待处理图中像素的列循环
      11              int color=sourceBuf.getRGB(j,i);//获取指定位置处的像素
      12              //拆分出RGB 3个色彩通道的值
      13              int r=(color >> 16)& 0xff;int g=(color >> 8)& 0xff;int b=(color)& 0xff;
      14              float c=(r+g+b)/3.0f/255.0f;   //求出折算后此像素的高度
      15              if(i==0||j==width-1){           //若为最左侧一列或最上面一行的像素不用计算
      16              targetBuf.setRGB(j,i,0xFF8080FF);
      17              continue;
      18           }
      19           //取出正上方像素的值并折算成高度
      20           int colorUp=sourceBuf.getRGB(j,i-1);
      21           int rUp=(colorUp >> 16)& 0xff; int gUp=(colorUp >> 8)& 0xff; int
                      bUp=(colorUp)& 0xff;
      22           float cUp=(rUp+gUp+bUp)/3.0f/255.0f;
      23           //取出正右侧像素的值并折算成高度
      24           int colorRight=sourceBuf.getRGB(j+1,i);
      25              int rRight=(colorRight>>16)&0xff;int gRight=(colorRight>>8)&0xff;
      26           int bRight=(colorRight)& 0xff;
      27           float cRight=(rRight+gRight+bRight)/3.0f/255.0f;
      28           //计算出两个差分向量
      29           float[] vec1={1,0,cUp-c}; float[] vec2={0,1,cRight-c};
      30           float[] vResult=VectorUtil.getCrossProduct( //将差分向量叉积得到结果向量
      31              vec1[0],vec1[1],vec1[2]*4,vec2[0],vec2[1],vec2[2]*4);
      32           vResult=VectorUtil.vectorNormal(vResult);
      33           //将结果向量各分量值折算到0~255内
      34           int cResultRed=(int)(vResult[0]*128)+128;
      35           int cResultGreen=(int)(vResult[1]*128)+128;
      36           int cResultBlue=(int)(vResult[2]*128)+128;
      37           cResultRed=(cResultRed>255)?255:cResultRed;
      38           cResultGreen=(cResultGreen>255)?255:cResultGreen;
      39           cResultBlue=(cResultBlue>255)?255:cResultBlue;
      40           //将结果向量送入像素
      41           int cResult=0xFF000000;
      42           cResult+=cResultRed<<16; cResult+=cResultGreen<<8; cResult+=cResultBlue;
      43           targetBuf.setRGB(j,i,cResult);
      44          }
      45       }
      46       return targetBuf;                                  //返回结果
      47   }

提示

上述代码实现了前面介绍的将高度域灰度图转换成法向量纹理图的算法,具体情况在注释中有详细的说明。

了解了转换工具的核心代码后,下面简单介绍一下此工具的使用,运行工具中的主类NormalMapUtil,将出现如图3-13所示的界面,要求用户选择一幅高度域灰度图。选择完毕单击打开按钮后,将出现如图3-14所示的结果界面,同时程序也将结果法向量纹理图存入磁盘下指定的resultnt.jpg文件中。

3.5.3 案例的开发

介绍完了凹凸映射的基本原理以及法向量纹理图的生成后,就可以正式进行本案例的开发了。本案例主要是基于前面介绍模型加载的章节中最后一个案例(加载的模型中带有纹理坐标的)升级而成的,因此,这里仅介绍代码改动较大且有代表性的部分,具体内容如下所列。

▲图3-13 文件选择界面

▲图3-14 转换结果界面

(1)前面已经介绍过,凹凸映射计算中进行变换时需要用到切向量,其是由Java程序传入渲染管线的,因此在加载模型的工具类LoadUtil中,首先需要增加计算切向量的工具方法fromNormalToTangent,其代码如下。

代码位置:见随书光盘中源代码/第3章/Sample3_5/com/bn/Sample3_5目录下的LoadUtil.java。

      1     public static float[] fromNormalToTangent(float[] normal,float[] position){
      2          //取出求切面点法式平面方程需要的参数
      3          float A=normal[0];float B=normal[1];float C=normal[2];
      4          float x0=position[0];float y0=position[1];float z0=position[2];
      5          float[] resultY=null;                   //声明用于存放第一个切向量各分量值的数组
      6          //求出切平面上离position位置xz分量各差1个单位的点的坐标
      7          float x1=x0+1; float z1=z0+1; float y1=(C*(z0-z1)+A*(x0-x1))/B+y0;
      8          resultY=new float[]{x1-x0,y1-y0,z1-z0};//求出第一个切向量
      9          float[] resultZ=null;                   //声明用于存放第二个切向量各分量值的数组
      10              //求出切平面上离position位置xy分量各差1个单位的点的坐标
      11              x1=x0+1;y1=y0+1; z1=(A*(x0-x1)+B*(y0-y1))/C+z0;
      12              resultZ=new float[]{x1-x0,y1-y0,z1-z0};//求出第二个切向量
      13              float[] resultX=null;              //声明用于存放第三个切向量各分量值的数组
      14              //求出切平面上离position位置yz分量各差1个单位的点的坐标
      15              y1=y0+1; z1=z0+1; x1=(B*(y0-y1)+C*(z0-z1))/A+x0;
      16              resultX=new float[]{x1-x0,y1-y0,z1-z0}; //求出第三个切向量
      17              if(resultX[0]<50){return resultX;} //若第一个切向量在指定范围内则取第一个
      18              else if(resultY[1]<50){return resultY;}//若第二个切向量在指定范围内则取第二个
      19              else{return resultZ;}              //否则取第三个
      20         }

说明

切向量与法向量不同,平滑物体表面上的某点仅有一个法向量,但有无数个切向量,这些切向量都位于切平面中。本方法首先通过传入的点坐标位置及法向量的3个分量得到点法式平面方程需要的各个参数,然后根据点法式方程求得3个不同方向的切向量,并取3个向量中值良好的作为选中的切向量返回。点法式是空间解析几何中的一种平面表示方法,具体的情况请读者参阅其他相关资料。

(2)完成了fromNormalToTangent工具方法的开发后,就可在LoadUtil类中执行加载任务的loadFromFile方法中增加计算切向量的相关代码,具体内容如下。

代码位置:见随书光盘中源代码/第3章/Sample3_5/com/bn/Sample3_5目录下的LoadUtil.java。

      1    public static LoadedObjectVertexNormalTexture loadFromFile
      2  (String fname, Resources r,MySurfaceView mv){
      3        ……//此处省略了很多与原来案例相同的代码,需要的读者请参考随书光盘
      4        float[] tnXYZ=new float[nXYZ.length];         //创建存放顶点切向量数据的数组
      5        int kc=nXYZ.length/3;                         //顶点数量
      6        for(int i=0;i<kc;i++){                       //对每个顶点循环,计算切向量
      7          float[] normal=new float[]{nXYZ[i*3],nXYZ[i*3+1],nXYZ[i*3+2]}; //获取顶点法向量
      8          float[] position=new float[]{vXYZ[i*3],vXYZ[i*3+1],vXYZ[i*3+2]}; //获取顶点坐标
      9          float[] tangent=fromNormalToTangent(normal,position);
                                                                //通过顶点坐标与法向量计算切向量
      10              //将切向量存入存放顶点切向量数据的数组
      11              tnXYZ[i*3]=tangent[0];tnXYZ[i*3+1]=tangent[1]; tnXYZ[i*3+2]=tangent[2];
      12              }
      13              ……//此处省略了一些与原来案例相同的代码,需要的读者请参考随书光盘
      14              lo=new LoadedObjectVertexNormalTexture(mv,vXYZ,nXYZ,tST,tnXYZ);
                                                                    //创建加载的物体对象
      15              ……//此处省略了一些与原来案例相同的代码,需要的读者请参考随书光盘
      16              return lo;                                  //返回加载后的对象
      17         }

说明

上述方法中主要增加了对每个顶点进行循环,通过顶点的位置坐标及法向量数据计算出切向量数据的代码。最后通过顶点位置数据、顶点法向量数据、顶点切向量数据创建了加载的物体对象,并将加载后物体对象的引用返回。

(3)从上述代码的改动中可以看出,LoadedObjectVertexNormalTexture类的构造器增加了用于接收顶点切向量数据的参数,改动后LoadedObjectVertexNormalTexture类的代码如下。

代码位置:见随书光盘中源代码/第3章/Sample3_5/com/bn/Sample3_5目录下的LoadedObject VertexNormalTexture.java。

      1    package com.bn.gles20.ex42;
      2    ……//省略了一些import语句
      3    public class LoadedObjectVertexNormalTexture{
      4    ……//省略了很多与前面案例相同的成员变量声明,需要的读者请参考随书光盘
      5     int maTangentHandle;                      //顶点切向量属性引用
      6     FloatBuffer   mTangentBuffer;             //顶点切向量数据缓冲
      7     int uTexHandle;                           //外观纹理一致变量引用
      8     int uNormalTexHandle;                     //法向量纹理一致变量引用
      9        public LoadedObjectVertexNormalTexture(
      10         MySurfaceView mv,float[] vertices,float[] normals,float[] texCoors,float[]
                      tangent){
      11         initVertexData(vertices,normals,texCoors,tangent); //初始化顶点坐标与着色数据
      12         initShader(mv);                                       //初始化shader
      13       }
      14       public void initVertexData(float[] vertices, //初始化顶点坐标与着色数据的方法
      15       float[] normals,float texCoors[],float[] tangent){
      16          ……//省略了很多与前面案例相同的成员变量声明,需要的读者请参考随书光盘
      17          //将顶点切向量送入缓冲
      18          ByteBuffer tnbb = ByteBuffer.allocateDirect(tangent.length*4);
      19          tnbb.order(ByteOrder.nativeOrder());      //设置字节顺序
      20          mTangentBuffer = tnbb.asFloatBuffer();    //转换为Float型缓冲
      21          mTangentBuffer.put(tangent);               //向缓冲区中放入顶点切向量数据
      22          mTangentBuffer.position(0);                //设置缓冲区起始位置
      23       }
      24       public void intShader(MySurfaceView mv){    //初始化着色器的方法
      25          ……//省略了很多与前面案例相同的成员变量声明,需要的读者请参考随书光盘
      26           //获取顶点切向量属性引用
      27          maTangentHandle= GLES20.glGetAttribLocation(mProgram, "tNormal");
      28          //获取外观、法线两个纹理一致变量引用
      29          uTexHandle=GLES20.glGetUniformLocation(mProgram, "sTextureWg");
      30          uNormalTexHandle=GLES20.glGetUniformLocation(mProgram,"sTextureNormal");
      31       }
      32       public void drawSelf(int texId,int texIdNormal){          //绘制物体的方法
      33            ……//省略了很多与前面案例相同的成员变量声明,需要的读者请参考随书光盘
      34           GLES20.glVertexAttribPointer(maTangentHandle,3, //将顶点切向量数据送入渲染管线
      35           GLES20.GL_FLOAT,false,3*4, mTangentBuffer);
      36           GLES20.glEnableVertexAttribArray(maPositionHandle);   //启用顶点位置数据
      37           GLES20.glEnableVertexAttribArray(maNormalHandle);    //启用顶点法向量数据
      38           GLES20.glEnableVertexAttribArray(maTangentHandle);    //启用顶点切向量数据
      39           GLES20.glEnableVertexAttribArray(maTexCoorHandle); //启用顶点纹理坐标数据
      40           GLES20.glActiveTexture(GLES20.GL_TEXTURE0);       //启用0号纹理
      41           GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);    //绑定外观纹理
      42           GLES20.glActiveTexture(GLES20.GL_TEXTURE1);            //启用1号纹理
      43           GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texIdNormal); //绑定法向量纹理
      44           GLES20.glUniform1i(uTexHandle, 0);                 //通过引用指定外观纹理
      45           GLES20.glUniform1i(uNormalTexHandle, 1);          //通过引用指定法向量纹理
      46           GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vCount);  //绘制加载的物体
      47       }
      48   }

说明

从上述代码中可以看出,主要是增加了将顶点切向量及法向量纹理送入渲染管线供着色器使用的代码,其他部分与前面章节中加载模型的案例基本相同。

(4)完成了LoadedObjectVertexNormalTexture类的修改后,Java代码的修改就基本完成了。下面需要修改的就是着色器的代码了,首先是顶点着色器,其代码如下。

代码位置:见随书光盘中源代码/第3章/Sample3_5/assets目录下的vertex_ut.sh。

      1    uniform mat4 uMVPMatrix;        //总变换矩阵
      2    attribute vec3 aPosition;       //顶点位置
      3    attribute vec2 aTexCoor;        //顶点纹理坐标
      4    attribute vec3 aNormal;         //法向量
      5    attribute vec3 tNormal;         //切向量
      6    varying vec2 vTextureCoord;    //用于传递给片元着色器的纹理坐标
      7    varying vec3 fNormal;           //用于传递给片元着色器的法向量
      8    varying vec3 ftNormal;          //用于传递给片元着色器的切向量
      9    varying vec3 vPosition;         //用于传递给片元着色器的顶点位置
      10        void main(){
      11          gl_Position = uMVPMatrix * vec4(aPosition,1); //根据总变换矩阵计算此次绘制此
                                                                //顶点的位置
      12          vTextureCoord=aTexCoor;                    //将顶点的纹理坐标传给片元着色器
      13          fNormal=aNormal;                           //将顶点的法向量传给片元着色器
      14          ftNormal=tNormal;                          //将顶点的切向量传给片元着色器
      15          vPosition=aPosition;                       //将顶点的位置传给片元着色器
      16        }

说明

上述顶点着色器中首先增加了从管线接收切向量属性变量值的代码,然后增加了将切向量值从顶点着色器传递给片元着色器的代码。

(5)完成了顶点着色器的修改后,就可以修改片元着色器的代码了,其代码如下。

代码位置:见随书光盘中源代码/第3章/Sample3_5/assets目录下的frag_ut.sh。

      1    precision mediump float;             //给出默认浮点精度
      2    varying vec2 vTextureCoord;          //接收从顶点着色器传递过来的纹理坐标
      3    varying vec3 fNormal;                //接收从顶点着色器传递过来的法向量
      4    varying vec3 ftNormal;               //接收从顶点着色器传递过来的切向量
      5    varying vec3 vPosition;              //接收从顶点着色器传递过来的顶点位置
      6    uniform sampler2D sTextureWg;        //纹理内容数据(外观)
      7    uniform sampler2D sTextureNormal;    //纹理内容数据(法线)
      8    uniform mat4 uMMatrix;               //变换矩阵
      9    uniform vec3 uCamera;                //摄像机位置
      10   uniform vec3 uLightLocationSun;      //光源位置
      11   void pointLight(                   //定位光光照计算的方法
      12     in vec3 normal,                     //扰动后法向量
      13     out vec4 ambient,                   //最终环境光强度
      14     out vec4 diffuse,                   //最终散射光强度
      15     out vec4 specular,                  //最终镜面光强度
      16     in vec3 vp,                         /变换到标准法向量所属坐标系的表面点到光源位置的向量
      17     in vec3 eye,                        //变换到标准法向量所属坐标系的视线向量
      18     in vec4 lightAmbient,              //环境光强度
      19     in vec4 lightDiffuse,              //散射光强度
      20     in vec4 lightSpecular              //镜面光强度
      21 ){
      22     ambient=lightAmbient;                      //直接得出环境光的最终强度
      23     vec3 halfVector=normalize(vp+eye);         //求视线与光线的半向量
      24     float shininess=50.0;                      //粗糙度,越小越光滑
      25     float nDotViewPosition=max(0.0,dot(normal,vp)); //求法向量与vp的点积与0的最大值
      26     diffuse=lightDiffuse*nDotViewPosition;         //计算散射光的最终强度
      27     float nDotViewHalfVector=dot(normal,halfVector);   //法向量与半向量的点积
      28     float powerFactor=max(0.0,pow(nDotViewHalfVector,shininess)); //镜面反射光强度因子
      29     specular=lightSpecular*powerFactor;        //计算镜面光的最终强度
      30   }
      31   void main(){
      32      vec4 ambient,diffuse,specular;            //用来接收光3个通道最终强度的变量
      33      vec4 normalColor = texture2D(sTextureNormal, vTextureCoord); //从法线纹理图中读出值
      34      //将值恢复到-1~+1
      35      vec3 cNormal=vec3(2.0*(normalColor.r-0.5),2.0*(normalColor.g-0.5),2.0*
                (normalColor.b-0.5));
      36      cNormal=normalize(cNormal);   //将扰动结果向量规格化
      37      //计算变换后的法向量
      38      vec3 normalTarget=vPosition+fNormal;
      39      vec3 newNormal=(uMMatrix*vec4(normalTarget,1)).xyz-(uMMatrix*vec4(vPosition,
                    1)).xyz;
      40      newNormal=normalize(newNormal);
      41      //计算变换后的切向量
      42      vec3 tangentTarget=vPosition+ftNormal;
      43     vec3 newTangent=(uMMatrix*vec4(tangentTarget,1)).xyz-(uMMatrix*vec4(vPosition,
                    1)).xyz;
      44      newTangent=normalize(newTangent);
      45      vec3 binormal=normalize(cross(newTangent,newNormal));  //计算副法向量
      46      //用切向量、副法向量、法向量搭建变换矩阵,此矩阵用于将向量从实际坐标系
      47      //变换到标准法向量所属的坐标系
      48      mat3 rotation=mat3(newTangent,binormal,newNormal);
      49      vec3 newPosition=(uMMatrix*vec4(vPosition,1)).xyz;     //变换后的片元位置
      50      vec3 vp= normalize(uLightLocationSun-newPosition);//求表面点到光源位置的向量vp并规格
      化
      51      vp=normalize(rotation*vp);                              //变换并规格化vp向量
      52      //求出从表面点到摄像机的视线向量然后进行变换并规格化
      53      vec3 eye= normalize(rotation*normalize(uCamera-newPosition));
      54      pointLight(cNormal,ambient,diffuse,specular,vp,eye,
      55         vec4(0.05,0.05,0.05,1.0),vec4(1.0,1.0,1.0,1.0),vec4(0.3,0.3,0.3,1.0));
      56      vec4 finalColor=texture2D(sTextureWg, vTextureCoord);//根据纹理坐标采样出片元颜色值
      57      //综合3个通道光的最终强度及片元的颜色计算出最终片元的颜色并传递给渲染管线
      58      gl_FragColor = finalColor*ambient+finalColor*specular+finalColor*diffuse;
      59   }

● 第11-30行的pointLight方法有了一些变化,接收的参数增加了视线向量与表面点到光源位置的向量。随着参数的变化,计算也简单了一些,方法中不再需要计算视线向量与表面点到光源位置的向量了,总体计算思路不变。

● 第31-59行的main方法除了完成原来的任务外,还增加了从法向量纹理图中读出扰动后法向量,并将视线向量、表面点到光源位置的向量变换到标准法向量所属坐标系的代码。

说明

总的来说,上述片元着色器代码实现了3.5.1小节中介绍的凹凸映射算法。另外,上述着色器具有一定的通用性,读者需要开发出凹凸映射效果的场景时可以直接使用。

到此为止如何通过凹凸映射技术渲染表面具有细节结构的物体就介绍完了,最后需要提醒读者注意的是:只有在应用光照的场合凹凸映射才能起作用,因为其本质上并没有实现表面的细节几何结构。