简析OpenGL对光线的软件模拟
OpenGL之类的3D图形库,让我们可以用计算机模拟现实世界的视觉效果,在顶点和多边形的帮助下,我们可以通过计算机来模拟现实中所有物体的几何形状,在纹理的帮助下,我们可以通过计算机来模拟现实物体表面的外观。但即使在有了顶点和纹理,计算机中的3D世界仍然显得不够真实。
到底差距在哪里?想必大家都知道一种叫“素描”的绘画的技巧:假如你想在白纸上画一个球体,如果你只是画上一个圆形边框,然后染上一种单一的颜色,没有人看的出来这画的是一个球还是一个圆,但如果你用素描的技巧,想象好光源的方向,在暗光和高光的区域上不同的颜色,一个立体的球形便可以跃然纸上。没错,素描这种技巧很好的表达了光线对于物体表面外观的影响。
那如何用计算机来素描呢?最容易想到的当然是用计算机来模拟光线,模拟之前,我们先来分析下真实世界中光线和物体表面颜色的关系:
现实中的光照模型
颜色是什么?
在没有学过光学的人的认识中,颜色是物体的自身属性,很多的物体的颜色是一成不变的,比如雪是白色,树叶是绿色,血液是红色。但如果我们试着呆在一个隔绝任何光线的纯黑小屋中,我们会发现所有颜色都消失了,于是我们可以得到这样的结论:颜色并不是这样物体自身的属性,不同的颜色,是物体表面光学特性和光线相互作用的结果。
日光是所有可见光的集合,照射与物体表面时,某些波长的光被吸收,某些波长的光线则被反射,反射的光线最终进入人的眼睛,投影在视网膜,形成了颜色。比如红色就是物体表面反射红光,吸收其余光线的结果。
物体表面和光线不同夹角对颜色产生的影响:
我们都有这样的经历,玻璃杯,瓷器等表面光滑的物件,无论表面原来的颜色是什么,在日光下总会有一块纯白的“高光”区域,并且这个区域会随着光照方向的变化或者人观察角度的变化,在物体表面移动。说明对于表面光滑的物体而言,颜色不但和表面特性,光线相关,还和光照角度和观察角度相关。
物体表面光滑度对颜色的影响:
同一束光线,照在同样颜色的光滑表面上和粗糙表面上,视觉效果也是不同的,明显的区别是,在光滑表面上,通常都有那种前面说的高光效果,而且越光滑越明显,在粗糙表面上,那种高光效果消失了,颜色的分布变的很均匀,颜色的变化也变的很平滑, 改变光线照射的角度,依然会对颜色的分布产生影响,但是似乎已经和观察角度无关了。这种反射,被称为漫反射,实际上漫反射依然严格遵循光线的反射和折射定律,只不过光线在接触粗糙表面时,在微观尺度上表面的不规则形状之间进行了很多次的镜面反射,因此出射角度已经随机分布。 不过在计算机处理漫反射时,如果对微观尺度上的镜面反射进行完整的模拟,计算量过于庞大,所以会将其近似为均匀向所有方向散射。
自发光物体在光线中产生的颜色:
如果我们要模拟的物体本身是个光源,那么它在外部光源下会呈现什么? 我们可以自己动手实验 - 比如将发绿光的物体,置于红色外部光源下,可以观察到黄色。因此我们可以得到的结论是自发光体的颜色由自发光颜色和外部光源叠加产生。
OpenGL中的光照模型
针对前面对真实的光线和颜色特性的了解,OpenGL建立了很好的抽象-两类光源,四类光线:
环境光源:
Ambient:
环境光,在白天非阳光直射,和外部光源很多的情况下,现实环境中会弥散着几乎一致的亮度,OpenGL将这部分光称作环境光,它给3D世界整体亮度设定了基线。
点光源:
Diffuse:
漫反射光,只和表面与光线入射夹角有关的光线。
Specular:
镜面反射光,不但和表面与光线入射夹角有关,而且还和观察角度有关的光线,并且根据表面光滑程度,呈现不同亮度,越光滑,越容易产生高光区域。
其他:
Emissive:
自发光,它不是光源的属性,而是物体自身属性,但任对物体表面颜色产生影响。
同样的,对应于光线的类型,OpenGL也定义了3种材质属性(Material):
Ambient,Diffuse, Specular. 每种材质类型的值实际上是代表各自的光线类型在最终合成的亮度上的权重 。也可以看作在物体表面,镜面反射,漫反射和环境光反射各占多大成分。
如何决定入射光和表面的夹角 - 法向量:
OpenGL中的表面,都是由三角形组成的,三角形的三个顶点,可以决定一个平面,凡是平面,就会有法线,所以我们只要知道了这个平面的法向量,那么
入射光和平面的夹角= 90度-入射光向量和法向量的夹角。
不过不同于前述的一个三角形对应一个法向量,OpenGL中的法线向量是和顶点联系在一起的(具体原因也许是数据组织的方便性),因此在光照模型中,需要给每个顶点指定法向量,如不指定,则默认为(0,0,1)。最终三角形内部各个位置的法向量也由3个顶点的法向量线性插值而来。不过通常我们会保持三角形三个顶点的法向量相同。
OpenGL中顶点颜色的最终合成:
我们已经知道了这4种类型的光线,以及它们各自的反射亮度受哪些因素的影响,那么它们如何互相影响,最终决定一个顶点的颜色呢?
OpenGL给出了一套光线合成的标准公式,不论真实世界的光线是否真由如此组成,但这套公式无疑从经验上是最接近现实效果的。
以下是OpenGLES1.1的固定渲染管线中,它们的合成公式:
vertex color =
the material emission at that vertex +
the global ambient light scaled by the material's ambient property at that vertex +
the ambient, diffuse, and specular contributions from all the light sources, properly attenuated
最终合成的颜色,即由自发光,全局环境光和所有点光源三部分叠加而成. 其中全局环境光的公式为:
global ambient light = ambientlight model * ambientmaterial
点光源部分又由ambient,diffuse,specular三种光源各自的公式相加而成,并且光强按离光源的距离衰减:
contribution = attenuation factor * spotlight effect *
(ambient term + diffuse term + specular term)
其中环境光源公式:
ambient term=ambientlight *ambientmaterial
散射光源公式:
dot(L,n)* diffuselight * diffusematerial
镜面光源公式:
dot( normalize(L+v) , n) ^ shininess * specularlight * specularmaterial
散射和镜面光源公式中,L代表的是光线入射方向的反向,n为法向量,v为顶点到视角的向量。
可以从公式看出,散射光源的反射光强度,是和法向量在入射方向上的分量成正比的,也就是光线照射和表面越垂直,强度越高。并且diffuse的material也决定了diffuse的光强。
镜面光源的反射光强度和散射光类似,但是引入了视角方向,这也和本文开头探讨的镜面光对颜色的影响和观察角度有关的结论。
另外衰减因子(attenuation factor) 和距离相关,距离越大,衰减因子越小。如不考虑衰减,则可用常量1代替。
聚光灯效应(spotlight effect)则是为了模拟聚光灯锥形光线的中央部分比外圈的光强更高而加入的因子,如果不使用聚光灯,也可用常量1代替。
所以如果是使用OpenGLES2.0自己编写fragment shader的情况下,常把整个公式简化为:
gl_FragColor = emission + ambient*ambientMatertial + dot(L,n)* diffuselight * diffusematerial + pow(dot( normalize(L+v) , n) , shininess) * specularlight * specularmaterial .
在有纹理时的顶点颜色:
前面讨论了很多纯光源作用下的顶点颜色,但如果有纹理和光线同时作用于顶点,最终的颜色又该如何?
在OpenGLES2.0以前的固定渲染管线中,最终的颜色是纹理颜色和光线计算的结果共同叠加的结果,并且纹理映射是在光线计算之后发生的,因此在映射纹理的时候,需要指定纹理和光线计算结果的叠加模式。
常用的几种模式:
GL_Modulate: frag_color = tex_color * light_color .是和实际带纹理的物体在光线照射下效果最接近的方式。
GL_Replace: frag_color = tex_color.忽略光线的效果,完全用纹理的颜色替换。
GL_Blend: frag_color = (1-tex_color)*light_color + C*tex_color, 这是透明混合的效果,理论上是光线在某种纹理的表面背后透射而过产生的效果。
而在OpenGLES2.0中,我们可以自己定义texture和光线的混合方法,比如
gl_FragColor =texture2D(sampler,texCoord)* (emission + ambient*ambientMatertial + dot(L,n)* diffuselight * diffusematerial + pow(dot( normalize(L+v) , n) , shininess) * specularlight * specularmaterial ).
将texture和光线简单相乘,就可以得到GL_MODULATE的效果。
总结
本文目的并不是向读者介绍如何使用OpenGL光线渲染的功能,所有并没有提及任何OpenGL的接口,本文旨在通过联系现实的光照模型来解释OpenGL中对光线的演算过程,帮助更好的理解光照相关的接口和调试OpenGL光照效果。