第10章 阴 影(1)


光照可以产生丰富而立体的渲染效果。通过冯氏光照模型渲染对象,看起来效果完美,但如果绘制多个对象,就很容易发现这种光照模型的缺陷——对象之间相互没有遮挡,即对象A在对象B和光源中间时,对象A不会在对象B的表面产生阴影。这和现实情况不符。
本章将介绍如何通过技术手段来实现光照的阴影。
为了实现阴影,需要使用几项实用的技术,包括:切换着色器、渲染到纹理、阴影贴图。

提示:
切换着色器和渲染到纹理是实现阴影效果常用的技术,但这两项技术并非只是运用于绘制阴影,还可以运用到很多其他地方。因此,在讲解这两项技术时,不会只针对实现阴影来讲解。只有阴影贴图是专门用于绘制阴影的。

10.1 认识切换着色器

到目前为止,所有章的程序中都只使用到一个着色器。在实际的程序中,一个着色器并不能适用于绘制场景中的所有对象。比如,在同一个场景中,有的绘制对象有贴图,有的绘制对象是纯颜色的,有的对象响应光照,有的对象不响应光照。
所以在实际的程序中,往往会使用多个不同的着色器来渲染同类型的对象。每个着色器都有其自身的渲染逻辑,可以实现不同的渲染效果。之前已经介绍过如何创建着色器对象,此处不赘述。

提示:
在绘制场景时,通过gl.useProgram函数来选择当前使用的程序对象。

10.2 实例33:切换着色器绘制纯色立方体和贴图立方体

本节将绘制一个纯色的立方体和一个带贴图的立方体,如图10-1所示。因此,需要两套着色器来切换绘制。
图10-1  多个着色器绘制的最终效果

图10-1 多个着色器绘制的最终效果

下面将介绍绘制切换着色器的主要步骤。

1.创建程序对象

(1)准备两套着色器代码(每套代码包括顶点着色器代码和片元着色器代码)。
(2)通过utils.buildProgram函数创建绘制纯色立方体的程序对象。
(3)通过utils.buildProgram函数创建绘制贴图立方体的程序对象。

2.获取程序对象的变量地址

(4)获取程序对象1的变量地址,并存储到程序对象1中。
(5)获取程序对象2的变量地址,并存储到程序对象2中。

3.定义顶点数据,并创建顶点缓冲区对象

(6)创建立方体的顶点数据并创建顶点缓冲区对象。本节中的两个立方体大小一样,所以两个立方体可以共享顶点缓冲区对象。
(7)创建索引缓冲区对象。同样的道理,两个立方体可以共享索引缓冲区对象。

4.创建贴图

(8)加载贴图资源。
(9)创建贴图对象。

5.切换到颜色着色器并绘制纯色立方体

(10)通过gl.useProgram函数切换当前的程序对象为第(2)步创建的程序对象(下面称之为第1个着程序对象)。
(11)启用第1个程序对象的attribute变量,通过缓冲区对象访问数据,并把第(6)步创建的缓冲区对象分配给第1个程序对象的attribute变量。
(12)调用gl.drawElements函数绘制纯色的立方体。

6.切换到贴图着色器并绘制贴图立方体

(13)通过gl.useProgram函数切换当前的程序对象为第(3)步创建的程序对象(下面称之为第2个程序对象)。
(14)启用第2个程序对象的attribute变量,通过缓冲区对象访问数据,并把第(6)步创建的缓冲区对象分配给第2个程序对象的attribute变量.
(15)调用gl.drawElements函数绘制带贴图的立方体。
以上是主要的步骤,下面会详细介绍程序。

10.2.1 实例代码展示

本实例的主文件是MultipleProgram.js。由于需要多个着色器,所以着色器的代码在本节独立了出来,写在了shaders.js文件中,并未写在主文件中。
下面分别介绍MultipleProgram.js文件和shaders.js文件中的代码。

1.shaders.js的代码展示

