WebGL 之添加光照

前期准备

先使用之前所学的知识渲染一个纯色立方体如下:

由于各个面都是同样的灰色,所以想要看出它是个立方体,需要一定的想象力的加持。在 c4d 等构建三维物体的软件里,我们是可以添加灯光对象的,从而让物体表面呈现出明暗对比,让人一眼看出物体的形状:

在 webgl 中,我们也可以给物体添加光照,并且请注意,在现实世界中,我们之所以能看到物体,是因为物体被光线照射后反射了一部分光进入了人的眼睛。所以我们要给目标物体添加光照的效果,要考虑 2 大部分 ------ 光源和反射。

光源

先来分析光源,根据光源发出的光线方向的不同,我们可以将光源分为 2 种:

平行光(directional light)

光线互相平行,射向同一个方向,如太阳光。定义时只需要光线的方向和光的颜色,至于光源的位置,可以认为是特别远的地方,无需定义。

点光源(point light)

本案例中要使用的就是点光源,它的光线从一点向周围放射,比如灯泡。定义点光源时,需要定义光源的颜色、位置和光线的方向。我们可以在顶点着色器源码的 main 函数中,定义一个类型为 vec3 的变量 pointLightColor 代表点光源的颜色;另外定义一个类型为 vec3 的变量 pointLightPosition 代表点光源的位置:

javascript 复制代码
// 代码片段 1.1
const vsSource = `
  void main() {
    // 定义点光源的颜色 - 红色
    vec3 pointLightColor = vec3(1.0, 0.0, 0.0);
    // 定义点光源的位置
    vec3 pointLightPosition = vec3(10.0, 10.0, 10.0);
  }
`

至于点光源的光线方向 lightDirect,则需要根据光源和物体的位置进行计算,其实就是做个向量的减法:

注意,这里说的光线的方向其实是光源入射方向的反方向,是用光源的位置 pointLightPosition 减去物体的顶点坐标 vPosition,然后用 GLSL 的内置函数 normalize 进行归一化处理得到的:

javascript 复制代码
// 代码片段 1.2
const vsSource = `
  void main() {
    // 点的坐标
    vec4 vPosition = uMatrix * aPosition;
    // 光线方向
    vec3 lightDirect = normalize(pointLightPosition - vec3(vPosition));
  }
`

环境光(ambient light)

除了平行光和点光源,还有一种间接光,是光源发出的光线照射到环境中的各个物体后产生的反射。环境光的光线会均匀地照射到目标对象表面,其强度差距很小,无需精确计算光线强度,只需定义光的颜色即可:

javascript 复制代码
// 代码片段 1.3
const vsSource = `
  void main() {
    // 定义环境光的颜色
    vec3 ambientLightColor = vec3(0.2, 0.0, 0.0);
  }
`

反射

现在来分析光的反射,反射可以大致分为以下 3 类:

环境反射(environment reflection)

先是最容易定义其反射颜色的环境反射。物体对环境光的反射就是环境反射,反射的方向为入射光的反方向。环境反射的颜色 = 环境光的颜色 * 物体表面的颜色:

javascript 复制代码
// 代码片段 2.1
const vsSource = `
  void main() {
    // 物体表面的颜色 - 灰色
    vec4 aColor = vec4(0.8, 0.8, 0.8, 1.0);
    // 环境反射的颜色
    vec3 ambientColor = ambientLightColor * vec3(aColor);
  }
`

漫反射(diffuse reflection)

接着是现实生活中最常见的漫反射。因为大多数物体的表面都是粗糙不平的,所以在接收到光源的入射光后,反射的光线会均匀地射向四周,称为漫反射。漫反射的反射光的颜色 =入射光的颜色 * 物体表面的颜色 * cosα。

javascript 复制代码
// 代码片段 2.2
const vsSource = `
  void main() {
    // 漫反射光的颜色
    vec3 diffuseColor = pointLightColor * vec3(aColor) * deg;
  }
`

入射角

代码片段 2.2 中的 deg 为入射光与物体表面的法向量形成的入射角 α 的余弦值:

由下图易知,在同样的光源下,在 0 -90°的区间内,入射角越小,则物体表面接收到的光线数量就越大:

所以在求漫反射的反射光颜色的公式中,就用入射角的余弦值 cosα 来表示物体的受光程度。 因为 l · n = |l| * |n| * cosαln 为单位向量,则 |l||n| 均为 1,所以 cosα = l · n。注意,向量的点积是符合乘法交换律的:

javascript 复制代码
// 代码片段 2.3
const vsSource = `
  attribute vec4 aNormal;
  void main() {
    // 入射角
    float deg = dot(lightDirect, vec3(aNormal));
  }
`

lightDirect 为代码片段 1.2 中定义的光线方向,aNormal 则为各个顶点的法向量。

法向量

aNormal 为 attribute 变量,我们可以定义好立方体 6 个面的法向量,赋值给变量 normals 中,然后将 normals 存入缓冲区对象aNormal 去读取:

javascript 复制代码
// 代码片段 2.4
const aNormal = gl.getAttribLocation(program, 'aNormal')
// 法向量
const 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,

  // 顶面
  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,

  // 左侧面
  -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, 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
])

// 创建缓冲区对象
const normalBuffer = gl.createBuffer()
// 绑定缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer)
// 将数据存入缓冲区对象
gl.bufferData(gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW)
gl.vertexAttribPointer(aNormal, 3, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(aNormal)

注意,因为在创建顶点数据 points 时每个面都有 4 个顶点数据,所以 normals 中每个面也需要对应的建立 4 次法线数据。

镜面反射(specular reflection)

最后一种是镜面反射。物体接收到光源的入射光后,会将光线以与物体表面法线对称的方向反射出去。

物体的最终颜色

当同时存在漫反射和环境反射时,物体的最终颜色就应该是漫反射光颜色 + 环境反射光颜色:

javascript 复制代码
// 代码片段 3.1
const vsSource = `
  vColor = vec4(ambientColor + diffuseColor, aColor.a);
`

注意,代码片段 2.1 获取的 ambientColor 和代码片段 2.2 获取的 diffuseColor 都是 vec3 类型的,而最终赋给 gl_FragColor 的值需要是 vec4 类型的,所以使用了矢量构造函数 vec4(),代表透明度的参数则传入物体表面原本的颜色 aColora 分量。

最终效果如下,可以看到添加了红光后物体,有了明暗效果,一眼看出是个立方体:

相关推荐
丁总学Java10 分钟前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
It'sMyGo19 分钟前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式
懒羊羊大王呀21 分钟前
CSS——属性值计算
前端·css
李是啥也不会35 分钟前
数组的概念
javascript
无咎.lsy1 小时前
vue之vuex的使用及举例
前端·javascript·vue.js
fishmemory7sec1 小时前
Electron 主进程与渲染进程、预加载preload.js
前端·javascript·electron
fishmemory7sec1 小时前
Electron 使⽤ electron-builder 打包应用
前端·javascript·electron
豆豆2 小时前
为什么用PageAdmin CMS建设网站?
服务器·开发语言·前端·php·软件构建
JUNAI_Strive_ving2 小时前
番茄小说逆向爬取
javascript·python
看到请催我学习2 小时前
如何实现两个标签页之间的通信
javascript·css·typescript·node.js·html5