[WebGL入门]二十一,从平行光源发出的光

翻译 2014年08月28日 23:40:53
注:文章译自http://wgld.org/,原作者杉本雅広(doxas),文章中如果有我的额外说明,我会加上[lufy:],另外,鄙人webgl研究还不够深入,一些专业词语,如果翻译有误,欢迎大家指正。

照亮世界

上次绘制了一个像甜甜圈一样的圆环体模型,虽然没有涉及特别的新知识,但是也算成功的绘制出了3D模型了吧。
那么,这次来看一下光。
光在3D渲染中有很多种类和使用方法,想把光研究透彻,也是很不容易的。
现实世界中我们能看到物体,是因为物体反射的光进入我们的眼睛。也就是说,没有光的话,我们的眼睛是看不到任何东西的。在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');
最开始可能不容易把握,总值着色器和脚本是不可分割的,双方必须成对出现在代码中。

添加关于光的处理

那么最后,看一下将与光相关的参数传入着色器的处理,首先是代码。
>定义光和矩阵相关的数据
// 各矩阵的生成和初始化
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。
这次,光的方向是不变的,所以没有必要每次循环都设置,但是为了容易理解,所以放到了一起处理。注意,因为光向量是一个包含有三个元素的向量,所以和矩阵不同,使用的是uniform3fv,参数的个数也不一样。

总结

写的太长了,果然,就算是简单点说,关于光的处理也需要很长的描述。
重点是,3D渲染中没有办法完全模拟现实中的光,只是大致是那么回事而已。
完全模拟自然界的物理学的话,计算量是非常大的,所以代替这些的就是这次所介绍的,使用平行光源,法线,逆矩阵等技术,在一定程度上尽可能的让画面看起来真实。
理解这次文章的内容,需要一定程度的数学知识,向量,法线,矩阵,这些在平常生活中是不会出现的,但是好好考虑一下的话,应该是可以理解的。
demo的连接会在文章的最后给出,这次修改的内容比较多,所以一次贴出所有的代码。
lufy:代码太长,我就不贴了,大家直接打开demo用浏览器自己看吧。

下次,进一步深入介绍关于光的内容。

通过平行光源来绘制圆环体的demo



[WebGL入门]二十五,点光源的光照

用点光源的光照,概念基本上和平行光源一样。根据获取光向量和顶点的法线及视点向量的内积来添加阴影。和平行光源的不同之处,简单的说就是光向量是否是一个固定值。点光源使用的是模型坐标变换后的顶点的位置和光源...
  • lufy_Legend
  • lufy_Legend
  • 2014-10-13 00:22:09
  • 6426

【WebGL】茶壶和光照

一、实验目的和要求   在OpenGL观察实验的基础上,通过实现实验内容,掌握OpenGL中消隐和光照的设置,并验证课程中消隐和光照的内容。   二、实验内容和原理   使用WebGL完成...
  • lishichengyan
  • lishichengyan
  • 2017-12-17 19:37:57
  • 215

30 WebGL光照的概念

光照原理: 现实世界中的物体呗光纤照射时,会反射一部分光。只有当反射光纤进入你的眼睛时,你才能够看到物体并辨认出它的颜色。比如,白色的盒子会反射白光,当白光进入你的眼睛时,你才能看到盒子是白色的。 在...
  • qq_30100043
  • qq_30100043
  • 2017-06-05 10:42:44
  • 610

[WebGL入门]二十二,从环境光源发出的光

环境光,模拟了自然界的光的漫反射,弥补了平行光源的缺点。一般,这两种光会同时使用。只使用环境光的话,无法表现出模型的凹凸,只使用平行光源的话,阴影过于严重无法分清模型的轮廓。 3D模拟中的扩散光的代表...
  • lufy_Legend
  • lufy_Legend
  • 2014-09-06 00:52:18
  • 5892

[WebGL入门]二十三,反射光的光照效果

与目前为止所涉及到的算法相比,今天的算法也不算难,就是,计算从光源发出的光向量和视线向量之间的半向量,然后与面法线向量求内积,所以相对的负荷也不大。但是,这只是在一定程度上模拟了反射光的效果,并不是非...
  • lufy_Legend
  • lufy_Legend
  • 2014-10-06 21:47:31
  • 7950

WebGL风向图

前面关于webgl的基本知识点就没翻译,原文地址概述制作风向图通常步骤 1. 在屏幕上生成一系列随机粒子位置并绘制粒子。 2. 对于每一个粒子,查询风数据以获得其当前位置的粒子速度,并相应地移动它...
  • u013929284
  • u013929284
  • 2017-08-06 18:22:29
  • 995

WebGl学习入门心得

原文章发布在自己博客:http://aircloud.10000h.top/45写在前面想写这篇文章想好久了,这半年在学校的课程中,自己最认真学习的就是图形学这门课程的内容,当然主要是因为自己想要研究...
  • ul646691993
  • ul646691993
  • 2017-01-25 23:50:44
  • 2909

[WebGL入门]二十四,补色着色

这一次分别说了高氏着色和补色着色两种着色,高氏着色的优点是计算量比较低,而和补色着色相比的话,渲染效果不太自然。 补色着色正好相反,计算量很高,但是渲染效果非常完美。 到底选择那种方法,取决于模型的顶...
  • lufy_Legend
  • lufy_Legend
  • 2014-10-11 21:55:59
  • 5247

一、WebGL入门的开始

一直对WebGL很感兴趣,但一直没时间学习,现在总算有了点时间。这也是鄙人第一次写博客,写的得不好望指正。 我比较喜欢先看效果,再分析代码。 代码如下: webgl-lesson1 ...
  • a23366192007
  • a23366192007
  • 2016-01-15 19:15:19
  • 1141
收藏助手
不良信息举报
您举报文章:[WebGL入门]二十一,从平行光源发出的光
举报原因:
原因补充:

(最多只允许输入30个字)