likes
comments
collection
share

WebGL系列(5):光照

作者站长头像
站长
· 阅读数 89

前一篇文章WebGL系列(4):开启三维世界中最后描述了如何绘制一个立方体。但是绘制出来的立方体看起来仍有点差强人意。如下图,是一个纯白色的立方体,每个面颜色都相同。但是在现实世界中,即使每个面的颜色相同,但是我们所观察到的每个面还是有所差异。这就是因为光照的原因,本文就来学习一下光照。

WebGL系列(5):光照

一、光照原理

现实世界中的物体被光线照射时,会反射一部分光。只有当反射光线进入眼睛时,才能看到物体并辨认出它的颜色。当光线照射到物体上时,发生了两个现象:

  • 根据光源和光线方向,物体不同表面的明暗程度变得不一致
  • 根据光源和光线方向,物体向地面投下了影子。

三维图形学中,着色的真正含义是:根据光照条件重建“物体各表面明暗不一效果”的过程。物体向地面投下影子的现象,又被称为阴影

在深入了解之前,我们先来看看,发出光线的光源有哪些以及物体表面如何反射光线。

1.1 光源类型

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

  • 平行光(directional light):光线相互平行,且具有方向。平行光可以看作是无限远处的光源发出的光,例如太阳光。平行光可以用 一个方向一个颜色 来定义。
  • 点光源光(point light):点光源光是一个点向周围所有方向发出的光,例如人造灯泡的光。需要指定点光源的位置颜色。光线的方向将根据点光源的位置和被照射之处的位置计算出来,因为点光源的光线的方向在场景内的不同位置是不同的。

除了上述两种类型之外,我们还用环境光来模拟真实世界中的非直射光,

  • 环境光(ambient):指那些光源(点光源或平行光源)发出后,被墙壁等物体多次反射,然后照到物体表面上的光。环境光从各个角度照射物体,其强度都是一致的。环境光不用指定位置和方向,只需指定颜色即可。

实际上,环境光是各种光被各种表面经过多次反射后形成,我们认为环境光是“均匀”照射到物体表面的,因为没有必要去精确计算环境光的产生过程。

1.2 反射类型

物体向哪个方向反射光,反射的光是什么颜色,取决于以下两个因素:

  • 入射光:包括入射光的方向和颜色
  • 物体表面的类型:包括表面的固有颜色和反射特性

物体表面反射光线的方式有两种:漫反射(diffuse reflection)环境反射(environment/ambient reflection)

(1)漫反射

漫反射针对平行光或点光源而言。漫反射的反射光在各个方向上是均匀的。

WebGL系列(5):光照

在漫反射中,反射光的颜色取决于入射光的颜色表面的基底色入射光与表面形成的入射角。我们将入射角定义为入射光与表面法线形成的夹角(θ\thetaθ),那么漫反射光的颜色可根据如下公式计算得出:

<漫反射光颜色>=<入射光颜色>×<表面基底色>×cosθ<漫反射光颜色> = <入射光颜色>\times<表面基底色>\times cos\theta<>=<>×<>×cosθ

上述公式中的入射角θ\thetaθ,我们想要获取这个角度并不是那么简单,但是我们可以通过另一种方式来计算。例如我们有两个向量 a⃗\vec{a}ab⃗\vec{b}b,那么:

a⃗⋅b⃗=∣a⃗∣×∣b⃗∣×cosθ\vec{a} \cdot \vec{b} = |\vec{a}| \times |\vec{b}| \times cos\thetaab=a×b×cosθ

对上式进行换算可得:

cosθ=a⃗⋅b⃗∣a⃗∣×∣b⃗∣cos\theta = \frac{\vec{a} \cdot \vec{b}}{|\vec{a}| \times |\vec{b}|}cosθ=a×bab

那么当∣a⃗∣|\vec{a}|a∣b⃗∣|\vec{b}|b 的值为 1 时,那么就有:

cosθ=a⃗⋅b⃗cos\theta = \vec{a} \cdot \vec{b}cosθ=ab

