WebGL编程指南学习(6)
光照使场景变得逼真~
计算机图形学中着色(shading)的真正含义就是,根据光照条件重建“物体各表面明暗不一的效果”的过程
两个物理上的事情需要考虑:
物体反射光的方向、反射光的颜色,取决于两个因素:入射光(入射光的方向和颜色)和物体表面的类型(固有颜色和反射特性)
物体表面反射类型有两种:漫反射和环境反射
< 漫 反 射 光 颜 色 > = < 入 射 光 颜 色 > × < 表 面 基 底 色 > × cos θ <漫反射光颜色>=<入射光颜色>\times<表面基底色>\times \cos\theta <漫反射光颜色>=<入射光颜色>×<表面基底色>×cosθ
< 环 境 反 射 光 颜 色 > = < 入 射 光 颜 色 > × < 表 面 基 底 色 > <环境反射光颜色>=<入射光颜色>\times<表面基底色> <环境反射光颜色>=<入射光颜色>×<表面基底色>
两种反射同时存在时,将两者加起来,得到最终被观察到的颜色。
注意:不一定要按照上述公式计算!可以随便改!
cos θ = < 光 线 方 向 > ⋅ < 法 线 方 向 > \cos\theta = <光线方向>\cdot<法线方向> cosθ=<光线方向>⋅<法线方向>
一个表面具有两个法向量,“正面”的和“背面”的
平面的法向量唯一
如图所示,每个顶点对应3个法向量,就像之前每个顶点对应3个颜色值一样。
立方体比较特殊,各表面垂直相交,所以每个顶点对应3个法向量(同时在缓冲区被拆成3个顶点);但是,一些表面光滑的物体,通常其每个顶点只对应1个法向量
对一个立方体来说,一共8个顶点,但是使用gl.TRIANGLES
的话,需要画12个三角形,也就是如要定义36个顶点;使用gl.TRIANGLE_FAN
的话,需要画6个面,每个面4个顶点,一共要定义24个顶点
WebGL提供了一种方法:gl.drawElement()
替代gl.drawArrays()
,能够避免重复定义顶点,保持顶点数量最小,具体方法如下:
gl.drawElements(mode, count, type, offset){
// mode: 指定绘制的方式,可以接受gl.Point, gl.LINES, gl.LINE_STRIP, gl.LINE_LOOP, gl.TRIANGLES, gl.TRIANGLE_STRIP, 或gl.TRIANGLE_FAN
// count: 指定绘制顶点的个数(整型数)
// type: 指定索引值数据类型, gl.UNSIGNED_BYTE 或 gl.UNSIGNED_SHORT
// offset: 指定索引数组中开始绘制的位置,以字节为单位
}
改变:顶点索引缓冲区gl.ELEMENT_ARRAY_BUFFER
...
gl.drawElements(gl.TRIANGLES, n, gl.UNSINGED_BYTE, 0);
...
var indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // front
0, 3, 4, 0, 4, 5, // right
0, 5, 6, 0, 6, 1, // up
1, 6, 7, 1, 7, 2, // left
7, 4, 3, 7, 3, 2, // down
4, 7, 6, 4, 6, 5, // back
]);
...
var indexBuffer = gl.createBuffer();
...
// 把索引写入缓冲对象
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
...
return indices.length; // drawElement需要知道索引数组的长度
注意
gl.drawElement()
第二个参数n表示顶点索引数组的长度,也就是顶点着色器的执行次数,和gl.ARRAY_BUFFER
中的顶点个数不同通过索引方式访问顶点数据可以控制内存的开销;代价是需要通过索引间接地访问顶点,在某种程度上是程序复杂化了
新的问题:能否让立方体的每个面显示相同的颜色?
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'attribute vec4 a_Normal;\n' +
'uniform mat4 u_MvpMatrix;\n' +
'uniform vec3 u_LightColor;\n' +
'uniform vec3 u_LightDirection;\n' +
'varying vec4 v_Color;\n' +
'void main(){\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
' vec3 normal = normalize(a_Normal.xyz);\n' +
' float nDotL = max(dot(u_LightDirection, normal), 0.0);\n' +
' vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n' +
' v_Color = vec4(diffuse, a_Color.a);\n' +
'}\n';
// 主函数里为着色器传递新增的uniform变量值
...
var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
var u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor');
var u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');
...
小修
物体旋转时,表面的法向量可能会变化
怎么求旋转后物体表面法向量?
变换之前的法向量乘以模型矩阵的逆转置矩阵(inverse transpose matrix)
点光源发射的光线,在不同位置上方向不同。因此,需要在每个入射点计算点光源在该处的方向
点光源的方向不再是常量,而要根据每个顶点的位置逐一计算,着色器需要知道点光源光自身的所在位置,而不是光的方向
// 修改着色器
...
uniform mat4 u_ModelMatrix; // 模型矩阵
uniform vec3 u_LightPosition; // 光源位置
...
// 计算顶点的世界坐标
vec4 vextexPosition = u_ModelMatrix * a_Position;
// 计算光线方向并归一化
vec3 lightDirection = normalize(u_LightPosition - vec3(vertexPosition));
...
这是逐顶点的点光源光照模型Blinn-Phone光照模型
点光源照射到表面,产生的效果(每个片元获得的颜色)与简单使用4个顶点颜色内插出的效果并不完全相同
逐顶点的渲染效果
把顶点着色器中计算逐点光源位置还有方向的事儿交给片元着色器
根据varying变量的特性,每处理一个片元,就会计算一次光照
逐片元的渲染效果