Web 技术研究所

我一直坚信着,Web 将会成为未来应用程序的主流

WebGL(捌) 基本光处理

  “神说要有光,就有了光。神看光是好的,就把光暗分开了”。在这个3D世界中,咱就是神,所做的事情就是创造世界。如果想成为一个合格神,当然需要学会如何操纵光。于是这篇文章就来说说最基本的光线操作,如果有足够好的数学功底,这篇文章就会秒懂。
  首先要弄明白一堆概念性的东西,我一直对“漫反射”这货纠结不清。光照射到物体上会变成什么样主要是由物体的材质来决定的。简单的说,粗糙的物体会产生漫反射,光滑的物体就会产生镜面反射。这些都是物理学中的定义。但是3D绘图时还有个玩意儿叫做环境光,就像阴天太阳没有直射地面我们也能看到东西一样。环境光的产生也是由于漫反射造成的,这里又出个了漫反射,概念就和上面冲突了。于是我参考冯氏反射模型的概念得出以下结论:在计算机图形学中所谓“环境光”不是漫反射的概念。它只是“光”(有光的三原色),而不是“光线”(没有传播性)。有时候甚至可以理解为“环境亮度”。现在来整理下该概念吧
  光线:具有方向和颜色的光
    平行光:只有一个方向向量的光线
    点光:所有方向向量都向其周围发散的光线
    其他:聚光灯,等
  光效:光照在物体上产生的效果
    漫射:又称漫反射,是最普通的光照效果
    反射:又称镜面反射,也就是常说的高光效果
    其他:透射、折射,等
  接着就要说说如何计算这些东西了。环境光是不受物体坐标影响的,计算起来只需要把它的颜色向量和物体的颜色向量逐分量相乘即可。是不是感觉一头雾水?怎么突然冒出向量了?还是逐分量相乘?光是有颜色的,我们看到的白色光实际上是红绿蓝(RGB)三种颜色叠加而成的。如果是白色光,RGB三个分量的值就是相同的。而他们的值越大,则亮度越高。不信你可以打开PS调整图层亮度看RGB值的变化就知道了。假如物体的颜色是(1,0,0),也就是红色。而环境光是(0.8,0.8,0.8),也就是说环境光稍暗。把他们的每分量相乘就可以得到(0.8,0,0),这就是稍暗的红色。记住是逐分量相乘!向量的乘法有很多种,在GLSL中,两个向量直接用乘号“*”连接时就是每个分量相乘。如果需要做点乘和差乘(内积和外积)需要调用函数“dot”和“cross”来实现。
  知道了这些,我想不用我举例就可以做出环境光的效果了吧?可是光照哪有那么简单呢,环境光是最基础的东西,这篇文章要说的另一个东西是平行光的漫射。太阳光照在粗糙物体上,被光照到的地方比较亮,没有光找到的地方比较暗。光照在物体上的亮度和物体的摆放角度有有关。光直射在物体上时亮度最高,斜射时亮度就稍低。这也是为什么中午的太阳最毒辣的原因。要做这个计算就需要为每个顶点引入法向量的概念来确定顶点所在面的朝向。
  首先我们要从光的方向向量与平面的法向量之间计算出光对某个平面作用的强度。如果你知道如何计算合力的功就一定知道如何计算这个作用强度。原理是一样的,只要求出平面法向量与光的方向向量的点积即可。

  这里需要注意,由于光的照射方向和平面法向量是相反的,所以计算结果需要取相反数,也就是计算完点积之后加个负号(也可以直接在传入光的方向向量时使用逆向量)。这篇文章我们依然使用旋转立方体的例子,下面是例子的着色器代码。
//顶点着色器
attribute vec3 po;
attribute vec3 co;
attribute vec3 no; //顶点的法向量
uniform mat4 tra;
uniform mat4 pro;
uniform vec3 li_co; //平行光颜色
uniform vec3 li_di; //平行光方向
uniform vec3 li_en; //环境光颜色
varying vec3 co_v;
void main(){
  gl_Position=pro*tra*vec4(po,1.0);
  vec3 no_di=(tra*vec4(no,0.0)).xyz; //把顶点旋转应用到法线上
  float li_we=max(-dot(li_di,no_di),0.0); //计算法线与光线的点积
  vec3 li=li_we*li_co; //应用光的颜色
  co_v=co*li+li_en; //物体的颜和平行光的颜色混合,再叠加上环境光
}
//片段着色器
varying lowp vec3 co_v;
void main(){
  gl_FragColor=vec4(co_v,1.0);
}
  由于立方体是旋转的,它的面朝向当然也会旋转。也就是说,法向量也需要旋转。所以我们要先处理法向量的旋转。其实这是个很复杂的过程。在刚体的计算上,我们可以直接使用顶点的变换矩阵来旋转法向量,因为刚体的顶点变换只有平移和旋转两种。只要让平移操作不生效就可以作为法向量的变换矩阵。把原来3阶的法向量转换成4阶时,让w分量为0,这样就不会受矩阵中平移的影响了。另外,对于柔体就不能使用顶点的变换矩阵了,因为柔体的变换矩阵处理旋转平移之外还包含了扭曲和缩放。这时候就需要单独传入法向量的变换矩阵,这个以后说到柔体时再说吧。
  现在,我们把变换好的法向量还原成3阶的,并放入一个变量中。然后就要开始计算光的方向向量和刚计算出的法向量的点积了。前面也有提到的,调用dot函数把两个向量作为参数传入就可以得到他们的点积。记得计算结果加个负号来处理法向量和光的照射方向相反的问题。得到它们的点积之后还不能直接使用,因为里面有负数。如果直接使用会导致下一步叠加环境光时把环境光给抵消了。所以,我们使用max函数来把负数变成0(也可以使用clamp函数,具体参考文档)。这个计算结果就是光线对顶点的影响程度接着。接着,我们把它乘上光本来的颜色就可以得到这个光作用在顶点上时的实际颜色了。再把这个光的实际颜色和顶点本身的颜色逐分量相乘就可以得到光和平面的颜色混合后的颜色了。最后再加上环境光,也就是环境亮度就完成了。着色器的工作就是这些。现在我们看看其他代码。首先是传入顶点数据源的代码。