应用到计算漫反射光颜色的式子中,a⃗\vec{a}a 就是光线方向,b⃗\vec{b}b 就是法线方向,对光线方向和法线方向的向量进行归一化处理后,就可得到如下公式:

<漫反射光颜色>=<入射光颜色>×<表面基底色>×(<光线方向>⋅<法线方向>)<漫反射光颜色> = <入射光颜色>\times<表面基底色> \times (<光线方向>\cdot<法线方向>)<>=<>×<>×(<线><线>)

注意:上述公式中的 ×\times×⋅\cdot 运算不同,例如两个向量a⃗=(x1,y1,z1)\vec{a}=(x_1, y_1, z_1)a=(x1,y1,z1)a⃗=(x2,y2,z2)\vec{a}=(x_2, y_2, z_2)a=(x2,y2,z2)

a⃗×b⃗=(x1∗x2,y1∗y2,z1∗z2)\vec{a} \times \vec{b}=(x_1 * x_2, y_1 * y_2, z_1 * z_2)a×b=(x1x2,y1y2,z1z2)

a⃗⋅b⃗=x1∗x2+y1∗y2+z1∗z2\vec{a} \cdot \vec{b}=x_1 * x_2 + y_1 * y_2 + z_1 * z_2ab=x1x2+y1y2+z1z2

此外:∣a⃗∣=x12+y12+z12|\vec{a}|=\sqrt{x_1^2 + y_1^2 + z_1^2}a=x12+y12+z12

关于法向量,就是物体表面的朝向,即垂直于表面的方向。通常每个表面都有正面和反面两面,两个面各自具有一个法向量。在三维图形学中,表面的正面和反面取决于绘制表面顶点的顺序,可用右手法则来确定法线方向。

WebGL系列(5):光照

(2)环境反射

环境反射针对环境光而言。在环境反射中,反射光的方向可以认为是入射光的反方向。由于环境光照射物体的方式是个方向均匀、强度相等的,所以反射光也是个方向均匀的,可以用如下公式描述:

<环境反射光颜色>=<入射光颜色>×<表面基底色><环境反射光颜色>=<入射光颜色>\times<表面基底色><>=<>×<>

漫反射环境反射 同时存在时,两者加起来,就是物体最终被观察到的颜色:

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

二、平行光

下面我们就来绘制一个处于白色平行光照射下的红色三角形

function initArrayBuffer(gl, attribute, data, num, type) {
    // 创建坐标缓冲区对象
    const buffer = gl.createBuffer();
    // 将数据写入缓冲区
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
    // 将缓冲区对象分配给 attribute 变量
    const a_attribute = gl.getAttribLocation(gl.program, attribute);
    if (a_attribute < 0) {
        console.log(`获取 ${attribute} 的存储位置失败!`);
        return false;
    }
    gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);
    // 将缓冲区对象分配给 attribute 变量
    gl.enableVertexAttribArray(a_attribute);

    return true;
}

// 初始化顶点缓冲区
function initVertexBuffer(gl) {
    // 顶点坐标
    const vertices = new Float32Array([
        1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0,  // 前
        1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0,  // 右
        1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0,  // 上
        -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0,  // 左
        -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0,  // 下
        1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0   // 后

    ]);
    // 顶点颜色
    const colors = new Float32Array([
        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, 
        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, 
        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
    ])
    // 法向量
    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, 
        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, 
    ])
    // 顶点索引
    const indices = new Uint8Array([
        0, 1, 2, 0, 2, 3,  // 前
        4, 5, 6, 4, 6, 7,  // 右
        8, 9, 10, 8, 10, 11,  // 上
        12, 13, 14, 12, 14, 15,  // 左
        16, 17, 18, 16, 18, 19,// 下
        20, 21, 22, 20, 22, 23   // 后
    ])
    
    // 坐标信息写入缓冲区
    if (!initArrayBuffer(gl, 'a_Position', vertices, 3, gl.FLOAT)) {
        return -1;
    }
    // 颜色信息写入缓冲区
    if (!initArrayBuffer(gl, 'a_Color', colors, 3, gl.FLOAT)) {
        return -1;
    }
    // 法向量写入缓冲区
    if (!initArrayBuffer(gl, 'a_Normal', normals, 3, gl.FLOAT)) {
        return -1;
    }

    // 创建索引缓冲区
    const indexBuffer  =gl.createBuffer();
    if (!indexBuffer) {
        console.log('创建索引缓冲区对象失败!');
        return -1;
    }
    // 将顶点索引数据写入缓冲区对象
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
    return indices.length;
}


