WebGL打开 3D 世界的大门(七):光照

196 阅读6分钟

这一章我们来讲解webgl中的光照概念,我们来先讲解一下平行光源,接下来的文章讨论点光源和聚光灯。

平行光源和法向量

假如没有光照,我们就什么都看不到,在不考虑反射的情况下,我们认为物体是漫发射,光线垂直照射的时候,我们应该看到最亮的。为了简化我们认为射进来的是白光,先不考虑颜色的叠加效应,效果大概是下面这个样子:
其实就是光线的方向和平面法线方向的余弦值,然后再乘以当前的颜色值,这样就有了光照。
那么什么是法线呢?如图

1747485908597.png 法线方向就是和物体表面切面垂直的方向。而法向量就是在法线方向上的单位向量。即在3D坐标系中长度为1的向量,

片元着色器

根据上面的内容,我们来介绍一下片元着色器:

precision mediump float;
// Passed in from the vertex shader.
varying vec3 v_normal;

uniform vec3 u_reverseLightDirection;
uniform vec4 u_color;

void main() {
  // because v_normal is a varying it's interpolated
  // so it will not be a unit vector. Normalizing it
  // will make it a unit vector again
  vec3 normal = normalize(v_normal);

  float light = dot(normal, u_reverseLightDirection);

  gl_FragColor = u_color;

  // Lets multiply just the color portion (not the alpha)
  // by the light
  gl_FragColor.rgb *= light;
}

我们很容易看出,片元着色器最后取的值就是:当前颜色的rgb值乘以两个单位向量夹角的余弦值。

顶点着色器

顶点着色器变化不大:

  // an attribute will receive data from a buffer
  attribute vec3 a_position;
  attribute vec3 a_normal;

  uniform vec3 u_resolution;
  uniform mat4 matrix_projection;
  uniform mat4 matrix_translate;
  varying vec3 v_normal;

  void main() {
    // gl_Position is a special variable a vertex shader
    vec3 position = (matrix_translate  * vec4(a_position, 1)).xyz;
    // vec3 pro_position = (matrix_projection * vec4(position, 1)).xyz;
    vec3 zeroToOne = position - u_resolution / 2.0;

    // 再把 0->1 转换 0->2
    vec3 zeroToTwo = zeroToOne / u_resolution * 2.0;

    // 把 0->2 转换到 -1->+1 (裁剪空间)
    vec3 clipSpace = zeroToTwo * vec3(1.0,-1.0,-1.0);
    v_normal = a_normal;
    gl_Position = vec4(clipSpace, 1);
  }

这里需要把矩阵再优化一下。从坐标空间到裁剪空间需要使用矩阵变化。

这里再回忆一下Shader 中变量的类型与作用
  1. attribute 变量

    • 只能在顶点着色器中使用
    • 表示每个顶点特有的数据(如位置、法线、纹理坐标等)
    • 由JavaScript通过gl.vertexAttribPointer()传入
  2. uniform 变量

    • 在顶点和片元着色器中都可使用
    • 表示所有顶点/片元共用的数据(如变换矩阵、光源位置、颜色等)
    • 由JavaScript通过gl.uniformXXX()系列函数传入
  3. varying 变量

    • 用于从顶点着色器向片元着色器传递数据
    • 在顶点着色器中赋值,在片元着色器中读取
    • 数据会在光栅化过程中被插值
还有webgl中的数据类型:

** 顶点属性数据类型(Vertex Attribute Types)**

这些类型用于 gl.vertexAttribPointer(type, size, normalized, stride, offset) 的 type 参数,指定顶点缓冲区中数据的存储格式。

数据类型常量字节大小范围或描述典型用途
gl.BYTE1有符号字节 [-128, 127]顶点坐标(整数)
gl.UNSIGNED_BYTE1无符号字节 [0, 255]颜色(RGB/A)
gl.SHORT2有符号短整型 [-32768, 32767]顶点坐标
gl.UNSIGNED_SHORT2无符号短整型 [0, 65535]索引缓冲区(gl.ELEMENT_ARRAY_BUFFER
gl.INT4有符号整型 [-2³¹, 2³¹-1]特殊用途(较少使用)
gl.UNSIGNED_INT4无符号整型 [0, 2³²-1]索引缓冲区(大型模型)
gl.FLOAT432位浮点数顶点坐标、法线、UV
gl.HALF_FLOAT (WebGL2)216位半精度浮点数节省内存的顶点数据

为什么要回忆这些?我在写代码的过程中出了点错,非常难排查。结果发现是数据类型问题,这里引用记录一下。

字母法向量

我们可以使用deepseek帮我们生成一下,当前顶点坐标的法向量。例如我的法向量是:

var vetrilNormal = [    // ===== 前面的 E (z=500) =====    // 横线(顶部) - 正面法向量 (0, 0, 1)    ...Array(6).fill([0, 0, 1]).flat(),
    
    // 竖线(左侧) - 正面法向量 (0, 0, 1)
    ...Array(6).fill([0, 0, 1]).flat(),
    
    // 横线(底部) - 正面法向量 (0, 0, 1)
    ...Array(6).fill([0, 0, 1]).flat(),
    
    // 中间横线 - 正面法向量 (0, 0, 1)
    ...Array(6).fill([0, 0, 1]).flat(),

    // ===== 后面的 E (z=400) =====
    // 横线(顶部) - 背面法向量 (0, 0, -1)
    ...Array(6).fill([0, 0, -1]).flat(),
    
    // 竖线(左侧) - 背面法向量 (0, 0, -1)
    ...Array(6).fill([0, 0, -1]).flat(),
    
    // 横线(底部) - 背面法向量 (0, 0, -1)
    ...Array(6).fill([0, 0, -1]).flat(),
    
    // 中间横线 - 背面法向量 (0, 0, -1)
    ...Array(6).fill([0, 0, -1]).flat(),

    // ===== 侧面(连接前后 E)=====
    // 顶部横线侧面 - 上表面法向量 (0, 1, 0)
    ...Array(6).fill([0, 1, 0]).flat(),
    
    // 顶部横线前侧面 - 右侧法向量 (1, 0, 0)
    ...Array(6).fill([1, 0, 0]).flat(),
    
    // 顶部横线后侧面 - 左侧法向量 (-1, 0, 0)
    ...Array(6).fill([-1, 0, 0]).flat(),
    
    // 顶部横线下侧面 - 下表面法向量 (0, -1, 0)
    ...Array(6).fill([0, -1, 0]).flat(),

    // 左侧竖线侧面 - 左侧法向量 (-1, 0, 0)
    ...Array(6).fill([-1, 0, 0]).flat(),
    
    // 左侧竖线右侧面 - 右侧法向量 (1, 0, 0)
    ...Array(6).fill([1, 0, 0]).flat(),
    
    // 左侧竖线前侧面 - 前表面法向量 (0, 0, 1)
    ...Array(6).fill([0, 0, 1]).flat(),
    
    // 左侧竖线后侧面 - 后表面法向量 (0, 0, -1)
    ...Array(6).fill([0, 0, -1]).flat(),

    // 底部横线侧面 - 下表面法向量 (0, -1, 0)
    ...Array(12).fill([0, -1, 0]).flat(),
    
    // 中间横线侧面 - 前表面法向量 (0, 0, 1) 和后表面法向量 (0, 0, -1)
    ...Array(6).fill([0, 0, 1]).flat(),
    ...Array(6).fill([0, 0, -1]).flat(),
    
    // 中间横线右侧面 - 右侧法向量 (1, 0, 0)
    ...Array(6).fill([1, 0, 0]).flat()
];

有了法向量接下来的事情就简单了。

属性赋值

有了这些素材,接下来就简单了,对属性进行赋值就行了。

颜色赋值
var colorLocation = gl.getUniformLocation(program, "u_color");
gl.uniform4fv(colorLocation, [0.2, 1, 0.2, 1]); // green
光照赋值
var reverseLightDirectionLocation =
  gl.getUniformLocation(program, "u_reverseLightDirection");
let lightDirection = vec3.create();  // 先创建向量
vec3.normalize(lightDirection, [1, 1, 0.5]);  // 然后归一化
console.log('lightDirection',lightDirection);
gl.uniform3fv(reverseLightDirectionLocation, lightDirection);
法向量赋值
// 创建缓冲存储法向量
var normalBuffer = gl.createBuffer();
// 绑定到 ARRAY_BUFFER (可以看作 ARRAY_BUFFER = normalBuffer)
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vetrilNormal), gl.STATIC_DRAW);
var normalLocation = gl.getAttribLocation(program, "a_normal");
    // Turn on the normal attribute
gl.enableVertexAttribArray(normalLocation);

// Tell the attribute how to get data out of normalBuffer (ARRAY_BUFFER)
var v_size = 3;          // 3 components per iteration
var v_type = gl.FLOAT;   // the data is 32bit floating point values
var v_normalize = false; // normalize the data (convert from 0-255 to 0-1)
var v_stride = 0;        // 0 = move forward size * sizeof(type) each iteration to get the next position
var v_offset = 0;        // start at the beginning of the buffer
gl.vertexAttribPointer(
    normalLocation, v_size, v_type, v_normalize, v_stride, v_offset);
   

然后就是用原来的代码展示出来,

1747562992337.jpg 这已经看到了明显的光照效果。 代码地址: gitee.com/feng-lianxi…

可是你运行以后会发现一个问题:当物体旋转的时候,光照似乎跟着旋转,不太像是平行光源。我们需要把这个问题处理掉。这究竟是怎么回事呢?根本原因是在字母旋转的过程中,我们的法向量并没有和字母一起旋转,所以当字母旋转的时候我们对法向量一起旋转就行了。 代码地址: gitee.com/feng-lianxi…