在 24 课中我们介绍了阴影纹理的基础 —— 首先在一个渲染通道中在光源的视角处渲染整个场景,之后再另一个渲染通道中从相机视角渲染整个场景并利用前一个通道的渲染结果进行阴影计算。对于这一点,很多程序员可能会问:这个方法对于平行光光源或者聚光灯光源是可行的,但是如何生成点光源的阴影呢?因为对点光源来说光照没有特定的方向,解决这个问题的方法就是本课所讨论的主题。
这个问题的解决方法就是要认识到点光源发出的光照是朝向所有方向的,所以我们不应该将阴影投影到一个矩形的阴影纹理中(这样只能接受到一部分光照)而是应该将光源放在一个立方体纹理中间。现在我们就有了 6 个矩形的阴影纹理光源的所有方向的光照都能够记录下来了。每一个光束都会照射到六个阴影纹理中的一个之上,我们可以使用和之前一样的方法从这个阴影纹理上进行采样并进行阴影计算。我们在实现天空盒那一课中早已见过立方体纹理,所有应该不会陌生了。
为了模拟点光源朝不同方向发射光线的特点我们会在光源周围创建六个阴影纹理渲染通道,但是每个通道都针对不同方向的光线,为了简单起见我们将使不同的每个面于下列坐标轴对其: X 轴正方向和负方向,Y 轴正方向和负方向,Z 轴正方向和负方向。 最终立方体纹理的每个面都会包含场景中离光源最近的像素的深度信息(在光源视口之下)。在光照阶段通过对每个像素到光源的距离的比较就能知道当前像素是否是处于阴影中。
看看下面的图片:
我们的场景中包含一个蓝色的球体,在它的附近有一个点光源(黄色的灯泡)。在第一个渲染通道中我们使用一个立方体纹理作为帧缓存,需要记住的是在这个阶段我们并不关心相机的位置和朝向,我们将相机放置在点光源处这样相机就像是始终位于立方体纹理的中心。在上面的例子中我们可以看到当前相机朝向的是 Z 轴镇方向(朝向黄色的面)。到了这里我们又回到了之前的阴影纹理处理过程,通过使用黄色阴影纹理中的深度值我们就能得到蓝色球体的阴影(球体的深度值都存放在黑色的圆形内,在第二阶段才会真正将阴影渲染出来)。
下面这个图片中展示了我们在第一个渲染阶段会使用到的六个相机的朝向:
由于在第一阶段我们需要对同样的场景渲染 6 次,所以我们将这个技术叫做多通道阴影映射。
(shadow_map_fbo.h) |
class ShadowMapFBO |
{ |
public: |
ShadowMapFBO(); |
~ShadowMapFBO(); |
bool Init(unsigned int WindowWidth, unsigned int WindowHeight); |
void BindForWriting(GLenum CubeFace); |
void BindForReading(GLenum TextureUnit); |
private: |
GLuint m_fbo; |
GLuint m_shadowMap; |
GLuint m_depth; |
}; |
(shadow_map_fbo.cpp:46) |
bool ShadowMapFBO::Init(unsigned int WindowWidth, unsigned int WindowHeight) |
{ |
// 创建 FBO |
glGenFramebuffers(1, &m_fbo); |
// 创建深度缓存 |
glGenTextures(1, &m_depth); |
glBindTexture(GL_TEXTURE_2D, m_depth); |
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32, WindowWidth, WindowHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL); |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); |
glBindTexture(GL_TEXTURE_2D, 0); |
// 创建立方体纹理 |
glGenTextures(1, &m_shadowMap); |
glBindTexture(GL_TEXTURE_CUBE_MAP, m_shadowMap); |
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); |
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); |
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); |
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); |
for (uint i = 0 ; i < 6 ; i++) { |
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_R32F, WindowWidth, WindowHeight, 0, GL_RED, GL_FLOAT, NULL); |
} |
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); |
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_depth, 0); |
// 禁止向颜色缓存的写入 |
glDrawBuffer(GL_NONE); |
// 禁止从颜色缓存的读取 |
glReadBuffer(GL_NONE); |
GLenum Status = glCheckFramebufferStatus(GL_FRAMEBUFFER); |
if (Status != GL_FRAMEBUFFER_COMPLETE) { |
printf("FB error, status: 0x%x\n", Status); |
return false; |
} |
glBindFramebuffer(GL_FRAMEBUFFER, 0); |
return GLCheckError(); |
} |
(tutorial43.cpp:183) |
virtual void RenderSceneCB() |
{ |
CalcFPS(); |
m_scale += 0.05f; |
m_pGameCamera->OnRender(); |
ShadowMapPass(); |
RenderPass(); |
RenderFPS(); |
glutSwapBuffers(); |
} |
(tutorial43.cpp:200) |
void ShadowMapPass() |
{ |
glCullFace(GL_FRONT); |
m_shadowMapEffect.Enable(); |
PersProjInfo ProjInfo; |
ProjInfo.FOV = 90.0f; |
ProjInfo.Height = WINDOW_HEIGHT; |
ProjInfo.Width = WINDOW_WIDTH; |
ProjInfo.zNear = 1.0f; |
ProjInfo.zFar = 100.0f; |
Pipeline p; |
p.SetPerspectiveProj(m_persProjInfo); |
glClearColor(FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX); |
for (uint i = 0 ; i < NUM_OF_LAYERS ; i++) { |
m_shadowMapFBO.BindForWriting(gCameraDirections[i].CubemapFace); |
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); |
p.SetCamera(m_pointLight.Position, gCameraDirections[i].Target, gCameraDirections[i].Up); |
p.Orient(m_mesh1Orientation); |
m_shadowMapEffect.SetWorld(p.GetWorldTrans()); |
m_shadowMapEffect.SetWVP(p.GetWVPTrans()); |
m_mesh.Render(); |
p.Orient(m_mesh2Orientation); |
m_shadowMapEffect.SetWorld(p.GetWorldTrans()); |
m_shadowMapEffect.SetWVP(p.GetWVPTrans()); |
m_mesh.Render(); |
} |
} |
这是阴影纹理阶段,这里有一些与普通阴影纹理不同的地方需要我们注意,首先视野角这里我们设置为 90 度,这是因为我们希望将整个场景都渲染到立方体纹理中,为了使相机完美的对上每个面,我们将其设置为四分之一个圆(360 度)。
之后立方体纹理的默认值设置为浮点类型的最大值(FLT_MAX),在渲染的时候每个纹素中的值实际上会小的多,“真正的”像素中的值会比没有渲染的像素中的值。
最后,我们遍历立方体贴图的每个面,使用 gCameraDirections (看下面)指定要渲染到的 FBO 中的面,并使得相机朝向这个面。
(tutorial43.cpp:45) |
struct CameraDirection |
{ |
GLenum CubemapFace; |
Vector3f Target; |
Vector3f Up; |
}; |
CameraDirection gCameraDirections[NUM_OF_LAYERS] = |
{ |
{ GL_TEXTURE_CUBE_MAP_POSITIVE_X, Vector3f(1.0f, 0.0f, 0.0f), Vector3f(0.0f, -1.0f, 0.0f) }, |
{ GL_TEXTURE_CUBE_MAP_NEGATIVE_X, Vector3f(-1.0f, 0.0f, 0.0f), Vector3f(0.0f, -1.0f, 0.0f) }, |
{ GL_TEXTURE_CUBE_MAP_POSITIVE_Y, Vector3f(0.0f, 1.0f, 0.0f), Vector3f(0.0f, 0.0f, -1.0f) }, |
{ GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, Vector3f(0.0f, -1.0f, 0.0f), Vector3f(0.0f, 0.0f, 1.0f) }, |
{ GL_TEXTURE_CUBE_MAP_POSITIVE_Z, Vector3f(0.0f, 0.0f, 1.0f), Vector3f(0.0f, -1.0f, 0.0f) }, |
{ GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, Vector3f(0.0f, 0.0f, -1.0f), Vector3f(0.0f, -1.0f, 0.0f) } |
}; |
(shadow_map_fbo.cpp:96) |
void ShadowMapFBO::BindForWriting(GLenum CubeFace) |
{ |
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo); |
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, CubeFace, m_shadowMap, 0); |
glDrawBuffer(GL_COLOR_ATTACHMENT0); |
} |
(tutorial43.cpp:237) |
void RenderPass() |
{ |
glCullFace(GL_BACK); |
glBindFramebuffer(GL_FRAMEBUFFER, 0); |
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); |
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); |
m_lightingEffect.Enable(); |
m_shadowMapFBO.BindForReading(SHADOW_TEXTURE_UNIT); |
m_lightingEffect.SetEyeWorldPos(m_pGameCamera->GetPos()); |
Pipeline p; |
p.SetPerspectiveProj(m_persProjInfo); |
p.SetCamera(*m_pGameCamera); |
// Render the quads |
m_pGroundTex->Bind(COLOR_TEXTURE_UNIT); |
p.Orient(m_quad1Orientation); |
m_lightingEffect.SetWorldMatrix(p.GetWorldTrans()); |
m_lightingEffect.SetWVP(p.GetWVPTrans()); |
m_quad.Render(); |
p.Orient(m_quad2Orientation); |
m_lightingEffect.SetWorldMatrix(p.GetWorldTrans()); |
m_lightingEffect.SetWVP(p.GetWVPTrans()); |
m_quad.Render(); |
// Render the meshes |
p.Orient(m_mesh1Orientation); |
m_lightingEffect.SetWorldMatrix(p.GetWorldTrans()); |
m_lightingEffect.SetWVP(p.GetWVPTrans()); |
m_mesh.Render(); |
p.Orient(m_mesh2Orientation); |
m_lightingEffect.SetWorldMatrix(p.GetWorldTrans()); |
m_lightingEffect.SetWVP(p.GetWVPTrans()); |
m_mesh.Render(); |
} |
(shadow_map.vs) |
#version 330 |
layout (location = 0) in vec3 Position; |
layout (location = 1) in vec2 TexCoord; |
layout (location = 2) in vec3 Normal; |
uniform mat4 gWVP; |
uniform mat4 gWorld; |
out vec3 WorldPos; |
void main() |
{ |
vec4 Pos4 = vec4(Position, 1.0); |
gl_Position = gWVP * Pos4; |
WorldPos = (gWorld * Pos4).xyz; |
} |
(shadow_map.fs) |
#version 330 |
in vec3 WorldPos; |
uniform vec3 gLightWorldPos; |
out float FragColor; |
void main() |
{ |
vec3 LightToVertex = WorldPos - gLightWorldPos; |
float LightToPixelDistance = length(LightToVertex); |
FragColor = LightToPixelDistance; |
} |
(lighting.vs) |
#version 330 |
layout (location = 0) in vec3 Position; |
layout (location = 1) in vec2 TexCoord; |
layout (location = 2) in vec3 Normal; |
out vec2 TexCoord0; |
out vec3 Normal0; |
out vec3 WorldPos0; |
uniform mat4 gWVP; |
uniform mat4 gWorld; |
void main() |
{ |
gl_Position = gWVP * vec4(Position, 1.0); |
TexCoord0 = TexCoord; |
Normal0 = (gWorld * vec4(Normal, 0.0)).xyz; |
WorldPos0 = (gWorld * vec4(Position, 1.0)).xyz; |
} |
(lighting.fs) |
... |
uniform samplerCube gShadowMap; |
... |
float CalcShadowFactor(vec3 LightDirection) |
{ |
float SampledDistance = texture(gShadowMap, LightDirection).r; |
float Distance = length(LightDirection); |
if (Distance <= SampledDistance + EPSILON) |
return 1.0; // Inside the light |
else |
return 0.5; // Inside the shadow |
} |