【《WebGL编程指南》读书笔记-光照】

本文为《WebGL编程指南》第八章读书笔记
总目录链接:https://blog.csdn.net/floating_heart/article/details/124001572
本文包括以下内容:

  1. 介绍了点光源、平行光的漫反射模型和环境光反射模型
  2. 在着色器中根据光照模型构建公式,实现平行光和点光源的光照效果,JavaScript为着色器提供数据支持
  3. 分别在顶点着色器和片元着色器中计算颜色值,展示逐顶点计算和逐片元计算的不同:逐片元的光照效果更加自然。

第8章 光照

这一章主要讨论光照问题,讨论如何在三维场景中实现不同类型的光照以及其产生的效果。光照使场景更具有层次感,如果你希望建立逼真的三维场景,就应当使用光照。

这一章的主要内容如下:

  • 明暗、阴影、不同类型的光:点光源光、平行光和散射光。
  • 物体表面反射光线的方式:漫反射和环境反射。
  • 编写代码实现光照效果,使三维模型(比如上一章的纯白色立方体)看上去更逼真。

在这一章结束时,你将具有足够多的知识去实现三维场景中的光照效果。

这一章节理论部分几乎每一句话都很重要,此处仅保留其梗概,以供后期参考。


Lighting 3D Objects

相关内容:1. 光照的基本原理,提出着色和阴影,反射光的公式;2. 静止图像的光照效果,计算平行光(漫反射)和环境光(环境反射,在物理上也是漫反射)
GLSL ES函数:normalize()归一化;dot()矢量点积;max()取大值

细节:1.已知的访问vec4类型数据各个分量的索引有xyzw和rgba,可以vec4.x访问单一值,也可以vec4.xyz访问组合值。2. 我们通过GLSL ES中的数学函数计算处颜色,再进行着色,所以是通过旧的功能获得新的效果。

学过物理的都应该知道,我们能看到物体是因为光线照到物体上后反射的光进入了眼睛,一切的视觉渲染从此开始。

在现实世界,光纤照射到物体上时,发生了两个重要现象:

  • 根据光源和光线方向,物体不同表面的明暗程度变得不一致。(光的反射、散射和折射)
  • 根据光源和光纤方向,物体向地面投下了影子。(不知道在WebGL中是否考虑本影和半影)

正是物体的阴影和表面的明暗差异给了我们立体的视觉效果。

在这里插入图片描述

  • 在三维图形学中术语着色(shading)的含义是,根据光照条件重建“物体个表面明暗不一的效果”的过程。GLSL ES就是“着色器”语言,着色器最初被发明出来就是为了重建光照产生的效果。
  • 物体向地面投下影子的现象,被称为阴影(shadowing)。

本章主要讨论前者,在讨论着色过程之前,需要考虑以下两点:

  1. 发出光线的光源的类型。
  2. 物体表面如何反射光线。

光源类型

真实世界中的光主要有两种类型:

  • 平行光(directional light),类似于自然中的太阳光;
  • 点光源光(point light),类似于人造灯泡的光。

此外:

  • 环境光(ambient light),用于模拟真实世界中的非直射光(由光源发出后经过墙壁或其他物体反射后的光)

三维图形学还使用一些其他类型的光,如聚光灯光(spot light)来模拟电筒、车前灯等。本书只讨论前三种基本类型的光,其它光源类型可以参考OpenGL ES 2.0 Programming Guide一书。

在这里插入图片描述

本书讨论如图三种类型的光源。

平行光: 顾名思义,平行光的光线是相互平行的,平行光具有方向。平行光可以看作是无限远处的光源(比如太阳)发出的光。因为太阳距离地球很远,所以阳光到达地球时可以认为是平行的。平行光很简单,可以用一个方向和一个颜色(此处的颜色已经包含强度,如(1,1,1)和(2,2,2)强度差距两倍)来定义

点光源光:点光源光是从一个点向周围的所有方向发出的光。点光源光可以用来表示现实中的灯泡、火焰等。我们需要指定点光源的位置和颜色。光线的方向将根据点光源的位置和被照射之处的位置计算出来,因为点光源的光线的方向在场景内的不同位置是不同的。(本书未进行光源强度的衰减,可参考OpenGL ES 2.0 Programming Guide一书)

