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 分量。

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

相关推荐
Lupino24 分钟前
被 React “玩弄”的 24 小时:为了修一个不存在的 Bug,我给大模型送了顿火锅钱
前端·react.js
米丘31 分钟前
了解 Javascript 模块化,更好地掌握 Vite 、Webpack、Rollup 等打包工具
前端
Heo32 分钟前
深入 React19 Diff 算法
前端·javascript·面试
滕青山33 分钟前
个人所得税计算器 在线工具核心JS实现
前端·javascript·vue.js
小怪点点34 分钟前
手写promise
前端·promise
国思RDIF框架43 分钟前
RDIFramework.NET Web 敏捷开发框架 V6.3 发布 (.NET8+、Framework 双引擎)
前端
颜酱44 分钟前
从0到1实现LFU缓存:思路拆解+代码落地
javascript·后端·算法
Mintopia44 分钟前
如何在有限的时间里,活出几倍的人生
前端
炫饭第一名44 分钟前
速通Canvas指北🦮——变形、渐变与阴影篇
前端·javascript·程序员
Neptune11 小时前
让我带你迅速吃透React组件通信:从入门到精通(上篇)
前端·javascript