1、光照原理
现实世界中,依据光源和方向,光源照射到物体上时,发生了两个重要的现象;
- 物体不同的表面明暗程度不一致;
- 物体向地面投下了影子;
三围图形学术语中 着色的真正含义是:根据光照条件重建 “物体各表面明暗不一的效果”的过程,着色器最初被发明出来就是为了重建光照产生的明暗现象;
在讨论着色之前需要考虑一下两件事:
- 发出光线的光源的类型;
- 物体表面如何反射光线;
2、光源类型
平行光:光线是平行的,并且具有方向,需要指定方向和颜色;
点光源光:是从一个点向周围的所有方向发出的光,需要指定位置和颜色;
环境光:指那些经光源(点光或平行光)发出后,被墙壁等物体多次反射,然后照到物体上的光;
环境光,从各个角度照射物体,其强度都是一致的,环境光不需要指定位置和方向,只需要指定颜色即可;
3、反射类型
物体向哪个方向反射光,反射光是什么颜色,取决于以下两个因素;
- 入射光和物体表面的类型
入射光的信息包括入射光的方向和颜色,物体表面的信息包括表面固有的颜色,也称基底色和反射特性;
物体表面反射光线有两种方式:
- 漫反射
- 环境反射
1、漫反射
漫反射是针对平行光和点光源而言的,漫反射的反射光在各个方向上是均匀的,如果物体表面像镜子一样光滑,那么光线就会以特定的角度反射出去,但现实中大部分材质都是粗糙的,因此在这种情况下,反射光酒不会以不固定的角度反射出去;
反射光的颜色取决于:入射光的颜色、表面的基底色、入射光与表面形成的入射角;
<漫反射光颜色> = <入射光颜色> * <表面基底色> * cosθ
上面式子中,<入射光颜色>指的是点光源或者平行光的颜色,乘法操作时在颜色矢量上逐分量RGB
进行的,因为漫反射光在各个方向上都是均匀
的,所以从任何角度看上去强度都是相等的;
2、环境反射
环境反射式针对环境光而言的,在环境反射中,反射光的方向可以认为就是入射光的反方向,由于环境光照射物体的方式就是个方向均匀、强度相等的,所以反射光也是各项均匀的;
<环境反射光颜色> = <入射光颜色> * <表面颜色>
上面的入射光颜色,就是环境光的颜色
当漫反射和环境反射同时存在时,将两者加起来,就会得到物体最终被观察到的颜色;
<表面的反射光颜色> = <漫反射光颜色> + <环境反射光颜色>
4、平行光下的漫反射
漫反射的反射光,其颜色与如入射光在入射点的入射角θ有关,平行光的方向式唯一的,对于一个平面上的所有点,入射角式相同的;
物体的表面基底色是“物体本来的颜色”,或者说是“物体在标准白光下的颜色”;
通过下面的式子可以计算反射的颜色
<漫反射光颜色> = <入射光颜色> * <表面基底色> * cosθ
1、根据光线和表面的方向计算入射角
在程序中,我们无法直接指定入射角θ为多少度,需要根据入射光的方向和物体表面的朝向(即法线方向)来计算入射角;
通过计算两个矢量的点积,来计算这两个矢量的夹角余弦值,cosθ,点积运算GLSL ES
内置了点积运算函数;
<漫反射颜色> = <入射光颜色> * <表面基底色> * (<光线方向> . <法线方向>)
有以下两点需要注意:
- 光想方向矢量和表面法向矢量的长度必须为
1
,否则反射光的颜色就会过暗或过亮; - 光线方向实际上就是入射方向的反方向,即从入射点指向光源方向,该方向与法线方向的夹角才是入射角;
2、法线
物体表面的朝向,即垂直于表面的方向,又称为法线或者法向量,法向量有三个分量,(nx,ny,nz)
表示从原点(0, 0, 0)
指向点(nx,ny,nz)
的方向,
一个表面具有两个法向量
每个表面都有两个面,正面和背面,即两个面各具有一个法向量;
平面的法向量的唯一性
由于法向量表示的是方向,与位置无关,所以一个平面只有一个法向量,换句话说平面的任意一点都具有相同的法向量,法向量与位置无关;
5、示例程序
顶点着色器
attribute vec4 a_Position;
attribute vec4 a_Color;
attribute vec4 a_Normal;// 法向量
uniform mat4 u_MvpMatrix;// 模型视图投影矩阵
uniform vec3 u_LightColor;// 光线颜色
uniform vec3 u_LightDirection;//光线位置,归一化世界坐标
varying vec4 v_Color;
void main(){
gl_Position = u_MvpMatrix * a_Position;
// 对法向量进行归一化
vec3 normal = normalize(vec3(a_Normal));
// 计算光线方向和法向量的点积
float nDotL = max(dot(u_LightDirection, normal), 0.0);
// 计算反射光的颜色
vec3 diffuse = u_LightColor * vec3(a_Color) * nDotL;
// 颜色值就是漫反射的颜色
v_Color = vec4(diffuse, a_Color.a);
}
片元着色器只负责接收颜色值
...
varying vec4 v_Color;
void main(){
gl_FragColor = v_Color;
}
javaScript
程序流程
//...
var u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor');
var u_LightDirection = gl.getUniformLocation(gl.program,'u_LightDirection');
...
// 设置光线颜色
gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0);
// 设置光线方向,世界坐标系下的
var lightDirection = new Vector3([0.5, 3.0, 4.0]);
lightDirection.normalize();//归一化
gl.uniform3fv(u_LightDirection, lightDirection.elements);
6、环境光下的漫反射
上个示例我们看到,灯光没有照到立方体的面都是黑色,真实情况下就算灯光照不到,立方体也会有一点泛白色,这是因为没有添加环境光的原因。
环境光的颜色取决于 光的颜色 和 物体表面的颜色;
<环境反射光颜色> = <入射光颜色> * <表面基底色>
此时再加上漫反射光的颜色,就是表面的反射光的颜色
<表面的反射光的颜色> = <漫反射光的颜色> + <环境反射光颜色>
着色器部分代码
...
uniform vec3 u_LightColor;// 光线颜色
uniform vec3 u_LightDirection;//归一化的世界坐标
uniform vec3 u_AmbientLight;// 环境光颜色
...
void main(){
...
// 计算环境光产生的反射光颜色
vec3 ambient = u_AmbientLight * a_Color.rgb;
// 将以上两者相加得到物体最终的颜色
v_Color = vec4(diffuse + ambient, a_Color.a);
}
查看以上示例,可以看到此时立方体的表面不是那么黑了,但是当立方体旋转的时候,灯光部分是不跟着变化的,这是因为立方体上的法线并没有随着模型旋转的旋转而变换,物体平移、缩放、旋转都可以用坐标变换来表示,因此物体运动会改变每个表面的法向量,从而导致光照效果发生变化;
7、运动物体光照效果
立方体的坐标变换,会使每个表面的法向量也变化,但时平移变换不会改变法向量,旋转会,因为旋转改变了物体的方向;
1、逆转置矩阵
对顶点进行变换的矩阵称为 模型矩阵,所以变换之后的法向量就是变换之前的法向量乘以模型矩阵的 逆转置矩阵
逆转置矩阵:逆矩阵的转置
逆矩阵的含义,如果矩阵M的逆矩阵是R,那么R * M或者 M*R 的结果都是单位阵,转置的意思是,将矩阵的行列进行调换,看上去就像是沿着 左上 - 右下对角线进行了翻转
规则:用法向量乘以模型矩阵的逆转置矩阵,就可以求得变换后的法向量
步骤:1、求原矩阵的逆矩阵,2、将上一步求得的逆矩阵进行转置
《webgl
编程指南》作者提供了一个Matrix4
对象,提供了以下的对矩阵进行操作的方法
方法 | 描述 |
---|---|
Matrix4.setInverseof(m) | 使自身(调用本方法的Matrix4 类型的实例)成为矩阵m 的逆矩阵 |
Matrix4.transpose() | 对自身进行转置操作,并将自身设为转置后的结果 |
var normalMatrix = new Matrix4();
// 根据模型矩阵计算用来变换法向量的矩阵
normalMatrix.setInverseof(modelMatrix);
normalMatrix.transpose();
修改示例二效果如下
8、点光源光
与平行光相比,点光源发出的光,在三维空间中的不同位置上其方向也不同,所以在对点光源下的物体进行着色时,需要字每个入射点计算点光源光咋该处的方向;
前一节根据每个顶点的法向量,和平行光入射方向来计算反射光的颜色,但是点光源的方向不再是恒定不变的,需要根据每个顶点的位置逐一计算,着色器需要知道点光源自身的所在位置,而不是光的方向;
顶点着色器代码如下,片元着色器不变
attribute vec4 a_Position;
attribute vec4 a_Color;
attribute vec4 a_Normal;
uniform mat4 u_MvpMatrix;//法向量
uniform mat4 u_ModelMatrix;// 模型矩阵
uniform mat4 u_NormalMatrix;// 用来变换法向量的矩阵
uniform vec3 u_LightPosition;// 光源位置 归一化世界坐标
uniform vec3 u_AmbientLight;// 环境光颜色
varying vec4 v_Color;
void main(){
// 经过矩阵变换后的顶点坐标
gl_Position = u_MvpMatrix * a_Position;
// 计算后的法向量并归一化
vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));
...
}
9、逐片元光照
上面的灯光案例都没有用到片元着色器,其实上面的灯光颜色时经过内插出来的,因此会看着灯光效果还是有点差距,为了使效果更加逼真,因此需要对每一个都计算光照效果;
要在表面的每一点上计算光照产生的颜色,需要逐片元的进行计算,因此需要调用片元着色器;
// 顶点着色器
var v_shader_source = '' +
'attribute vec4 a_Position;' +
'attribute vec4 a_Normal;' +
'uniform mat4 u_MvpMatrix;' +
'uniform mat4 u_ModelMatrix;' +// 模型矩阵
'uniform mat4 u_NormalMatrix;' +// 法向量旋转矩阵
'varying vec4 v_Color;' +
'varying vec3 v_Normal;' +
'varying vec3 v_Position;' +
'void main(){' +
' vec4 color = vec4(1.0, 1.0, 1.0, 1.0);' + // 物体颜色
' gl_Position = u_MvpMatrix * a_Position;' +
' v_Position = vec3(u_ModelMatrix * a_Position);' +
' v_Normal = normalize(vec3(u_NormalMatrix * a_Normal));' +
' v_Color = color;' +
'}';
// 片元着色器
var f_shader_source = '' +
'precision mediump float;' +
'uniform vec3 u_LightColor;' + // 灯光颜色
'uniform vec3 u_LightPosition;' + // 灯光位置
'uniform vec3 u_AmbientLight;' + // 环境光颜色
'varying vec3 v_Normal;' +
'varying vec3 v_Position;' +
'varying vec4 v_Color;' +
'void main(){' +
// 对法线进行归一化,因为其内插之后长度不一定是1.0
' vec3 normal = normalize(v_Normal);' +
// 计算光线方向并归一化
' vec3 lightDirection = normalize(u_LightPosition - v_Position);' +
// 计算光线方向和法向量的点积
' float nDotL = max(dot(lightDirection, normal), 0.0);' +
// 计算漫反射和环境反射以及最终的颜色值
' vec3 diffuse = u_LightColor * v_Color.rgb * nDotL;' +
' vec3 ambient = u_AmbientLight * v_Color.rgb;' +
' gl_FragColor = vec4(diffuse + ambient, v_Color.a);' +
'}';
此时查看一下示例,对比上一个示例你会发现,光照的渐变效果显得更加自然;