// 顶点着色器程序
const 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' +  // varying 变量
    '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' +
    '  v_Color = vec4(diffuse, a_Color.a);\n' +  // 将数据传递给片元着色器
    '}\n';

const FSHADER_SOURCE = 
    'precision mediump float;\n' + 
    'varying vec4 v_Color;\n' +  // varying 变量
    'void main() {\n' +
    '  gl_FragColor = v_Color;\n' +  // 从顶点着色器接收数据
    '}\n';

function main() {
    // 获取canvas元素
    const canvas = document.getElementById('gl');
    // 获取WebGL绘图上下文
    const gl = canvas.getContext('webgl');
    // 确认WebGL支持性
    if (!gl) {
        console.log('浏览器不支持WebGL');
        return;
    }
    // 初始化着色器
    if(!initShader(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
        console.log('初始化着色器失败!');
        return;
    }

    // 设置顶点位置
    const n = initVertexBuffer(gl);
    if (n < 0) {
        console.log('设置顶点位置失败!');
        return;
    } 

    // 获取 u_LightColor 变量的存储地址
    const u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor');
    if (u_LightColor < 0) {
        console.log('u_LightColor 变量的存储地址获取失败!');
        return;
    }
    // 获取 u_LightDirection 变量的存储地址
    const u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');
    if (u_LightDirection < 0) {
        console.log('u_LightDirection 变量的存储地址获取失败!');
        return;
    }
    // 获取 u_MvpMatrix 变量的存储地址
    const u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
    if (u_MvpMatrix < 0) {
        console.log('u_MvpMatrix 变量的存储地址获取失败!');
        return;
    }

    // 设置光线颜色
    gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0);

    // 设置光线方向
    const lightDirection = new Vector3([0.5, 3.0, 4.0]);
    lightDirection.normalize();  // 归一化
    gl.uniform3fv(u_LightDirection, lightDirection.elements);

    // 设置视点、视线和上方向
    const mvpMatrix = new Matrix4();
    mvpMatrix.setPerspective(30, canvas.clientWidth / canvas.clientHeight, 1, 100)
    mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0)
    // 将试图矩阵传递给 u_MvpMatrix 变量
    gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

    // 设置canvas背景色
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.enable(gl.DEPTH_TEST);
    // 情况canvas
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 

    // 绘制
    gl.drawElements(gl.TRIANGLES , n, gl.UNSIGNED_BYTE, 0);
}

WebGL系列(5):光照

我们首先来看顶点着色器中的代码:

const 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' +  // varying 变量
    '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';
  • a_Color:表面的基底色
  • a_Normal:表面法线方向
  • u_LightColor:入射光颜色
  • u_LightDirection:入射光方向

max(dot(u_LightDirection, normal)用于计算<光线方向><法线方向><光线方向><法线方向><线><线>,如果点积大于0,就将点积赋值给nDotL变量,如果小于0,则赋值0. 点积值小于0,即 cosθcos\thetacosθ值小于0,也就意味着入射角θ\thetaθ大于90度。θ\thetaθ大于90度说明光线照射在表面的背面上。

上述图片中立方体的侧面是黑色的,几乎看不见颜色,但是实际并非如此。那些背光的面是被非直射光照亮的,即环境光。前面也说到:

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

所以我们在程序中加入环境光的照射,再来看看效果:

// 顶点着色器程序
const 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' +  // varying 变量
    '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';

// 获取 u_AmbientLight 变量的存储地址
const u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');
if (u_AmbientLight < 0) {
    console.log('u_AmbientLight 变量的存储地址获取失败!');
    return;
}
// 设置环境光
gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2);

