立方体添加局部贴图(切换着色器程序)

源码下载

  本节课通过立方体添加局部贴图,学习多组顶点、片元着色器切换使用的方法。大家可以回忆一下前面的课程,你可以发现纹理映射的顶点、片元着色器程序和绘制立方体的顶点、片元着色器程序不相同, 而GPU着色器执行的程序又依赖执行Javascript程序的CPU传送,这种情况下实现本节课的目标就需要编写两组着色器程序,一组纹理映射的顶点、片元着色器程序,一组立方体的顶点、片元着色器程序。 着色器程序以字符串的形式存在,经过WebGL API编译处理后,执行useProgram()方法可以实现着色器程序的切换和调用。

整体思路

  前面的课程中每一个WebGL案例程序中,都有着色器程序初始化函数initShader(),执行该函数的作用就是把着色器语句组成的字符串处理编译后传递到着色器处理单元执行。 初始化函数initShader()中有一个useProgram()方法,这个方法的作用就像绘制方法drawArrays()一样起到发令枪的作用,drawArrays()执行WebGL图形系统就会处理顶点数据生成像素, 一组着色器程序对应的程序对象program作为参数执行useProgram()方法后,GPU的程序执行单元着色器就会执行useProgram()方法指定的着色器程序,当你再次使用新的程序对象program作为参数执行useProgram()方法时候, GPU着色器就会切换到新的程序对象program对应的着色器程序。

  本课案例中的程序基本没有新的知识点,主要要注意的是一些命令使用的先后顺序,比如没有执行useProgram()方法把相关着色器程序传递给GPU的时候, 你就不能执行vertexAttribPointer()方法把缓冲区中的顶点数据关联到着色器中的顶点变量,不能执行uniformMatrix4fv()方法把旋转矩阵传递给着色器,这很好理解,GPU没有执行相关的着色器程序程序, 自然无法通过WebGL API关联该着色器程序中的变量。

代码复用

  下面定义了一个配置顶点数据的函数,完整的功能就是,创建显卡显存中开辟一个顶点缓冲区,把顶点数据导入顶点缓冲区中,然后把缓冲区中的顶点数据和着色器中顶点关联起来,保证着色器可以获取缓冲区中的顶点数据逐顶点处理。

  在程序中每传递关联一次顶点数据,就需要重复编写下外面的程序,定义这样一个函数的目的就是为了实现代码的复用,每次只需要调用该函数即可。WebGL三维引擎存在的作用就是复用代码,封装底层WebGL API,这也给给我们一个提示, 边学习便去尝试封装WebGL,理解代码的时候从整体上去理解,不去深究每一个API背后的硬件知识,只要你理解了渲染管线这个可编程的自动化流水线,即使不熟悉每一个WebGL API,也可以在别人代码的基础上去更改添加。

198      /**
199       * 顶点数据配置函数vertexBuffer()
200       * 顶点数据data
201       * 顶点位置position
202       * 间隔数量n
203       **/
204      function vertexBuffer(data,position,n){
205          var buffer=gl.createBuffer();
206          gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
207          gl.bufferData(gl.ARRAY_BUFFER,data,gl.STATIC_DRAW);
208          gl.vertexAttribPointer(position,n,gl.FLOAT,false,0,0);
209          gl.enableVertexAttribArray(position);
210      }

  纹理映射的时候调用顶点配置函数vertexBuffer(),注意要先执行gl.useProgram(textureProgram)语句,否则无法执行函数中的vertexAttribPointer()方法。

170          /**调用函数vertexBuffer(),配置顶点数据**/
171          vertexBuffer(data,texturePosition,3);
172          vertexBuffer(textureData,a_TexCoord,2);

  绘制立方体的时候调用顶点配置函数vertexBuffer(),同上,执行useProgram()方法,不过参数是程序对象cubeProgram,该对象已经通过第79行代码调用初始化函数initShader()关联绘制立方体的着色器程序。

183          /**调用函数vertexBuffer(),配置顶点数据**/
184          vertexBuffer(cubeData,cubePosition,3);
185          vertexBuffer(colorData,a_color,3);
186          vertexBuffer(normalData,a_normal,3);

旋转矩阵

  立方体的顶点是一组顶点,纹理贴图映射的顶点是一组顶点,它们是相互独立,处理他们的着色器程序也不是同一个,因为程序实现的效果是就像一个商标粘贴到一个商品物体上,把纹理贴图映射到立方体的一个面上, 准确的是说是纹理贴图映射的顶点z轴坐标值和要粘贴贴图的立方体面的顶点z轴坐标值是相同的,纹理贴图与立方体的顶点没有任何映射关系。

97   /**
98    * 纹理映射、立方体共用的旋转矩阵数据
99    **/
100  var angle = Math.PI/6;//起始角度
101  var sin = Math.sin(angle);//旋转角度正弦值
102  var cos = Math.cos(angle);//旋转角度余弦值
103  /**绕x轴旋转30度**/
104  var mxArr = new Float32Array([1,0,0,0, 0,cos,-sin,0, 0,sin,cos,0, 0,0,0,1]);
105  /**绕y轴旋转30度**/
106  var myArr = new Float32Array([cos,0,-sin,0,  0,1,0,0,  sin,0,cos,0,  0,0,0,1]);

