快速导航(持续更新中)
WebGL系列教程一(开篇)
WebGL系列教程二(环境搭建及着色器初始化)
WebGL系列教程三(使用缓冲区绘制三角形)
WebGL系列教程四(绘制彩色三角形)
WebGL系列教程五(使用索引绘制彩色立方体)
WebGL系列教程六(纹理映射与立方体贴图)
WebGL系列教程七(二维及三维旋转、平移、缩放)
WebGL系列教程八(GLSL着色器基础语法)
WebGL系列教程九(动画)
WebGL系列教程十(模型Model、视图View、投影Projection变换)
WebGL系列教程十一(光照原理及Blinn Phong着色模型)
目录
1 前言
什么是光照?光照就是模拟出物体被光照射时的效果,使得渲染场景看起来更真实。那么WebGL
在干什么?WebGL
其实就是在计算继而还原每个像素的颜色和亮度。这就是我们这一节所要讲的内容,对一个立方体进行光照的渲染。
2 光源的分类
我们常用的光源有点光源、面光源以及环境光。面光源是平行光,因为太阳离地球非常非常远,所以我们处理自然光时将太阳光也认为是面光源。常见的点光源有灯泡、火焰等,点光源照射在物体表面的角度是不一样的,因为会对着色有较大影响。环境光是指被墙壁等物体经过多次反射之后的光,环境光会从各个角度去照射物体,目前我们认为他们的强度都是一样的,且强度很小,因此通过一个微小的的值来代替(想要精确还原的同学可以自行学习光线追踪来进行模拟)。
3 光照的渲染
在进行光照的渲染时,可以采用不同的方式,比如对面进行着色(Flat Shading
),对顶点进行着色(Ground Shading
),对像素进行着色(Phong Shading
)。
本文中采用的是逐像素进行着色,即 Blinn Phong Shading
,是在Phong Shading
的基础上对高光项进行了改进。Blinn Phong Shading
对光照进行渲染时分成了三个部分,即高光、漫反射、环境光。
3.1 高光
如图所示,v
为观察方向,R
为镜面反射方向,当R
和v
足够接近时,就会产生高光。这两向量的夹角可以通过单位向量点乘得到。因为这个角度不好计算,因此取入射方向加上出射方向结果的一半,即半程向量h
和法线n
的夹角来计算这个角度。在实际应用中,会对这个夹角的余弦值取m
次方来满足需要。
可以看出,次方m
取值越大,高光的范围越小。
3.2 漫反射
漫反射的反射光在各个方向上是均匀分布的,现实中的很多材质,比如纸张、岩石、塑料,表面都是粗糙的,在这种情况下,反射光将会以不固定的角度反射出去,漫反射正是在此基础上建立的反射模型。
3.3 环境光
环境光下的反射称为环境反射,环境反射光的方向可以认为就是入射光的反方向,由于环境光照射物体的方式是各方向均匀的、强度相等的,所以反射光也是各项均匀的。
4 代码实现
好了,讲完了理论,我们来进行一下实操。现在我们要对每个像素进行操作,因此先在片元着色器中进行声明:
// u_LightColor: 光源的颜色
uniform vec3 u_LightColor;
// u_LightPosition: 光源的位置(世界坐标系中)
uniform vec3 u_LightPosition;
// u_AmbientLight: 环境光的颜色
uniform vec3 u_AmbientLight;
// u_ViewPosition: 观察者的位置(世界坐标系中)
uniform vec3 u_ViewPosition;
// u_Shininess: 高光反射的光泽度系数
uniform float u_Shininess;
// v_Normal: 从顶点着色器传递过来的法向量(世界坐标系中)
varying vec3 v_Normal;
// v_Position: 从顶点着色器传递过来的顶点位置(世界坐标系中)
varying vec3 v_Position;
// v_Color: 从顶点着色器传递过来的顶点颜色
varying vec4 v_Color;
4.1 计算逆转置矩阵
首先我们要搞明白什么是逆转置矩阵?对一个矩阵先求逆矩阵,然后再求转置,就得到了逆转置矩阵。那么逆转置矩阵是干什么用的?我们之前讲过模型矩阵是对顶点进行变换的,逆转置矩阵是对法线进行变换的。当我们对模型进行了旋转、平移、缩放后,模型的法线也要跟随着变化,这时用逆转置矩阵就可以很方便的求出新的法线了。因此逆转置矩阵针对的是模型矩阵。
var modelMatrix = new Matrix4(); // 模型矩阵
// 计算用于变换法向量的矩阵,即逆转置矩阵
normalMatrix.setInverseOf(modelMatrix);
normalMatrix.transpose();
4.2 对法向量进行变换
// u_NormalMatrix: 法向量变换矩阵,用于正确变换法向量
//glsl
uniform mat4 u_NormalMatrix;
// 对法向量进行变换并归一化
v_Normal = normalize(vec3(u_NormalMatrix * a_Normal));
var u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix');
// 将法向量变换矩阵传递给 u_NormalMatrix
gl.uniformMatrix4fv(u_NormalMatrix, false, normalMatrix.elements);
4.3 计算法向量和光线方向的点积
// 计算从光源到片段的方向向量并进行归一化
vec3 lightDirection = normalize(u_LightPosition - v_Position);
// 计算法向量和光线方向的点积(用于漫反射计算)
float nDotL = max(dot(lightDirection, normal), 0.0);
4.4 计算漫反射分量
// 计算漫反射分量
vec3 diffuse = u_LightColor * nDotL;
4.5 计算环境光分量
// 计算环境光分量
vec3 ambient = u_AmbientLight * v_Color.rgb;
4.6 计算视线方向单位向量
// 计算视线方向向量(从观察者到片段的位置)并进行归一化
vec3 viewDirection = normalize(u_ViewPosition - v_Position);
4.7 计算反射向量
// 计算反射方向向量
vec3 reflectDirection = reflect(-lightDirection, normal);
4.8 计算视线方向和反射方向的点积
// 计算视线方向和反射方向的点积,并根据光泽度系数计算高光反射分量
float spec = pow(max(dot(viewDirection, reflectDirection), 0.0), u_Shininess);
4.9 计算高光
// 计算高光分量,使其接近白色
vec3 specular = vec3(1.0, 1.0, 1.0) * spec;
4.10 组合漫反射、环境光和高光
// 最终颜色由漫反射、环境光和高光三部分组成
gl_FragColor = vec4(diffuse + ambient + specular, v_Color.a);
4.11 完整代码
// 顶点着色器
<script id="vertex-shader" type="x-shader/x-vertex">
// a_Position: 顶点位置
// a_Color: 顶点颜色
// a_Normal: 顶点法向量
attribute vec4 a_Position;
attribute vec4 a_Color;
attribute vec4 a_Normal;
// u_MvpMatrix: 模型视图投影矩阵,用于将顶点从模型坐标转换为裁剪坐标
uniform mat4 u_MvpMatrix;
// u_ModelMatrix: 模型矩阵,用于将顶点从局部坐标变换到世界坐标
uniform mat4 u_ModelMatrix;
// u_NormalMatrix: 法向量变换矩阵,用于正确变换法向量
uniform mat4 u_NormalMatrix;
// v_Color: 传递给片段着色器的顶点颜色
varying vec4 v_Color;
// v_Normal: 传递给片段着色器的法向量(经过变换后的世界坐标系中的法向量)
varying vec3 v_Normal;
// v_Position: 传递给片段着色器的顶点位置(世界坐标系中的位置)
varying vec3 v_Position;
void main() {
// 计算顶点位置在裁剪坐标系中的位置
gl_Position = u_MvpMatrix * a_Position;
// 计算顶点在世界坐标系中的位置
v_Position = vec3(u_ModelMatrix * a_Position);
// 对法向量进行变换并归一化
v_Normal = normalize(vec3(u_NormalMatrix * a_Normal));
// 将顶点颜色传递给片段着色器
v_Color = a_Color;
}
</script>
// 片段着色器
<script id="vertex-shader" type="x-shader/x-vertex">
#ifdef GL_ES
precision mediump float; // 设置浮点数精度
#endif
// u_LightColor: 光源的颜色
uniform vec3 u_LightColor;
// u_LightPosition: 光源的位置(世界坐标系中)
uniform vec3 u_LightPosition;
// u_AmbientLight: 环境光的颜色
uniform vec3 u_AmbientLight;
// u_ViewPosition: 观察者的位置(世界坐标系中)
uniform vec3 u_ViewPosition;
// u_Shininess: 高光反射的光泽度系数
uniform float u_Shininess;
// v_Normal: 从顶点着色器传递过来的法向量(世界坐标系中)
varying vec3 v_Normal;
// v_Position: 从顶点着色器传递过来的顶点位置(世界坐标系中)
varying vec3 v_Position;
// v_Color: 从顶点着色器传递过来的顶点颜色
varying vec4 v_Color;
void main() {
// 对法向量进行归一化处理
vec3 normal = normalize(v_Normal);
// 计算从光源到片段的方向向量并进行归一化
vec3 lightDirection = normalize(u_LightPosition - v_Position);
// 计算法向量和光线方向的点积(用于漫反射计算)
float nDotL = max(dot(lightDirection, normal), 0.0);
// 计算漫反射分量
vec3 diffuse = u_LightColor * nDotL;
// 计算环境光分量
vec3 ambient = u_AmbientLight * v_Color.rgb;
// 计算视线方向向量(从观察者到片段的位置)并进行归一化
vec3 viewDirection = normalize(u_ViewPosition - v_Position);
// 计算反射方向向量
vec3 reflectDirection = reflect(-lightDirection, normal);
// 计算视线方向和反射方向的点积,并根据光泽度系数计算高光反射分量
float spec = pow(max(dot(viewDirection, reflectDirection), 0.0), u_Shininess);
// 计算高光分量,使其接近白色
vec3 specular = vec3(1.0, 1.0, 1.0) * spec;
// 最终颜色由漫反射、环境光和高光三部分组成
gl_FragColor = vec4(diffuse + ambient + specular, v_Color.a);
}
</script>
function main() {
// 获取 <canvas> 元素
var canvas = document.getElementById('webgl');
// 获取 WebGL 的渲染上下文
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('无法获取 WebGL 的渲染上下文');
return;
}
// 初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('无法初始化着色器');
return;
}
// 初始化顶点缓冲区
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('无法设置顶点信息');
return;
}
// 设置清除颜色并启用深度测试
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.enable(gl.DEPTH_TEST);
// 获取 uniform 变量的存储位置
var u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix');
var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
var u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix');
var u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor');
var u_LightPosition = gl.getUniformLocation(gl.program, 'u_LightPosition');
var u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');
var u_ViewPosition = gl.getUniformLocation(gl.program, 'u_ViewPosition');
var u_Shininess = gl.getUniformLocation(gl.program, 'u_Shininess');
if (!u_ModelMatrix || !u_MvpMatrix || !u_NormalMatrix || !u_LightColor || !u_LightPosition || !u_AmbientLight || !u_ViewPosition || !u_Shininess) {
console.log('无法获取存储位置');
return;
}
// 设置光的颜色(白色)
gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0);
// 设置光源的位置(世界坐标系中)
gl.uniform3f(u_LightPosition, 8.5, 4.0, 3.5);
// 设置环境光
gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2);
// 设置观察者的位置
gl.uniform3f(u_ViewPosition, 6.0, 6.0, 14.0);
// 设置光泽度系数
gl.uniform1f(u_Shininess, 128.0);
var modelMatrix = new Matrix4(); // 模型矩阵
var mvpMatrix = new Matrix4(); // 模型视图投影矩阵
var normalMatrix = new Matrix4(); // 法向量变换矩阵
var currentAngle = 0.0; // Current rotation angle
function tick(){
currentAngle = animate(currentAngle); // Update the rotation angle
// 计算模型矩阵
modelMatrix.setRotate(currentAngle, 0, 1, 0); // 绕 y 轴旋转
// 计算视图投影矩阵
mvpMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
mvpMatrix.lookAt(6, 6, 14, 0, 0, 0, 0, 1, 0);
mvpMatrix.multiply(modelMatrix);
// 计算用于变换法向量的矩阵,即逆转置矩阵
normalMatrix.setInverseOf(modelMatrix);
normalMatrix.transpose();
// 将模型矩阵传递给 u_ModelMatrix
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements);
// 将模型视图投影矩阵传递给 u_MvpMatrix
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
// 将法向量变换矩阵传递给 u_NormalMatrix
gl.uniformMatrix4fv(u_NormalMatrix, false, normalMatrix.elements);
// 清除颜色和深度缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 绘制立方体
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
requestAnimationFrame(tick, canvas); // 开启动画,每一帧都去调用
}
tick();
}
function initVertexBuffers(gl) {
// 创建立方体
// v6----- v5
// /| /|
// v1------v0|
// | | | |
// | |v7---|-|v4
// |/ |/
// v2------v3
// 顶点坐标
var vertices = new Float32Array([
2.0, 2.0, 2.0, -2.0, 2.0, 2.0, -2.0,-2.0, 2.0, 2.0,-2.0, 2.0, // v0-v1-v2-v3 前面
2.0, 2.0, 2.0, 2.0,-2.0, 2.0, 2.0,-2.0,-2.0, 2.0, 2.0,-2.0, // v0-v3-v4-v5 右面
2.0, 2.0, 2.0, 2.0, 2.0,-2.0, -2.0, 2.0,-2.0, -2.0, 2.0, 2.0, // v0-v5-v6-v1 上面
-2.0, 2.0, 2.0, -2.0, 2.0,-2.0, -2.0,-2.0,-2.0, -2.0,-2.0, 2.0, // v1-v6-v7-v2 左面
-2.0,-2.0,-2.0, 2.0,-2.0,-2.0, 2.0,-2.0, 2.0, -2.0,-2.0, 2.0, // v7-v4-v3-v2 下面
2.0,-2.0,-2.0, -2.0,-2.0,-2.0, -2.0, 2.0,-2.0, 2.0, 2.0,-2.0 // v4-v7-v6-v5 后面
]);
// 颜色
var colors = 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, // v0-v1-v2-v3 前面
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, // v0-v3-v4-v5 右面
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, // v0-v5-v6-v1 上面
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, // v1-v6-v7-v2 左面
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, // v7-v4-v3-v2 下面
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0 // v4-v7-v6-v5 后面
]);
// 法向量
var 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, // v0-v1-v2-v3 前面
1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, // v0-v3-v4-v5 右面
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // v0-v5-v6-v1 上面
-1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, // v1-v6-v7-v2 左面
0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, // v7-v4-v3-v2 下面
0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0 // v4-v7-v6-v5 后面
]);
// 顶点的索引
var 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)) return -1;
if (!initArrayBuffer(gl, 'a_Color', colors, 3)) return -1;
if (!initArrayBuffer(gl, 'a_Normal', normals, 3)) return -1;
// 解除绑定缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 将顶点索引写入缓冲区对象
var indexBuffer = gl.createBuffer();
if (!indexBuffer) {
console.log('无法创建缓冲区对象');
return false;
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
return indices.length;
}
function initArrayBuffer(gl, attribute, data, num) {
// 创建缓冲区对象
var buffer = gl.createBuffer();
if (!buffer) {
console.log('无法创建缓冲区对象');
return false;
}
// 将数据写入缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
// 将缓冲区对象分配给 attribute 变量
var a_attribute = gl.getAttribLocation(gl.program, attribute);
if (a_attribute < 0) {
console.log('无法获取 ' + attribute + ' 的存储位置');
return false;
}
gl.vertexAttribPointer(a_attribute, num, gl.FLOAT, false, 0, 0);
// 启用缓冲区对象分配
gl.enableVertexAttribArray(a_attribute);
return true;
}
// Rotation angle (degrees/second)
var ANGLE_STEP = 30.0;
// Last time that this function was called
var g_last = Date.now();
function animate(angle) {
// Calculate the elapsed time
var now = Date.now();
var elapsed = now - g_last;
g_last = now;
// Update the current rotation angle (adjusted by the elapsed time)
var newAngle = angle + (ANGLE_STEP * elapsed) / 1000.0;
return newAngle %= 360;
}
4.12 效果
看下动画效果
5 总结
本文介绍了光照原理的基础,并在此基础上讲解了法线计算、入射光计算、反射光计算、逆转置矩阵等等,最后我们使用Blinn Phong
着色模型,通过组合高光、漫反射和环境光,实现了对一个立方体的动态光照效果渲染。本文在理解上有一定的难度,希望读者仔细体会,回见~