注:文章译自http://wgld.org/,原作者杉本雅広(doxas),文章中假设有我的额外说明。我会加上[lufy:],另外,鄙人webgl研究还不够深入,一些专业词语。假设翻译有误,欢迎大家指正。
照亮世界
上次绘制了一个像甜甜圈一样的圆环体模型,尽管没有涉及特别的新知识,可是也算成功的绘制出了3D模型了吧。
那么,这次来看一下光。
光在3D渲染中有非常多种类和用法。想把光研究透彻。也是非常不easy的。
现实世界中我们能看到物体,是由于物体反射的光进入我们的眼睛。也就是说。没有光的话,我们的眼睛是看不到不论什么东西的。在3D的编程世界里,就算没有光,也能够对模型进行渲染。眼下为止,并没有使用过光照处理,也一样绘制了多边形。可是,假设在模拟的世界中加入了光,那么3D的视觉效果会得到巨大的飞跃。
这次介绍的光,是从一般的平行光源(定向灯)发出的光,是实现起来比較简单的一种光。在具体介绍平行光源之前,先来简单说一下光。
光的模拟
平行光源发出的光,就要处理光的遮挡,也就是说要处理影子的效果。这个看一下这次的demo的执行结果。和上篇文章中的demo对照一下就明确了。
处理光的时候,和光碰撞的部分颜色应该是明的,而没有和光碰撞的部分颜色应该是暗的。假设没有光的话,全部的颜色的亮度都应该是一样的。模拟光的时候,在没有光的一側。应该加入影子。
WebGL中,颜色的强度的范围在0 ~ 1 之间。依据RGBA的各个要素中,设定的值的不同来决定。处理光的时候,在原来的RGBA的值上乘与对应的系数。这个系数的范围也是0 ~ 1 之间,有光的一面,显示为原色相近的状态。而背光的一面则使用较暗的颜色。
比方说,RGBA的各个元素为0.5的话,光系数为0.5,这样相乘的话。得到RGBA的各元素为 0.5 x 0.5 = 0.25,这样就比原来的颜色暗了。依照这个原理。分别计算对应的光的强度和颜色的强度,然后相乘,终于就能处理光和影了。
什么是平行光源
平行光源,是从无限远的地方发出,并使得发出的光在整个三维空间中始终保持平行的光源。这个概念听起来比較难理解。
主要是说,光的方向保持一致,相对于三维空间中的不论什么一个模型来说。光照的方向都是一样的,例如以下图
黄色的箭头表示光的方向。
平行光源发出的光计算的负担并不算大,实现起来比較简单。所以在3D编程中常常使用。并且。和平行光源发出的光的碰撞,须要知道光的方向。能够用向量来定义。然后传给着色器,就能够实现了。 可是,实际上。仅仅有光的方向是实现不了光照的效果的,还须要顶点的法线情报。那么法线是什么呢,以下来具体介绍。
法线向量和光向量
对3D编程和数学不太了解的人。基本上没怎么听说过法线这个词。简单来说。法线就是一个带有方向的向量。在二维空间中为了表示某线相垂直的方向,在三维空间中为了表示某个面的方向。要使用法线向量。 可是,为什么实现光照效果的时候,除了光的方向还须要光的法线呢?
现实世界中。从太阳或者灯发出来的光,照到物体上之后会发生反射,所以考虑到反射。从光源发出的光。碰到物体表面之后。发生反射,光的方向发生了改变。 上图中的粉色的线表示光的轨道,和面向光的面相撞后,方向就会发生改变。这样,形成模型的面的方向就能左右光的轨道。 3D中的光,仅仅是在一定程度上进行光的模拟演算,没有必要全然模拟现实世界中的光的轨道和运动。由于全然模拟的话,那计算量就太大了。这次的平行光源发出的光,以顶点的法线向量和光的方向(光向量)为基础,一定程度上计算光的扩散,反射等。
光垂直射向一个面的话。这个面会将光全然反射,也就是说,对光的影响非常大。反之,一个面没有光的话。光也就全然不会扩散,例如以下图。
光向量和法线向量之间的夹角超过90度的话,就对光没有影响力了。这个计算,用向量之间的内积能够得到。*内积这里就不详解了。想具体了解的人能够自己查一下相关资料。
内积能够通过着色器内置的函数轻松的进行计算,这个不须要操心。仅仅要准备好正确的数据。剩下的计算交给WebGL即可了。
所以。这一回必须要改动顶点着色器。当然javascript部分也须要进行改动,来慢慢看吧。
定向灯的着色器
那么,先来看着色器的部分吧。这次对着色器的改动仅仅是针对顶点着色器。在顶点着色器中进行光的计算。然后将计算结果传给片段着色器。
>顶点着色器的代码
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform mat4 mvpMatrix;
uniform mat4 invMatrix;
uniform vec3 lightDirection;
varying vec4 vColor;
void main(void){
vec3 invLight = normalize(invMatrix * vec4(lightDirection, 0.0)).xyz;
float diffuse = clamp(dot(normal, invLight), 0.1, 1.0);
vColor = color * vec4(vec3(diffuse), 1.0);
gl_Position = mvpMatrix * vec4(position, 1.0);
}
如今的demo和之前有非常大不同,乍一看貌似挺复杂,来看一下具体变更点。
首先,从变量開始。
着色器的attribute变量中,新加入了normal,这个变量用来储存顶点的法线信息。而uniform函数添加了两个。一个是用来接收模型坐标变换矩阵的逆矩阵的变量invMatrix,还有一个是用来接收光的方向。也就是从平行光源发出的光的方向的向量的变量lightDirection。
什么是逆矩阵呢? 这次在顶点着色器中加入的invMatrix是用来保存模型坐标变换矩阵的逆矩阵的变量,预计大多数人都不知道什么叫做逆矩阵吧。 平行光源发出的光(定向灯发出的光)通常须要光向量,三维空间中的全部的模型都被同一方向的光照耀。可是,试想一下。通过模型坐标变换。能够对模型的放大缩小,旋转。移动。假设仅仅通过法线和光向量进行计算的话,会受到光的方向。位置,模型的方向,位置等的影响。 本来正确的光的位置和方向。由于受到模型坐标变换的影响,就得不到正确的结果了。因此,通过对模型的坐标变换进行全然的逆变换,来抵消模型坐标变换的影响。 模型沿着x轴旋转45度的话,就向反方向旋转45度,这样就抵消了旋转。模型即使发生了旋转,光源位置和光的方向也能够固定。相同,对模型进行缩放的话,是矩阵相乘运算,能够通过和逆矩阵相乘来抵消。 这样,就须要为光准备一个模型坐标变换矩阵的逆矩阵,在minMatrix.js中提供了生成逆矩阵的函数,本站点使用它来进行光的计算。 |
接着。光照的时候,还须要计算光系数,这部分代码例如以下。
>光照系数的计算
vec3 invLight = normalize(invMatrix * vec4(lightDirection, 0.0)).xyz;
float diffuse = clamp(dot(normal, invLight), 0.1, 1.0);
vColor = color * vec4(vec3(diffuse), 1.0);
首先,最開始声明一个vec3类型的变量invLight,并且进行了一些计算。
最開始的normalize是一个内置函数。作用是将向量标准化。使用这个函数,将模型坐标变换的逆矩阵和光向量相乘的结果进行标准化。模型进行了旋转等坐标变换的话,也能够通过逆变换来抵消。这个计算的后面还有个.xyz,这个是为了把变换结果作为正确的三维向量来代入。
接着是float类型的变量diffuse的值的获取。事实上这里是求法线和光向量的内积。这里出现的clamp和dot都是GLSL的内置函数,clamp是将值限制在一定的范围内,第二个參数是最小值,第三个參数是最大值。
之所以要限制范围。是由于向量的内积可能出现负数值。为了防止这样的情况而进行的处理。
还有一个内置函数是dot是用来求内积的,參数一个是法线。还有一个是经过逆矩阵处理后的光向量。
最后,将算出的光系数。和顶点颜色相乘,将结果传给varying变量。片段着色器中。通过接收到的这个參数。来决定终于的颜色。
向VBO中追加法线信息
这次改动的地方比較多,javascript也来看一下吧。
上一篇中,生成圆环体的顶点数据的函数略微做了一些改动,改动的内容是。将法线的信息也一起返回。上一篇为止,仅仅返回了位置,颜色,索引这三个,法线情报也须要返回。
法线就是上面说的那样,是一个表示方向的向量,和位置情报一样,用 X Y Z 这三个元素来表示。另外,法线标准化之后的范围在0 ~ 1之间。 >生成圆环体和法线信息的加入
// 生成圆环体的函数
function torus(row, column, irad, orad){
var pos = new Array(), nor = new Array(),
col = new Array(), idx = new Array();
for(var i = 0; i <= row; i++){
var r = Math.PI * 2 / row * i;
var rr = Math.cos(r);
var ry = Math.sin(r);
for(var ii = 0; ii <= column; ii++){
var tr = Math.PI * 2 / column * ii;
var tx = (rr * irad + orad) * Math.cos(tr);
var ty = ry * irad;
var tz = (rr * irad + orad) * Math.sin(tr);
var rx = rr * Math.cos(tr);
var rz = rr * Math.sin(tr);
pos.push(tx, ty, tz);
nor.push(rx, ry, rz);
var tc = hsva(360 / column * ii, 1, 1, 1);
col.push(tc[0], tc[1], tc[2], tc[3]);
}
}
for(i = 0; i < row; i++){
for(ii = 0; ii < column; ii++){
r = (column + 1) * i + ii;
idx.push(r, r + column + 1, r + 1);
idx.push(r + column + 1, r + column + 2, r + 1);
}
}
return [pos, nor, col, idx];
}
从生成圆环体的函数,返回了对应的法线信息。须要注意的是,生成圆环体的函数中,返回的数组中元素的顺序是[ 位置信息 ]?[ 法线信息 ]?[ 顶点颜色 ]?[ 索引 ]。
函数中都做了什么。可能一眼看不出来,可是想要做的处理和前面一样。就是将法线情报标准化,圆环体的顶点坐标的输出部分和法线信息的输出部分都分别做了处理。
接着,来看一下在生成圆环体的函数被调用的部分。
>关于顶点数据的处理
// 获取attributeLocation并放入数组
var attLocation = new Array();
attLocation[0] = gl.getAttribLocation(prg, ‘position‘);
attLocation[1] = gl.getAttribLocation(prg, ‘normal‘);
attLocation[2] = gl.getAttribLocation(prg, ‘color‘);
// 将attribute的元素个数保存到数组中
var attStride = new Array();
attStride[0] = 3;
attStride[1] = 3;
attStride[2] = 4;
// 生成圆环体的顶点数据
var torusData = torus(32, 32, 1.0, 2.0);
var position = torusData[0];
var normal = torusData[1];
var color = torusData[2];
var index = torusData[3];
// 生成VBO
var pos_vbo = create_vbo(position);
var nor_vbo = create_vbo(normal);
var col_vbo = create_vbo(color);
和曾经的demo不同,为了处理法线。添加了一个数组normal,并且利用这个数组生成了一个VBO。而为了在顶点着色器中接收法线信息。声明了一个attribute类型的变量。所以不要忘了获取attributeLocation。
另外。还添加了一个uniform类型的变量。所以也须要追加一个获取uniformLocation的处理。
>uniform相关的处理
// 获取uniformLocation并保存到数组中
var uniLocation = new Array();
uniLocation[0] = gl.getUniformLocation(prg, ‘mvpMatrix‘);
uniLocation[1] = gl.getUniformLocation(prg, ‘invMatrix‘);
uniLocation[2] = gl.getUniformLocation(prg, ‘lightDirection‘);
最開始可能不easy把握。总值着色器和脚本是不可切割的。两方必须成对出如今代码中。
加入关于光的处理
那么最后,看一下将与光相关的參数传入着色器的处理,首先是代码。
>定义光和矩阵相关的数据
// 各矩阵的生成和初始化
var mMatrix = m.identity(m.create());
var vMatrix = m.identity(m.create());
var pMatrix = m.identity(m.create());
var tmpMatrix = m.identity(m.create());
var mvpMatrix = m.identity(m.create());
var invMatrix = m.identity(m.create());
// 视图x投影坐标变换矩阵
m.lookAt([0.0, 0.0, 20.0], [0, 0, 0], [0, 1, 0], vMatrix);
m.perspective(45, c.width / c.height, 0.1, 100, pMatrix);
m.multiply(pMatrix, vMatrix, tmpMatrix);
// 平行光源的方向
var lightDirection = [-0.5, 0.5, 0.5];
定义了一个含有三个元素的向量lightDirection,这一次定义的光。是从左后方向原点前进的光。另外。矩阵的初始化部分,添加了新的invMatrix,这个invMatrix中的数据例如以下。
>逆矩阵的定义和生成
// 计数器自增
count++;
// 用计数器计算角度
var rad = (count % 360) * Math.PI / 180;
// 模型坐标变换矩阵的生成
m.identity(mMatrix);
m.rotate(mMatrix, rad, [0, 1, 1], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
// 依据模型坐标变换矩阵生成逆矩阵
m.inverse(mMatrix, invMatrix);
// uniform变量
gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
gl.uniformMatrix4fv(uniLocation[1], false, invMatrix);
gl.uniform3fv(uniLocation[2], lightDirection);
利用minMatrix.js内置的inverse函数来计算模型坐标变换矩阵的逆矩阵。并指定正确的uniformLocation。同一时候设置光的方向lightDirection。
这次,光的方向是不变的,所以没有必要每次循环都设置,可是为了easy理解,所以放到了一起处理。注意,由于光向量是一个包括有三个元素的向量,所以和矩阵不同,使用的是uniform3fv,參数的个数也不一样。
总结
写的太长了。果然,就算是简单点说,关于光的处理也须要非常长的描写叙述。
重点是。3D渲染中没有办法全然模拟现实中的光,仅仅是大致是那么回事而已。 全然模拟自然界的物理学的话,计算量是非常大的。所以取代这些的就是这次所介绍的。使用平行光源,法线,逆矩阵等技术。在一定程度上尽可能的让画面看起来真实。
理解这次文章的内容,须要一定程度的数学知识,向量,法线,矩阵。这些在寻常生活中是不会出现的,可是好好考虑一下的话,应该是能够理解的。
demo的连接会在文章的最后给出,这次改动的内容比較多,所以一次贴出全部的代码。 lufy:代码太长,我就不贴了,大家直接打开demo用浏览器自己看吧。
通过平行光源来绘制圆环体的demo