环境光: 环境光(间接光)是指那些经光源(点光源或平行光源)发出后,被墙壁等物体多次反射,然后照到物体表面上的光。环境光从各个角度照射物体,其强度都是一致的。比如说,在夜间打开冰箱的门,整个厨房都会有些微微亮,这就是环境光的作用。 环境光不用指定位置和方向,只需要指定颜色即可。(暂时如此认为,不计算多次反射的光)


反射类型

物体反射光的方向和颜色与入射光物体表面类型有关。入射光信息包括入射光方向和颜色,物体表面信息包括表面的固有颜色(基底色)和反射特性。物体表面反射光线有两种:漫反射(diffuse reflection)和环境反射(envirment ambient reflection)。

本节需要考虑如何根据入射光和物体表面类型计算反射光颜色。(此处没有讨论镜面反射。)

  • 漫反射

漫反射是针对平行光或点光源而言的。漫反射的反射光在各个方向上是均匀的。如果物体表面像镜子一样光滑,那么光线就会以特定的角度反射出去(镜面反射);但现实中的大部分材质,比如纸张、岩石、塑料等,其表面都是粗糙的,在这种情况下反射光就会以不固定的角度反射出去。漫反射就是针对后一种情况而建立的理想反射模型。

在这里插入图片描述

在漫反射中,反射光的颜色取决于入射光的颜色、表面的基底色、入射光与表面形成的入射角。我们将人射角定义为入射光与表面的法线形成的夹角,并用θ表示,漫反射光的颜色可以根据下式计算得到:
< 漫 反 射 光 颜 色 > = < 入 射 光 颜 色 > × < 表 面 基 底 色 > × cos ⁡ θ <漫反射光颜色>=<入射光颜色>\times<表面基底色>\times\cos\theta <>=<>×<>×cosθ
式中:

  1. 入射光颜色是指点光源或平行光的颜色
  2. 乘法操作是在颜色矢量上逐分量(R,G,B)进行的,不是矢量积
  3. 因为漫反射光在各个方向上都是“均匀”的,所以从任何角度看上去其强度都相等。
  • 环境反射

环境反射是针对环境光而言的。在环境反射中,反射光的方向可以认为就是入射光的反方向(注意是可以认为)。由于环境光照射物体的方式就是各方向均匀、强度相等的,所以反射光也是各向均匀的。我们可以这样来描述它:
< 环 境 反 射 光 颜 色 > = < 入 射 光 颜 色 > × < 表 面 基 底 色 > < 入 射 光 颜 色 > 就 是 环 境 光 颜 色 <环境反射光颜色>=<入射光颜色>\times<表面基底色>\\ <入射光颜色>就是环境光颜色 <>=<>×<><>
在这里插入图片描述

  • 二者叠加

< 表 面 的 反 射 光 颜 色 > = < 漫 反 射 光 颜 色 > + < 环 境 反 射 光 颜 色 > <表面的反射光颜色>=<漫反射光颜色>+<环境反射光颜色> <>=<>+<>

我们可以使用简单的加法进行二者叠加,但需要注意,两种反射光并不一定总是存在,也不一定要完全按照上述公式来进行计算,我们可以根据项目需要来修改公式,达到想要的效果。

本节准备建立一个示例程序,在合适的位置放置一个光源,对场景进行着色。首先对示例的光照情况进行分析。


平行光下的漫反射
< 漫 反 射 光 颜 色 > = < 入 射 光 颜 色 > × < 表 面 基 底 色 > × cos ⁡ θ <漫反射光颜色>=<入射光颜色>\times<表面基底色>\times\cos\theta <>=<>×<>×cosθ
分析漫反射光,需要前述公式中的三项数据:入射光的颜色、表面基底色和入射角θ。

这几项数据中,入射光的颜色(包括强度)采用RGB值来表示,比如标准强度的白光的颜色值是(1.0,1.0,1.0);表面的基底色是物体本身的颜色,或者说物体在标准白光下的颜色。

