【笔记】《WebGL编程指南》学习(6)

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,否则会导致反射光的颜色过暗或过亮
  • 这里算出的光线方向,实际上是入射方向的反方向,即从入射点指向光源方向
  • 这里用到了表面的法线方向,但是如何获取表面的法线方向?

法线:表面的朝向

一个表面具有两个法向量,“正面”的和“背面”的

平面的法向量唯一

【笔记】《WebGL编程指南》学习(6)_第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中的顶点个数不同

通过索引方式访问顶点数据可以控制内存的开销;代价是需要通过索引间接地访问顶点,在某种程度上是程序复杂化了

新的问题:能否让立方体的每个面显示相同的颜色?

  • 把归属不同面的顶点拆分开,或者说,创建多个具有相同坐标的顶点,不同的拷贝对应不同的颜色
  • 依然采用索引的方式,只更新了顶点数组和索引数组

【笔记】《WebGL编程指南》学习(6)_第2张图片

正传:加上方向光
  • 在上面代码的基础上,修改顶点着色器,利用前述方程计算逐点的颜色;同时增加了法向量属性;
  • 对应在外部,增加法向量数组,以及传入着色器的相关代码
// 顶点着色器
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');
...

【笔记】《WebGL编程指南》学习(6)_第3张图片

6.3 环境光下的漫反射

  • 环境光反射产生的颜色只取决于光的颜色和表面基底色
  • 最终表面反射光颜色为漫反射光颜色+环境反射光颜色

小修

  • 着色器里加上环境光颜色属性,修改反射光计算公式
  • JavaScript处理环境光的传入

【笔记】《WebGL编程指南》学习(6)_第4张图片

6.4 运动物体的光照

物体旋转时,表面的法向量可能会变化

  • 平移变换不会改变法向量
  • 旋转变换会改变法向量
  • 缩放变换对法向量的影响较为复杂:如果缩放比例在所有轴上都一致的话,法向量不会变(各向同);即使不一致,也不一定会变化(大多数情况会变化)

怎么求旋转后物体表面法向量?

变换之前的法向量乘以模型矩阵的逆转置矩阵(inverse transpose matrix)

6.5 点光源

点光源发射的光线,在不同位置上方向不同。因此,需要在每个入射点计算点光源在该处的方向

点光源的方向不再是常量,而要根据每个顶点的位置逐一计算,着色器需要知道点光源光自身的所在位置,而不是光的方向

  • 顶点着色器:需要计算光线方向

计算光线在顶点处的方向

  1. 首先使用模型矩阵变换顶点坐标,得到顶点在世界坐标系中的坐标,以便计算点光源在顶点处的方向
  2. 顶点处的光线方向是由点光源坐标减去顶点坐标得到的矢量,然后归一化
  3. 然后计算光线方向矢量与顶点表面法向量的点积,最终算出每个顶点的颜色
// 修改着色器
...
uniform mat4 u_ModelMatrix; // 模型矩阵
uniform vec3 u_LightPosition; // 光源位置
...
  // 计算顶点的世界坐标
	vec4 vextexPosition = u_ModelMatrix * a_Position;
	// 计算光线方向并归一化
	vec3 lightDirection = normalize(u_LightPosition - vec3(vertexPosition));
...

这是逐顶点的点光源光照模型Blinn-Phone光照模型

点光源照射到表面,产生的效果(每个片元获得的颜色)与简单使用4个顶点颜色内插出的效果并不完全相同

【笔记】《WebGL编程指南》学习(6)_第5张图片

逐顶点的渲染效果

更逼真:逐片元光照

把顶点着色器中计算逐点光源位置还有方向的事儿交给片元着色器

  • 利用varying变量,把顶点位置还有法向变成varying变量交给片段着色器
  • 计算光源位置和方向放到片元着色器

根据varying变量的特性,每处理一个片元,就会计算一次光照

【笔记】《WebGL编程指南》学习(6)_第6张图片

逐片元的渲染效果

你可能感兴趣的:(计算机图形学,图形渲染,算法,虚拟现实)