WebGL系列(5):光照

如图所示,完全没有被平行光照射到的表面不是全黑,而是呈现较暗的颜色,与真实世界更加相符。

物体变换后的光照效果

前面我们说过,图形的有平移、旋转、缩放三种变换。它们会对法向量有如下影响:

  • 平移:不会改变法向量,因为平移不会改变物体的方向
  • 旋转:会改变法向量,因为旋转改变了物体的方向
  • 缩放:视情况而定(某些会改变,某些不会改变)

这些变换对于法向量的影响,我们可以通过魔法矩阵——逆转置矩阵来计算。假设各种变换矩阵相乘得到矩阵为模型矩阵,那么变换后的法向量,只需将变换之前的法向量乘以模型矩阵的逆转置矩阵(inverse transpose matrix) 即可。

假设,一个矩阵先进行平移变换,再进行旋转变换,那么最后的法向量计算如下公式所示。

<模型矩阵>=<旋转变换矩阵>×<平移变换矩阵><模型矩阵> = <旋转变换矩阵> \times <平移变换矩阵><>=<>×<>
<变换后的法向量>=<模型矩阵的逆转置矩阵>×<原法向量><变换后的法向量> = <模型矩阵的逆转置矩阵> \times <原法向量><>=<>×<>

逆矩阵:如果矩阵M的逆矩阵为R,那么 R*M 和 M*R 都是单位矩阵

逆转置矩阵:逆矩阵的转置矩阵

下面我们看一个例子:

// 顶点着色器程序
const VSHADER_SOURCE = 
...
    'uniform mat4 u_NormalMatrix;\n' + // 用来变换法向量的矩阵
...
    // 计算变换后的法向量并归一化
    '  vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
...

function main() {
...
    // 获取 u_NormalMatrix 变量的存储地址
    const u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix');
    if (u_NormalMatrix < 0) {
        console.log('u_NormalMatrix 变量的存储地址获取失败!');
        return;
    }

...
    const modelMatrix = new Matrix4();  // 模型矩阵
    const normalMatrix = new Matrix4();  // 用于变换法向量的矩阵
    // 计算模型矩阵
    modelMatrix.setTranslate(0, 1, 0);  // 绕Y轴平移
    modelMatrix.rotate(90, 0, 0, 1);  // 绕Z轴旋转
    // 根据模型矩阵计算变换法向量的矩阵
    normalMatrix.setInverseOf(modelMatrix);
    normalMatrix.transpose();
    // 将变换法向量的矩阵传递给 u_NormalMatrix 变量
    gl.uniformMatrix4fv(u_NormalMatrix, false, normalMatrix.elements);
...
    
}

WebGL系列(5):光照

三、点光源光

点光源发出的光,在三维空间的不同位置上其方向也不同。对点光源光的物体进行着色时,需要每个入射点计算点光源光所处的方向。

// 顶点着色器程序
const VSHADER_SOURCE = 
    'attribute vec4 a_Position;\n' +
    'attribute vec4 a_Color;\n' +
    'attribute vec4 a_Normal;\n' +  // 法向量
    'uniform mat4 u_ModelMatrix;\n' + // 模型矩阵
    'uniform mat4 u_MvpMatrix;\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' +  // varying 变量
    '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' +
    // 计算光线方向和法向量点积
    '  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';

const FSHADER_SOURCE = 
    'precision mediump float;\n' + 
    'varying vec4 v_Color;\n' +  // varying 变量
    'void main() {\n' +
    '  gl_FragColor = v_Color;\n' +  // 从顶点着色器接收数据
    '}\n';