假设入射光是白色(1.0,1.0,1.0),物体表面基底色是红色(1.0,0.0,0.0),入射角度θ为0.0(垂直入射),此时漫反射光的颜色为红色:
( 1.0 , 1.0 , 1.0 ) × ( 1.0 , 0.0 , 0.0 ) × cos ⁡ 0 ° = ( 1.0 , 0.0 , 0.0 ) (1.0,1.0,1.0)\times(1.0,0.0,0.0)\times\cos0°=(1.0,0.0,0.0) (1.0,1.0,1.0)×(1.0,0.0,0.0)×cos0°=(1.0,0.0,0.0)


根据光线和表面的方向计算入射角

在实际操作中,我们只知道光线的方向和物体表面的朝向,需要根据二者计算出光线入射角。

根据矢量点积(内积)的性质,我们可以简单获得入射角的余弦值。
cos ⁡ θ = < 光 线 方 向 > ⋅ < 法 线 方 向 > \cos\theta=<光线方向>·<法线方向> cosθ=<线><线>

对矢量n和l进行点积运算的公式为: n ⋅ l = ∣ n ∣ × ∣ l ∣ × cos ⁡ θ n·l=|n|\times|l|\times\cos\theta nl=n×l×cosθ
假设矢量n为(nx,ny,nx),l为(lx,ly,lz),那么: n ⋅ l = n x ∗ l x + n y ∗ l y + n z ∗ l z n·l=nx*lx+ny*ly+nz*lz nl=nxlx+nyly+nzlz

此时需要遵循如下规定:

  • 光线方向矢量和表面法线方向矢量的长度必须为1(|n|*|l|=1),方便直接获得入射角的余弦值而不需要额外常数的参与。
    将矢量长度调整为1同时保持方向不变的过程称为归一化(normalization),只需要将矢量的每个分量除以它的模即可,GLSL ES内置了归一化的函数。
  • 公式中的“光线方向”是入射方向的反方向,即从入射点指向光源方向,如此该方向与法线方向的夹角才是入射角。(使用入射方向也可以,但会得到 − cos ⁡ θ -\cos\theta cosθ,后续再进行换算。)

在这里插入图片描述


获取法线:表面的朝向

这一部分中文书中有些错误,建议参考英文书

物体表面的朝向,就是垂直于表面的方向,也称作法线或法向量。

关于法向量,相信各位读者都有了解,笔记结合书中内容进行一定总结:

  1. 法向量(x,y,z)指从原点指向(x,y,z)方向的矢量,一个平面法向量唯一。
  2. 一个表面具有两面,所以有两个法向量,一个在属于“正面”,一个属于“背面”。“正面”或“背面”由顶点的绘制顺序决定,法向量与顶点绘制顺序遵循“右手法则”。如下面左图为正面法向量,右图为背面法向量:

在这里插入图片描述

法向量可以通过平面内不共线的两个矢量的叉乘获得,此处书中没有提到。

下图展示了立方体中各个平面的法向量,每个顶点分属不同的平面,所以一个顶点有三个法向量:

在这里插入图片描述


示例程序LightedCube.js

通过以上说明,漫反射光颜色即可计算得出。此处示例程序绘制了一个处于白色平行光照射下的红色三角形。

在这里插入图片描述

示例代码基于ColoredCube.js改写,原程序每一个顶点定义了三次,具有不同的颜色,顶点和颜色通过两个缓冲区保存,顶点通过索引进行绘制。

下面对一些重要的地方进行说明:

  • 顶点着色器

为了计算漫反射光,引入了光线颜色、光线方向和法向量,将归一化和点积计算放到了着色器中,具体如下:

// 顶点着色器
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(vec3(a_Normal));\n' +
  // 计算光线方向和法向量的点积
  ' float nDotL = max(dot(u_LightDirection,normal),0.0);\n' +
  // 计算漫反射光的颜色
  ' vec3 diffuse = u_LightColor * vec3(a_Color) * nDotL;\n' +
  ' v_Color = vec4(diffuse,a_Color.a);\n' +
  '}\n'

补充一:max(dot(u_LightDirection,normal),0.0)将小于0的值自动归为0,即入射角θ大于90度,光线照射不到(照到了背面)的情况,反射光视为0。

所以,本例是一个简单的例子,没有考虑物体遮挡光源的情况。

补充二:示例中法向量从vec4到vec3数据类型发生了变换,之所以要接收vec4类型的数据是为了方便后面扩展。

