在光照相关文章中已经知道了如何创建实时照明效果,甚至可能会尝试在单个场景中多创建几个点光源。但是很多光源同时运作,运行效率会越来越低。所以使用着色器进行传统照明的技术并不适合在单个场景中处理大量的光源。本篇将展示如何在一个场景中处理大量的光源,这种渲染技术称为延迟渲染。
Deferred Rendering
随着场景中物体和光源数量的增加,使用片元着色器进行照明的问题变得越明显。当每个顶点通过渲染管道运行并被转换成片元时,每个片元会通过光照方程运行,但是场景中的片元由于被遮挡的原因可能不会出现在屏幕上,但是这些被遮挡的片元依旧会在片元着色器上运行,这对整个场景的渲染来讲造成了很大的浪费。如果场景中只有一个光源,那还行,但随着光源越来越多,处理消耗上的时间浪费也将越来越多。基于上述问题,如果光照计算能在图像输出之前统一处理,也就是光照计算在图像空间中执行(运行在最终的后台缓冲区中作为像素结束的片元上)。幸运的是,延迟渲染就能够做到。延迟渲染允许同时在屏幕上绘制许多光源,而昂贵的片元处理只在那些肯定受光源影响的片元上执行。就像在PostProcess篇中实现的模糊后处理,延迟渲染也是一个多通道渲染算法,只是与模糊后处理不同,它不是两个通道,而是三个通道:一个通道绘制场景,一个通道处理场景光照,一个通道在屏幕上输出图像结果。为了在单独的渲染通道之间传递数据,我们需要使用大量的纹理缓冲,利用帧缓冲对象的能力来拥有多个颜色附件。
G-Buffers
延迟渲染的工作原理是将执行光照计算所需的数据分割成许多屏幕大小的缓冲区,这些缓冲区统称为G-Buffer。从创建三角形开始我们就已经在接触缓冲区了,它的图像输出是直接上屏的,如果我们把它称为默认缓冲区,那么该缓冲区至少使用一种颜色缓冲区,一种深度缓冲区,也许还有模板缓冲区。延迟渲染所做的是对它进行扩展,其中包含的额外缓冲区,用于计算我们的实时光照。所以,除了渲染颜色缓冲和深度缓冲外,我们还会有一个包含TBN矩阵转换法线的缓冲区。如果要渲染的顶点具有高光的输出效果,无论是作为顶点属性还是从高光贴图生成的,我们依旧可以将其写入缓冲区。通过使用现代图形硬件的帧缓冲对象功能,所有这些缓冲区都可以在一个渲染过程中创建。就像我们在凹凸贴图加载法线一样,我们可以将它们写入帧缓冲附件纹理(纹理只是数据,不要简单的认为纹理即是图片)。
通常,每个缓冲区的像素位都是32位大小(显卡内部被优化为处理32位的倍数)。这引发了一些问题,比如bump maps中加载的法线是24位的(因为将x,y,z轴值存储为了无符号位)。因此,将它们以24位的形式写入G-Buffer也是可行的。但是这在我们的G-Buffer中留下了8位的空闲位,因为alpha通道不会被使用,虽然让员工空闲是企业的人文关怀,但是让内存有空闲那是绝对不允许的,如何处理这8位空闲位就取决于延迟渲染的具体实现。通用的解决方案是添加额外的逐片元数据,比如应用多少高光,或者使用一些标志位来决定光照是否应该影响模型,或者是否应该在后期处理过程中进行模糊处理。有时候,也可以以更高的精度来存储法线以填充满这32位,比如可以让x和y轴值具备12位,z轴值在片元着色器中重新计算。总之,渲染场景所需的任何逐像素信息都被放置到这个G-Buffer中,并在一个渲染通道中完成。
使用延迟渲染的城市景观G-Buffer输出。左上角开始顺时针方向分别是: 漫反射颜色,世界空间法线,深度信息,材质信息(阴影区域或fullbright等)
Light Volumes
与传统的照明技术不同,在传统的照明技术中,照明是在与顶点转换的同一渲染管道中计算的,而延迟渲染的光照计算是在G-Buffer 完成渲染之后进行(即第二遍渲染进行)。在这个过程中,我们使用与渲染GBuffer过程相同的视图和投影矩阵将light volumes(光体积)渲染到屏幕上,因此light volumes(光体积)的计算还是与当前的相机视图相关。
但什么是light volumes(光体积)?之前有讨论过光衰减的概念,其中每个点光源都有一个位置和半径。这意味着我们的点光源本质上是一个光球,在该半径范围内的物体都会受其影响。这就是light volumes,一种定义了我们希望光照亮的区域形状——它可以是类似点光源的球体,或是聚光灯的圆锥体——乃至于我们可以计算衰减函数的任何形状都可以用于light volumes。
a光源范围内没有物体受影响,b光源照亮了部分树叶,所以该部分树叶的片元将执行光照计算
但是,在视图空间中light volumes可能看起来内部包含了某个物体,但实际上该物体离光源太远而无法在light volumes内部。因此,light volumes有可能看上去捕获了不在其内部的物体,所以必须在第二次渲染过程中检查是否会有这种看似错误包含的情况。类似下面的情况:
左边的小绵羊在光体积内,但是右边的小绵羊的z坐标为-100,所以右边的小绵羊实际上并不在光体积内,但是看上去确实处在光体积内部
Deferred Lighting Considerations
那么,将我们的渲染分成两个阶段如何帮助我们渲染大量光源?通过使用light volumes,并且只在light volumes范围内的片元才执行光照计算,这样我们可以大大减少无意义并且多余的片元光照计算的数量。
回到Forward Shading的过程进行对比: 假设场景中有16盏光源,并且需要使用凹凸贴图着色器计算光照结果, 那么对每一个片元我们都必须执行16组的光照计算。
绝大多数时候,对于每个特定的片元只能被一两盏光源照亮, 所以这本身在处理上造成了大量的浪费(即使我们使用discard语句丢弃片元,让该片元不继续进行光照的计算, 但是哪个片元需要discard呢?)。如果我们渲染的是更加复杂的场景,有很多物体相互堆叠,这意味着许多片元将被覆盖,所以花在计算它们的光照上的时间实际上是浪费了。即使图形硬件帮我们开启了early-z功能,按照从前到后的顺序绘制,并使用面剔除来跳过背面三角形的绘制,我们仍然会有多次无意义计算光照的片元。如果我们想绘制更多的光源,我们还必须执行多次的渲染通道,多次绘制虚拟世界中的所有物体。
延迟渲染解决了所有上述的这些问题。当在第二遍中执行光照计算时,我们已经能知道哪些片元已经绘制到了G-Buffer,所以我们在片元处理上不会有多余的光照计算——多个light volumes 内的 G-Buffer 纹素将多个光照计算结果叠加在一起并对永远不会被照亮的片元忽略光照计算来节省处理时间。所以延迟管线具备一次处理多个光源的能力就不难理解了。
延迟渲染有优点,必然也有其缺陷,第一个最明显的问题是空间占用问题——G-Buffer包含多个屏幕大小的纹理,所有这些纹理都必须存储在图形内存中,所以对于显存较小的主机来讲确实是个负担。
第二个问题与上述问题类似。因为这些G-Buffer中的片元必须在每个光源计算过程中至少被采样一次,在大量灯光互相重叠的情况下,这可能会增加每帧的大量数据传输。带宽的增加在一定程度上抵消了在单个光照通道中完成所有事情的能力,但是如果传输数据,那么几乎可以肯定会被使用。
第三个问题是延迟渲染并不能帮助我们解决透明物体的问题。如果我们将透明物体写入G-Buffer,透明物体后面的内容将无法正确着色,因为读取的法线和深度将应用于透明对象。与前向渲染一样,通常只对不透明物体执行延迟照明,然后再绘制透明物体,再使用Forward Shading方式的照明技术对透明物体执行光照计算,或者甚至根本不计算照明。
另外我们只能将指定数据渲染到G-Buffer中,类似顶点属性之类的数据并没有在G-Buffer中,这会限制一些额外的效果开发,不过也能理解,如果还对G-Buffer增加纹理空间的要求,本身就是个负担。
最后,根据所使用的 API,我们可能无法使用延迟渲染执行抗锯齿。抗锯齿是去除“锯齿”的过程 - 沿着多边形边缘的锯齿边缘。较旧的硬件无法在 G 缓冲区纹理中存储执行硬件抗锯齿所需的信息。使用边缘检测和模糊过滤器进行抗锯齿已成为一种流行的解决方案。
尽管有这些缺点,延迟渲染已经成为绘制高度复杂灯光场景的标准方法,很多引擎或游戏都内置支持这种渲染方式。
Rendering Stages
基本的延迟渲染分为三个渲染通道——第一个通道将图形数据渲染到 G-Buffer 中,第二个通道使用该数据计算每个可见像素的累积光照效果,第三个通道则将基本纹理和照明组合到最终场景中.
- Fill the G-Buffer
首先,必须填充 G-Buffer。这个阶段类似于我们在引入逐像素光照之前渲染事物的方式。对于场景中的每个物体,我们使用“ModelViewProjection”矩阵变换它们的顶点,并在片元着色器中采样这些模型物体的纹理。然而,我们没有将渲染结果输出到后台缓冲区,而是作为执行照明时对应FBO的多个ColorAttachments的数据源。
- Perform Lighting Calculations
将 G-Buffer 信息渲染成纹理后,就可以进行第二个通道的光照信息计算了。通过将球体和锥体等light volumn(光体积)渲染到场景中,进行“捕获”可能被照亮的片元,并使用光照片元着色器进行处理。在片元着色器中,正在处理的片元在世界空间中的位置会在 G-Buffer 深度图的帮助下重建,使用它我们可以执行光照计算,但是如果片元离light volumn(光体积)太远则会丢弃该片元的光照计算并且必须检查light volumn“捕获”的片元是否在光的衰减范围之外。
- Combine Into Final Image
在最后的绘制过程中,我们绘制一个屏幕大小的四边形,就像执行后处理技术一样。在片元着色器中,需要简单地对照明附件纹理和漫反射纹理 G-Buffer 进行采样,并将它们混合在一起以创建最终的图像。
Example Program
Renderer头文件:
#pragma once
#define LIGHTNUM 8
class Renderer : public OGLRenderer {
public:
Renderer(Window& parent);
virtual ~Renderer(void);
virtual void RenderScene();
virtual void UpdateScene(float msec);
protected:
// Gbuffer渲染阶段
void GbufferStage();
// 光照渲染阶段
void LightStage();
// 合并输出阶段
void CombineStage();
// 生成fbo纹理
void GenerateScreenTexture(GLuint &into, bool depth = false);
// 用于gbuffer阶段的着色器
Shader *gbufferStageShader;
// 用于光照阶段的着色器
Shader *lightStageShader;
// 合并gbuffer与光照阶段
Shader *combineStageShader;
// 光照数组
Light *pointLights;
// 地形对象
HeightMap *heightMap;
// light volumn
OBJMesh* sphere;
// 全屏四边形
Mesh *quad;
// 相机
Camera *camera;
// 旋转值
float rotation;
// G-buffer fbo
GLuint gBufferFBO;
// G-buffer颜色附件
GLuint gBufferColourTex;
// G-buffer法线附件
GLuint gBufferNormalTex;
// G-buffer深度附件
GLuint gBufferDepthTex;
// 光照阶段fbo
GLuint lightStageFBO;
// emissive light color
GLuint lightEmissiveTex;
// specular lighting
GLuint lightSpecularTex;
};
Renderer.cpp实现:
#include "Renderer.h"
Renderer::Renderer(Window& parent) : OGLRenderer(parent) {
rotation = 0.0f;
camera = new Camera();
quad = Mesh::GenerateQuad();
camera -> SetPosition(Vector3(RAW_WIDTH * HEIGHTMAP_X / 2.0f,
500.0f, RAW_WIDTH * HEIGHTMAP_X));
pointLights = new Light[LIGHTNUM * LIGHTNUM];
// 设置所有光源的位置,颜色以及影响范围
for (int x = 0; x < LIGHTNUM; ++x) {
for (int z = 0; z < LIGHTNUM; ++z) {
Light& l = pointLights[(x * LIGHTNUM) + z];
float xPos = (RAW_WIDTH * HEIGHTMAP_X / (LIGHTNUM - 1)) * x;
float zPos = (RAW_HEIGHT * HEIGHTMAP_Z / (LIGHTNUM - 1)) * z;
l.SetPosition(Vector3(xPos, 100.0f, zPos));
float r = 0.5f + (float)(rand() % 129) / 128.0f;
float g = 0.5f + (float)(rand() % 129) / 128.0f;
float b = 0.5f + (float)(rand() % 129) / 128.0f;
l.SetColour(Vector4(r, g, b, 1.0f));
float radius = (RAW_WIDTH * HEIGHTMAP_X / LIGHTNUM);
l.SetRadius(radius);
}
}
heightMap->SetTexture(loadTexture(
TEXTUREDIR"Barren Reds.JPG", SOIL_LOAD_AUTO,
SOIL_CREATE_NEW_ID, SOIL_FLAG_MIPMAPS));
heightMap->SetBumpMap(loadTexture(
TEXTUREDIR"Barren RedsDOT3.JPG", SOIL_LOAD_AUTO,
SOIL_CREATE_NEW_ID, SOIL_FLAG_MIPMAPS));
SetTextureRepeating(heightMap -> GetTexture(), true);
SetTextureRepeating(heightMap -> GetBumpMap(), true);
sphere = new OBJMesh();
if (!sphere -> LoadOBJMesh(MESHDIR"ico.obj")) {
return;
}
gbufferStageShader = new Shader(SHADERDIR"BumpVertex.glsl", SHADERDIR"GbufferFragment.glsl");
if (!gbufferStageShader->LinkProgram()) {
return;
}
combineStageShader = new Shader(SHADERDIR"CombineVert.glsl", SHADERDIR"CombineFrag.glsl");
if (!combineStageShader->LinkProgram()) {
return;
}
lightStageShader = new Shader(SHADERDIR"LightStageVert.glsl", SHADERDIR"LightStageFrag.glsl");
if (!lightStageShader->LinkProgram()) {
return;
}
glGenFramebuffers(1, &gBufferFBO);
glGenFramebuffers(1, &lightStageFBO);
GLenum buffers[2];
buffers[0] = GL_COLOR_ATTACHMENT0;
buffers[1] = GL_COLOR_ATTACHMENT1;
// 生成深度贴图
GenerateScreenTexture(gBufferDepthTex, true);
GenerateScreenTexture(gBufferColourTex);
GenerateScreenTexture(gBufferNormalTex);
GenerateScreenTexture(lightEmissiveTex);
GenerateScreenTexture(lightSpecularTex);
// 让gBufferFBO绑定各个附件
glBindFramebuffer(GL_FRAMEBUFFER, gBufferFBO);
// 绑定颜色
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gBufferColourTex, 0);
// 绑定法线
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gBufferNormalTex, 0);
// 绑定深度
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, gBufferDepthTex, 0);
// 告诉gl当前fbo需要绘制两个颜色附件
glDrawBuffers(2, buffers);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
return;
}
// 接下来,我们简单地对第二个渲染通道做同样的操作。
// 在这个通道中,我们没有深度附件-会在着色器中重建世界位置,
// 并且使用第一个渲染通道的深度缓冲区,它作为一个普通纹理传入。
glBindFramebuffer(GL_FRAMEBUFFER, lightStageFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, lightEmissiveTex, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, lightSpecularTex, 0);
glDrawBuffers(2, buffers);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
return;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glEnable(GL_BLEND);
init = true;
}
Renderer::~Renderer(void) {
delete camera;
delete heightMap;
delete sphere;
delete quad;
delete gbufferStageShader;
delete lightStageShader;
delete combineStageShader;
delete[] pointLights;
glDeleteTextures(1, &gBufferColourTex);
glDeleteTextures(1, &gBufferNormalTex);
glDeleteTextures(1, &gBufferDepthTex);
glDeleteTextures(1, &lightEmissiveTex);
glDeleteTextures(1, &lightSpecularTex);
glDeleteFramebuffers(1, &gBufferFBO);
glDeleteFramebuffers(1, &lightStageFBO);
currentShader = 0;
}
void Renderer::UpdateScene(float msec) {
camera -> UpdateCamera(msec);
viewMatrix = camera -> BuildViewMatrix();
// 为64盏灯光生成一致的旋转值
rotation = msec * 0.01f;
}
void Renderer::GbufferStage()
{
glBindFramebuffer(GL_FRAMEBUFFER, gBufferFBO);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
SetCurrentShader(gbufferStageShader);
glUniform1i(glGetUniformLocation(currentShader->GetProgram(), "diffuseTex"), 0);
glUniform1i(glGetUniformLocation(currentShader->GetProgram(), "bumpTex"), 1);
projMatrix = Matrix4::Perspective(1.0f, 10000.0f, static_cast<float>(width) / static_cast<float>(height), 45.f);
modelMatrix.ToIdentity();
UpdateShaderMatrices();
heightMap->Draw();
glUseProgram(0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
// 在该pass中,我们不会像平常一样将颜色清除为深灰色,
// 而是将其清除为黑色。那是因为我们将在第三个pass中混合该pass的输出颜色,
// 因此我们希望未受光源影响的像素为黑色。如果清除的背景颜色是灰色的,就
// 好像整个场景都被灰色的光照亮了,显得有些褪色。
// 我们还希望被多个灯光“捕获”的片元将这些灯光颜色的混合结果作为其颜色
// 所以需要开启混合
void Renderer::LightStage()
{
SetCurrentShader(lightStageShader);
glBindFramebuffer(GL_FRAMEBUFFER, lightStageFBO);
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
glBlendFunc(GL_ONE, GL_ONE);
glUniform1i(glGetUniformLocation(currentShader->GetProgram(), "depthTex"), 3);
glUniform1i(glGetUniformLocation(currentShader->GetProgram(), "normTex"), 4);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, gBufferDepthTex);
glActiveTexture(GL_TEXTURE4);
glBindTexture(GL_TEXTURE_2D, gBufferNormalTex);
glUniform3fv(glGetUniformLocation(currentShader -> GetProgram(),
"cameraPos"), 1, (float*)&camera -> GetPosition());
glUniform2f(glGetUniformLocation(currentShader -> GetProgram(),
"pixelSize"), 1.0f / width, 1.0f / height);
Vector3 translate = Vector3((RAW_HEIGHT * HEIGHTMAP_X / 2.0f), 500,
(RAW_HEIGHT * HEIGHTMAP_Z / 2.0f));
Matrix4 pushMatrix = Matrix4::Translation(translate);
Matrix4 popMatrix = Matrix4::Translation(-translate);
for (int x = 0; x < LIGHTNUM; x++) {
for (int z = 0; z < LIGHTNUM; ++z) {
Light& l = pointLights[(x * LIGHTNUM) + z];
float radius = l.GetRadius();
modelMatrix =
pushMatrix *
Matrix4::Rotation(rotation, Vector3(0, 1, 0)) *
popMatrix *
Matrix4::Translation(l.GetPosition()) *
Matrix4::Scale(Vector3(radius, radius, radius));
l.SetPosition(modelMatrix.GetPositionVector());
SetShaderLight(&l);
UpdateShaderMatrices();
float dist = (l.GetPosition() - camera->GetPosition()).Length();
// 需要计算相机是否在light volume内部,当相机在
// light volume内部时,开启正面剔除,此时才能看到light volume内部
// 如果不画出light volume,就无法捕获到任何片元
// 那为什么不同时开启正面与背面剔除呢?还能减少判断
// 如果同时开启正面与背面剔除,那么受光照影响的片元
// 将被采样两次,那么光照的输出结果将是不正确的
if (dist < radius) {
// 相机在light volume内部
glCullFace(GL_FRONT);
}
else
{
glCullFace(GL_BACK);
}
sphere->Draw();
}
}
// 把所有状态设置为默认状态
glCullFace(GL_BACK);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glClearColor(0.2f, 0.2f, 0.2f, 1);
// 解除fbo绑定
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glUseProgram(0);
}
// CombineBuffers 执行第三个延迟渲染通道,它接收来自
// 第一个渲染通道的漫反射纹理颜色和来自第二个渲染通道
// 的照明组件,并使用 combineStageShader 着色器将它们全部组
// 合成最终图像。它将此作为后处理通道执行,因此我们只需
// 通过正交投影渲染一个屏幕大小的四边形,设置三个纹理,
// 然后渲染四边形。我们使用第三、第四和第五个纹理单元绑定上述的
// 纹理组件,这样 Mesh 类的 Draw 函数就不会解除我们的任
// 何纹理的绑定(Mesh类中已经把第一个纹理单元
// 设置为它的漫反射纹理单元,第二个纹理单元设置为了凹凸贴图)
void Renderer::CombineStage()
{
SetCurrentShader(combineStageShader);
projMatrix = Matrix4::Orthographic(-1, 1, 1, -1, -1, 1);
UpdateShaderMatrices();
glUniform1i(glGetUniformLocation(currentShader->GetProgram(), "diffuseTex"), 2);
glUniform1i(glGetUniformLocation(currentShader->GetProgram(), "emissiveTex"), 3);
glUniform1i(glGetUniformLocation(currentShader->GetProgram(), "specularTex"), 4);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, gBufferColourTex);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, lightEmissiveTex);
glActiveTexture(GL_TEXTURE4);
glBindTexture(GL_TEXTURE_2D, lightSpecularTex);
quad->Draw();
glUseProgram(0);
}
// 生成fbo纹理
void Renderer::GenerateScreenTexture(GLuint& into, bool depth)
{
glGenTextures(1, &into);
glBindTexture(GL_TEXTURE_2D, into);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0,
depth ? GL_DEPTH_COMPONENT24 : GL_RGBA8,
width, height, 0,
depth ? GL_DEPTH_COMPONENT : GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glBindTexture(GL_TEXTURE_2D, 0);
}
void Renderer::RenderScene() {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
// 渲染gbuffer
GbufferStage();
// 渲染光照
LightStage();
// 合并渲染结果
CombineStage();
// 绘制完成后交换后置缓冲区
SwapBuffers();
}
main函数实现:
#include "Renderer.h"
int main() {
Window w("Deferred Rendering!", 1280,720,false);
if(!w.HasInitialised()) {
return -1;
}
srand((unsigned int)w.GetTimer()->GetMS() * 1000.0f);
Renderer renderer(w);
if(!renderer.HasInitialised()) {
return -1;
}
w.LockMouseToWindow(true);
w.ShowOSPointer(false);
while(w.UpdateWindow() && !Window::GetKeyboard()->KeyDown(KEYBOARD_ESCAPE)){
renderer.UpdateScene(w.GetTimer()->GetTimedMS());
renderer.RenderScene();
}
return 0;
}
接下来实现GBuffer渲染阶段的着色器,顶点着色器还是使用 Dot3 BumpMapping/NormalMap 一文中的BumpVertex顶点着色器。但是需要新增片元着色器的实现。与BumpFragment 着色器类似,定义了两个纹理采样器——一个用于获取漫反射纹理,另一个用于凹凸贴图纹理。还需要计算出 TBN矩阵 和 逐片元的世界空间法线,但没有立即计算片元的光照颜色,而是将法线输出到第二个 FBO 的颜色附件。颜色附件数组通过 out vec4 fragColour[2] 定义:
uniform sampler2D diffuseTex; // Diffuse texture map
uniform sampler2D bumpTex; // Bump map
in Vertex {
vec4 colour;
vec2 texCoord;
vec3 normal;
vec3 tangent;
vec3 binormal;
vec3 worldPos;
} IN;
out vec4 fragColour[2]; // 颜色附件数组 !
void main (void) {
mat3 TBN = mat3(IN.tangent, IN.binormal, IN.normal);
vec3 normal = normalize(TBN * (texture2D(bumpTex, IN.texCoord).rgb)* 2.0 - 1.0);
fragColour[0] = texture2D(diffuseTex, IN.texCoord);
// 需要限制法线的范围为0 ~ 1间,以匹配附件的格式
fragColour[1] = vec4(normal.xyz * 0.5 + 0.5, 1.0);
}
接着是用于在屏幕上绘制Light Volume的着色器,在顶点着色器中仅仅使用MVP矩阵用于变换顶点数据,并求取视图矩阵和投影矩阵相乘后的逆矩阵,而其它顶点属性并未使用,更多的处理将在片元着色器中进行。片元着色器会将照明结果到两个颜色附件中,一种用于diffuse light,另一种用于specular light。顶点着色器实现如下:
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projMatrix;
uniform mat4 textureMatrix;
in vec3 position;
out mat4 inverseProjView;
void main ( void ) {
gl_Position = (projMatrix * viewMatrix * modelMatrix) * vec4(position, 1.0);
inverseProjView = inverse(projMatrix * viewMatrix);
}
Recalculating the world space position
通过Light Volume捕获到的Gbuffer Stage渲染后的逐片元结果,需要进一步计算出它们在世界空间中的位置(通过位置进一步计算正确的衰减值以及确定该片元是否在Light Volume内)。
首先得计算片元的屏幕空间位置。我们可以通过 GLSL 命令函数 gl_FragCoord 计算,该函数返回当前处理片元的屏幕坐标,所以如果屏幕是 800 像素宽,那么屏幕最右边的FragCoord.x 值就是 800。但是,在我们计算世界空间位置之前,我们需要将该值限制在 0.0 到 1.0 的范围内,只有这样我们才可以使用该范围值对 G-Buffer 的附件纹理进行采样。因此,我们将其乘于pixelSize,这为我们提供了 0-1 范围内的 x 和 y 轴,但是 z 值如何求取呢?可以使用这些 x 和 y 坐标从深度缓冲区中采样 z 坐标,从而为我们提供 0-1 范围内的所有 3 维空间中的值。
vec3 pos = vec3((gl_FragCoord.x * pixelSize.x), (gl_FragCoord.y * pixelSize.y), 0.0);
pos.z = texture(depthTex, pos.xy).r;
上述求取后pos的x与y值还可以用于采样法线的颜色附件纹理,我们在GBuffer中把法线纹理的范围值限定在了(0 ~ 1)间,所以对法线纹理采样成功后,需要还原法线的范围值为(-1 ~ 1)间。
vec3 normal = normalize ( texture ( normTex , pos . xy ).xyz * 2.0 - 1.0);
法线坐标处理成功后,需要进一步处理位置坐标数据,上述计算限定了pos的范围为0~1间,但是裁剪空间的范围是-1 ~ 1间,所以需要进一步限定pos的坐标范围为 -1 ~ 1间(通过乘于2 - 1得到),再把结果与inverseProjView相乘,得到的三维坐标结果除于w值即为当前片元(顶点插值)在世界空间中的结果。
vec4 clip = inverseProjView * vec4(pos * 2.0 - 1.0, 1.0);
pos = clip.xyz / clip.w ;
接下来是具体的光照处理,我们还是需要输出两张纹理,第一张输出的是漫反射纹理(lambert光照模型),第二张纹理输出的是高光纹理(可参照Phong光照模型 ),首先通过光源的位置与顶点位置(片元插值)求取距离,通过该距离与光源影响的范围lightRadius进行计算,求取衰减值,当衰减值为0,则代表该片元不受该光源影响,那就可以不进行光照计算,否则就分别对两光照模型结果输出:
float dist = length ( lightPos - pos );
float atten = 1.0 - clamp ( dist / lightRadius , 0.0 , 1.0);
if( atten == 0.0) {
discard ;
}
vec3 incident = normalize ( lightPos - pos );
vec3 viewDir = normalize ( cameraPos - pos );
vec3 halfDir = normalize ( incident + viewDir );
float lambert = clamp ( dot ( incident , normal ) ,0.0 ,1.0);
float rFactor = clamp ( dot ( halfDir , normal ) ,0.0 ,1.0);
float sFactor = pow ( rFactor , 33.0 );
fragColour [0] = vec4 ( lightColour . xyz * lambert * atten , 1.0);
fragColour [1] = vec4 ( lightColour . xyz * sFactor * atten *0.33 ,1.0);
Buffer Combine Shader
Combine Stage是延迟管线的最后一个流程,它使用GBuffer Stage中纹理颜色附件与Light Stage中的两张照明纹理渲染到屏幕大小的四边形中,并且还会在片元着色器中进行环境照明,以照亮没有被Light Volume影响的片元区域。
因为我们渲染的是一个单一的四边形,所以顶点着色器是非常简单的,转换四边形的顶点,并输出它的纹理坐标:
uniform mat4 projMatrix ;
in vec3 position ;
in vec2 texCoord ;
out Vertex {
vec2 texCoord ;
}OUT;
void main ( void ) {
gl_Position = projMatrix * vec4(position, 1.0);
OUT.texCoord = texCoord ;
}
片元着色器也不复杂。通过提供的三张纹理——一张是GBuffer的颜色附件纹理,另外两张纹理是Light Stage输出的纹理。我们简单地采样这些纹理,并将它们混合在一起,其中包括了环境光颜色的计算。
uniform sampler2D diffuseTex;
uniform sampler2D emissiveTex;
uniform sampler2D specularTex;
in Vertex {
vec2 texCoord;
}IN;
out vec4 fragColour;
void main (void) {
vec3 diffuse = texture(diffuseTex, IN.texCoord).xyz;
vec3 light = texture(emissiveTex, IN.texCoord).xyz;
vec3 specular = texture(specularTex, IN.texCoord).xyz;
fragColour.xyz = diffuse * 0.2; // ambient
fragColour.xyz += diffuse * light; // lambert
fragColour.xyz += specular; // Specular
fragColour.a = 1.0;
}
最后渲染结果如下: