关于光照,参考
图形学笔记六 Shading 光照 渲染管线
UnityShader精要笔记五 基础光照(漫反射+高光反射)
一、漫反射
此处示例程序LightedCube.js绘制了一个处于白色平行光照射下的红色三角形。
![](https://img.haomeiwen.com/i2354823/02fc51f4d304af6c.png)
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'attribute vec4 a_Normal;\n' + // 法向量
'uniform mat4 u_MvpMatrix;\n' +
'uniform vec3 u_LightColor;\n' + // 光线颜色
'uniform vec3 u_LightDirection;\n' + // 光线方向(归一化的世界坐标)
'varying vec4 v_Color;\n' +
'void main(){\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
// 对法向量进行归一化
' vec3 normal = normalize(vec3(a_Normal));\n' +
// 计算光线方向和法向量的点积
' float nDotL = max(dot(u_LightDirection,normal),0.0);\n' +
// 计算漫反射光的颜色
' vec3 diffuse = u_LightColor * vec3(a_Color) * nDotL;\n' +
' v_Color = vec4(diffuse,a_Color.a);\n' +
'}\n'
![](https://img.haomeiwen.com/i2354823/5545f34b90d121a9.png)
对照一下这个公式,代码还是很容易看懂的。
1.JavaScript的配合——设置光线颜色
// main()
// 光线颜色
let u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor')
if (!u_LightColor) {
console.log('Failed to get the storage loaction of u_LightColor')
return
}
gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0)//白色平行光
2.JavaScript的配合——光线方向
Vector3()对象定义在cuon-matrix.js中,normalize是其归一化的方法
// main()
// 光线方向(世界坐标系)
let u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection')
if (!u_LightDirection) {
console.log('Failed to get the storage loaction of u_LightDirection')
return
}
let lightDirection = new Vector3([0.5, 3.0, 4.0])
lightDirection.normalize() // 归一化
gl.uniform3fv(u_LightDirection, lightDirection.elements)
3.JavaScript的配合——法向量
// initVertexBuffers()
...
// 法向量
var normals = new Float32Array([
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,
-1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0,
0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0,
0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0
]);
...
// 将顶点法向量写入缓冲区并开启分配
if (!initArrayBuffer(gl, normals, 3, gl.FLOAT, 'a_Normal')) {
return -1
}
...
二、环境光
![](https://img.haomeiwen.com/i2354823/dff7a2320d258985.png)
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'attribute vec4 a_Normal;\n' + // 法向量
'uniform mat4 u_MvpMatrix;\n' +
'uniform vec3 u_LightColor;\n' + // 光线颜色
'uniform vec3 u_LightDirection;\n' + // 光线方向(归一化的世界坐标)
'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
'varying vec4 v_Color;\n' +
'void main(){\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
// 对法向量进行归一化
' vec3 normal = normalize(vec3(a_Normal));\n' +
// 计算光线方向和法向量的点积
' float nDotL = max(dot(u_LightDirection,normal),0.0);\n' +
// 计算漫反射光的颜色
' vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n' +
// 计算环境光产生的反射光颜色
' vec3 ambient = u_AmbientLight * a_Color.rgb;\n' +
// 最终颜色
' v_Color = vec4(diffuse + ambient,a_Color.a);\n' +
'}\n'
main()函数中配置环境光
// 环境光
let u_AmbientLight = gl.getUniformLocation(gl.program,'u_AmbientLight')
if (!u_AmbientLight) {
console.log('Failed to get the storage loaction of u_AmbientLight')
return
}
gl.uniform3f(u_AmbientLight, 0.2,0.2,0.2)
三、法线变换 逆转置矩阵
参考
《WebGL编程指南》附录E逆转置矩阵,449页。
《UnityShader入门精要》 4.7节(注:这个推导节奏过快,建议先看《WebGL编程指南》书中附录E的推导过程)
法向量变换矩阵
1.问题是什么
在计算机图形学中,法向量(垂直于某个面的向量)有着广泛的应用,比如处理光照需要用到法向量来计算光线的入射角,最终渲染出逼真的场景。
3D建模工具设计出的模型文件一般都是包含了顶点坐标和每个面的法向量信息,也就是说法向量是已知的,模型一旦确定,每个面的法向量就确定了。但是,事情没有那么简单,场景要动起来才会丰富多彩,完全静止的东西往往缺乏生气,这就需要用到模型变换,比如平移、缩放(等比例/不等比例)、旋转等,而不管如何变换,法向量和对应面的垂直关系必须成立,否则场景渲染的效果就会失真甚至完全不正常。
3D模型是由大量的顶点坐标组成的,只要模型变换矩阵乘以顶点坐标就完成了模型的变换,但是法向量却不能直接与模型变换矩阵相乘,因为向量是有向的线段,而顶点坐标仅仅表示一个点,这个本质的不同导致变换矩阵与法向量相乘后很可能不再和变换后的面垂直。
举个简单的例子,参见下图,假设针对3D模型做沿y轴向上平移1个单位的变换,向量p为垂直于其中某个面的法向量。如果向量p=(1,0)执行同样的平移操作,则变成了向量p'=(1,1),p和p'并不是平行关系,显然针对法向量做相同的平移变换之后,p'不可能再垂直于平移后的平面,因为平面沿y轴向上平移1个单位之后仍然与变换前平面平行。
![](https://img.haomeiwen.com/i2354823/7fd540b7377fb535.png)
这仅仅是平移变换带来的问题,还有众多其他类型的变换,不太可能分析所有变换对法向量的影响是怎样的,然后进行一一修正。显然,需要找到一种通用的方法来处理法向量的变换。
2.使用逆转置矩阵
如图,一个物体表面的法向量如何随着物体的坐标变换而改变,取决于变换的类型。使用逆转置矩阵,可以安全地解决该问题,而无须陷入过度复杂的计算中。
![](https://img.haomeiwen.com/i2354823/0e00da591e27058c.png)
在某些特殊情况下,可以通过模型矩阵来确定物体变换后的法向量,这取决于模型矩阵中已经包含的变换类型。
如果模型矩阵中已经包含了平移变换,法向量就会被当作顶点坐标平移,从而导致法向量与原有的表面朝向不一致。比如说,一个法向量是(1,0,0),沿Y轴平移2.0个单位后,就变成了(1,2,0)。事实上,如果你从4x4的模型矩阵左上角抽出3x3的子矩阵,然后乘以法向量,就可以避免该问题。如下所示:
attribute vec4 a_Normal; //法向量
uniform mat4 u_ModelMatrix; //模型矩阵
void main() {
...
vec3 normal = normalize(mat3(u_ModelMatrix) * a_Normal.xyz);
...
}
在模型矩阵中,位于最右侧的一列元素取决于平移矩阵中的位移,如图所示:
![](https://img.haomeiwen.com/i2354823/aefd7dc0f48e5b51.png)
由于3x3的子矩阵实际上包含了旋转矩阵和缩放矩阵的信息,我们来逐一考虑。
- (1) 如果只进行旋转操作:你可以使用模型矩阵的3x3子矩阵对法向量进行变换。如果在变换前法向量已经归一化了,那么变换后的法向量无须再归一化处理。
- (2) 如果只进行缩放操作且缩放因子相同:你可以使用模型矩阵的3x3子矩阵对法向量进行变换。但是,变换后的法向量需要再次归一化处理。
- (3) 如果只进行缩放操作但缩放因子不同:你必须使用模型矩阵的逆转置矩阵对法向量进行变换。变换后的法向量必须再次归一化处理。
![](https://img.haomeiwen.com/i2354823/dd8b95ae4b40c25c.png)
上图使一个矩形物体沿Y轴拉伸2.0倍。这里,为了获得变换后的法向量,我们试图使用模型矩阵乘以原先的法向量(1,1,0)。但是,得到的结果是(1,2,0),并不垂直于拉伸后的物体表面了。
解决该问题需要一些数学知识。我们令模型矩阵为M,令初始的法向量为n,令变换矩阵为M',也就是用来正确变换法向量n的矩阵。
![](https://img.haomeiwen.com/i2354823/af968035f6aef022.png)
还需要一些辅助向量:令垂直于n的向量为s,还有相应的n'和s'。然后构建方程如下:
n' . s ' = 0(垂直关系,点乘为0)
n' = M' x n
s' = M x s
将下面两个等式,代入第一个等式:
![](https://img.haomeiwen.com/i2354823/f4b2e6f60327ed9e.png)
现在,考虑另一个已知条件:n . s = 0,因为A.B = AT x B,可得n . s = nT x s = 0
对比之前得到的式E3,为了使其成立,中间的M'T x M必须是一个单位矩阵I,继而得到:
M' = (M-1)T
因为M已经可能包含了之前列举过的三种情况,所以计算M的逆转置矩阵,并乘以法向量,就可以得到对法向量进行变换的最佳方法。
显然,计算逆转置矩阵可能会比较耗时,如果你非常确定物体的变换只包含上述(1)(2)的情形,你也可以简单地使用模型矩阵的3x3子矩阵来处理。
四、处理旋转后的光照
假如物体发生运动,影响反射光的因素中法线方向也会随之变动,影响物体的光照效果。本例考虑如何在物体运动的时候,获取法线方向,视线光照效果。
![](https://img.haomeiwen.com/i2354823/530e57052aa1a031.png)
示例程序LightedTranslatedRotatedCube.js。相比于上一个示例,本示例除了添加模型矩阵之外,只需要对法向量进行矩阵变换即可。主要改动如下:
1.在顶点着色器中声明新矩阵并对法向量进行变换:
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'attribute vec4 a_Normal;\n' + // 法向量
'uniform mat4 u_MvpMatrix;\n' +
'uniform mat4 u_NormalMatrix;\n' + // 法向量变换矩阵
'uniform vec3 u_LightColor;\n' + // 光线颜色
'uniform vec3 u_LightDirection;\n' + // 光线方向(归一化的世界坐标)
'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
'varying vec4 v_Color;\n' +
'void main(){\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
// 对法向量进行矩阵变换和归一化
' vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
// 计算光线方向和法向量的点积
' float nDotL = max(dot(u_LightDirection,normal),0.0);\n' +
// 计算漫反射光的颜色
' vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n' +
// 计算环境光产生的反射光颜色
' vec3 ambient = u_AmbientLight * a_Color.rgb;\n' +
// 最终颜色
' v_Color = vec4(diffuse + ambient,a_Color.a);\n' +
'}\n'
2.在模型视图投影矩阵中加入几何变化:
// 模型视图投影矩阵
let u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix')
if (!u_MvpMatrix) {
console.log('Failed to get the storage loaction of u_MvpMatrix')
return
}
let mvpMatrix = new Matrix4()
// 计算矩阵
// 模型矩阵
let modelMatrix = new Matrix4()
// 先旋转再平移
modelMatrix.setTranslate(0,1,0)
modelMatrix.rotate(-30,0,0,1)
// 投影和视图矩阵
mvpMatrix.setPerspective(30, 1, 1, 100)
mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0)
mvpMatrix.multiply(modelMatrix)
// 矩阵传值
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements)
3.主函数中设置法向量变换矩阵:
// 法向量变换矩阵
let u_NormalMatrix = gl.getUniformLocation(gl.program, 'u_NormalMatrix')
if (!u_NormalMatrix) {
console.log('Failed to get the storage loaction of u_NormalMatrix')
return
}
let normalMatrix = new Matrix4()
normalMatrix.setInverseOf(modelMatrix) // 求逆
normalMatrix.transpose() // 转置
gl.uniformMatrix4fv(u_NormalMatrix,false,normalMatrix.elements)
此处用到了Matrix4对象的两种方法:setInverseOf()获得参数(也是Matrix4对象)的逆矩阵,transpose()对自身转置。
五、处理点光源的光照
与平行光相比,点光源发出的光在三维空间的不同位置上方向不同。所以,要实现点光源光照效果,进行着色前需要在每个入射点计算点光源光的方向。
1.顶点着色器加入点光源的计算过程:
顶点着色器除了法向量等,新加入了模型矩阵、光源位置。此处单独输入模型矩阵,是为了根据模型矩阵获得变换后的顶点坐标,配合光源位置计算入射角。
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'attribute vec4 a_Normal;\n' + // 法向量
'uniform mat4 u_MvpMatrix;\n' +
'uniform mat4 u_ModelMatrix;\n' + // 模型矩阵
'uniform mat4 u_NormalMatrix;\n' + // 变换法向量的矩阵
'uniform vec3 u_LightColor;\n' + // 光线颜色
'uniform vec3 u_LightPosition;\n' + // 光源位置(世界坐标系)
'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
'varying vec4 v_Color;\n' +
'void main(){\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
// 对法向量进行变换和归一化
' vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
// 计算顶点的世界坐标
' vec4 vertexPosition = u_ModelMatrix * a_Position;\n' +
// 计算光线方向并归一化
' vec3 lightDirection = normalize(u_LightPosition - vec3(vertexPosition));\n' +
// 计算光线方向和法向量的点积 cosθ
' float nDotL = max(dot(lightDirection,normal),0.0);\n' +
// 计算漫反射光的颜色
' vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n' +
// 计算环境光产生的反射光颜色
' vec3 ambient = u_AmbientLight * a_Color.rgb;\n' +
// 最终颜色
' v_Color = vec4(diffuse + ambient,a_Color.a);\n' +
'}\n'
2.光线颜色和光源位置设计如下:(环境光依然保留(0.2,0.2,0.2)的设置,不再注明)
// 光线颜色
let u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor')
if (!u_LightColor) {
console.log('Failed to get the storage loaction of u_LightColor')
return
}
gl.uniform3f(u_LightColor, 1.0, 1.0, 1.0)
// 光源位置
let u_LightPosition = gl.getUniformLocation(gl.program,'u_LightPosition')
if (!u_LightPosition) {
console.log('Failed to get the storage loaction of u_LightPosition')
return
}
gl.uniform3f(u_LightPosition, 1.0, 1.5, 2.0)
六、逐片元光照
![](https://img.haomeiwen.com/i2354823/bdbd16c0c3ec08c0.png)
1.光照的计算挪到了片元着色器
// 片元着色器
var FSHADER_SOURCE =
'precision mediump float;\n' +
'uniform vec3 u_LightColor;\n' + // 光线颜色
'uniform vec3 u_LightPosition;\n' + // 光源位置(世界坐标系)
'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
'varying vec4 v_Color;\n' + // 基底色
'varying vec3 v_Normal;\n' + // 法向量
'varying vec3 v_Position;\n' + // 片元位置(世界坐标系)
'void main(){\n' +
// 法线归一化(内插后长度不一定为1.0)
' vec3 normal = normalize(v_Normal);\n' +
// 计算光线方向并归一化
' vec3 lightDirection = normalize(u_LightPosition - v_Position);\n' +
// 计算光线方向和法向量的点积 cosθ
' float nDotL = max(dot(lightDirection,normal),0.0);\n' +
// 计算漫反射光的颜色
' vec3 diffuse = u_LightColor * v_Color.rgb * nDotL;\n' +
// 计算环境光产生的反射光颜色
' vec3 ambient = u_AmbientLight * v_Color.rgb;\n' +
// 最终颜色
' gl_FragColor = vec4(diffuse+ambient,v_Color.a);\n' +
'}\n'
接收不随顶点变动的变量,包括光线颜色、光源位置(世界坐标系)、环境光颜色。
接收逐片元内插得到的片元世界坐标系坐标(原本是顶点,不使用gl_FragCoord是因为该坐标经过了视图和投影矩阵的改造)、法向量(最起码在立方体中,一个面法向量不变,法向量内插不会出现颜色内插的问题)、基底色。
2.顶点着色器
将计算反射光需要的、在顶点着色器中接收或计算的逐顶点的内容:顶点世界坐标、法向量和基底色传递给片元着色器
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'attribute vec4 a_Normal;\n' + // 法向量
'uniform mat4 u_MvpMatrix;\n' + // 模型视图投影矩阵
'uniform mat4 u_ModelMatrix;\n' + // 模型矩阵
'uniform mat4 u_NormalMatrix;\n' + // 变换法向量的矩阵
'varying vec4 v_Color;\n' +
'varying vec3 v_Normal;\n' +
'varying vec3 v_Position;\n' +
'void main(){\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
// 对法向量进行变换和归一化
' v_Normal = normalize(vec3(u_NormalMatrix * a_Normal));\n' +
// 计算顶点的世界坐标
' v_Position = vec3(u_ModelMatrix * a_Position);\n' +
// 基底色
' v_Color = a_Color;\n' +
'}\n'
网友评论