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

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



本次的demo的运行结果


点光源

上次介绍了高氏着色和补色着色。
使用补色着色的手法,可以渲染更加自然的阴影,3D效果更加真实。但是会有计算量比较大的缺点。这个只能case by case,根据不同的情况来处理了,是个挺烦人的地方。
那么,这次,还是讲光源。我貌似听到了“不会吧......”此类的声音了......
这次的话题是点光源的封装。点光源就像它的名字一样,和顶点一样,光源就是一个点。
到目前为止,所有的光源处理都是使用了平行光源,平行光源可以看作是从无限远的地方发出的方向固定的光的光源,三维空间中的所有的模型都受到同样方向的光的照射。而点光源的处理,光源的位置在三维空间中是固定的,三维空间中的模型根据它所在的位置,受到不同方向的光照。
现实世界中类似电光源的有灯泡等。但是,实际上灯泡的光是会变弱的,距离越远光的强度就越弱。而这次封装的电光源的处理并没有考虑光的减弱,无论对象距离光源有多元,都受到同样的强度的光的影响,所以,并不是完全模拟现实世界中的电光源。


电光源的考虑方法

电光源的封装其实也并不难。
平行光源的时候是光向量,就是说光的方向是固定的。点光源的时候,光源的位置是确定的,需要算出从光源到顶点的向量作为光向量,用这个光向量来计算阴影。
因为必须计算出光源到顶点的向量,所以比平行光源的计算量要大,但是解决了光向量的计算之后,就可以沿用之前平行光源的计算,所以也不会太难。


顶点着色器的修改

这次和上次一样,使用补色着色来进行着色,虽然大部分修改都是在片段着色器中进行的,但是顶点着色器中也有少许修改。
点光源的处理,就想刚才说的那样,需要计算光源到顶点的向量,而要计算光向量的话,那就必然需要顶点的位置情报了。
要将顶点着色器中的位置情报传给片段着色器,那肯定需要新的varying变量了,但是只传顶点的位置情报的话,还有一些问题。
顶点的位置情报通常以局部坐标的形式传给顶点着色器,所以如果用模型坐标变换对模型进行了移动或者旋转,顶点的位置就会发生变化了,就是说,即使局域坐标是(1.0, 1.0, 1.0)的顶点经过移动,旋转等变换,坐标可能会变成(例 0.5, 2.0, 5.5 )等等。
从点光源发出的光的光向量,必须要考虑模型坐标变换后的顶点的位置。所以,必须向顶点着色器中传入新的模型坐标变换矩阵,那么来修改一下顶点着色器的代码吧。

>顶点着色器代码
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform   mat4 mvpMatrix;
uniform   mat4 mMatrix;
varying   vec3 vPosition;
varying   vec3 vNormal;
varying   vec4 vColor;


void main(void){
    vPosition   = (mMatrix * vec4(position, 1.0)).xyz;
    vNormal     = normal;
    vColor      = color;
    gl_Position = mvpMatrix * vec4(position, 1.0);
}
跟上次比较,变更点有两个。
第一个变更点是为了向片段着色器传入顶点的位置情报,追加了varying变量vPosition。因为表示的顶点的位置情报,所以定义了vec3类型。
第二个变更点是追加了新的uniform变量mMatrix。像刚才写的那样,因为顶点着色器中的顶点的位置坐标是局域坐标系,所以为了将模型坐标变换矩阵变换成适当的形式(就是世界坐标系),使用uniform修饰符定义的变量,以便在着色器一侧接受到模型坐标变换矩阵。
向片段着色器中传递顶点的位置情报的时候,表示模型坐标变换矩阵的mMatrix和表示顶点的局部坐标的position相乘,结果带入到vPosition中。这样的话,片段着色器就能使用模型坐标变换后的顶点位置了。


片段着色器的修改

接着是片段着色器一侧的修改,片段着色器中需要使用顶点的位置和点光源的位置算出光向量。
这时的光向量的计算方法是非常简单的,只需要单纯的减法就可以了。
另外,这次是根据电光源做处理的,所以用表示电光源的位置的uniform变量lightPosition来代替表示光向量的uniform变量lightDirection。

>片段着色器代码
precision mediump float;

uniform mat4 invMatrix;
uniform vec3 lightPosition;
uniform vec3 eyeDirection;
uniform vec4 ambientColor;
varying vec3 vPosition;
varying vec3 vNormal;
varying vec4 vColor;