function main() {
    ...
    // 获取 u_NormalMatrix 变量的存储地址
    const u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix');
    if (u_NormalMatrix < 0) {
        console.log('u_NormalMatrix 变量的存储地址获取失败!');
        return;
    }
    // 获取 u_ModelMatrix 变量的存储地址
    const u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix');
    if (u_ModelMatrix < 0) {
        console.log('u_ModelMatrix 变量的存储地址获取失败!');
        return;
    }
    // 获取 u_LightPosition 变量的存储地址
    const u_LightPosition = gl.getUniformLocation(gl.program, 'u_LightPosition');
    if (u_LightPosition < 0) {
        console.log('u_LightPosition 变量的存储地址获取失败!');
        return;
    }

    // 设置光线颜色
    gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0);

    // 设置光线方向
    gl.uniform3f(u_LightPosition, 0.0, 3.0, 4.0);

    // 设置环境光
    gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2);

    const modelMatrix = new Matrix4();  // 模型矩阵
    const normalMatrix = new Matrix4();  // 用于变换法向量的矩阵
    // 计算模型矩阵
    modelMatrix.rotate(90, 0, 0, 1);  // 绕Z轴旋转
    gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements);
    // 根据模型矩阵计算变换法向量的矩阵
    normalMatrix.setInverseOf(modelMatrix);
    normalMatrix.transpose();
    // 将变换法向量的矩阵传递给 u_NormalMatrix 变量
    gl.uniformMatrix4fv(u_NormalMatrix, false, normalMatrix.elements);

    // 设置视点、视线和上方向
    const mvpMatrix = new Matrix4();
    mvpMatrix.setPerspective(30, canvas.clientWidth / canvas.clientHeight, 1, 100)
    mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0)
    // 将试图矩阵传递给 u_MvpMatrix 变量
    gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
...
    
}

WebGL系列(5):光照

四、逐片元计算光照

为了逐片元计算光照,需要知道:

  • 片元在世界坐标系下的坐标
  • 片元处表面的法向量

可以在顶点着色器中,将顶点的世界坐标和法向量以varying变量的形式传入片元着色器,片元着色器中的同名变量就已经是内插后的逐片元值了。

// 顶点着色器程序
const VSHADER_SOURCE = 
    'attribute vec4 a_Position;\n' +
    'attribute vec4 a_Color;\n' +
    'attribute vec4 a_Normal;\n' +  // 法向量
    'uniform mat4 u_ModelMatrix;\n' + // 模型矩阵
    'uniform mat4 u_MvpMatrix;\n' +
    'uniform mat4 u_NormalMatrix;\n' + // 用来变换法向量的矩阵
    'varying vec4 v_Color;\n' +  // varying 变量
    'varying vec3 v_Normal;\n' +
    'varying vec3 v_Position;\n' +
    'void main() {\n' +
    '  gl_Position = u_MvpMatrix * a_Position;\n' +   // 设置顶点坐标
    // 计算顶点的世界坐标
    '  v_Position = vec3(u_ModelMatrix * a_Position);\n' +
    // 计算变换后的法向量并归一化
    '  v_Normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
    '  v_Color = a_Color;\n' +
    '}\n';

const FSHADER_SOURCE = 
    'precision mediump float;\n' + 
    'uniform vec3 u_LightColor;\n' +  // 光线颜色
    'uniform vec3 u_LightPosition;\n' +  // 光源位置
    'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
    'varying vec3 v_Normal;\n' +
    'varying vec3 v_Position;\n' +
    'varying vec4 v_Color;\n' +  // varying 变量
    'void main() {\n' +
    // 对法线进行归一化,因为其内插后长度不一定是1.0
    '  vec3 normal = normalize(v_Normal);\n' +
    // 计算光线方向并归一化
    '  vec3 lightDirection = normalize(u_LightPosition - v_Position);\n' +
    // 计算光线方向和法向量点积
    '  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';

WebGL系列(5):光照

本文就到这里了,主要介绍了在光照效果下,三维场景更加逼真。

参考:

[1] 《WebGL 编程指南》

转载自:https://juejin.cn/post/7158633464622678030
评论
请登录