案例查看地址:点击这里
通过上一节的介绍,我们可以知道,平行光下的漫反射光的颜色可以由一下式子得出:
<漫反射光颜色>=<入射光颜色>x<表面基底色>x cos θ
所以,我们要计算出来漫反射的光的颜色需要三项数据:
(1)平行入射光的颜色
(2)表面的基底色
(3)入射光与表面形成的入射角 θ
但是,我们没有办法直接就确定 θ值是多少,我们必须根据入射光的方向和物体表面的朝向(即法线方向)来计算出入射角。这需要我们先确定每个表面的朝向,在指定光源的时候,再确定光的方向,来计算出入射角,并确定 θ的值。
幸运的是,我们可以通过计算两个矢量的点积,来计算着两个矢量的夹角余弦值cos θ。点积运算的使用非常频繁,GLSL ES内置了点积运算函数(详见GLSL ES中的第11节,11 WebGL 着色器编程语言GLSL ES的内置函数)。在公式中,我们使用点符号 · 来表示点积运算。这样,cos θ就可以通过下式计算出来:
cos θ = <光线方向> · <法线方向>
所以,上面计算平行光下的漫反射的颜色的式子就可以修改成:
<漫反射光颜色> = <入射光颜色> x <表面基底色> x (<光线方向> · <法线方向>)
这里有两点需要注意:
(1)光线方向矢量和表面法线矢量的长度必须为1,否则反射光的颜色就会过暗或过亮。将一个矢量的长度调整为1,同时保持方向不变的过程称之为归一化。GLSL ES提供了内置的归一化函数,你可以直接使用。
(2)这里所谓的“光线方向”,实际上是入射方向的反方向,即从入射点指向光源方向(因为这样,该方向与法线方向的夹角才是入射角),如图。
法线:表面的朝向
物体表面的朝向,即垂直于表面的方向,又称法线或法向量。法向量有三个分量,向量(nx,ny,nz)表示从原点(0,0,0)指向点(nx,ny,nz)的方向。比如说,向量(1,0,0)表示x轴正方向,向量(0,0,1)表示z轴正方向。涉及到表面和法向量的问题时,必须考虑以下两点:
(1)一个表面具有两个法向量
每个表面都有两个面,“正面”和“背面”。两个面各自具有一个法向量。比如,垂直于z轴的x-y平面,其正面的法向量为z正半轴,即(0,0,1),背面的法向量为z负半轴,即(0,0,-1)。
在三维图形学中,表面的正面和背面取决于绘制表面时的顶点顺序。当你按照v0,v1,v2,v3的顶点顺序绘制了一个平面,那么当你从正面观察这个表面时,这4个顶点是顺时针的,而你从背面观察该表面,这四个顶点就是逆时针的。
(2)平面的法向量唯一
由于法向量表示的是方向,与位置无关,所以一个平面只有一个法向量。换句话说,平面的任意一点都具有相同的法向量。
进一步说,即使有两个不同的平面,只要朝向相同(也就是两个平面平行),法向量也相同。
一旦计算好每个平面的法向量,接下来的任务就是将数据传给着色器程序。以前的程序把颜色作为“逐顶点数据”存储在缓冲区中,并传给着色器。对法向量数据也可以这样做。上图所示,每个顶点对应3个法向量,就像之前每个顶点都对应3个颜色值一样。我们明白了这一点以后,就可以通过法向量获取平行光照后的颜色值了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Title</title>
<style>
body {
margin: 0;
text-align: center;
}
#canvas {
margin: 0;
}
</style>
</head>
<body οnlοad="main()">
<canvas id="canvas" height="800" width="1200"></canvas>
</body>
<script src="lib/webgl-utils.js"></script>
<script src="lib/webgl-debug.js"></script>
<script src="lib/cuon-utils.js"></script>
<script src="lib/cuon-matrix.js"></script>
<script>
//顶点着色器
var 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" +
"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" +
"}";
//片元着色器
var FSHADER_SOURCE = "" +
"#ifdef GL_ES\n" +
"precision mediump float;\n" +
"#endif\n" +
"varying vec4 v_Color;\n" +
"void main(){\n" +
" gl_FragColor = v_Color;\n" +
"}";
//主函数,页面加载完成触发
function main() {
//获取canvas对象
var canvas = document.getElementById("canvas");
//获取WebGL上下文
var gl = getWebGLContext(canvas);
if (!gl) {
console("您的浏览器不支持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, 1);
gl.enable(gl.DEPTH_TEST);
//获取模型视图投影矩阵、光线颜色变量和归一化世界坐标uniform变量的存储位置
var u_MvpMatrix = gl.getUniformLocation(gl.program, "u_MvpMatrix");
var u_LightColor = gl.getUniformLocation(gl.program, "u_LightColor");
var u_LightDirection = gl.getUniformLocation(gl.program, "u_LightDirection");
if (!u_MvpMatrix || !u_LightColor || !u_LightDirection) {
console.log("无法获取相关的存储位置,或者未定义");
return;
}
//设置光线颜色(白色)
gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0);
//设置光线方向(世界坐标系下的)
var lightDirection = new Vector3([0.5, 3.0, 4.0]);
lightDirection.normalize(); //归一化
gl.uniform3fv(u_LightDirection, lightDirection.elements);
//计算模型视图投影矩阵
var mvpMatrix = new Matrix4(); //声明一个矩阵模型
mvpMatrix.setPerspective(30, canvas.width / canvas.height, 1, 100);//设置透视矩阵
mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0);
//将模型视图投影矩阵传给u_MvpMatrix变量
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
//清除底色和深度缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0); //绘制图形
}
function initVertexBuffers(gl) {
// 绘制一个立方体
// v6----- v5
// /| /|
// v1------v0|
// | | | |
// | |v7---|-|v4
// |/ |/
// v2------v3
var 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, // v0-v1-v2-v3 front
1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, // v0-v3-v4-v5 right
1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, // v0-v5-v6-v1 up
-1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, // v1-v6-v7-v2 left
-1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, // v7-v4-v3-v2 down
1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0 // v4-v7-v6-v5 back
]);
var colors = new Float32Array([ // 顶点颜色
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0-v1-v2-v3 front
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0-v3-v4-v5 right
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0-v5-v6-v1 up
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v1-v6-v7-v2 left
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v7-v4-v3-v2 down
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0 // v4-v7-v6-v5 back
]);
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 front
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 right
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 up
-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 left
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 down
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 back
]);
// 绘制点的顺序下标
var indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // front
4, 5, 6, 4, 6, 7, // right
8, 9, 10, 8, 10, 11, // up
12, 13, 14, 12, 14, 15, // left
16, 17, 18, 16, 18, 19, // down
20, 21, 22, 20, 22, 23 // back
]);
// 通过initArrayBuffer方法将顶点数据保存到缓冲区
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;
// 创建顶点索引缓冲区对象
var 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;
}
function initArrayBuffer(gl, attribute, data, num, type) {
//创建缓冲区对象
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);
//获取相关变量存储位置,赋值并开启缓冲区
var 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)
//开启数据,并解绑缓冲区
gl.enableVertexAttribArray(a_attribute);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
return true;
}
</script>
</html>
在顶点着色器中,我们实现了漫反射光颜色的计算,使用的是:
<漫反射光颜色> = <入射光颜色> x <表面基底色> x (<光线方向> · <法线方向>)
计算漫反射光颜色需要:(1)入射光颜色 (2)表面基底色 (3)入射光方向 (4)表面法线方向。其中后两者都必须是归一化的(即长度为1.0)。
在上面的案例代码中:
a_Color 变量表示表面基底色
a_Normal变量表示表面法线方向
u_LightColor变量表示入射光颜色
u_LightDirection变量表示入射光方向(是在世界坐标系下的,在javascript中归一化了,可以避免着色器每次执行都对它进行归一化)
相关的变量已经获取,下面是进行处理:
着色器部分处理:
首先对a_Normal法线变量进行归一化,虽然写的值是已经归一化过的,但是顶点着色器可不知道传入的矢量是否经过了归一化,而且这里没有节省开销的理由(法向量是逐顶点的),由于使用x,y和z三个分量就可以表示法线方向,我们需要将这三个分量提取出来,使用到了vec3()方法,然后使用内置函数normalize()函数进行归一化。
接下来,我们计算点积<光线方向> · <法线方向>。法线方向已经被归一化,光线方向在传入进来之前已经被归一化。直接使用GLSL ES提供的内置函数dot()进行点积运算。然后又使用max()函数时因为如果计算出来的数大于90度的话,就已经在背面了。我们这是计算的不透明的,所以在背面就是0。
计算出来点积后,就可以继续套用公式进行漫反射光的颜色,计算出漫反射光的rgb颜色。
最后再把rgba的a给补上,最后就把颜色给计算了出来,再把颜色通过varying变量v_Color传到片元着色器。
js部分处理:
首先就给u_LightColor赋值了光线的颜色。
然后给u_LightDirection赋值光线方向,再使用cuon-matrix.js里面封装好的函数,直接在对象上调用一下,使对象的值归一化,最后把值赋值给了u_LightDirection。
最后在颜色下面声明了一个存储法向量的Float32Array将值存储,和顶点和颜色一起赋值。