下面列出了shaders.js文件中的主要代码(省去了部分),完整代码请在本书相关资源中查看。代码后面会分别介绍其中的知识点。
[代码10-1] shaders.js

1)    var colorVS = ` 
……
6)        void main(){
7)            gl_Position = uProjectMatrix * uViewMatrix * uModelMatrix*aPosition ;
8)        }
9)    `
10)    var colorFS = `
……
13)        void main(){
14)            gl_FragColor = uColor;
15)        }
16)    `
17)    
18)    var textureVS = `
……
25)        void main(){
26)            vUv = aUv;
27)            gl_Position = uProjectMatrix * uViewMatrix * uModelMatrix*aPosition ;
28)        }
29)    `
30)    
31)    var textureFS = `
……
34)        uniform sampler2D uTexture;
35)        void main(){
36)            gl_FragColor = texture2D(uTexture, vUv);
37)        }
38)    `

在shaders.js中包含了绘制纯色立方体和贴图立方体的着色器代码。

  • colorVS:绘制纯色立方体的顶点着色器代码。
  • colorFS:绘制纯色立方体的片元着色器代码。
  • textureVS:绘制贴图立方体的顶点着色器代码。
  • textureFS:绘制贴图立方体的片元着色器代码。

在之前的章中,着色器代码的编写方式是使用多行字符串相加的,而本节中着色器代码直接写在两个反引号(`)中。这是JavaScript的新的语言规范,这种方式可以直接定义多行字符串。相比而言,这种方式定义的着色器代码更加方便书写和阅读,只需浏览器支持这种书写规范即可,而最新浏览器基本都支持这种规范。

上面主要介绍了shaders.js中的两套着色器代码,分别用于绘制纯色立方体和贴图立方体。至于着色器代码本身,都是读者已经学习过的内容,所以不赘述。

2.MultipleProgram.js代码展示

下面列出了MultipleProgram.js文件中的主要代码(省去了部分),完整代码请在本书相关资源中查看。此处只列出主要部分,方便读者查阅,代码具体讲解会从10.2.2小节开始。

[代码10-2] MultipleProgram.js

1)    function load(){
……
9)          //创建纯色程序对象
10)         var colorProgram = utils.buildProgram(gl,colorVS,colorFS); 
11)         //创建贴图程序对象  
12)         var textureProgram = utils.buildProgram(gl,textureVS,textureFS); 
13)         //获取纯色程序对象的变量地址 并存储在纯色程序对象上
14)         utils.getProgramVariableLocations(gl,colorProgram,['aPosition','uColor', 'uModelMatrix','uViewMatrix','uProjectMatrix']);
15)         //获取贴图程序对象的变量地址,并存储在贴图程序对象上
16)         utils.getProgramVariableLocations(gl,textureProgram,['aPosition','aUv', 'uTexture','uModelMatrix','uViewMatrix','uProjectMatrix']);
17)      
18)         var verticesUvs = new Float32Array([
19)               //前面的四个顶点
20)                 -10,  10,  10, 0,1,//v0  
……
49)          ]);
50)    
51)          var floatSize = verticesUvs.BYTES_PER_ELEMENT;
52)          var vertexUvBuffer = utils.initVertexBufferObject(gl,verticesUvs);
……
55)         var indices = new Uint8Array([
56)                0,1,2 ,2,1,3, //前表面索引
……
63)          var indexBuffer = gl.createBuffer(); //创建缓冲区
64)          gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,indexBuffer); //绑定缓冲区对象
65)          gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indices,gl.STATIC_DRAW); //给缓冲区填充数据
66)    
67)          function loadTexture(gl,src){
68)              var image = new Image();
69)              image.src = src;
70)              image.onload = function (argument) {
71)                  var texture = gl.createTexture(); //创建贴图对象
72)                  ……
79)              }
80)          }
81)          loadTexture(gl,'./images/fence.png');     //加载贴图资源
……
87)          function draw(){
……
93)                //绘制纯色的立方体
94)                gl.useProgram(colorProgram);            //使用纯色程序对象 
95)                gl.enableVertexAttribArray(colorProgram.aPosition);
96)                 //把缓冲区对象分配给attribute变量  
97)                gl.vertexAttribPointer(colorProgram.aPosition,3,gl.FLOAT,false, floatSize * 5 ,0);
98)    mat4.fromTranslation(modelMatrix,[-10,0,0]);      
99)                gl.uniformMatrix4fv(colorProgram.uModelMatrix,false,modelMatrix);            
……
103)                gl.drawElements(gl.TRIANGLES,indices.length,gl.UNSIGNED_BYTE,0);
104)                
105)                //绘制贴图的立方体
106)                gl.useProgram(textureProgram);   //使用贴图程序对象
107)                gl.enableVertexAttribArray(textureProgram.aPosition);
108)                gl.enableVertexAttribArray(textureProgram.aUv);
109)                //把缓冲区对象分配给attribute变量  
110)                gl.vertexAttribPointer(textureProgram.aPosition,3,gl.FLOAT,false, floatSize * 5 ,0); 
111)                gl.vertexAttribPointer(textureProgram.aUv,2,gl.FLOAT,false,floatSize * 5 ,floatSize * 3); 
112)    mat4.fromTranslation(modelMatrix,[10,0,0]); 
113)                gl.uniformMatrix4fv(textureProgram.uModelMatrix,false,modelMatrix);  
……
116)     gl.uniform1i(textureProgram.uTexture, 1);
117)     gl.drawElements(gl.TRIANGLES,indices.length,gl.UNSIGNED_BYTE,0);
……
125)    }

shaders.js中的代码都是读者已经学习过的内容,不再详细赘述。10.2.2~10.2.7小节将介绍MultipleProgram.js中的主要代码。

10.2.2 创建程序对象

在MultipleProgram.js中,通过shaders.js中的顶点着色器代码和片元着色器代码创建程序对象,具体代码见“代码10-2”中的第10~12行:

10)    var colorProgram = utils.buildProgram(gl,colorVS,colorFS); 
11)    //创建贴图程序对象
12)    var textureProgram = utils.buildProgram(gl,textureVS,textureFS);

通过colorVS和colorFS创建绘制纯色立方体程序对象colorPorgram。通过textureVS和textureFS创建绘制贴图立方体程序对象textureProgram。

10.2.3 获取程序对象变量地址,并存储到程序对象中

在MultipleProgram.js中,接下来需要获取两个程序对象中attribute变量和uniform变量的地址。由于两个程序对象中有相同名称的变量,所以直接定义变量来存储这个地址会有冲突。处理方式是,把着色器变量地址作为程序对象的属性存储下来。
代码中通过一个封装好的函数来实现这个功能,具体代码见“代码10-2”中的第14~16行:

14)utils.getProgramVariableLocations(gl,colorProgram,['aPosition','uColor','uModelMatrix','uViewMatrix','uProjectMatrix']);
15)    //获取贴图程序对象的变量地址,并存储在贴图程序对象上 
16)    utils.getProgramVariableLocations(gl,textureProgram,['aPosition','aUv', 'uTexture','uModelMatrix','uViewMatrix','uProjectMatrix']);

上述代码中,通过函数utils.getProgramVariableLocations可以获取程序对象中的变量地址,并把变量地址作为程序对象的属性值,其中属性名为变量的名称。
utils.getProgramVariableLocations中有3个参数:

  • 第1个参数是WebGL上下文对象gl。
  • 第2个参数是要获取变量地址的程序对象。
  • 第3个参数是要获取地址的变量名称数组。

执行该函数后,colorProgram的对象上会增加aPosition、uColor等属性,这些属性的值就是着色器变量aPosition、uColor等的变量地址,如图10-2所示。
图10-2  附着在着色器上的变量

图10-2 附着在着色器上的变量

textureProgram与colorProgram类似,此处不详述。

10.2.4 定义顶点数据,并创建顶点缓冲区对象

接下来需要定义立方体的顶点数据。为了简单,本实例中的顶点数据只包括顶点坐标和纹理坐标。顶点坐标在所有着色器中都是必需的,而纹理坐标只是在绘制贴图时需要用到。

1.创建顶点缓冲区对象

首先定义顶点的数据,然后通过顶点数据创建顶点缓冲区对象。
定义顶点数据和创建顶点缓冲区对象的代码见“代码10-2”中的18~52行。

18)    var verticesUvs = new Float32Array([
19)          //前面四个顶点
20)          -10,  10,  10, 0,1,//v0  
……
49)    ]);
50)    
51)    var floatSize = verticesUvs.BYTES_PER_ELEMENT;
52)    var vertexUvBuffer = utils.initVertexBufferObject(gl,verticesUvs);

以上代码在之前的章已经讲述,此处不赘述。不同的是,之前章在创建顶点缓冲区对象后便把缓冲区对象分配给attribute变量。如有多个程序对象,则每次在调用gl.useProgram函数切换程序对象后,需要启用该程序对象的attribute变量从缓冲区对象中获取数据,以及给该attribute变量分配缓冲区对象。

下面假设有两个程序对象programA和programB为例说明:
(1)通过gl.useProgram(programA)设置当前的程序对象为programA。
(2)启用程序对象programA的attribute变量从缓冲区对象获取数据,并把缓冲区对象分配给programA的attribute变量。
(3)调用程序对象programA绘制相关对象。
(4)通过gl.useProgram(programB)设置当前的程序对象为programB。
(5)启用程序对象programB的attribute变量从缓冲区对象获取数据,并把缓冲区对象分配给programB的attribute变量。
(6)调用程序对象programB绘制相关对象。
在本节实例中,两个立方体的大小形状相同,所以共享了同一个缓冲区对象。

提示:
如果使用不同的缓冲区对象,程序上也是允许的,但这会造成资源的浪费和性能损失。通用的做法是:相同的模型共享缓冲区对象。

2.创建索引缓冲区对象

创建顶点缓冲区对象后,开始创建索引缓冲区对象。同样的道理,两个立方体共享同一个索引缓冲区对象,具体代码见“代码10-2”中的第55~65行:

55)    var indices = new Uint8Array([
56)          0,1,2 ,2,1,3, //前表面索引值          
……
63)    var indexBuffer = gl.createBuffer();                    //创建缓冲区
64)    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,indexBuffer);     //绑定缓冲区对象
65)    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indices,gl.STATIC_DRAW); //给缓冲区填充数据

10.2.5 加载贴图资源,并创建贴图对象

由于需要绘制带贴图立方体,所以需要提前把贴图准备好,具体代码见“代码10-2”中的第67~81行:

67)    function loadTexture(gl,src){
68)         var image = new Image();
69)         image.src = src;
70)         image.onload = function (argument) {
71)              var texture = gl.createTexture();     //创建贴图对象
……
79)         }
80)    }
81)    loadTexture(gl,'./images/fence.png');       //加载贴图资源

加载贴图资源并创建贴图对象的相关代码之前已经讲述过,此处不赘述。

10.2.6 绘制纯色立方体

接下来便可以开始绘制两个立方体了。
绘制的两个立方体大小、形状相同,为了把它们分开,所以给两个立方体都设置了位置,第1个立方体向x轴负方向平移10个单位,第2个立方体向x轴正方向平移10个单位。在具体代码中,通过模型变换来实现平移。而视图变换和投影变换对两个立方体而言则是相同的。
绘制第1个立方体的代码见“代码10-2”中的第93~103行:

93)    //绘制纯色的立方体
94)    gl.useProgram(colorProgram); //使用纯色程序对象
95)    gl.enableVertexAttribArray(colorProgram.aPosition);
96)    //把缓冲区对象分配给attribute变量  
97)    gl.vertexAttribPointer(colorProgram.aPosition,3,gl.FLOAT,false,floatSize * 5 ,0);
98)    mat4.fromTranslation(modelMatrix,[-10,0,0]);      
99)    gl.uniformMatrix4fv(colorProgram.uModelMatrix,false,modelMatrix);            
……
103)    gl.drawElements(gl.TRIANGLES,indices.length,gl.UNSIGNED_BYTE,0);

代码说明如下:
(1)通过gl.useProgam函数指定当前绘制所使用的的程序对象是colorProgram(见代码第94行)。
(2)启用colorProgram的attribute变量aPosition,并把缓冲区对象分配给attribute变量(见代码第95行)。
(3)创建一个在x轴负方向平移10个单位的模型变换矩阵(见代码第98行)。
(4)把模型变换、视图变换、投影变换矩阵和颜色值(红色)传递给colorProgram相应的uniform变量(见代码第99~102行)。
(5)通过gl.drawElements函数绘制一个红色的立方体(见代码第103行)。

10.2.7 绘制贴图立方体

绘制第2个立方体的代码见“代码10-2”中的第105~117行:

105)    //绘制贴图的立方体
106)    gl.useProgram(textureProgram);   //使用贴图程序对象
107)    gl.enableVertexAttribArray(textureProgram.aPosition);
108)    gl.enableVertexAttribArray(textureProgram.aUv);
109)    //把缓冲区对象分配给attribute变量  
110)                gl.vertexAttribPointer(textureProgram.aPosition,3,gl.FLOAT,false, floatSize * 5 ,0); 
111)    gl.vertexAttribPointer(textureProgram.aUv,2,gl.FLOAT,false,floatSize * 5 ,floatSize * 3); 
112)    mat4.fromTranslation(modelMatrix,[10,0,0]); 
113)    gl.uniformMatrix4fv(textureProgram.uModelMatrix,false,modelMatrix);  
……
116)    gl.uniform1i(textureProgram.uTexture, 1);
117)    gl.drawElements(gl.TRIANGLES,indices.length,gl.UNSIGNED_BYTE,0)

首先通过gl.useProgam函数指定当前绘制所使用的程序对象是textureProgram(见代码第106行)。接下来的过程和绘制第1个立方体的过程几乎一样,差别的地方是:第2个程序多了一个aUv变量,以及在第116行指定了立方体对应的贴图单元。
以上就是切换程序对象主要代码的讲解,最终的绘制效果如图10-1所示。
学会了切换程序对象,读者便可以利用这种技术来绘制有多种不同类型的渲染对象的复杂场景了。

从本实例的讲解中还可以总结出一些重要的结论,这些结论对于读者更好地理解以上程序有很大帮助:
(1)在WebGL系统中可以创建多个程序对象。
(2)每个程序对象可以创建一个或者多个attribute、uniform变量,而这些变量只属于创建它们的程序对象,即程序对象A的变量只能在程序对象A中使用。
(3)顶点缓冲区对象、索引缓冲区对象是全局的,不依赖于程序对象,可以跨越多个程序对象使用。
(4)贴图对象也是全局的,不依赖于程序对象,可以跨越多个程序对象使用。

提示:
切换程序对象不仅可以用于绘制存在多种不同类型的对象的场景,还可以用于多个渲染通道的情况,10.3节要讲的渲染到纹理就属于这种情况。

10.3 渲染到纹理

通过三维渲染技术可以把三维场景绘制到canvas上,还可以把三维场景绘制到一个纹理对象上,把渲染结果作为贴图使用。这相当于是动态地生成图片,而不是从静态的图片创建纹理。

提示:
在把渲染结果作为贴图之前,还可以对渲染结果进行一些后期处理,比如模糊处理等。

因此,通过渲染到纹理技术可以实现一些比较高级的效果,比如阴影贴图、环境贴图等。

  • 阴影贴图可以实现场景的阴影效果,10.4节将介绍阴影贴图。
  • 环境贴图可以实现把周边环境映射到一个对象表面的效果,类似于在对象表面产生镜子一样的效果。

渲染到纹理是一项很重要的技术,下面将介绍实现渲染到纹理的相关技术。

10.3.1 帧缓冲区对象

帧缓冲区对象是实现渲染到纹理的关键。首先来看不使用帧缓冲区对象时,绘制过程中发生了什么。

  • WebGL会在颜色缓冲区中存储渲染的颜色结果。
  • 如果启用了深度缓冲区,则WebGL会把深度信息写入深度缓冲区中,并利用深度缓冲区来计算遮挡关系。
  • 如果启用了模板缓冲功能,还会用到模板缓冲区。模板缓冲区的知识会在后面章介绍。

可以看出,默认的绘制用到了颜色缓冲区、深度缓冲区和模板缓冲区。而帧缓冲区对象可以看成是颜色缓冲区、深度缓冲区、模板缓冲区的集合。事实上,在帧缓冲区对象上可以关联以下三种对象。

  • 颜色关联对象(color attachment):用来替代默认绘制下的颜色缓冲区。
  • 深度关联对象(depth attachment):用来替代默认绘制下的深度缓冲区。
  • 模板关联对象(stencil attachment):用来替代默认绘制下的模板缓冲区。

图10-3展示了帧缓冲区对象和三种关联对象的关系。
图10-3  帧缓冲区对象包含三种关联对象

图10-3 帧缓冲区对象包含三种关联对象

提示:
在没有指定帧缓冲区对象的情况下,可以认为WebGL使用了一个默认的“帧缓冲区对象”,该“帧缓冲区对象”包含了颜色缓冲区、深度缓冲区和模板缓冲区。

10.3.2 渲染缓冲对象

在绑定了帧缓冲区对象之后,WebGL就会向帧缓冲区对象写入数据。

提示:
向帧缓冲区对象写入数据,并不是直接写到帧缓冲区对象上,而是写到它的关联对象上,就像默认情况下写入颜色缓冲区、深度缓冲区和模板缓冲区一样。

帧缓冲区对象的关联对象可以是以下两种类型。

1.纹理对象

纹理对象在之前的章已经介绍过,但是此处需要注意的是:
(1)之前介绍的纹理对象都是作为输出源(即纹理关联图片)而存在的,然后在渲染时,它作为对象表面的贴图。
(2)把纹理对象作为帧缓冲区对象上的颜色关联对象时,WebGL会在该纹理上进行绘图,把场景绘制到纹理对象上,即此时的纹理对象可以认为是一个输入源。另外,在WebGL对该纹理对象完成输出后,该纹理对象又可以作为输出源,作为其他对象的贴图。这也是“渲染到纹理”的基本原理和应用。

2.渲染缓冲对象

渲染缓冲对象(Renderer Object)表示一种通用的绘图区域,可以向其中写入多种不同类型的数据。和纹理对象不同的是,在渲染缓冲对象上绘制的结果不能被获取到。
一般而言,深度关联对象和模板关联对象会使用渲染区缓冲对象。

10.3.3 渲染到纹理

在绑定了帧缓冲区对象后,下一步便可以把场景渲染到纹理。一般而言,要把场景的颜色信息渲染到纹理,所以需要把纹理对象作为帧缓冲区对象的颜色关联对象。然后在帧缓冲区对象区上进行绘制操作,此时场景就会被绘制到纹理上去。为了能够实现深度测试(隐藏面消除)功能,还需要创建一个渲染缓冲对象,作为帧缓冲区对象的深度关联对象。

渲染到纹理的主要步骤如下:
(1)创建并绑定帧缓冲区对象。
(2)创建纹理对象作为帧缓冲区对象的颜色关联对象,创建渲染缓冲对象作为帧缓冲区对象的深度关联对象。
(3)在帧缓冲区对象上绘制需要渲染到纹理的场景。
(4)取消绑定帧缓冲区对象。

top Created with Sketch.