补充三:此处通过a_Color.a访问vec4类型数据的第四个分量,已知的访问vec4类型数据各个分量的索引有xyzw和rgba。

补充四:本例暂时认为物体都是不透明的,方便计算。

示例主要的改动都在顶点着色器中体现了,在JavaScript中只需要提供着色器相关支持即可。

  • JavaScript的配合——光线颜色
  // main()
  // 光线颜色
  let u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor')
  if (!u_LightColor) {
    console.log('Failed to get the storage loaction of u_LightColor')
    return
  }
  gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0)
  • JavaScript的配合——光线方向
  1. Vector3()对象定义在cuon-matrix.js中,normalize是其归一化的方法

注意需要归一化!

  // main()
  // 光线方向(世界坐标系)
  let u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection')
  if (!u_LightDirection) {
    console.log('Failed to get the storage loaction of u_LightDirection')
    return
  }
  let lightDirection = new Vector3([0.5, 3.0, 4.0])
  lightDirection.normalize() // 归一化
  gl.uniform3fv(u_LightDirection, lightDirection.elements)
  • JavaScript的配合——法向量
  // initVertexBuffers()
...
  // 法向量
  var normals = new Float32Array([
  0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0,
  1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,
  0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,
  -1.0, 0.0, 0.0,  -1.0, 0.0, 0.0,  -1.0, 0.0, 0.0,  -1.0, 0.0, 0.0,
  0.0,-1.0, 0.0,   0.0,-1.0, 0.0,   0.0,-1.0, 0.0,   0.0,-1.0, 0.0,
  0.0, 0.0,-1.0,   0.0, 0.0,-1.0,   0.0, 0.0,-1.0,   0.0, 0.0,-1.0
  ]);
...
  // 将顶点法向量写入缓冲区并开启分配
  if (!initArrayBuffer(gl, normals, 3, gl.FLOAT, 'a_Normal')) {
    return -1
  }
...

为立方体补充环境光——示例LightedCube_ambient,js

示例LightedCube_ambient,js在上一个示例的基础上,加入了环境光的影响,使其更符合现实环境。

在这里插入图片描述

可以看到,示例图像整体变亮了很多。

新的示例在原示例基础上加入了环境光变量,主要改动如下:

  • 顶点着色器中引入环境光
// 顶点着色器
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' + // 光线方向(归一化的世界坐标)
  'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
  'varying vec4 v_Color;\n' +
  'void main(){\n' +
  ' gl_Position = u_MvpMatrix * a_Position;\n' +
  // 对法向量进行归一化
  ' vec3 normal = normalize(vec3(a_Normal));\n' +
  // 计算光线方向和法向量的点积
  ' float nDotL = max(dot(u_LightDirection,normal),0.0);\n' +
  // 计算漫反射光的颜色
  ' vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n' +
  // 计算环境光产生的反射光颜色
  ' vec3 ambient = u_AmbientLight * a_Color.rgb;\n' +
  // 最终颜色
  ' v_Color = vec4(diffuse + ambient,a_Color.a);\n' +
  '}\n'
  • main()函数中配置环境光
  // 环境光
  let u_AmbientLight = gl.getUniformLocation(gl.program,'u_AmbientLight')
  if (!u_AmbientLight) {
    console.log('Failed to get the storage loaction of u_AmbientLight')
    return
  }
  gl.uniform3f(u_AmbientLight, 0.2,0.2,0.2)

Lighting the Translated-Rotated Object

相关内容:实现运动物体的光照效果:求新的法线;
相关函数:1. Matrix4对象的两种方法:setInverseOf()获得参数(也是Matrix4对象)的逆矩阵,transpose()对自身转置。

小结:可以使用模型矩阵子矩阵或模型矩阵的逆转置矩阵计算模型变换后的法线方向

在上一个示例中,模型矩阵为单位矩阵,立方体的坐标没有发生几何变换。假如物体发生运动,影响反射光的因素中法线方向也会随之变动,影响物体的光照效果。本例考虑如何在物体运动的时候,获取法线方向,视线光照效果。


模型矩阵的逆转置矩阵计算法线方向