纹理映射顶点

  前面课程中的纹理映射的顶点没有定义z轴坐标值,程序默认都是1,本节课为了实现视觉上好像纹理图片粘贴在一个立方体的表面上,所以z轴的值设置为0.5或-0.5,保证与立方体的某个面在同一平面上, 但是下面数组中的值是-0.51不是-0.5,这一点很好理解,如果深度值z相同,像素就会发生深度冲突,影响显示效果,那个像素在前,哪个在后,WebGL图形系统无法分清,所以增加了0.01实现前后区分,保证纹理映射的顶点在立方体顶点的外面而不是里面。

134      /**纹理映射顶点数据**/
135      var data=new Float32Array([
136          -0.4, 0.2,-0.51,//左上角——v0
137          -0.4,-0.2,-0.51,//左下角——v1
138          0.4,  0.2,-0.51,//右上角——v2
139          0.4, -0.2,-0.51//右下角——v3
140      ]);

WebGL坐标系:视点

  WebGL图形系统默认的观察方向,也就是图形的投影方向是z,物体有前后之分,后面的看不到,这时候就需要知道系统默认的投影方向是沿着z轴的负方向还是正方向,你可以把-0.51更累为0.51,你会发现你看不到纹理贴图了,这也就说出名了投影方向是沿着z轴的正方向。

program对象

  程序对象program的使用一直贯穿整个教程,并没有深入讲解,第一节课就提到了下面的着色器程序初始化函数,下面函数的作用就是实现CPU与GPU的通信,通信接口就是Javascript语言可以控制的WebGL API,也就是gl对象的方法。 通过下面的着色器初始化函数可以实现把着色器程序编译后传递给GPU渲染管线执行,同时会返回一个program对象,该对象包含了着色器程序中的变量,通过该对象和相关的方法可以返回变量的地址,把相关的数据再关联变量地址。 这里要注意下面的着色器初始化函数把前面所有课程中使用的useProgram方法注释掉了,因为该方法执行后会立即配置GPU的着色器单元。这样的话代码中第77行和第79调用该函数会分别返回一个程序对象,但GPU不会执行这两个程序对象关联的着色器程序只是通过attachShader()方法关联了而已, 这时候可以通过getAttribLocation()/getUniformLocation()获得两个程序对象关联的着色器程序中所有变量的地址。每次需要执行drawArrays绘制之前,先执行useProgram()调用相关着色器程序配置初始化GPU渲染管线的着色器单元,然后才可以把缓冲区中的顶点数据关联到着色器变量的地址。 只要领会了程序的执行顺序,一切问题都会迎刃而解,哪怕某个WebGL API是什么意思你也不太清楚,所以阅读代码的时候不要过于去关注每个WebGL方法的使用方式和具体功能,重心中的重心放在整个程序执行的领会上,心中始终要思考渲染管线的运行规律,流水线上的每个功能单元, 比如深度检测单元,需要通过第72行代码gl.enable(gl.DEPTH_TEST);去开启,它的作用是配置GPU渲染管线,开启流水线上深度检测这个工位的检测职能,所以要在drawwArrays执行之前编写这句代码。比如顶点着色器单元执行的是顶点着色器程序,片元着色器单元执行的是片元着色器程序。 CPU是计算机的一把手,GPU是主管图形的二把手,CPU与GPU的通信控制必然影响程序的执行问题,虽然GPU可以独立的执行着色器语言,但是什么时候能执行着色器语言,执行哪一组顶点、片元着色器程序,要通过useProgram方法()指定,着色器程序配置好GPU渲染管线的着色器单元, enable()方法开启渲染管线某个工位的职能后,需要把顶点数据,光的颜色方向数据,顶点变换的矩阵数据全部与着色器硬件单元建立联系,才能够执行绘制方法drawwArrays()命令渲染管线开始逐顶点、逐片元处理数据,最后生成像素的颜色数据存入帧缓存的颜色缓冲区, 每一个像素的深度值存入帧缓存的深度缓冲区。渲染管线就像工业产品生产的流水线一样,深度测试单元就是一个工序,可以通过WebGl API告诉GPU渲染管线是否打开,顶点着色器单元就像一台数控机床可以执行多个工序,而且支持可编程,灵活性更强。 随着时间的发展GPU渲染管线不仅拥有可编程的顶点着色器、片元着色器,还引入了几何着色器、细分着色器。

211  /**
212   着色器程序初始化函数initShader()
213   **/
214  function initShader(gl,vertexShaderSource,fragmentShaderSource){
215     var vertexShader = gl.createShader(gl.VERTEX_SHADER);//创建顶点着色器对象
216     var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);//创建片元着色器对象
217     gl.shaderSource(vertexShader,vertexShaderSource);//引入片元着色器源代码
218     gl.shaderSource(fragmentShader,fragmentShaderSource);//引入片元着色器源代码
219     gl.compileShader(vertexShader);//编译顶点着色器程序
220     gl.compileShader(fragmentShader);//编译片元着色器程序
221     var program = gl.createProgram();//创建程序对象program
222     gl.attachShader(program,vertexShader);//附着顶点着色器对象到程序对象program
223     gl.attachShader(program,fragmentShader);//附着片元着色器对象到程序对象program
224     gl.linkProgram(program);//链接程序对象program
225     //gl.useProgram(program);//使用程序对象program(注释掉useProgram()方法调用)
226     return program;//返回程序program对象
227  }
73   /**
74    * 初始化着色器
75    **/
76   //执行初始化函数initShader(),立方体着色器程序作为参数
77   var textureProgram = initShader(gl,textureVertexShaderSource,textureFragShaderSource);
78   //执行初始化函数initShader(),纹理映射着色器程序作为参数
79   var cubeProgram = initShader(gl,cubeVertexShaderSource,cubeFragShaderSource);
80   /**