现在都pbr时代了,还讨论那些上古时代的技术?当作后面pbr的铺垫吧,或许有额外收获?谁知道呢?
早在GPU硬件加速成为行业标准之前,游戏引擎就已经有应用光照方面算法的例子了,这可以追溯到《毁灭战士》等游戏中的逐区域光照。随着图像硬件的改进,游戏中使用的光照模型也得到了改进,光照计算从逐面、逐顶点,最后到逐片元的发展。无论在哪种方式上计算光照,都有各种各样的光照模型用于确定当前被照亮表面的最终颜色,尽管它们通常都是基于Phong反射模型,它将反射方式分为ambience、diffusion和specularity等概念,不管哪种方式都会生成对应的颜色输出到图像中。
Ambience
现实意义的环境光能够在一个场景中被无数的物体表面反射。由于计算机的运算能力有限,所以一般都会简单定义环境光,环境光没有方向,只有亮度,因此计算光源产生的环境光颜色是很简单的:
Diffusion
漫反射是指来自给定光源的光线在表面上向各个方向散射的量——(只考虑光均匀散射的情况)。一个表面离光源越近,它受到光线照射的强度就越强,因此它看起来就越亮。这可以使用面法线计算,面法线介绍可看 VertexNormal,FaceNormal与NormalMatrix 。我们可以使用Lambert余弦定律精确地计算出当前表面扩散了多少光量,该定律指出,表面的亮度直接与表面的法向量和入射向量之间的余弦成正比。入射向量是光源和表面之间的标准化方向向量,可以简单地计算出来:
我们可以通过计算面法线与入射光线两个标准化方向向量的点积来确定它们的余弦值,这个操作执行方式如下:
由于余弦值范围在-1.0到1.0之间,点积结果应该被固定在0.0到1.0之间,以正确计算出最终从一个表面反射的漫反射的颜色强度:
上述对法线与入射光线的叉积计算结果做了限制,因为叉积中余弦值小于0.0意味着一个表面处在背面,因此处在背面是不接受光照效果的。
当光线越远离表面,光照强度越弱
Specularity
漫反射描述了光线如何在各个方向均匀散射,而Specularity高光模型则描述了光线如何直接被反射到视角方向上。这种高光反射模型呈现的效果有点类似物体被另类的"聚光灯"所照射,它的细节清晰度与反射表面的平滑度成正比,它可以用微表面(microfacet)来解释:平面是由数以百万计的小表面组成的,每个小表面都有其特定不同的方向变化。粗糙平面的微表面(microfacet)朝向的方向与平面法线的夹角更大,因此直接反射出来的光线更少。Specularity反射模型的图示如下:
上图的NormalVector表示平面的法线向量,IncidentVector表示入射光线,ViewVector表示视图向量(这几个向量都是标准化后的向量),HalfAngleVector(半角向量)的求取方式如下:
为什么使用半角向量?是为了简化Phong高光反射方式而做出的改进,因为在半角向量解决方案之前通过入射光线的反射向量(ReflectVector)与ViewVector进行计算,由于计算反射向量在早期的计算机中有一定成本,所以半角向量是算法的改进,通过半角向量计算模型的高光颜色可通过下面公式:
公式中的n是一个指数,用来模拟表面的光滑程度。
上图是根据n指数的不同,模拟物体表面的光滑程度,从左到右n的值分别为:1,10,100。
Attenuation
我们现在知道如何基于Phong模型计算环境,漫反射和高光反射的方式。但是还需要解决的一个问题是现实世界的光线是会衰减的,想象一根蜡烛,距离蜡烛远的位置比较暗,较近的位置比较亮,所以我们真正想要的,是让光源发出的光随距离衰减。虽然衰减的计算模拟的不是很准确,但它计算成本低,且可以很容易地计算出当前表面离光源有多远,表面离光源的距离通过下面的方式计算:
有了距离我们就需要求取光照的衰减算法,有很多种方法可以计算光随距离的衰减程度,其中最简单的方法是线性衰减。如果我们的光源有照亮一个表面的最大距离(本质上是一个半径,在光的位置周围形成一个球体),我们可以计算出线性衰减如下:
衰减限制在0.0和1.0之间,否则我们将得到负的结果。
Lighting Calculation
环境光、漫反射、高光和衰减的Phong反射模型的最终结果如下:
实际开发中,有些游戏会忽略surface specular colour与light specular colour,以便提高处理的效率并节省内存空间。另外环境光的处理上是不进行衰减的:
Phong反射模型,从左上角开始并从左到右的顺序,分别是:Ambient,diffuse, specular, final image
Lighting Models
Phong反射模型可以以不同方式应用于模型物体,每种方式都有不同的计算复杂度。
Flat Shading
Flat shading(平面着色)是实时照明中最简单的方法。每个表面都有唯一的法线,可对该法线计算照明。它不会产生插值(任何给定表面的每个片元都是相同的光照)。
Gouraud Shading
Gouraud shading(高洛德着色)的漫反射、高光和衰减是按每个顶点计算的,然后在顶点之间插值来计算每个像素的最终颜色,它与插值顶点颜色的方式有点类似,这种方式比平面着色在计算上需要更多的代价,但是它比逐片元的计算方式拥有更高的效率,因为它通过插值计算的方式。Gouraud着色的最终效果如何取决于模型中有多少个顶点——顶点越多,插值就越准确。
Phong Shading
在Phong Shading中,所有的计算都是在逐个片元的基础上计算的,尽管法线仍然是通过在顶点之间插值获得。显然,这是最费性能的照明模型,但现代GPU硬件还是能够对多个光源进行实时着色。
相同网格分别使用Flat, Phong和Gourard照明模型渲染
Static Lighting
另一种照明方式是使用光照贴图。与实时执行高昂的光照计算不同,某些游戏中的关卡需要经过大量的预处理步骤,即离线处理游戏世界中每个多边形面的光照效果,并将每个面的光照信息存储在一个特定的光照贴图纹理中。光照贴图可以在运行时使用多重纹理与所需的纹理图混合,该方式可在游戏世界中创建一个真实的,但是静态的光和影表现。每个面对应的光照贴图分辨率通常比它们将被混合的纹理的分辨率低得多,这是为了节省空间,内部通过过滤来创建平滑的光照梯度。光线贴图没有高光信息——因为高光信息是由相机的位置决定的,只能在运行时确定。
Example Program
新增了GenerateNormals函数,该函数的主要作用是重新计算顶点法线,如果顶点数据是通过索引的方式生成的则通过 VertexNormal,FaceNormal与NormalMatrix(Normal issues) 介绍的方式生成顶点法线,如果没用到索引数据,则顶点是不共用的,所以面法线即为顶点法线。
void Mesh::GenerateNormals()
{
if (!normals) {
normals = new Vector3[numVertices];
}
// 默认顶点法线为(0, 0, 0)
for (GLuint i = 0; i < numVertices; ++i) {
normals[i] = Vector3();
}
if (indices) { // 生成顶点法线,此时共用顶点,需要通过法线那篇文章介绍的方法生成顶点法线
for (GLuint i = 0; i < numIndices; i += 3) {
unsigned int a = indices[i];
unsigned int b = indices[i + 1];
unsigned int c = indices[i + 2];
Vector3 normal = Vector3::Cross(
(vertices[b] - vertices[a]), (vertices[c] - vertices[a]));
normals[a] += normal;
normals[b] += normal;
normals[c] += normal;
}
}
else { // 如果没用到索引数据,则顶点是不共用的,所以计算的面法线即为顶点法线
for (GLuint i = 0; i < numVertices; i += 3) {
Vector3 &a = vertices[i];
Vector3 &b = vertices[i + 1];
Vector3 &c = vertices[i + 2];
Vector3 normal = Vector3::Cross(b - a, c - a);
normals[i] = normal;
normals[i + 1] = normal;
normals[i + 2] = normal;
}
}
for (GLuint i = 0; i < numVertices; ++i) {
normals[i].Normalise();
}
}
Renderer类还是通用的代码:
#include "Renderer.h"
Renderer::Renderer(const Window& parent): BaseRenderer(parent) {
heightMap = new HeightMap(TEXTUREDIR"terrain.raw");
camera = new Camera(0.0f, 0.0f, Vector3(
RAW_WIDTH * HEIGHTMAP_X / 2.0f, 500, RAW_HEIGHT * HEIGHTMAP_Z));
// 设置源着色器为PerPixelVertex
currentShader = new Shader(SHADERDIR"PerPixelVertex.glsl",
SHADERDIR"PerPixelFragment.glsl");
if (!currentShader->LinkProgram()) {
return;
}
// 设置地形贴图
heightMap->SetTexture(loadTexture(
TEXTUREDIR"Barren Reds.JPG",
1, 1, 0));
if (!heightMap->GetTexture() || !currentShader -> LinkProgram()) {
return;
}
SetTextureRepeating(heightMap->GetTexture(), true);
light = new Light(Vector3((RAW_HEIGHT * HEIGHTMAP_X / 2.0f),
500.0f, (RAW_HEIGHT * HEIGHTMAP_Z / 2.0f)),
Vector4(1, 1, 1, 1), (RAW_WIDTH * HEIGHTMAP_X) / 2.0f);
// 设置投影矩阵
projMatrix = Matrix4::Perspective(1.0f, 10000.0f,
(float)width / (float)height, 45.0f);
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
init = true;
}
Renderer::~Renderer(void) {
delete heightMap;
delete camera;
delete light;
}
void Renderer::UpdateScene(float msec) {
camera->UpdateCamera(msec);
// 生成视图矩阵
viewMatrix = camera->BuildViewMatrix();
}
void Renderer::RenderScene() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(currentShader->GetProgram());
// 设置着色器中的各种矩阵
UpdateShaderMatrices();
glUniform1i(glGetUniformLocation(currentShader->GetProgram(),
"diffuseTex"), 0);
glUniform3fv(glGetUniformLocation(currentShader->GetProgram(),
"cameraPos"), 1, (float*)&camera->GetPosition());
// 设置着色器光源数据
SetShaderLight(light);
// 绘制地形
heightMap->Draw();
glUseProgram(0);
SwapBuffers();
}
顶点着色器PerPixelVertex.glsl代码:
// 模型矩阵
uniform mat4 modelMatrix;
// 视图矩阵
uniform mat4 viewMatrix;
// 投影矩阵
uniform mat4 projMatrix;
uniform mat4 textureMatrix;
in vec3 position;
in vec4 colour;
// 法线数据
in vec3 normal;
in vec2 texCoord;
out Vertex {
vec4 colour;
vec2 texCoord;
vec3 normal;
vec3 worldPos;
} OUT;
void main() {
OUT.colour = colour;
OUT.texCoord = (textureMatrix * vec4(texCoord, 0.0f, 1.0f)).xy;
// 生成法线矩阵
mat3 normalMatrix = transpose(inverse(mat3(modelMatrix)));
// 变换法线到世界空间中
OUT.normal = normalize(normalMatrix * normalize(normal));
// 变换顶点到世界空间中
OUT.worldPos = (modelMatrix * vec4(position, 1.0f)).xyz;
gl_Position = (projMatrix * viewMatrix * modelMatrix) * vec4(position, 1.0f);
}
片元着色器PerPixelFragment.glsl代码:
uniform sampler2D diffuseTex;
uniform vec3 cameraPos;
uniform vec4 lightColour;
uniform vec3 lightPos;
uniform float lightRadius;
in Vertex {
vec3 colour;
vec2 texCoord;
vec3 normal;
vec3 worldPos;
}IN;
out vec4 fragColour;
void main () {
vec4 diffuse = texture(diffuseTex, IN.texCoord);
// 入射光线
vec3 incident = normalize(lightPos - IN.worldPos);
// lambert光照
float lambert = max(0.0f, dot(incident, IN.normal));
// 世界空间中光源位置与当前片元的距离
float dist = length(lightPos - IN.worldPos);
// 计算光源衰减
float atten = 1.0f - clamp(dist/lightRadius, 0.0f, 1.0f);
// 视图向量
vec3 viewDir = normalize(cameraPos-IN.worldPos);
// 半角向量
vec3 halfDir = normalize(incident + viewDir);
// 设置高光阈值
float rFactor = max(0.0f, dot(halfDir, IN.normal));
// 设置取面粗糙程度
float sFactor = pow(rFactor, 50.0f);
// 设置环境颜色
vec3 colour = (diffuse.rgb * lightColour.rgb);
// 添加高光颜色
colour += (lightColour.rgb * sFactor) * 0.33f;
// 添加lambert光照并考虑衰减程度
fragColour = vec4(colour * atten * lambert, diffuse.a);
// 为了让环境更亮
fragColour.rgb += (diffuse.rgb * lightColour.rgb) * 0.1f;
}
运行效果如下: