# 延迟着色法(Deferred Shading) 到目前为止,我们渲染三维场景的方式称为正向渲染法(Forward Rendering)。我们首先渲染3D对象,并在片元着色器中应用纹理和光照效果。如果我们有一个具有多种光照和复杂效果的复杂片元着色器处理阶段,这种方法就不是很高效。此外,我们最终可能会将这些效果应用到稍后可能由于深度测试而被丢弃的片元上(尽管这并不完全正确,如果我们启用[早期片元测试](https://www.khronos.org/opengl/wiki/Early_Fragment_Test)的话)。 为了缓解上述问题,我们可以改变方式,使用一种称为延迟着色法的技术来渲染场景。通过延迟着色法,我们先将后期(在片元着色器中)所需的几何信息渲染到缓冲区。当使用储存在这些缓冲区中的信息时,片元着色器所需的复杂运算被推迟(Postpone)、延迟(Defer)到之后的阶段。 因此,使用延迟着色法,我们进行两个渲染阶段(Pass)。第一个是几何处理阶段,我们将场景渲染到储存以下数据的缓冲区中: * 位置(在本例中,在光照视图坐标系中,尽管你可以看到使用世界坐标的其他示例)。 * 每个位置的漫反射颜色。 * 每个位置的镜面反射分量。 * 每个位置的法线(也在光照视图坐标系中)。 * 有向光的阴影图(你可能会发现这一步在其他实现中是单独完成的)。 所有这些数据都存储在一个称为G缓冲区(G-Buffer)的缓冲区中。 第二个阶段称为光照处理阶段,该阶段需要一个充满整个屏幕的矩形,并使用G缓冲区中储存的信息为每个片元生成颜色信息。当我们进行光照处理阶段时,深度测试将删除所有看不到的场景数据。因此,要执行的操作数量仅限于将在屏幕上显示的。 ![流程图](_static/28/schema.png) 你可能会问进行额外的渲染阶段能否提高性能,答案是视情况而定。延迟着色法通常在有许多不同的光照处理阶段时使用,在此情况下,额外的渲染阶段将通过减少将在片元着色器中执行的操作来补偿。 让我们开始编码吧,首先要做的是为G缓冲区创建一个新类,名为`GBuffer`,其定义如下所示: ```java package org.lwjglb.engine.graph; import org.lwjgl.system.MemoryStack; import org.lwjglb.engine.Window; import java.nio.ByteBuffer; import java.nio.IntBuffer; import static org.lwjgl.opengl.GL11.*; import static org.lwjgl.opengl.GL20.*; import static org.lwjgl.opengl.GL30.*; public class GBuffer { private static final int TOTAL_TEXTURES = 6; private int gBufferId; private int[] textureIds; private int width; private int height; ``` 类定义了一个常量,该常量为要使用的最大缓冲区数建模。与G缓冲区本身关联的ID,以及单个缓冲区的数组,屏幕的大小也会被储存。 让我们看一下构造函数: ```java public GBuffer(Window window) throws Exception { // 创建G缓冲区 gBufferId = glGenFramebuffers(); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gBufferId); textureIds = new int[TOTAL_TEXTURES]; glGenTextures(textureIds); this.width = window.getWidth(); this.height = window.getHeight(); // 为位置、漫反射颜色、镜面反射颜色、法线、阴影因子和深度创建纹理 // 所有坐标都在世界坐标系中 for(int i=0; i 0 ) { vs_selected = selectedInstanced; modelMatrix = modelInstancedMatrix; initPos = vec4(position, 1.0); initNormal = vec4(vertexNormal, 0.0); } else { vs_selected = selectedNonInstanced; modelMatrix = modelNonInstancedMatrix; int count = 0; for(int i = 0; i < MAX_WEIGHTS; i++) { float weight = jointWeights[i]; if(weight > 0) { count++; int jointIndex = jointIndices[i]; vec4 tmpPos = jointsMatrix[jointIndex] * vec4(position, 1.0); initPos += weight * tmpPos; vec4 tmpNormal = jointsMatrix[jointIndex] * vec4(vertexNormal, 0.0); initNormal += weight * tmpNormal; } } if (count == 0) { initPos = vec4(position, 1.0); initNormal = vec4(vertexNormal, 0.0); } } mat4 modelViewMatrix = viewMatrix * modelMatrix; vs_mvVertexPos = modelViewMatrix * initPos; gl_Position = projectionMatrix * vs_mvVertexPos; // Support for texture atlas, update texture coordinates float x = (texCoord.x / numCols + texOffset.x); float y = (texCoord.y / numRows + texOffset.y); vs_textcoord = vec2(x, y); vs_normal = normalize(modelViewMatrix * initNormal).xyz; for (int i = 0 ; i < NUM_CASCADES ; i++) { vs_mlightviewVertexPos[i] = orthoProjectionMatrix[i] * lightViewMatrix[i] * modelMatrix * initPos; } vs_modelMatrix = modelMatrix; } ``` 此着色器与上一章中用于渲染场景的顶点着色器非常相似,输出变量的名称有一些变化,但本质上是相同的着色器。事实上,它应该是几近一致的,我们渲染顶点的方式不应该改变,主要的更改是在片元着色器中,它是这样定义的(`gbuffer_fragment.fs`): ```glsl #version 330 const int NUM_CASCADES = 3; in vec2 vs_textcoord; in vec3 vs_normal; in vec4 vs_mvVertexPos; in vec4 vs_mlightviewVertexPos[NUM_CASCADES]; in mat4 vs_modelMatrix; in float vs_selected; layout (location = 0) out vec3 fs_worldpos; layout (location = 1) out vec3 fs_diffuse; layout (location = 2) out vec3 fs_specular; layout (location = 3) out vec3 fs_normal; layout (location = 4) out vec2 fs_shadow; uniform mat4 viewMatrix; struct Material { vec4 diffuse; vec4 specular; int hasTexture; int hasNormalMap; float reflectance; }; uniform sampler2D texture_sampler; uniform sampler2D normalMap; uniform Material material; uniform sampler2D shadowMap_0; uniform sampler2D shadowMap_1; uniform sampler2D shadowMap_2; uniform float cascadeFarPlanes[NUM_CASCADES]; uniform mat4 orthoProjectionMatrix[NUM_CASCADES]; uniform int renderShadow; vec4 diffuseC; vec4 speculrC; void getColour(Material material, vec2 textCoord) { if (material.hasTexture == 1) { diffuseC = texture(texture_sampler, textCoord); speculrC = diffuseC; } else { diffuseC = material.diffuse; speculrC = material.specular; } } vec3 calcNormal(Material material, vec3 normal, vec2 text_coord, mat4 modelMatrix) { vec3 newNormal = normal; if ( material.hasNormalMap == 1 ) { newNormal = texture(normalMap, text_coord).rgb; newNormal = normalize(newNormal * 2 - 1); newNormal = normalize(viewMatrix * modelMatrix * vec4(newNormal, 0.0)).xyz; } return newNormal; } float calcShadow(vec4 position, int idx) { if ( renderShadow == 0 ) { return 1.0; } vec3 projCoords = position.xyz; // Transform from screen coordinates to texture coordinates projCoords = projCoords * 0.5 + 0.5; float bias = 0.005; float shadowFactor = 0.0; vec2 inc; if (idx == 0) { inc = 1.0 / textureSize(shadowMap_0, 0); } else if (idx == 1) { inc = 1.0 / textureSize(shadowMap_1, 0); } else { inc = 1.0 / textureSize(shadowMap_2, 0); } for(int row = -1; row <= 1; ++row) { for(int col = -1; col <= 1; ++col) { float textDepth; if (idx == 0) { textDepth = texture(shadowMap_0, projCoords.xy + vec2(row, col) * inc).r; } else if (idx == 1) { textDepth = texture(shadowMap_1, projCoords.xy + vec2(row, col) * inc).r; } else { textDepth = texture(shadowMap_2, projCoords.xy + vec2(row, col) * inc).r; } shadowFactor += projCoords.z - bias > textDepth ? 1.0 : 0.0; } } shadowFactor /= 9.0; if(projCoords.z > 1.0) { shadowFactor = 1.0; } return 1 - shadowFactor; } void main() { getColour(material, vs_textcoord); fs_worldpos = vs_mvVertexPos.xyz; fs_diffuse = diffuseC.xyz; fs_specular = speculrC.xyz; fs_normal = normalize(calcNormal(material, vs_normal, vs_textcoord, vs_modelMatrix)); int idx; for (int i=0; i 0 ) { fs_diffuse = vec3(fs_diffuse.x, fs_diffuse.y, 1); } } ``` 最关键的行是: ```glsl layout (location = 0) out vec3 fs_worldpos; layout (location = 1) out vec3 fs_diffuse; layout (location = 2) out vec3 fs_specular; layout (location = 3) out vec3 fs_normal; layout (location = 4) out vec2 fs_shadow; ``` 这就是我们要引用该片元着色器将写入的纹理的地方。如你所见,我们只是转储位置(在光照视图坐标系中)、漫反射颜色(可以是材质相关的纹理的颜色分量)、镜面反射分量、法线和阴影图的深度值。 旁注:我们已经简化了`Material`类的定义,删除了环境颜色分量。 回到`Renderer`类,`render`方法的定义如下: ```java public void render(Window window, Camera camera, Scene scene, boolean sceneChanged) { clear(); if (window.getOptions().frustumCulling) { frustumFilter.updateFrustum(window.getProjectionMatrix(), camera.getViewMatrix()); frustumFilter.filter(scene.getGameMeshes()); frustumFilter.filter(scene.getGameInstancedMeshes()); } // 在设置视口之前渲染深度图 if (scene.isRenderShadows() && sceneChanged) { shadowRenderer.render(window, scene, camera, transformation, this); } glViewport(0, 0, window.getWidth(), window.getHeight()); // 每个渲染周期更新投影矩阵一次 window.updateProjectionMatrix(); renderGeometry(window, camera, scene); initLightRendering(); renderPointLights(window, camera, scene); renderDirectionalLight(window, camera, scene); endLightRendering(); renderFog(window, camera, scene); renderSkyBox(window, camera, scene); renderParticles(window, camera, scene); } ``` 几何处理阶段是在`renderGeometry`方法中完成的(你可以看到我们不再有`renderScene`)。光照处理阶段分几个步骤完成,首先设置要使用的缓冲区和其他参数(在`initLightRendering`中),然后渲染点光源(在`renderPointLights`中)和平行光(在`renderDirectionalLight`中),最后恢复状态(在`endLightRendering`中)。 让我们从几何处理阶段开始。`renderGeometry`方法几乎等同于在前几章中使用的`renderScene`方法: ```java private void renderGeometry(Window window, Camera camera, Scene scene) { // 渲染G缓冲区以便写入 glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gBuffer.getGBufferId()); clear(); glDisable(GL_BLEND); gBufferShaderProgram.bind(); Matrix4f viewMatrix = camera.getViewMatrix(); Matrix4f projectionMatrix = window.getProjectionMatrix(); gBufferShaderProgram.setUniform("viewMatrix", viewMatrix); gBufferShaderProgram.setUniform("projectionMatrix", projectionMatrix); gBufferShaderProgram.setUniform("texture_sampler", 0); gBufferShaderProgram.setUniform("normalMap", 1); List shadowCascades = shadowRenderer.getShadowCascades(); for (int i = 0; i < ShadowRenderer.NUM_CASCADES; i++) { ShadowCascade shadowCascade = shadowCascades.get(i); gBufferShaderProgram.setUniform("orthoProjectionMatrix", shadowCascade.getOrthoProjMatrix(), i); gBufferShaderProgram.setUniform("cascadeFarPlanes", ShadowRenderer.CASCADE_SPLITS[i], i); gBufferShaderProgram.setUniform("lightViewMatrix", shadowCascade.getLightViewMatrix(), i); } shadowRenderer.bindTextures(GL_TEXTURE2); int start = 2; for (int i = 0; i < ShadowRenderer.NUM_CASCADES; i++) { gBufferShaderProgram.setUniform("shadowMap_" + i, start + i); } gBufferShaderProgram.setUniform("renderShadow", scene.isRenderShadows() ? 1 : 0); renderNonInstancedMeshes(scene); renderInstancedMeshes(scene, viewMatrix); gBufferShaderProgram.unbind(); glEnable(GL_BLEND); } ``` 仅有的区别是: * 我们绑定到G缓冲区而不是屏幕。 * 禁用混合,因为我们只想使用最接近摄像机的值(最小深度值),所以不需要混合。 如果使用OpenGL调试器(如RenderDoc)调试示例,则可以看到几何处理阶段期间生成的纹理。位置纹理将如下所示: ![位置纹理](_static/28/text_positions.png) 储存漫反射分量值的纹理如下所示: ![漫反射纹理](_static/28/text_diffuse.png) 储存法线值的纹理如下所示: ![法线纹理](_static/28/text_normals.png) 现在轮到光照处理阶段了。我们首先需要在渲染之前设置一些东西,这是在`initLightRendering`方法中完成的: ```java private void initLightRendering() { // 绑定场景缓冲区 glBindFramebuffer(GL_FRAMEBUFFER, sceneBuffer.getBufferId()); // 清理G缓冲区 clear(); // 禁用深度测试以允许绘制具有相同深度的多个图层 glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendEquation(GL_FUNC_ADD); glBlendFunc(GL_ONE, GL_ONE); // 绑定G缓冲区以便读取 glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer.getGBufferId()); } ``` 因为我们不会渲染到屏幕上,所以首先需要绑定到纹理上,该纹理将保存光照处理阶段的结果。然后清除缓冲区并禁用深度测试。因为已经在几何处理阶段进行了深度测试,这就不再需要了。另一重要步骤是启用混合。最后一个操作是启用G缓冲区进行读取,它将在光照处理阶段期间使用。 在分析不同光照的渲染方法之前,让我们稍微考虑一下如何渲染光照。我们需要使用G缓冲区的内容,但为了使用它们,需要先渲染一些东西。但是,我们已经绘制了场景,现在要渲染什么?答案很简单,我们只需要渲染一个充满了整个屏幕的矩形。对于该矩形的每个片元,我们将使用G缓冲区中储存的数据并生成正确的输出颜色。你还记得我们在`Renderer`类的`init`方法中加载的`Mesh`吗?它被命名为`bufferPassMesh`,它只储存一个充满整个屏幕的矩形。 那么,光照处理阶段的顶点着色器看起来是怎样的? ```glsl #version 330 layout (location=0) in vec3 position; uniform mat4 projectionMatrix; uniform mat4 modelMatrix; void main() { gl_Position = projectionMatrix * modelMatrix * vec4(position, 1.0); } ``` 上述代码是渲染点光源和平行光时使用的顶点着色器(`light_vertex.vs`)。它只是使用模型矩阵和投影矩阵来转储顶点。不需要使用观察矩阵,因为这里不需要摄像机。 点光源的片元着色器(`point_light_fragment.fs`)的定义如下: ```glsl #version 330 out vec4 fragColor; struct Attenuation { float constant; float linear; float exponent; }; struct PointLight { vec3 colour; // Light position is assumed to be in view coordinates vec3 position; float intensity; Attenuation att; }; uniform sampler2D positionsText; uniform sampler2D diffuseText; uniform sampler2D specularText; uniform sampler2D normalsText; uniform sampler2D shadowText; uniform sampler2D depthText; uniform vec2 screenSize; uniform float specularPower; uniform PointLight pointLight; vec2 getTextCoord() { return gl_FragCoord.xy / screenSize; } vec4 calcLightColour(vec4 diffuseC, vec4 speculrC, float reflectance, vec3 light_colour, float light_intensity, vec3 position, vec3 to_light_dir, vec3 normal) { vec4 diffuseColour = vec4(0, 0, 0, 1); vec4 specColour = vec4(0, 0, 0, 1); // Diffuse Light float diffuseFactor = max(dot(normal, to_light_dir), 0.0); diffuseColour = diffuseC * vec4(light_colour, 1.0) * light_intensity * diffuseFactor; // Specular Light vec3 camera_direction = normalize(-position); vec3 from_light_dir = -to_light_dir; vec3 reflected_light = normalize(reflect(from_light_dir , normal)); float specularFactor = max( dot(camera_direction, reflected_light), 0.0); specularFactor = pow(specularFactor, specularPower); specColour = speculrC * light_intensity * specularFactor * reflectance * vec4(light_colour, 1.0); return (diffuseColour + specColour); } vec4 calcPointLight(vec4 diffuseC, vec4 speculrC, float reflectance, PointLight light, vec3 position, vec3 normal) { vec3 light_direction = light.position - position; vec3 to_light_dir = normalize(light_direction); vec4 light_colour = calcLightColour(diffuseC, speculrC, reflectance, light.colour, light.intensity, position, to_light_dir, normal); // Apply Attenuation float distance = length(light_direction); float attenuationInv = light.att.constant + light.att.linear * distance + light.att.exponent * distance * distance; return light_colour / attenuationInv; } void main() { vec2 textCoord = getTextCoord(); float depth = texture(depthText, textCoord).r; vec3 worldPos = texture(positionsText, textCoord).xyz; vec4 diffuseC = texture(diffuseText, textCoord); vec4 speculrC = texture(specularText, textCoord); vec3 normal = texture(normalsText, textCoord).xyz; float shadowFactor = texture(shadowText, textCoord).r; float reflectance = texture(shadowText, textCoord).g; fragColor = calcPointLight(diffuseC, speculrC, reflectance, pointLight, worldPos.xyz, normal.xyz) * shadowFactor; } ``` 如你所见,它包含你应该熟悉的函数,它们在前几章的场景片元着色器中使用过。这里需要注意的重要事项如下: ```glsl uniform sampler2D positionsText; uniform sampler2D diffuseText; uniform sampler2D specularText; uniform sampler2D normalsText; uniform sampler2D shadowText; uniform sampler2D depthText; ``` 这些Uniform建模构成了G缓冲区的不同纹理。我们将使用它们来访问数据。你现在可能会问,当我们渲染一个片元时,如何知道要从这些纹理中获取哪个像素?答案是使用`gl_FragCoord`输入变量,此变量储存当前片元与窗口的相对坐标。因为要从坐标系转换为纹理,我们使用以下函数: ```glsl vec2 getTextCoord() { return gl_FragCoord.xy / screenSize; } ``` 平行光的片元着色器也很相似,可以查看源代码。现在已经有了着色器,我们回到`Renderer`类。对于点光源,我们将做尽可能多的光照处理,我们只是绑定用于此类型光源的着色器,并为每个着色器绘制矩形。 ```java private void renderPointLights(Window window, Camera camera, Scene scene) { pointLightShaderProgram.bind(); Matrix4f viewMatrix = camera.getViewMatrix(); Matrix4f projectionMatrix = window.getProjectionMatrix(); pointLightShaderProgram.setUniform("modelMatrix", bufferPassModelMatrix); pointLightShaderProgram.setUniform("projectionMatrix", projectionMatrix); // 镜面反射系数 pointLightShaderProgram.setUniform("specularPower", specularPower); // 绑定G缓冲区纹理 int[] textureIds = this.gBuffer.getTextureIds(); int numTextures = textureIds != null ? textureIds.length : 0; for (int i=0; i