在计算机图形学领域,光照仿真是一个重要的研究领域,它对游戏画面的提升、电影和电视节目中的电脑生成图像(CGI)等方面产生了显著影响。通过使用不同的光照算法,我们可以改变场景的外观,例如模拟从白天到夜晚的过渡,或者在山体上产生山峰、峡谷和裂隙的效果。即使是二维场景,也可以通过光照特性来创造出视觉深度或爆炸效果。
在本章节中,我们将探索如何利用光照算法来增强山体的视觉效果。首先,我们会学习使用方向光源来实现漫反射,这会使得场景看起来像是被天空中的太阳照射。接着,我们将添加环境光以减少阴影区域的黑暗程度。
之后,我们将关闭天空盒,使其变暗,并学习如何使用点光源来照亮每个粒子喷泉。为了开始这个项目,我们将继续在上一章节的项目开始。
仿真光照效果
我们所看到的世界,实际上是由无数微小的光子累积而成的效果。这些光子从光源,比如太阳发射出来,经过长距离的旅行后,会从物体上反射或折射,最终到达我们的眼睛。眼睛和大脑接收这些光子,并重建它们形成的图像,这就是我们所看到的世界。
在计算机图形学中,模拟光子的行为来仿真光照效果是一项挑战。一种方法是使用射线跟踪器,它通过发射射线来模拟光子,并计算射线与场景中物体的交互,从而实现反射、折射和焦散等效果。尽管射线跟踪器非常强大,但它在实时渲染中的计算成本过高。
因此,大多数游戏和应用程序采用简化的方法来近似模拟光线的行为,而不是直接模拟光子。这些简化的算法广泛使用,并且有多种方法可以模拟反射、折射等效果。这些技术能够将大部分计算负载分配给GPU,即使在移动设备上也能快速运行。
在OpenGL中使用光照
在OpenGL中,我们可以通过不同类型的光源来为场景添加光照效果。这些光源包括:
环境光:它似乎从各个方向均匀地照亮场景,类似于天空的光照。环境光有助于避免阴影区域变得完全黑暗。
方向光:这种光看起来来自一个特定的方向,就像太阳或月亮的光照,光源似乎非常遥远。
点光:点光从场景中的一个点发出,随着距离的增加,光照强度逐渐减弱,类似于灯泡或蜡烛的照明效果。
聚光:与点光相似,但聚光只向一个特定方向投射,类似于手电筒或舞台聚光灯的效果。
此外,光线在物体表面的反射方式也可以分为两种:
漫反射:光线均匀地向各个方向散射,适合模拟没有光泽的表面,如地毯或混凝土墙。
镜面反射:光线在特定方向上强烈反射,适合模拟光滑或闪亮的材质,如金属或打过蜡的汽车表面。
许多材质同时具有这两种反射特性。例如,沥青路面通常看起来在各个方向上都是一样的,但在某些条件下,如太阳低空时,可能会在特定方向上产生强烈的反射,这有时会导致司机视线受阻,甚至引发交通事故。
博朗反射实现方向光
为了在图形渲染中实现漫反射效果,我们可以使用一种称为朗伯体反射的技术。这种技术以18世纪的瑞士数学家和天文学家Johann Heinrich Lambert的名字命名。朗伯体反射描述了一种表面,无论光线从哪个方向照射,它都能均匀地反射光线,使得从任何观察点看,表面看起来都是一样的。这种反射效果仅取决于表面与光源的相对位置和距离。
以一个简单的例子来说明:假设有一个水平表面和一盏方向光源,光源的强度不随距离减弱。在这种情况下,影响反射的唯一因素是表面相对于光源的方向。如下图所示,如果表面垂直于光源,它将捕捉并反射最多的光线。而在下图中,如果表面相对于光源旋转45度,它捕捉和反射的光线就会减少。
具体来说,当表面旋转45度时,它反射的光线量会减少到原来的0.707倍,这与旋转角度的余弦值有关。要计算表面接收到的光线量,我们只需计算出它直接面向光源时接收的光线量,然后乘以该角度的余弦值。
例如,如果一个朗伯体表面在与光源成0度角时接收了5流明的光线,那么当它与光源成45度角时,它将反射大约3.5流明的光线(5 × cos 45°)。理解朗伯体反射的关键在于理解这种光线接收量与角度余弦值之间的关系。
1.计算高度图的方位
在为高度图添加朗伯体反射效果之前,我们需要一种方法来确定其表面的朝向。由于高度图不是完全水平的,我们需要计算高度图上每个点的朝向。我们可以通过表面法线来表示这个朝向,这是一种特殊的向量,它垂直于表面且长度为1。
在计算每个点的法线时,我们实际上是将该点周围的相邻点结合起来,形成一个平面。我们用两个向量来定义这个平面:一个向量从右侧的点指向左侧的点,另一个向量从上面的点指向下面的点。通过计算这两个向量的叉积,我们可以得到一个垂直于该平面的向量,这就是我们所说的表面法线。然后,我们将这个向量归一化,使其长度为1,从而得到中间点的表面法线。
简而言之,通过计算高度图上每个点周围相邻点形成的平面的法线,我们能够确定每个点的表面朝向,这对于实现朗伯体反射效果至关重要。
我们来看看下图的一个例子:
在处理高度图时,我们通常假设每个点占据一个单位立方体,其中x坐标向右增加,z坐标向下增加。为了计算表面法线,我们需要考虑每个点周围的相邻点。
计算步骤:
确定相邻点的高度:假设一个点的上边、左边、右边和下边的点的高度分别是0.2、0.1、0.1和0.1。
计算向量:
- 从右向左的向量:用右边的点的高度减去左边的点的高度,得到向量(-2, 0, 0)。
- 从上向下的向量:用上边的点的高度减下去边的点的高度,得到向量(0, -0.1, 2)。
生成表面法线:
- 计算叉积:将这两个向量进行叉积运算,得到向量(0, 4, 0.2)。
- 归一化:将得到的向量归一化,得到表面法线(0, 0.9988, 0.05)。
选择向量方向的原因:
右手规则:我们使用从右向左的向量,因为我们希望表面法线指向上方,离开高度图。通过使用右手规则,我们可以确保叉积的方向是正确的。
这种方法确保了我们能够准确地计算出高度图中每个点的表面法线,这对于后续的光照计算和渲染效果至关重要。
现在我们已经知道了要做什么,让我们打开HeightMap类,开始改写代码。首先,加入一些新的常量:
private val POSITION_COMPONENT_COUNT = 3
private val NORMAL_COMPONENT_COUNT = 3
private val TOTAL_COMPONENT_COUNT = POSITION_COMPONENT_COUNT + NORMAL_COMPONENT_COUNT
private val STRIDE = (POSITION_COMPONENT_COUNT+NORMAL_COMPONENT_COUNT)*BYTES_PER_FLOAT
我们将改变顶点缓冲区,以使它存储位置和法线,为此,我们需要知道全部的分量计数和跨距。让我们更新loadBitmapData()中heightmapVertices的赋值语句,为法线增加一些空间:
val heightmapVertices = FloatArray(width*height*TOTAL_COMPONENT_COUNT)
这可以保证有足够的空间留给位置和法线。还是在loadBitmapData()内部,在我们生成位置的那个循环体内,把它更新为如下代码:
val point = getPoint(pixels,row,col)
heightmapVertices[offset++] = point.x
heightmapVertices[offset++] = point.y
heightmapVertices[offset++] = point.z
我们已经把位置的生成提取为一个独立的方法了,待会来定义它。让我们再添加些代码获取当前点的邻接点,并为其生成表面法线:
val top = getPoint(pixels,row-1,col)
val left = getPoint(pixels,row,col-1)
val right = getPoint(pixels,row,col+1)
val bottom = getPoint(pixels,row+1,col)
val rightToLeft = Geometry.vectorBetween(right,left)
val topToBottom = Geometry.vectorBetween(top,bottom)
val normal = rightToLeft.crossProduct(topToBottom).normalize()
heightmapVertices[offset++] = normal.x
heightmapVertices[offset++] = normal.y
heightmapVertices[offset++] = normal.z
为了生成这个法线,我们遵循了前面总结的算法:首先,获取其邻接点;然后,用这些点创建代表其平面的两个向量;最后,采用这两个向量的叉积,并把它归一化以得到其表面法线。我们还没有定义normalize(),因此,让我们打开Geometry类,找到Vector的定义,按如下代码加入这个方法:
fun normalize():Vector{
return scale(1f/length())
}
让我们继续编写 Heightmap类,加入 getPoint()的定义:
private fun getPoint(pixels:IntArray,row:Int,col:Int): Point {
var x = col.toFloat()/(width-1).toFloat() - 0.5f
var z = row.toFloat()/(height-1).toFloat() -0.5f
var rw = clamp(row,0,width - 1)
var cl = clamp(col,0,height - 1)
var y = Color.red(pixels[rw*height+cl]).toFloat()/255f
return Point(x,y,z)
}
这段代码的工作与之前其在循环体内的代码是一样的,但是,它现在对邻接点超出边界的情况做了限制。比如,当我们为(0,0)生成法线时,要取其上边和左边的点,这些点实际上在高度图中是不存在的,这种情况下,我们假定它们存在,并给它们赋予与中间顶点一样的高度。这样,我们还可以为那个顶点生成一个表面法线。
让我们完成所有的改变,把bindData()更新为如下代码:
fun bindData(heightmapProgram:HeightmapShaderProgram){
vertexBuffer.setVertexAttribPointer(0,heightmapProgram.aPositionLocation,POSITION_COMPONENT_COUNT,STRIDE)
vertexBuffer.setVertexAttribPointer(0,heightmapProgram.aNormalLocation,NORMAL_COMPONENT_COUNT,STRIDE)
}
因为在同一个顶点缓冲区对象中既存储了位置,又存储了法线数据,我们现在不得不把跨距传递给调用glVertexAttribPointer()的辅助函数setVertexAttribPointer(),以使OpenGL知道在每个元素之间需要跳过多少字节。第二个setVertexAttribPointer()调用也非常重要,因为我们也为法线指定了以字节为单位的起始偏移值;否则,OpenGL就会读入一部分位置和一部分法线,并把那个值当作法线,这看起来会非常怪异。
2.给着色器加入方向光
既然高度图包括了法线,我们的下一个任务就是更新高度图着色器,为其加入支持方向光的代码。先给 heightmap_vertex_shader.glsl加入一个新的 uniform,代码如下:
uniform vec3 u_VectorToLight;
这个向量将存储指向方向光源的归一化向量。我们还需要一个用于高度图法线的新属性:
attribute vec3 a_Normal;
现在,准备就绪了,给着色器的主体加人如下代码:
vec3 scaledNormal = a_Normal;
scaledNormal.y *= 2.0;
scaledNormal = normalize(scaledNormal);
你可能记得,当我们绘制高度图的时候,使用scaleM()扩展了它,使它变成2倍高和20倍宽,换句话说,高度图现在宽于它的高度10倍。以这种方式缩放改变了高度图的形状,意味着,我们预先产生的法线也不正确了。为了弥补这一点,我们按相反的方向缩放法线,使法线高于它的宽度10倍。重新归一化法线之后,它现在就会匹配这个新的几何形状了。
这个工作原理涉及一些高等数学,因此,就目前而言,你只需接受这个结果。稍后,我们会在看一个更通用的调整法线的方法。
我们已经调整了表面法线,接下来计算朗伯体反射:
float diffuse = max(dot(scaledNormal,u_VectorToLight),0.0);
v_Color *= diffuse;
要计算表面与光线之间夹角的余弦值,我们要计算指向光源的向量与表面法线的点积。它的工作原理是,当两个向量都是归一化的向量时,那两个向量的点积就是它们之间夹角的余弦,这恰恰就是我们计算朗伯体反射所需要的。
为了避免出现负的结果,我们用max()把最小余弦值限制为0,然后,应用这个光线,把当前顶点的颜色与余弦值相乘。余弦值在0和1之间,因此,最终的颜色将是处于黑色和原色之间的某个颜色。
3.更新着色器封装类代码
我们现在需要更新封装类以反应这些新的改变。首先,为ShaderProgram加入如下这些新的常量:
protected val U_VECTOR_TO_LIGHT = "u_VectorToLight"
protected val A_NORMAL = "a_Normal"
切换到HeightmapShaderProgram,为方向光的uniform的位置和法线属性的位置加入新的成员变量:
var uVectorToLightLocation = 0
var aNormalLocation = 0
在构造函数的结尾处加入如下代码,为这些新位置赋值:
uVectorToLightLocation = findUniformLocationByName(U_VECTOR_TO_LIGHT)
aNormalLocation = findAttribLocationByName(A_NORMAL)
我们需要更新setUniformsO,以便可以更新这个新的uniform,如下代码所示:
fun setUniforms(matrix:FloatArray,vectorToLight:Vector){
GLES20.glUniformMatrix4fv(uMVMatrixLocation, 1,false,matrix,0)
GLES20.glUniform3f(uVectorToLightLocation,vectorToLight.x,vectorToLight.y,vectorToLight.z)
}
4.观看方向光效果
打开Renderer,给我们的光源定义实际的向量:
private var vectorToLight = Vector(0.61f,0.64f,-0.47f).normalize()
这个向量大约指向天空盒中的太阳。你可以用下面这些步骤得出一个相似的结果:
-
1.创建一个指向(0,0,-1)的向量,也就是指向正上方。
-
2.按场景旋转方向的反向旋转这个向量。
-
3.加入日志语句打印这个向量当前的方向,然后运行这个应用,旋转场景直到太阳处于屏幕的中间。
我们同样归一化这个向量,以便将它传递给着色器,并用它计算朗伯体反射。让我们将drawHeightmap()中的heightmapProgram.setUniforms()调用更新为下面的代码,把这个向量传递给着色器:
heightmapProgram.setUniforms(modelViewProjectionMatrix,vectorToLight)
就是这些!让这个应用运行一次,看看我们得到了什么?(图略)我们现在可以看到山的形状和形式了,但是你可能注意到了,黑暗的区域太黑了。问题是由于我们没有全局照明;在实际生活中,光在到达眼睛前通过天空漫射并在许多物体上发生了反射,因此,太阳投下的阴影没有一处是近乎漆黑的。我们可以在场景中加入环境光伪造这个现象,环境光平等地照射到所有物体上。让我们回到heightmap_vertex_shader.glsl,在vColor和漫反射相乘的那行代码后面加入如下代码:
float ambient = 0.2;
v_Color += ambient;
这给整个高度图加入一个照明的基础量,这样,就没有东西会呈现得太暗。再看一下这个效果,看看我们得到了什么。(图略),阴影现在看起来更加合理了。
添加点光
现在我们准备好给场景增加一些点光源了,这样就可以让粒子喷泉发光了。在一个明亮的背景下,我们无法很好地看到这个效果,因此,我们要做的第一件事就是切换到夜晚的天空盒。我们将会使用下面几张图(后,下,前,左,右,上),并把它放在项目的“/res/drawable-nodpi”文件夹中。
要切换到夜晚的天空盒,让我们回到ParticlesRenderer类,并按如下代码更新skyboxTexture的赋值语句:
skyboxTexture = TextureHelper.loadCubeMap(context, intArrayOf(
R.drawable.night_left,R.drawable.night_right,
R.drawable.night_bottom,R.drawable.night_top,
R.drawable.night_front,R.drawable.night_back
))
然后,把指向光源的向量更新为如下代码:
private var vectorToLight = Vector(0.30f,0.35f,-0.89f).normalize()
这个新的向量指向天空盒中的月亮。我们还需要在着色器中调低光的强度,因此,让我们返回heightmap_vertex_shader.glsl,在漫反射与v_Color相乘之前,给它加入如下的调整量:
diffuse *= 0.3;
我们还应该调低环境光:
float ambient = 0.1;
如果你现在继续运行这个应用,应该看到一个夜晚的背景和相应变暗的高度图。
1.理解点光源
我们一直在讲述漫反射和朗伯体反射模型,点光与方向光在数学上都是相似的,然而,我们需要记住两个关键的不同点:
-
对于方向光,我们只存储指向其光源的向量,因为这个向量对于场景中的所有点都是一样的;对于点光则相反,我们将存储其光源的位置,用这个位置计算场景中每个点指向这个光源的向量。
-
在实际生活中,点光源的亮度会随着距离的平方降低;这叫作平方反比定律(The Inverse Square Law)。我们将使用点光源的位置计算出其与场景中每个点的距离。
2.给着色器加入点光
要实现点光,我们需要改变着色器,借助这次机会我们将采用一个更加结构化且通用的方法在着色器中加入光照。让我们看一看最重要的一些改变:
-
我们将把位置和法线放人眼空间(eyespace),在这个空间里,所有的位置和法线都是相对于照相机的位置和方位;这样做是为了使我们可以在同一个坐标空间中比较所有事物的距离和方位。我们为什么使用眼空间而不是世界空间呢?因为镜面光也依赖于照相机的位置,即使我们在本章中没有使用镜面光,学习如何使用眼空间还是一个好主意,这样,我们在以后就可以直接使用它了。
-
要把一个位置放进眼空间中,我们只需要让它与模型矩阵相乘,把它放入世界空间中,然后再把它与视图矩阵相乘,这样就把它放入眼空间了。为了简化操作,我们可以把视图矩阵与模型矩阵相乘得到一个单一的矩阵,称为模型视图矩阵,再用这个矩阵把我们的位置放入眼空间中。
-
如果模型视图矩阵只包含平移或旋转,这对法线也是有用的,但是,如果我们缩放了一个物体,会怎么样?如果缩放在所有方向上都是一样的,我们只需要重新归一化法线,使它的长度保持为1,但是如果物体在某个方向上被压扁了,那么我们要补偿那一点。
当我们加入方向光时,我们确切地知道高度图缩放了多少,因此,可以直接补偿。
这不是一个灵活的方案,有一个通用的方法可以实现这些,倒置模型视图矩阵,转置这个倒置的矩阵,让法线与那个矩阵相乘,然后归一化其结果。这个方法可行的原因涉及一些高等数学;如果你感兴趣,提供两个很好的解释链接,它们讲述了很多详细的内容。
首先,用下面的内容替换heightmap_vertex_shader.glsl:
uniform mat4 u_MVMatrix;
uniform mat4 u_IT_MVMatrix;
uniform mat4 u_MVPMatrix;
uniform vec3 u_VectorToLight;
uniform vec4 u_PointLightPositions[3];
uniform vec3 u_PointLightColors[3];
attribute vec4 a_Position;
attribute vec3 a_Normal;
varying vec3 v_Color;
vec3 materialColor;
vec4 eyeSpacePosition;
vec3 eyeSpaceNormal;
vec3 getAmbientLighting();
vec3 getDirectionalLighting();
vec3 getPointLighting();
我们现在将使用u_MVMatrix表示模型视图矩阵,使用u_IT_MVMatrix表示那个倒置矩阵的转置,并用u_MVPMatrix表示合并后的模型视图投影矩阵,正如我们以前使用u_Matrix一样。
方向光向量还与以前一样,不同之处在于,我们现在希望它被定义在眼空间中。我们用u_PointLightPositions传递点光源的位置,它也被定义在眼空间中,并用u_PointLightColors传递颜色。最后两个uniform被定义为数组,以便我们可以通过一个uniform传递多个向量。
对于属性,我们现在用一个vec4类型表示其位置,以减少vec3和vec4之间的转换次数。我们不需要改变顶点数据,因为OpenGL会用默认值1设置第4个分量,但是还要小心:uniform就不一样了,它们必须要指定所有分量的值。
varying还与以前一样;varying定义之后,我们加入了几个新的变量,我们要使用它们计算光照,我们还有三个新函数的声明,稍后会在着色器中定义它们。
继续在着色器中加入下面的代码:
void main(){
materialColor = mix(vec3(0.180,0.467,0.153),
vec3(0.660,0.670,0.680),
a_Position.y);
eyeSpacePosition = u_MVMatrix * a_Position;
eyeSpaceNormal = normalize(vec3(u_IT_MVMatrix * vec4(a_Normal,0.0)));
v_Color = getAmbientLighting();
v_Color += getDirectionalLighting();
v_Color += getPointLighting();
gl_Position = u_MVPMatrix * a_Position;
}
在着色器的主方法内,我们像以前一样给材质的颜色赋值,并在眼空间中计算当前的位置和法线。然后,我们计算每一种光的类型,并把其结果颜色累加到v_Color上,最后,像以前一样投影其位置。
继续编写如下代码:
vec3 getAmbientLighting(){
return materialColor * 0.1;
}
vec3 getDirectionalLighting(){
return materialColor * 0.3 * max(dot(eyeSpaceNormal,u_VectorToLight),0.0);
}
正如我们之前所做的一样,这两个函数计算环境光和方向光。让我们用下面为点光定义的函数结束这个着色器:
vec3 getPointLighting(){
vec3 lightingSum = vec3(0.0);
for(int i = 0; i < 3; i++){
vec3 toPointLight = vec3(u_PointLightPositions[i]) - vec3(eyeSpacePosition);
float distance = length(toPointLight);
toPointLight = normalize(toPointLight);
float cosine = max(dot(eyeSpaceNormal,toPointLight),0.0);
lightingSum += (materialColor * u_PointLightColors[i] * 5.0 * cosine) / distance;
}
return lightingSum;
}
介绍一下它的工作原理,我们循环计算每个通过的点光,计算其每一个的光照,并把其结果加入lightingSum。这段代码用朗伯体反射计算其光照量,就像之前计算方向光一样,但还是有些很重要的不同点:
-
对于每个点光源,我们都要计算当前位置到那个光源的向量,也要计算当前位置与那个光源的距离。
-
一旦得到了归一化的向量,我们就可以计算其朗伯体反射。然后,把那个材质的颜色与点光的颜色相乘,并把其结果颜色应用于当前的顶点。我们用5放大这个结果,使其显得更明亮一点,并把它与其余弦值相乘,计算其朗伯体反射。
-
在把其结果累加到lightingSum之前,用前一步的结果除以其距离,以使其光照密度随距离而减少。
最后的计算完成了,我们的着色器也写完了。
3.显示设备的非线性本质
由于OpenGL和显示设备处理光照和颜色的方法不同,要在 OpengGL中获得正确的光照和颜色有时候是非常棘手的。对于OpenGL来说,颜色是落在线性频谱上的,因此,一个值为1.0的颜色的亮度是值为0.5的颜色的两倍。然而,由于许多显示设备的非线性本质,对于显示器上的亮度,其实际的区别可能要比这个大多了。
其以这种方式工作的原因,部分是由于历史的原因。曾几何时,大家都用过大的、笨重的CRT显示器作为主要的显示设备,而这些显示器是通过把电子束射击到屏幕上工作的。这些荧光粉往往有一个指数响应,而不是线性响应,就使得1.0的颜色的亮度比0.5的颜色大两倍多。出于兼容性等原因,许多显示设备直到今天还维持着类似的行为。
这种非线性的行为会搞乱我们的光照,使事物比实际要暗。通常情况下,计算光照的衰减是用其密度除以其距离的平方,但为了不让点光衰减得太快,我们去掉了指数,只除以其距离。
4.更新着色器的封装代码
我们现在需要更新着色器封装代码以匹配这个新的着色器。打开 ShaderProgram,并加入一些新的常量:
protected val U_MV_MATRIX = "u_MVMatrix"
protected val U_IT_MV_MATRIX = "u_IT_MVMatrix"
protected val U_MVP_MATRIX = "u_MVPMatrix"
protected val U_POINT_LIGHT_POSITIONS = "u_PointLightPositions"
protected val U_POINT_LIGHT_COLORS = "u_PointLightColors"
我们还需要改动HeightmapShaderProgram。去掉uMatrixLocation和与其相关的代码,并加入如下这些新成员:
var uMVMatrixLocation = 0
var uIT_MVMatrixLocation = 0
var uMVPMatrixLocation = 0
var uPointLightPositionsLocation = 0
var uPointLightColorsLocation = 0
我们还需要更新其构造函数:
uMVMatrixLocation = findUniformLocationByName(U_MV_MATRIX)
uIT_MVMatrixLocation = findUniformLocationByName(U_IT_MV_MATRIX)
uMVPMatrixLocation = findUniformLocationByName(U_MVP_MATRIX)
uPointLightPositionsLocation = findUniformLocationByName(U_POINT_LIGHT_POSITIONS)
uPointLightColorsLocation = findUniformLocationByName(U_POINT_LIGHT_COLORS)
要完成这些改变,要需要更新setUniforms,如下代码所示:
fun setUniforms(mvMatrix:FloatArray,
it_mvMatrix:FloatArray,
mvpMatrix:FloatArray,
vectorToDirectionalLight:FloatArray,
pointLightPositions:FloatArray,
pointLightColors:FloatArray
){
GLES20.glUniformMatrix4fv(uMVMatrixLocation,1,false,mvMatrix,0)
GLES20.glUniformMatrix4fv(uIT_MVMatrixLocation, 1, false, it_mvMatrix, 0)
GLES20.glUniformMatrix4fv(uMVPMatrixLocation, 1, false, mvpMatrix, 0)
GLES20.glUniform3fv(uVectorToLightLocation, 1, vectorToDirectionalLight, 0);
GLES20.glUniform4fv(uPointLightPositionsLocation, 3, pointLightPositions, 0);
GLES20.glUniform3fv(uPointLightColorsLocation, 3, pointLightColors, 0);
}
我们现在传递了几个矩阵,以及方向光和点光的位置与颜色。这个方法体的前三行代码把所有矩阵传递给着色器。
第四行传递方向光的向量给着色器,接下来的两行把点光的位置和颜色也传递给着色器。我们把着色器中的最后两个uniform定义为有三个向量的数组,因此,对于每个uniform,我们调用 glUniform*fv()时,都把第二个参数设为3,3是其计数。这告诉OpenGL需要从数组中为那个uniform读人3个向量。
5.更新Renderer类
我们现在只需要更新Renderer以便定义和传递这些新的uniform。首先,我们需要在类的顶部定义两个新的矩阵:
private var modelViewMatrix = FloatArray(16)
private var it_modelViewMatrix = FloatArray(16)
更新 updateMvpMatrix()来设置这两个新的矩阵:
private fun updateMvpMatrix(){
Matrix.multiplyMM(modelViewMatrix,0,viewMatrix,0,modelMatrix,0)
Matrix.invertM(tempMatrix,0,modelViewMatrix,0)
Matrix.transposeM(it_modelViewMatrix,0,tempMatrix,0)
Matrix.multiplyMM(modelViewProjectionMatrix,0,projectionMatrix,0,modelViewMatrix,0)
}
这段代码把 modelViewMatrix设置为合并后的模型视图矩阵,并把it_modelViewMatrix设置为那个反转矩阵的倒置。回到类的顶部,我们还需要为那些新的光加人一些新成员:
private var vectorToLight = floatArrayOf(0.30f,0.35f,-0.89f,0f)
private var pointLightPositions = floatArrayOf(
-1f,1f,0f,1f,
0f,1f,0f,1f,
1f,1f,0f,1f
)
private var pointLightColors = floatArrayOf(
1.00f,0.20f,0.02f,
0.02f,0.25f,0.02f,
0.02f,0.20f,1.00f
)
这个新的vectorToLight的定义要替换掉前面的定义;你不久就会明白我们为什么要把它存到一个普通的浮点数组中。我们也要把每个点光源的位置和颜色存到它们各自的数组中,这些位置和颜色与我们为每个粒子发射器设定的位置和颜色大致匹配。其主要的区别在于每个点光源都被放在它的粒子发射器上方一个单位处,因为地形是绿色的,绿色的光也稍微变暗些,这样它就不会压制住红光和蓝光。
现在,我们只需要用下面的代码替换drawHeightmap()中的setUniforms()调用:
val vectorToLightInEyeSpace = FloatArray(4)
val pointPositionsInEyeSpace = FloatArray(12)
Matrix.multiplyMV(vectorToLightInEyeSpace, 0, viewMatrix, 0, vectorToLight, 0)
Matrix.multiplyMV(pointPositionsInEyeSpace, 0, viewMatrix, 0, pointLightPositions, 0)
Matrix.multiplyMV(pointPositionsInEyeSpace, 4, viewMatrix, 0, pointLightPositions, 4)
Matrix.multiplyMV(pointPositionsInEyeSpace, 8, viewMatrix, 0, pointLightPositions, 8)
heightmapProgram.setUniforms(modelViewMatrix,it_modelViewMatrix,modelViewProjectionMatrix,
vectorToLightInEyeSpace,pointPositionsInEyeSpace,pointLightColors)
我们需要把方向光的向量和点光源的位置放入眼空间中,为此,我们使用Android的Matrix类把它们乘以视图矩阵。那些位置已经在世界空间中了,因此,不必事先把它们与模型矩阵相乘。一旦完成了这些,我们就可以用heightmapProgram.setUniforms()调用把所有数据传递给着色器。
让我们来看一眼!如果一切顺利,你的屏幕看上去应该与下图相似。
小结
在本篇中,我们深入探讨了环境光、方向光和点光的概念,并学习了如何利用朗伯反射模型来实现漫反射效果。这些光照计算的公式对于创建引人注目的游戏和动态壁纸至关重要。
我们建立的框架不仅有助于理解基础的光照模型,还可以扩展到更高级的光照类型,例如镜面反射。随着计算的复杂性增加,我们通常会在已有的基础上逐步构建更复杂的模型,就像点光源的计算是在方向光源的基础上进行的一样。