首先给出结论,模型矩阵的逆转置矩阵可以用于计算新的法线方向,说明如下。

我们接触的物体坐标变换主要有三种方式:平移、旋转和缩放。

在这里插入图片描述

  • 平移的情况下,物体表面法线方向不会发生变化。
  • 旋转的情况下,物体表面法线方向同样发生旋转。
  • 缩放的情况下,如果几个坐标轴缩放大小一致,法线方向不会变化,如果缩放大小不一致,则会发生变化。

对于法线方向发生变化的情况,为了获得新的法线方向,我们有时可以求诸于模型矩阵自身,但总是可以依靠模型矩阵的逆转置矩阵。

从模型矩阵自身求解:

“从模型矩阵自身求解”是指使用模型矩阵左上角3×3的子矩阵对原法线方向进行变换,获得新的法线方向。子矩阵中包含了物体旋转和缩放的信息,适用的情况如下:

  • **平移操作:**如果模型矩阵中包含了平移变换,直接适用模型矩阵处理原法线方向会使法向量被当作顶点平移,最终与平面的关系不符合要求。如下图所示,使用模型矩阵的左上角子矩阵即可忽略平移的影响。

在这里插入图片描述

  • 旋转操作: 没有缩放操作的情况下,上图所示的子矩阵就只包含了旋转的信息,可以用于处理法向量。如果原法向量已经归一化了,则新的法向量不需要再进行归一化。
  • 缩放因子相同的缩放操作: 此时可以使用左上角子矩阵处理法向量,但新的法向量需要进行归一化(只有此种缩放操作时,似乎不处理也可以)。
  • 缩放因子不同的缩放操作: 上图所示子矩阵无法处理此种情况,需要使用逆转置矩阵。

模型矩阵的逆转置矩阵:

首先,可以考虑为什么使用逆转置矩阵?(内容来自书中附录E,做了一定简化,已了解的学者可以跳过)

在这里插入图片描述

此处,令模型矩阵为M,初始法向量为n,垂直于原始法向量n的向量为s,令变换法向量的变换矩阵为M‘,如上图所示。此时可以获得如下关系:
n ′ = M ′ × n s ′ = M × s n'=M'\times n\\ s'=M\times s n=M×ns=M×s
已知两个相互垂直的矢量点积为0,所以:
n ′ ⋅ s ′ = 0 n'·s'=0 ns=0
将第一组等式带入第二组,可得:
( M ′ × n ) ⋅ ( M × s ) = 0 ( M ′ × n ) T × ( M × s ) = 0 n T × M ′ T × M × s = 0 (M'\times n)·(M\times s)=0\\ (M'\times n)^T\times(M\times s)=0\\ n^T\times M'^T\times M\times s=0 (M×n)(M×s)=0(M×n)T×(M×s)=0nT×MT×M×s=0
因为n和s相互垂直,所以:
n ⋅ s = 0 n T × s = 0 n·s=0\\ n^T\times s =0 ns=0nT×s=0
所以,为了使 n T × M ′ T × M × s = 0 n^T\times M'^T\times M\times s=0 nT×MT×M×s=0成立,需要 M ′ T × M M'^T\times M MT×M为单位矩阵 I I I,即:
M ′ T × M = I M'^T\times M=I MT×M=I
可得:
M ′ = ( M − 1 ) T M'=(M^{-1})^T M=(M1)T
模型矩阵M包括了所有模型矩阵可以定义的变换方式,所以其逆转置矩阵适用于所有的模型矩阵变换的情况,来求解新的法向量。

当然,计算逆转置矩阵的操作对于计算机来说比较耗时,如果可以确定物体的变换只包括模型矩阵子矩阵可以解决的情况,直接使用其子矩阵更简便。


示例程序LightedTranslatedRotatedCube.js

相比于上一个示例,本示例除了添加模型矩阵之外,只需要对法向量进行矩阵变换即可。主要改动如下:

  • 在顶点着色器中声明新矩阵并对法向量进行变换:
// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'attribute vec4 a_Normal;\n' + // 法向量
  'uniform mat4 u_MvpMatrix;\n' +
  'uniform mat4 u_NormalMatrix;\n' + // 法向量变换矩阵
  'uniform vec3 u_LightColor;\n' + // 光线颜色
  'uniform vec3 u_LightDirection;\n' + // 光线方向(归一化的世界坐标)
  'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
  'varying vec4 v_Color;\n' +
  'void main(){\n' +
  ' gl_Position = u_MvpMatrix * a_Position;\n' +
  // 对法向量进行矩阵变换和归一化
  ' vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
  // 计算光线方向和法向量的点积
  ' float nDotL = max(dot(u_LightDirection,normal),0.0);\n' +
  // 计算漫反射光的颜色
  ' vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n' +
  // 计算环境光产生的反射光颜色
  ' vec3 ambient = u_AmbientLight * a_Color.rgb;\n' +
  // 最终颜色
  ' v_Color = vec4(diffuse + ambient,a_Color.a);\n' +
  '}\n'
  • 在模型视图投影矩阵中加入几何变化:
  // 模型视图投影矩阵
  let u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix')
  if (!u_MvpMatrix) {
    console.log('Failed to get the storage loaction of u_MvpMatrix')
    return
  }
  let mvpMatrix = new Matrix4()
  // 计算矩阵
  // 模型矩阵
  let modelMatrix = new Matrix4()
  // 先旋转再平移
  modelMatrix.setTranslate(0,1,0) 
  modelMatrix.rotate(-30,0,0,1)
  // 投影和视图矩阵
  mvpMatrix.setPerspective(30, 1, 1, 100)
  mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0)
  mvpMatrix.multiply(modelMatrix)
  // 矩阵传值
  gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements)
  • 主函数中设置法向量变换矩阵:
  // 法向量变换矩阵
  let u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix')
  if (!u_NormalMatrix) {
    console.log('Failed to get the storage loaction of u_NormalMatrix')
    return
  }
  let normalMatrix = new Matrix4()
  normalMatrix.setInverseOf(modelMatrix) // 求逆
  normalMatrix.transpose() // 转置
  gl.uniformMatrix4fv(u_NormalMatrix,false,normalMatrix.elements)

此处用到了Matrix4对象的两种方法:setInverseOf()获得参数(也是Matrix4对象)的逆矩阵,transpose()对自身转置。

示例效果如下:

在这里插入图片描述

Using a Point Light Object

相关内容:1. 点光源光照效果实现:换了一个公式计算反射光,着色器负责反射光公式实现,JavaScript负责为着色器提供支持。2. 通过逐片元计算颜色(将计算颜色的过程放在片元着色器中,顶点着色器提供需要逐顶点计算的顶点坐标等内容,片元着色器使用内插出的坐标和法向量,结合输入的其它值计算反射光),可以有效避免颜色值线性内插造成的失真,其本质是点光源照射下颜色值在平面上的变化并非是线性的(笔者认为)。

如标题所示,这一部分讨论点光源光照效果的实现。

与平行光相比,点光源发出的光在三维空间的不同位置上方向不同。所以,要实现点光源光照效果,进行着色前需要在每个入射点计算点光源光的方向。

在这里插入图片描述


示例程序PointLightedCube.js

本节示例PointLightedCube.js在本章第一节加入环境光的示例LightedCube_ambient,js基础上改造而成,显示了一个点光源下的红色立方体。立方体表面依然是漫反射,环境光保持不变。点光源与平行光情况类似,只是换了一个公式计算反射光,着色器负责反射光计算,JavaScript负责为着色器提供支持。

在这里插入图片描述

因为改动了计算公式,示例对顶点着色器改动较多,加入了适合点光源的计算过程,JavaScript主要配合着色器进行改动,一些新的改动如下:

  • 顶点着色器加入点光源的计算过程:

顶点着色器除了法向量等,新加入了模型矩阵、光源位置。此处单独输入模型矩阵,是为了根据模型矩阵获得变换后的顶点坐标,配合光源位置计算入射角。

// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'attribute vec4 a_Normal;\n' + // 法向量
  'uniform mat4 u_MvpMatrix;\n' +
  'uniform mat4 u_ModelMatrix;\n' + // 模型矩阵
  'uniform mat4 u_NormalMatrix;\n' + // 变换法向量的矩阵
  'uniform vec3 u_LightColor;\n' + // 光线颜色
  'uniform vec3 u_LightPosition;\n' + // 光源位置(世界坐标系)
  'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
  'varying vec4 v_Color;\n' +
  'void main(){\n' +
  ' gl_Position = u_MvpMatrix * a_Position;\n' +
  // 对法向量进行变换和归一化
  ' vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
  // 计算顶点的世界坐标
  ' vec4 vertexPosition = u_ModelMatrix * a_Position;\n' +
  // 计算光线方向并归一化
  ' vec3 lightDirection = normalize(u_LightPosition - vec3(vertexPosition));\n' +
  // 计算光线方向和法向量的点积 cosθ
  ' float nDotL = max(dot(lightDirection,normal),0.0);\n' +
  // 计算漫反射光的颜色
  ' vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n' +
  // 计算环境光产生的反射光颜色
  ' vec3 ambient = u_AmbientLight * a_Color.rgb;\n' +
  // 最终颜色
  ' v_Color = vec4(diffuse + ambient,a_Color.a);\n' +
  '}\n'
  • 光线颜色和光源位置设计如下:(环境光依然保留(0.2,0.2,0.2)的设置,不再注明)
  // 光线颜色
  let u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor')
  if (!u_LightColor) {
    console.log('Failed to get the storage loaction of u_LightColor')
    return
  }
  gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0)

  // 光源位置
  let u_LightPosition = gl.getUniformLocation(gl.program,'u_LightPosition')
  if (!u_LightPosition) {
    console.log('Failed to get the storage loaction of u_LightPosition')
    return
  }
  gl.uniform3f(u_LightPosition, 1.0, 1.5, 2.0)
  • 模型矩阵中包含了沿Y轴旋转90度的变换过程,据此生成法向量的变换矩阵,此变换过程也加在了模型视图投影矩阵中:
  // 模型矩阵
  let u_ModelMatrix = gl.getUniformLocation(gl.program,'u_ModelMatrix')
  if (!u_ModelMatrix) {
    console.log('Failed to get the storage loaction of u_ModelMatrix')
    return
  }
  let modelMatrix = new Matrix4()
  modelMatrix.setRotate(90,0,1,0)
  gl.uniformMatrix4fv(u_ModelMatrix,false,modelMatrix.elements)

  // 变换法向量的矩阵
  let u_NormalMatrix = gl.getUniformLocation(gl.program,'u_NormalMatrix')
  if (!u_NormalMatrix) {
    console.log('Failed to get the storage loaction of u_NormalMatrix')
    return
  }
  let normalMatrix = new Matrix4()
  normalMatrix.setInverseOf(modelMatrix)
  normalMatrix.transpose()
  gl.uniformMatrix4fv(u_NormalMatrix,false,normalMatrix.elements)

  // 模型视图投影矩阵
  let u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix')
  if (!u_MvpMatrix) {
    console.log('Failed to get the storage loaction of u_MvpMatrix')
    return
  }
  let mvpMatrix = new Matrix4()
  // 计算矩阵
  mvpMatrix.setPerspective(30, 1, 1, 100)
  mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0)
  mvpMatrix.multiply(modelMatrix)
  // 矩阵传值
  gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements)

更逼真的方法:逐片元光照和PointLightedCube_perFragment.js

在上面的示例效果中,我们可以发现一些不自然的地方:在正方形面的对角线两侧有奇怪的颜色差异。这是因为在光栅化时,每个片元的颜色是根据顶点的颜色内插出的效果,虽然顶点的颜色是由点光源产生的,但实际情况下点光源照射到表面上的效果与简单使用4个顶点颜色内插出的效果并不完全相同,甚至有时差异很大。如果把示例换成球体,差异会更明显:(左图为逐顶点计算内插颜色的结果,右图是逐片元计算的结果)

在这里插入图片描述

笔者从数学的角度认为,造成逐顶点计算内插颜色出现错误的原因是:内插操作遵循的是线性内插,没有考虑顶点之间的颜色不是均匀变化的情况。而在实际情况下,两个顶点之间颜色变化与入射角的余弦值变化有关,依赖于顶点和光源的位置以及法向量的方向,单纯的余弦函数就不是直线而是曲线,如此一些信息叠加很难形成线性变化的结果。计算机图形学的大佬和数学大佬肯定有更细致的解释,待笔者之后考究。

