# 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
3
light 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
2
light vector = { 0, 10, -10} - {0, 0, 0} = {0, 10, -10}
obejct normal = {0, 1, 0}

用平白的语言来说,如果我们沿着光线向量出发移动,到达光线的位置。要归一化这个向量,我们让向量的每一个标量处以向量的长度:

1
2
light 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
2
dot 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
3
final 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
31
final 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
9
final 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 是点在空间中的大小为多少个像素。