void main(void){
    vec3  lightVec  = lightPosition - vPosition;
    vec3  invLight  = normalize(invMatrix * vec4(lightVec, 0.0)).xyz;
    vec3  invEye    = normalize(invMatrix * vec4(eyeDirection, 0.0)).xyz;
    vec3  halfLE    = normalize(invLight + invEye);
    float diffuse   = clamp(dot(vNormal, invLight), 0.0, 1.0) + 0.2;
    float specular  = pow(clamp(dot(vNormal, halfLE), 0.0, 1.0), 50.0);
    vec4  destColor = vColor * vec4(vec3(diffuse), 1.0) + vec4(vec3(specular), 1.0) + ambientColor;
    gl_FragColor    = destColor;
}
着色器中的main函数的第一行,将从点光源到顶点的光向量代入到变量lightVec中。像上面说的一样,使用了单纯的减法,很简单吧。并且,使用这里得到的光向量,和之前平行光源一样求逆矩阵以及半向量,计算扩散光和反射光。
理解了结构,也就应该明白,其实和之前的demo也没有多大的变化。主要是光向量的处理不同,光照的方法基本上大致相同。


javascript 的修正

着色器修改完之后,下面就是主程序的javascript的修改了。

这次细节部分修改的比较多,一点点的开始解说。目前为止的demo都只渲染了一个圆环体,这次在圆环体之外加一个球体,看文章一开始的图片就知道了。圆环体的顶点数据以及球体的顶点数据要另外准备。

球体模型的顶点数据的生成,使用以下的函数。和生成圆环体的顶点数据的函数是比较类似的。
>生成球体的顶点数据的函数
// 球体を生成する関数
function sphere(row, column, rad, color){
    var pos = new Array(), nor = new Array(),
        col = new Array(), idx = new Array();
    for(var i = 0; i <= row; i++){
        var r = Math.PI / row * i;
        var ry = Math.cos(r);
        var rr = Math.sin(r);
        for(var ii = 0; ii <= column; ii++){
            var tr = Math.PI * 2 / column * ii;
            var tx = rr * rad * Math.cos(tr);
            var ty = ry * rad;
            var tz = rr * rad * Math.sin(tr);
            var rx = rr * Math.cos(tr);
            var rz = rr * Math.sin(tr);
            if(color){
                var tc = color;
            }else{
                tc = hsva(360 / row * i, 1, 1, 1);
            }
            pos.push(tx, ty, tz);
            nor.push(rx, ry, rz);
            col.push(tc[0], tc[1], tc[2], tc[3]);
        }
    }
    r = 0;
    for(i = 0; i < row; i++){
        for(ii = 0; ii < column; ii++){
            r = (column + 1) * i + ii;
            idx.push(r, r + 1, r + column + 2);
            idx.push(r, r + column + 2, r + column + 1);
        }
    }
    return {p : pos, n : nor, c : col, i : idx};
}
形成球体的顶点,定义了一个用一个大的多边形群组成的膜裹成球的形状方法。这个sphere函数接受四个参数,第一个参数是形成球体的膜状的多边形板的纵向分割数(顶点数),用地球比喻的话就是纬度的方向。第二个参数则是横向分割数,这里用地球来说的话,就是经度的方向。第三个参数是球体的半径。第四个参数是球体的颜色,这个颜色是包含四个元素的数组,如果没有指定颜色,则会自动分配HSV颜色。
这个函数的使用方法,传入适当的参数,然后接收返回值。返回值是一个对象,使用的时候就参照这个对象的适当的属性。实际的代码如下。


>函数sphere的使用部分
// 用球体的顶点数据生成VBO并保存
var sphereData = sphere(64, 64, 2.0, [0.25, 0.25, 0.75, 1.0]);
var sPosition = create_vbo(sphereData.p);
var sNormal   = create_vbo(sphereData.n);
var sColor    = create_vbo(sphereData.c);
var sVBOList  = [sPosition, sNormal, sColor];


// 球体用IBO的生成
var sIndex = create_ibo(sphereData.i);
上面的代码,生成一个纵向和横向都是64个顶点的球体,半径是2.0,这次指定的颜色是蓝色。需要注意的是,为了后面的处理,将VBO保存到了数组中,这样做了之后,attributeLocation和VBO联系的工作就变的非常的方便了,这个后面会叙述。
接着是uniformLocation的获取部分,这次是从平行光源变到电光源,指定光的方向的部分要换成指定光的位置。


