# OpenGLES2.0 中的光照
翻译自 Android 课程二:环境光和漫反射光
# 1. 什么是光?
世界没有光就会什么也看不到,我们将无法感知这个世界活着物体。
在真实的世界里,光是由无数的光子聚合在一起形成的,它从一个光源出发,经过无数时间,最后传递到了我们的眼睛里
我们应该如何用计算机图像模拟光呢,现在有 2 种热门技术可以实现:光线跟踪和栅格化。光线跟踪通过数学计算跟踪真实的光线,能给出十分准确和真实的结果,但是不足之处是模拟所有的光线十分消耗计算资源,并且实时渲染速度很慢。由于这个限制,大多数的实时计算机图像使用的是栅格化技术,通过接近结果模拟光线。
# 2. 光线的分类
我们可以抽象出光工作的方式形成三种基本的光线类型
- 环境光
环境光是一种基础的光线,它遍布整个场景,它没有表现出来自任何其他的光源,因为它在到达你之前经过了无数的反射。这种光能阴天的户外,或是许多不同光源累积影响的室内。没有分别计算所有独立的光,我们可以为对象或场景设置一个基本的光照等级。
- 漫反射光
这种光在到达教你的眼睛里经过了物体之间反射。物体的光照等级随它与光线的角度不同而变化。直面它的时候更加明亮。同样我们感知物体时无论我们相对物体的位置在哪里,亮点都是相同的。这种现象也叫兰伯特余弦定律(Lambert's cosine law),漫反射和兰伯特反射在生活中是很常见的。
- 高光
和漫反射不同,高光随我们和物体的位置不同而不同,它让物体发亮和更加光滑
# 2. 模拟光
与在 3D 场景中的光有 3 种一样,光源也有 3 种:直接光源、Point lighting、Spot lighting。
# 1. 数学
学习来自一个点光源的环境光和漫反射光
# 环境光
环境光其实是漫反射光的一种,但是它也能被看作是充满整个场景的低级光。这样想的话,它会很容易计算
1
final color = material color * ambient light color
例如,有个物体是红色的,我们的环境光是灰白的。让我们假定将颜色存储为有 3 个颜色的数组红、绿、蓝,使用 RGB 颜色模型:
1
final color = { 1 , 0 , 0 } * { 0.1 , 0.1 , 0.1 } = { 0.1 , 0.0 , 0.0 }
最终物体的颜色会是淡红色。基础的场景光就是这样的,除非你想要更加高级的视觉技术
# 漫反射光 - 点光源
对漫反射光,我们需要增加衰减和光的位置。光的位置会用来计算光和表面的角度,它会影响表面的光照的整个等级。它还用来计算光和表面的距离,并决定了那个点的光照强度。
步骤一: 计算兰伯特因子
我们需要的第一个主要的计算是计算出表明和光的角度。直面光线的表层会处于光照的最大强度。计算出这个属性的合适的方式是使用兰伯特余弦定理。如果我们有 2 个向量,一个是从光线到表面的一个点,第二个是曲面法线,我们可以计算出余弦值:先将各个向量归一化,因此有它们的长度为 1,然后计算出 2 个向量的点积。这个操作能通过 OpenGL ES 的两个着色器轻松完成。
我们声明 lambert 因子,它的范围是 0 到 1
1
2
3light vector = light position - obejct position
cosine = dot product(object normal,normalize(light vector))
lambert factor = max(cosine,0)
首先我们通过对象位置减去光线位置计算出光线的向量,然后我们获取物体的法线和光线向量的点积,就得到了这个余弦了。归一化光线向量的意思就是改变它的长度为 1。因为点积德范围是 - 1 到 1,我们把它固定到(0,1)。
这是一个例子:有个光滑的平面,表面的法线笔直的指向天。光线的位置为「0,10,-10」,或是向上 10 个单位,向前 10 个单位,我们要计算出原地的光
1
2light vector = { 0, 10, -10} - {0, 0, 0} = {0, 10, -10}
obejct normal = {0, 1, 0}
用平白的语言来说,如果我们沿着光线向量出发移动,到达光线的位置。要归一化这个向量,我们让向量的每一个标量处以向量的长度:
1
2light vector length = square root( 0*0 + 10*10 + -10*-10) = square root(200) = 14.14
normalized light vector = {0/14.14, 10/14.14, -10/14.14} = {0, 0.707, -0.707}
然后我们计算点积
1
2dot product({0,1,0},{0,0.707, -0.707}) = 0*0 + 1*0.707 + 0*-0.707 = 0.707
lambert factor = max(0.707,0) = 0.707
OpenGL ES 2 的着色器语言已经内置了一些这种函数,所以我们不需要手动做所有的数学,但是这对我们的理解仍然有帮助。
步骤二:计算衰减因子
接着,我们需要计算衰减,真实的点光源的光线衰减遵从平方反比定律,它可以表述为:
luminosity = 1 / (distance* distance)
回到我们的例子,我们知道了一个距离为 14.14,所以最后我们的亮度看起来是:
luminosity = 1/ (14.14*14.14) = 0.005
你可以看到,平方反比定律在距离上会导致剧烈的衰减。这就是光在真实世界中从点光源出发是怎么回事,但是我们的图像显示有限制范围,因此抑制衰减因子,我们能得到更加真实的光照而不会使物体看起来太暗了。
步骤三:计算最后的颜色
现在我们同时有了余弦和衰减因子,我们能计算出最终的光照等级:
1
final color = meterial color * (light color * lambert factor * luminosity)
回到之前一个例子,我们有红色的材料和全白的光源,最终的计算:
1
2
3final color = {1, 0, 0} *({1,1,1}*0.707*0.005)
= {1,0,0}*{0.0035,0.0035,0.0035}
= {0.0035,0,0}
简单的总结一下,对漫反射光我们需要使用表面和光线的角度,还有表面和光线的距离来最终计算出整个漫反射照明等级。下面是步骤:
1
2
3
4
5
6
7
8
9
10//one
light vector = light position - object position
cosine = dot product(object normal,normalize(light vector))
lambert factor = max(cosine,0)
//two
luminosity = 1/(distance*distance)
//three
final color = material color * (light color*lambert factor * luminosity)
# 顶点着色器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31final String vertexShader =
"uniform mat4 u_MVPMatrix; \n" // 一个表示进行了Model变换、View变换、投影变换的矩阵
+ "uniform mat4 u_MVMatrix; \n" // 一个表示进行了Model变换和View变换的矩阵
+ "uniform vec3 u_LightPos; \n" // 在眼坐标系中光的位置
+ "attribute vec4 a_Position; \n" // 传入的顶点位置信息
+ "attribute vec4 a_Color; \n" // 传人的每个顶点的颜色信息
+ "attribute vec3 a_Normal; \n" // 每个顶点的法线信息
+ "varying vec4 v_Color; \n" // 颜色信息,这个变量会传递进片段着色器中
+ "void main() \n"
+ "{ \n"
// 将顶点转化为眼坐标系
+ " vec3 modelViewVertex = vec3(u_MVMatrix * a_Position); \n"
// 将法线方向转移进眼坐标系
+ " vec3 modelViewNormal = vec3(u_MVMatrix * vec4(a_Normal, 0.0)); \n"
// 计算出顶点和光线的距离
+ " float distance = length(u_LightPos - modelViewVertex); \n"
// 获取从光线处到顶点的光线向量
+ " vec3 lightVector = normalize(u_LightPos - modelViewVertex); \n"
// 计算lambert factor,也就是计算光线向量和顶点法线的点积. 如果法线向量和光线向量指向同一个方向,则会得出最大的光照强度.
+ " float lambert = max(dot(modelViewNormal, lightVector), 0.1); \n"
// 光线随距离的衰减
+ " float diffuse = lambert * (1.0 / (1.0 + (0.25 * distance * distance))); \n"
// 将颜色乘以衰减度, It will be interpolated across the triangle.
+ " v_Color = a_Color * diffuse; \n"
// gl_Position 存储最终的位置.
// 将这个向量乘以变换矩阵得到在归一屏幕坐标系中的点
+ " gl_Position = u_MVPMatrix * a_Position; \n"
+ "} \n";
# 片段着色器
1
2
3
4
5
6
7
8
9final String fragmentShader =
"precision mediump float; \n" // Set the default precision to medium. We don't need as high of a
// precision in the fragment shader.
+ "varying vec4 v_Color; \n" // This is the color from the vertex shader interpolated across the
// triangle per fragment.
+ "void main() \n" // The entry point for our fragment shader.
+ "{ \n"
+ " gl_FragColor = v_Color; \n" // Pass the color directly through the pipeline.
+ "} \n";
# 光源的顶点、片段着色器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// Define a simple shader program for our point.
final String pointVertexShader =
"uniform mat4 u_MVPMatrix; \n"
+ "attribute vec4 a_Position; \n"
+ "void main() \n"
+ "{ \n"
+ " gl_Position = u_MVPMatrix \n"
+ " * a_Position; \n"
+ " gl_PointSize = 5.0; \n"
+ "} \n";
final String pointFragmentShader =
"precision mediump float; \n"
+ "void main() \n"
+ "{ \n"
+ " gl_FragColor = vec4(1.0, \n" //直接指定片段颜色为白色
+ " 1.0, 1.0, 1.0); \n"
+ "} \n";
有一个新的属性叫 gl_PointSize
是点在空间中的大小为多少个像素。