所以,如果能够逐片元地计算颜色,就能在像素的尺度上避免这一问题。此时片元着色器终于能够摆脱只有几行代码的窘境。

示例实现也比较简单,只需要把计算反射光所需的内容通过varying变量传递给片元着色器,在着色器中计算反射光然后对gl_FragColor赋值即可。重要的改动如下:

  • 顶点着色器:

将计算反射光需要的、在顶点着色器中接收或计算的逐顶点的内容:顶点世界坐标、法向量和基底色传递给片元着色器:

// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'attribute vec4 a_Normal;\n' + // 法向量
  'uniform mat4 u_MvpMatrix;\n' + // 模型视图投影矩阵
  'uniform mat4 u_ModelMatrix;\n' + // 模型矩阵
  'uniform mat4 u_NormalMatrix;\n' + // 变换法向量的矩阵
  'varying vec4 v_Color;\n' +
  'varying vec3 v_Normal;\n' +
  'varying vec3 v_Position;\n' +
  'void main(){\n' +
  ' gl_Position = u_MvpMatrix * a_Position;\n' +
  // 对法向量进行变换和归一化
  ' v_Normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
  // 计算顶点的世界坐标
  ' v_Position = vec3(u_ModelMatrix * a_Position);\n' +
  // 基底色
  ' v_Color = a_Color;\n' +
  '}\n'
  • 片元着色器:

接收不随顶点变动的变量,包括光线颜色、光源位置(世界坐标系)、环境光颜色。

接收逐片元内插得到的片元世界坐标系坐标(原本是顶点,不使用gl_FragCoord是因为该坐标经过了视图和投影矩阵的改造)、法向量最起码在立方体中,一个面法向量不变,法向量内插不会出现颜色内插的问题)、基底色。

进行相关颜色计算,赋值给gl_FragColor。

// 片元着色器
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform vec3 u_LightColor;\n' + // 光线颜色
  'uniform vec3 u_LightPosition;\n' + // 光源位置(世界坐标系)
  'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
  'varying vec4 v_Color;\n' + // 基底色
  'varying vec3 v_Normal;\n' + // 法向量
  'varying vec3 v_Position;\n' + // 片元位置(世界坐标系)
  'void main(){\n' +
  // 法线归一化(内插后长度不一定为1.0)
  ' vec3 normal = normalize(v_Normal);\n' +
  // 计算光线方向并归一化
  ' vec3 lightDirection = normalize(u_LightPosition - v_Position);\n' +
  // 计算光线方向和法向量的点积 cosθ
  ' float nDotL = max(dot(lightDirection,normal),0.0);\n' +
  // 计算漫反射光的颜色
  ' vec3 diffuse = u_LightColor * v_Color.rgb * nDotL;\n' +
  // 计算环境光产生的反射光颜色
  ' vec3 ambient = u_AmbientLight * v_Color.rgb;\n' +
  // 最终颜色
  ' gl_FragColor = vec4(diffuse+ambient,v_Color.a);\n' +
  '}\n'

这样我们就可以得到一个视觉效果更好的立方体,二者对比如下,原始的逐顶点内插颜色的立方体为左图,本示例逐片元计算颜色的结果为右图:

在这里插入图片描述

Summary

书中小结如下:

这一章介绍了几种不同的光照类型和反射类型,讨论了如何为场景实现光照效果, 并基于这些知识实现了几种不同类型光源下的三维场景,探索了一些着色器的技巧,以增加效果的逼真程度。如你所见,掌握光照的技巧是很重要的。在正确的光照效果下, 三维场景会更加逼真,而缺了光照,它就会显得单调和枯燥。

笔者补充:

本章所言实现光照效果的技巧,实际上是了解了光照过程,建立数学模型(即相关公式)后,将公式的计算过程带入GLSL ES语言中,使着色器按照此模型计算颜色。本章包含的所有示例都遵循这一原则:着色器建立计算颜色的模型,JavaScript为该模型提供数据支持。

唯一的不同在于,WebGL系统中顶点着色器和片元着色器分属渲染流程的不同位置,在顶点着色器中计算和在片元着色器中(使用内插值)计算有不同的效果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值