>uniform的相关处理
// uniformLocationを配列に取得
var uniLocation = new Array();
uniLocation[0] = gl.getUniformLocation(prg, ‘mvpMatrix‘);
uniLocation[1] = gl.getUniformLocation(prg, ‘mMatrix‘);
uniLocation[2] = gl.getUniformLocation(prg, ‘invMatrix‘);
uniLocation[3] = gl.getUniformLocation(prg, ‘lightPosition‘);
uniLocation[4] = gl.getUniformLocation(prg, ‘eyeDirection‘);
uniLocation[5] = gl.getUniformLocation(prg, ‘ambientColor‘);


// 中略


// 点光源的位置
var lightPosition = [0.0, 0.0, 0.0];
着色器中进行的uniform修饰符变量的变更在这里都详细的反映出了,而且,demo的电光源的位置设定成了原点。
为了更容易明白点光源的效果,demo中的以点光源的位置为中心,圆环体和球体不断的进行旋转,里面包含了模型坐标变换矩阵的生成。因为同时绘制两个模型,在持续循环的过程中,使用适当的VBO和IBO进行模型的渲染。
代码稍微长了点,仔细看的话就明白了。主要是刚才写的那样,使用保存有VBO的数组,在自制的函数中对VBO进行绑定处理。


>持续循环的绘制处理
// カウンタをインクリメントする
count++;


// カウンタを元にラジアンと各種座標を算出
var rad = (count % 360) * Math.PI / 180;
var tx = Math.cos(rad) * 3.5;
var ty = Math.sin(rad) * 3.5;
var tz = Math.sin(rad) * 3.5;


// トーラスのVBOとIBOをセット
set_attribute(tVBOList, attLocation, attStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, tIndex);


// モデル座標変換行列の生成
m.identity(mMatrix);
m.translate(mMatrix, [tx, -ty, -tz], 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, mMatrix);
gl.uniformMatrix4fv(uniLocation[2], false, invMatrix);
gl.uniform3fv(uniLocation[3], lightPosition);
gl.uniform3fv(uniLocation[4], eyeDirection);
gl.uniform4fv(uniLocation[5], ambientColor);
gl.drawElements(gl.TRIANGLES, torusData.i.length, gl.UNSIGNED_SHORT, 0);


// 球体のVBOとIBOをセット
set_attribute(sVBOList, attLocation, attStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, sIndex);


// モデル座標変換行列の生成
m.identity(mMatrix);
m.translate(mMatrix, [-tx, ty, tz], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
m.inverse(mMatrix, invMatrix);


// uniform変数の登録と描画
gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
gl.uniformMatrix4fv(uniLocation[1], false, mMatrix);
gl.uniformMatrix4fv(uniLocation[2], false, invMatrix);
gl.drawElements(gl.TRIANGLES, sphereData.i.length, gl.UNSIGNED_SHORT, 0);


// コンテキストの再描画
gl.flush();
各种坐标变换矩阵的生成,以及逆矩阵的生成结束后,将点光源的位置和视点向量等一起传入着色器,以及VBO和IBO的绑定处理之后,发出绘图命令。

为了绘制两个模型而进行了一连串的处理,但是要注意不要重复,也没进行特别难的处理。


总结

用点光源的光照,概念基本上和平行光源一样。根据获取光向量和顶点的法线及视点向量的内积来添加阴影。和平行光源的不同之处,简单的说就是光向量是否是一个固定值。点光源使用的是模型坐标变换后的顶点的位置和光源的位置,这时再计算光向量,所以增加了若干的计算量。
平行光源的光的方向是一定的,整体都受到均等的光照。但是点光源根据实际顶点的坐标要进行具体的光的碰撞。这次的demo和上次一样在片段着色器中进行光的计算和补色着色,所以可以进行很漂亮的渲染。
这次的文章中只需要明白是进行了光照相关的基础部分的封装,WebGL中的着色根据功夫是否到家,可以渲染出各种效果,从现在开始要有应用的能力了,以后会进行一些特殊的技术的详细的介绍。
那么,这次也提供了实际的demo,请点击连接进行测试。


下次,要开始图片的渲染了,期待吧。

用点光源来渲染圆环体和球体

http://wgld.org/s/sample_013/


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

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。