WebGL——三维世界的光照

1,177 阅读4分钟

前言

本文将介绍用webgl写一个球体的核心逻辑,以及光照在webgl的具体应用,平行光、点光、聚光等原理,dat.gui如何快速配置,让你快速理解三维世界的光照

绘制球体

QQ录屏20230506214830.gif

前置工作

一般而已,我们会封装webgl-utils的工具库、封装通用的矩阵算法、封装创建几何体的方法

仓库地址 github.com/ccj-007/eas…

<script src="resources/webgl-utils.js"></script> //webgl的通用封装
<script src="resources/m4.js"></script>  // 矩阵通用方法
<script src="resources/primitives.js"></script>  // 几何体封装方法

TWGL

为了更好的方便我们开发,我们可以使用twgl.js轻量库github.com/greggman/tw… 来简化繁琐的api调用。

image.png

绘制球体的核心逻辑

function createSphere (gl, radius, divideByYAxis, divideByCircle) {
    let yUnitAngle = Math.PI / divideByYAxis;
    let circleUnitAngle = (Math.PI * 2) / divideByCircle;
    let positions = [];
    let normals = [];
    for (let i = 0; i <= divideByYAxis; i++) {
      let unitY = Math.cos(yUnitAngle * i);
      let yValue = radius * unitY;

      for (let j = 0; j <= divideByCircle; j++) {
          let u = i / divideByYAxis
          let v = j / divideByCircle
      
        let unitX = Math.sin(yUnitAngle * i) * Math.cos(circleUnitAngle * j);
        let unitZ = Math.sin(yUnitAngle * i) * Math.sin(circleUnitAngle * j);
        let xValue = radius * unitX;
        let zValue = radius * unitZ;
        positions.push(xValue, yValue, zValue);
        normals.push(unitX, unitY, unitZ);
        texCoords.push(1 - u, v);
      }
}

球体的每个面的点计算还是挺复杂的,我们先从Y轴来拆分层级,注意我们通过cos得到的是y轴的坐标,而不是x坐标。然后计算每层的x轴坐标,unitX = Math.cos(circleUnitAngle * j) 这样得到的结果你会发现是平面的图形,但是我们球面需要根据unitY对应的x坐标,来乘积得到符合曲线规律的x坐标,同理在这个平面的z轴也可以得出。

法向量和纹理uv

法向量一般表示图形面的朝向的单位向量,我们根据之前绘制的xyz的坐标就可以获得,注意是单位向量。同时纹理uv,其实是展开的二维贴图的坐标,我们根据需要,如果每一面要自定义纹理,就是对应的份数差。注意这里纹理坐标是相反的,所以1 - u

平行光原理

image.png

就是将光源向量和法向量做向量的点积, 最后在片元着色器的gl_FragColor的rgb相乘即可

  function dot (a, b) {
    return (a[0] * b[0]) + (a[1] * b[1]) + (a[2] * b[2]);
  }
  vec3 normal = normalize(v_normal);

  float light = dot(normal, u_reverseLightDirection);

点光源原理

image.png

点光源是四周扩展的效果,点光源明显的问题就是根据角度的不同,光照强度会不同,离点的角度越小,那么光照强度会越低。

核心就是先得到黑色线段的向量,然后将此向量和法向量做点积

v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;

vec3 surfaceToLightDirection = normalize(v_surfaceToLight); //转为法向量
light = dot(normal, surfaceToLightDirection);

点积公式

a * b = |a| * |b| * cosθ

由于a和b都是单位向量,实数是相等的,角度越大cos值越小,所以一旦法向量和反射的向量一致,点积为1,相反为-1

高光原理

如果光线直接在物体反射后,如果光线的方向正好是人眼,那么物体表面会有高光现象,但是越粗糙的物体,漫反射会越强,同时镜面反射会弱。所以金属、玻璃材质的会有更强的高光感受。normal和 halfVector的点积代表高光的强度。

-   vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
-   vec3 surfaceToViewDirection = normalize(v_surfaceToView);
-   vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewDirection);
-    
-   float specular = dot(normal, halfVector);

聚光灯原理

image.png

聚光灯其实就是限制了范围而已,我们通过limit变量控制范围,值是0到1的范围,那么对应的角度通过三角函数在cos的x值为0.94,那么对应的角度可以得出。

有一个 GLSL 函数叫做 step,它获取两个值,如果第二个值大于或等于第一个值就返回 1.0, 否则返回 0, 只要是1,就显示光照

  float dotFromDirection = dot(surfaceToLightDirection, -u_lightDirection);
   // 如果光线在聚光灯范围内 inLight 就为 1,否则为 0
  float inLight = step(u_limit, dotFromDirection);
  float light = inLight * dot(normal, surfaceToLightDirection);
  float specular = inLight * pow(dot(normal, halfVector), u_shininess);

通过dat.gui快速配置

dat.gui能快速根据配置项生成控制面板,方便我们开发和调试。

  const options = {
    lightX: 43,
    lightY: 13,
    lightZ: 82,
    shininess: 8,
    lightColor_r: .4,
    lightColor_g: 1,
    lightColor_b: 1,
    sphereColor_r: .7,
    sphereColor_g: .5,
    sphereColor_b: 1,
    innerLimit: 20,
    outerLimit: 200,
  }

  function createGUI () {
    const gui = new dat.GUI();
    gui.add(options, 'lightX', 0, 100)
    gui.add(options, 'lightY', 0, 100)
    gui.add(options, 'lightZ', 0, 100)
    gui.add(options, 'shininess', 0, 1000)
    gui.add(options, 'lightColor_r', 0, 1)
    gui.add(options, 'lightColor_g', 0, 1)
    gui.add(options, 'lightColor_b', 0, 1)
    gui.add(options, 'sphereColor_r', 0, 1)
    gui.add(options, 'sphereColor_g', 0, 1)
    gui.add(options, 'sphereColor_b', 0, 1)
    gui.add(options, 'innerLimit', 1, 600)
    gui.add(options, 'outerLimit', 1, 600)
  }

总结

webgl学得越深入,发现里面的坑无穷无尽🤕,想要图形化领域学得好,数学还真得下功夫。尤其是webgl的调试困难,api繁琐老旧,很多时候没有数学的支撑或者原理的理解,是很难找到问题所在的。

本文只是简单的概述了光照在gl的应用,实际渲染场景里,灯光是很重要的一环,一般常见场景的有太阳光、点光、聚光灯、线性灯等,太阳光一般我们用平行光模拟,当然你也可以用点光源。同时光源的计算强度有不同的单位控制。聚光灯为了更好的融合场景,会存在衰减的深度范围,包括一些材质可以实现自发光的效果,三维纹理的应用等等.....