var po,co,no;
po=webgl.getAttribLocation(program,"po");
co=webgl.getAttribLocation(program,"co");
no=webgl.getAttribLocation(program,"no");
webgl.enableVertexAttribArray(po);
webgl.enableVertexAttribArray(co);
webgl.enableVertexAttribArray(no);
webgl.bindBuffer(webgl.ARRAY_BUFFER,webgl.createBuffer());
webgl.bufferData(  //立方体的顶点数据
  webgl.ARRAY_BUFFER,new Float32Array([
    -1,-1,-1, 1,-1,-1, -1,1,-1, 1,1,-1,
    -1,-1,1, 1,-1,1, -1,1,1, 1,1,1,
    -1,-1,-1, -1,1,-1, -1,-1,1, -1,1,1,
    1,-1,-1, 1,1,-1, 1,-1,1, 1,1,1,
    -1,-1,-1, 1,-1,-1, -1,-1,1, 1,-1,1,
    -1,1,-1, 1,1,-1, -1,1,1, 1,1,1
  ]),webgl.STATIC_DRAW
);
webgl.vertexAttribPointer(po,3,webgl.FLOAT,false,0,0);
tmp=[[1,0,0],[0,1,0],[0,0,1],[1,1,0],[1,0,1],[0,1,1]];
for(dat=[],i=0;i<tmp.length;i++) //颜色数据
  for(j=0;j<4;j++)dat.push.apply(dat,tmp[i]);
webgl.bindBuffer(webgl.ARRAY_BUFFER,webgl.createBuffer());
webgl.bufferData(
  webgl.ARRAY_BUFFER,new Float32Array(dat),webgl.STATIC_DRAW
);
webgl.vertexAttribPointer(co,3,webgl.FLOAT,false,0,0);
tmp=[[0,0,-1],[0,0,1],[-1,0,0],[1,0,0],[0,-1,0],[0,1,0]];
for(dat=[],i=0;i<tmp.length;i++) //法向量数据
  for(j=0;j<4;j++)dat.push.apply(dat,tmp[i]);
webgl.bindBuffer(webgl.ARRAY_BUFFER,webgl.createBuffer());
webgl.bufferData(
  webgl.ARRAY_BUFFER,new Float32Array(dat),webgl.STATIC_DRAW
);
webgl.vertexAttribPointer(no,3,webgl.FLOAT,false,0,0);
  这里的法向量数据我也是用循环来做了,因为立方体的每个面都是矩形,同一个面上的所有顶点朝向都是相同的。如果换成其他形状就需要其他计算方法了。包括顶点坐标本身也是一样的,这些东西没有固定的模式,每一种形状都需要自己去计算,这需要一些空间几何的知识。如果只是想利用3D作出一些效果而不是研究,可以不需要考虑这些,因为很多3D的库都有封装各种常用对象的绘制。但是如果你想深入研究它,并做出自己的3D库,这些就很重要了。下面是uniform相关的
var tra,pro,lig;
tra=webgl.getUniformLocation(program,"tra");
pro=webgl.getUniformLocation(program,"pro");
li_co=webgl.getUniformLocation(program,"li_co");
li_di=webgl.getUniformLocation(program,"li_di");
li_en=webgl.getUniformLocation(program,"li_en");
webgl.uniformMatrix4fv(pro,false,[
  1,0,0,0, 0,1,0,0, 0,0,-1,-1, 0,0,-2,0
]);
webgl.uniform3fv(li_co,[0.8,0.8,0.8]);
webgl.uniform3fv(li_di,[0,0,-1]);
webgl.uniform3fv(li_en,[0.2,0.2,0.2]);
  这个真没啥好说的,为每个参数添加数据。这里设置了光的颜色是白色,亮度是0.8,方向是-z(眼睛看屏幕的方向)。环境光也是白色,亮度是0.2。也就是说,立方体没被平行光照射到的地方也有0.2的亮度,而照射最强的地方是0.8+0.2=1的亮度。

  内容已经很多了,这篇文章就到这儿吧。最后是完整的实例。

  WebGL基本光线处理
网名:
34.203.245.*
电子邮箱:
仅用于接收通知
提交 悄悄的告诉你,Ctrl+Enter 可以提交哦
神奇海螺
[查看全部主题]
各类Web技术问题讨论区
发起新主题
本模块采用即时聊天邮件通知的模式
让钛合金F5成为历史吧!
次碳酸钴的技术博客,文章原创,转载请保留原文链接 ^_^