WebGL编程指南学习(6)
6. 光照
光照使场景变得逼真~
6.1 从物理出发
计算机图形学中着色(shading)的真正含义就是,根据光照条件重建“物体各表面明暗不一的效果”的过程
两个物理上的事情需要考虑:
- 发出光线的光源的类型
- 物体表面如何反射光线
光源
- 平行光(directional light),或者叫方向光,类似太阳光;
- 点光源(point light),类似灯泡
- 环境光(ambient light),其他非直射的光
- 其他特殊的光源,如聚光灯(spot light)
反射类型
物体反射光的方向、反射光的颜色,取决于两个因素:入射光(入射光的方向和颜色)和物体表面的类型(固有颜色和反射特性)
物体表面反射类型有两种:漫反射和环境反射
- 漫反射。针对平行光或点光源。物体表面材质粗糙,反射光以任意角度射出。反射光的颜色取决于入射光的颜色、表面的基底色、入射光与表面形成的入射角
< 漫 反 射 光 颜 色 > = < 入 射 光 颜 色 > × < 表 面 基 底 色 > × cos θ <漫反射光颜色>=<入射光颜色>\times<表面基底色>\times \cos\theta <漫反射光颜色>=<入射光颜色>×<表面基底色>×cosθ
- 环境反射。针对环境光。反射光的方向可以认为是入射光的反方向。由于环境光照射物体的方式是各向同的,所以反射光也是各向同的
< 环 境 反 射 光 颜 色 > = < 入 射 光 颜 色 > × < 表 面 基 底 色 > <环境反射光颜色>=<入射光颜色>\times<表面基底色> <环境反射光颜色>=<入射光颜色>×<表面基底色>
两种反射同时存在时,将两者加起来,得到最终被观察到的颜色。
注意:不一定要按照上述公式计算!可以随便改!
6.2 平行光下的漫反射
- 入射光的颜色:RGB值表示
- 表面的基底色:“物体本来的颜色”,其实就是“物体在标准白光下的颜色”
- 入射角
根据光线和表面的方向计算入射角
cos θ = < 光 线 方 向 > ⋅ < 法 线 方 向 > \cos\theta = <光线方向>\cdot<法线方向> cosθ=<光线方向>⋅<法线方向>
- 光线方向矢量和表面法线矢量的长度必须为1,否则会导致反射光的颜色过暗或过亮
- 这里算出的光线方向,实际上是入射方向的反方向,即从入射点指向光源方向
- 这里用到了表面的法线方向,但是如何获取表面的法线方向?
法线:表面的朝向
一个表面具有两个法向量,“正面”的和“背面”的
平面的法向量唯一
如图所示,每个顶点对应3个法向量,就像之前每个顶点对应3个颜色值一样。
立方体比较特殊,各表面垂直相交,所以每个顶点对应3个法向量(同时在缓冲区被拆成3个顶点);但是,一些表面光滑的物体,通常其每个顶点只对应1个法向量
例程:发光的Cube
番外:绘制一个立方体
对一个立方体来说,一共8个顶点,但是使用gl.TRIANGLES
的话,需要画12个三角形,也就是如要定义36个顶点;使用gl.TRIANGLE_FAN
的话,需要画6个面,每个面4个顶点,一共要定义24个顶点
WebGL提供了一种方法:gl.drawElement()
替代gl.drawArrays()
,能够避免重复定义顶点,保持顶点数量最小,具体方法如下:
- 立方体一共8个顶点,用一个顶点列表表示。每个顶点有一个索引值,0~7
- 每3个索引值表示一个三角形,每个表面由两个三角形组成。这样的话,只需要调用不同的索引值即可
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);
- 创建indexBuffer,绑定indexBuffer并向gl.ELEMENT_ARRAY_BUFFER写入数据
...
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');
...
6.3 环境光下的漫反射
- 环境光反射产生的颜色只取决于光的颜色和表面基底色
- 最终表面反射光颜色为漫反射光颜色+环境反射光颜色
小修
- 着色器里加上环境光颜色属性,修改反射光计算公式
- JavaScript处理环境光的传入
6.4 运动物体的光照
物体旋转时,表面的法向量可能会变化
- 平移变换不会改变法向量
- 旋转变换会改变法向量
- 缩放变换对法向量的影响较为复杂:如果缩放比例在所有轴上都一致的话,法向量不会变(各向同);即使不一致,也不一定会变化(大多数情况会变化)
怎么求旋转后物体表面法向量?
变换之前的法向量乘以模型矩阵的逆转置矩阵(inverse transpose matrix)
6.5 点光源
点光源发射的光线,在不同位置上方向不同。因此,需要在每个入射点计算点光源在该处的方向
点光源的方向不再是常量,而要根据每个顶点的位置逐一计算,着色器需要知道点光源光自身的所在位置,而不是光的方向
- 顶点着色器:需要计算光线方向
计算光线在顶点处的方向
- 首先使用模型矩阵变换顶点坐标,得到顶点在世界坐标系中的坐标,以便计算点光源在顶点处的方向
- 顶点处的光线方向是由点光源坐标减去顶点坐标得到的矢量,然后归一化
- 然后计算光线方向矢量与顶点表面法向量的点积,最终算出每个顶点的颜色
// 修改着色器
...
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变量,把顶点位置还有法向变成varying变量交给片段着色器
- 计算光源位置和方向放到片元着色器
根据varying变量的特性,每处理一个片元,就会计算一次光照
逐片元的渲染效果