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

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

本次的demo的运行结果

照亮世界

上次绘制了一个像甜甜圈一样的圆环体模型,虽然没有涉及特别的新知识,但是也算成功的绘制出了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

http://wgld.org/s/sample_009/

转载请注明:转自lufy_legend的博客http://blog.csdn.net/lufy_legend

时间: 2024-10-24 12:44:43

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

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

注:文章译自http://wgld.org/,原作者杉本雅広(doxas),文章中如果有我的额外说明,我会加上[lufy:],另外,鄙人webgl研究还不够深入,一些专业词语,如果翻译有误,欢迎大家指正. 本次的demo的运行结果 平行光源的弱点 上次挑战了一下从平行光源发出的光.平行光源的光的方向是固定的.而且,为了模拟这些,需要用到模型变换矩阵的逆矩阵,以及需要向模型数据中加入法线情报等等.平行光源的计算负担比较小,在一定程度上模拟了光照效果,在3D模拟世界中经常被用到.但是,平行光源也有弱

[WebGL入门]二,开始WebGL之前,先了解一下canvas

注:文章译自http://wgld.org/,原作者杉本雅広(doxas),文章中如果有我的额外说明,我会加上[lufy:],另外,鄙人webgl研究还不够深入,一些专业词语,如果翻译有误,欢迎大家指正. HTML5和canvas标签 现在(2012年2月)HTML5依然处于草案阶段. HTML5支持网页端的多媒体功能和画布功能,追加了很多全新的更合理的Tag标签,各个浏览器也都在逐渐的完善这些新的特性. Canvas对象表示一个 HTML画布元素,如它的名字一样,它定义了一个API支持脚本化客

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

注:文章译自http://wgld.org/,原作者杉本雅広(doxas),文章中如果有我的额外说明,我会加上[lufy:],另外,鄙人webgl研究还不够深入,一些专业词语,如果翻译有误,欢迎大家指正. 本次的demo的运行结果 各种各样的光照 上次,以及上上次,介绍了通过顶点着色器来实现光照效果. 最开始介绍了从平行光源发出的光,上次介绍了平行光源的缺点,以及对应这个缺点的方法,就是环境光源. 这次是光照处理的第三篇,进一步介绍反射光照. 反射光和它的名字一样,就是模拟光的反射.通过反射光,

[WebGL入门]二十,绘制立体模型(圆环体)

注:文章译自http://wgld.org/,原作者杉本雅広(doxas),文章中如果有我的额外说明,我会加上[lufy:],另外,鄙人webgl研究还不够深入,一些专业词语,如果翻译有误,欢迎大家指正. 本次的demo的运行结果 立体的模型 这次稍微喘口气,开始绘制立体模型.这里说的[喘口气]是指本次的文章中没有出现任何新的技术知识点.只是利用到现在为止所介绍过的内容,来绘制一个立体的圆环体.到现在为止,只绘制了三角形和四边形,当然,在三维空间中绘制简单的多边形也没什么不对,但是缺点儿说服力.

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

注:文章译自http://wgld.org/,原作者杉本雅広(doxas),文章中如果有我的额外说明,我会加上[lufy:],另外,鄙人webgl研究还不够深入,一些专业词语,如果翻译有误,欢迎大家指正. 本次的demo的运行结果 点光源 上次介绍了高氏着色和补色着色.使用补色着色的手法,可以渲染更加自然的阴影,3D效果更加真实.但是会有计算量比较大的缺点.这个只能case by case,根据不同的情况来处理了,是个挺烦人的地方.那么,这次,还是讲光源.我貌似听到了"不会吧......&quo

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

注:文章译自http://wgld.org/,原作者杉本雅広(doxas),文章中如果有我的额外说明,我会加上[lufy:],另外,鄙人webgl研究还不够深入,一些专业词语,如果翻译有误,欢迎大家指正. 本次的demo的运行结果 着色方法 上次介绍了反射光,反射光是实现光泽的不可缺少的概念,到此为止,基本的光照效果都已经封装完毕了. 光照的效果主要就是扩散光,环境光和反射光三种方法,灵活运用这三种光照,应该就能实现非常逼真的照明效果了. 前几篇一直在说光照,这次稍微换个视点,看一下着色,着色是

[WebGL入门]二十七,多纹理

注:文章译自http://wgld.org/,原作者杉本雅広(doxas),文章中如果有我的额外说明,我会加上[lufy:],另外,鄙人webgl研究还不够深入,一些专业词语,如果翻译有误,欢迎大家指正. 本次的demo的运行结果 使用多个纹理 上次介绍了WebGL中的纹理的使用方法.简单的实现了将纹理贴到四边形中,果然是使用图片数据的话比较灵活吧. 那么,这次来说说使用多个纹理来合成图像的方法,学习了这个方法之后可以再一个多边形中使用多个纹理. 为了同时使用多个纹理,先来想想一下需要做些什么呢

[WebGL入门]二十六,纹理绘图

注:文章译自http://wgld.org/,原作者杉本雅広(doxas),文章中如果有我的额外说明,我会加上[lufy:],另外,鄙人webgl研究还不够深入,一些专业词语,如果翻译有误,欢迎大家指正. 本次的demo的运行结果 WebGL与纹理 上次介绍了用点光源的光来进行补色着色的方法.在片段着色器中对光进行计算,阴影,亮点等效果都非常的漂亮,3D场景的真实度大幅度提升.并且能和顶点颜色一起使用,理解了前面讲解的内容之后,就应该能进行比较高质量的3D渲染了.这一次,来看高级一点的纹理的使用

SpringBoot入门二十一,全局异常处理

一共两个文件,一个处理全局异常,保存信息到日志,另外一个负责返回异常信息给接口,只要将其文件添加到项目中,无需再做其他配置即可 1. MyExceptionHandler.java 全局异常处理类 优先执行此类,这里不好抓取404.403等错误信息getMaps()请参考获取request中传递过来的参数信息getHeaders()请参考获取request中传递过来的header信息代码如下: import java.util.Enumeration; import java.util.Hash