相关文章推荐
唠叨的猴子  ·  [VBA] ...·  6 月前    · 
爱旅游的手套  ·  android ...·  8 月前    · 
谦和的蘑菇  ·  winforms c# how to ...·  1 年前    · 

阴影贴图的GLSL实现

本文是OpenGL 4.0 Shading Language Cookbook的学习笔记。

阴影贴图 是一种非常流行的实时阴影生成技术。它的最基本实现,需要进行两遍处理。第一遍处理,从光源位置渲染场景,然后将深度信息存储在Shadowmap中。Shadowmap存储了从光源位置进行透视的物体可见性信息。换句话说,Shadowmap存储了光源到第一次遇到的物体表面的距离,所有比这个距离值小的对象会被照亮,否则,对象位于阴影中。

在第二遍处理,在片段着色器,每个片段的深度值和在光源透视下它的位置对应的Shadowmap值进行比较。根据比较结果,选择仅使用环境光进行光照着色还是正常进行着色。

下图使用上面描述的基础的Shadowmap技术进行渲染:

下面,让我们更加详细地讨论这个算法。

第一步是生成Shadowmap。我们将相机放在光源处,然后对准需要产生阴影地对象进行渲染,并将深度缓冲的数据保存在一张纹理上。这张纹理被叫做Shadowmap或(depth map)。我们可以认为它存储了光源到不同表面的距离值。

从技术上讲,Shadowmap是深度值,并不是距离值。深度值并不是从原点出发到某个位置的真实距离值,但它对于我们这里生成阴影的目的,可以近似作为这个距离值。

下图演示了基础的Shadowmap的生成。左图显示了光源的位置以及它的透视椎体。右图是生成的Shadowmap。Shadowmap的亮度对应它的深度值(越黑越接近光源)。

生成Shadowmap后,我们从观察者的位置渲染场景。这一次渲染,我们使用的片段着色器根据Shadowmap中的深度值确定片段如何着色。我们先将片段的位置坐标转换到光源的坐标系,然后使用光源的透视投影矩阵进行坐标的透视投影,最后对坐标进行变换(为了获取合法的纹理坐标),和Shadowmap中的深度值进行对比。如果片段的深度值比对应的Shadowmap的深度值大,那么说明光源和片段之间还存在其它对象,片段处于阴影中,应该仅使用环境光进行着色。否则,片段应该进行正常的光照着色。

这里的关键点是将片段的三维坐标转换到对应的Shadowmap上去。由于Shadowmap是一张二维纹理,我们需要顶点在光源投影椎体下的范围从0到1的坐标。光源的视图矩阵可以将世界坐标系下的坐标转换到光源的坐标系下。光源的投影矩阵可以完成光源椎体下的坐标到齐次裁剪坐标的转换。

剪切坐标系的由来是OpenGL内部的剪切功能是根据这一坐标系执行的。位于透视投影(或平行投影)椎体中的点通过投影矩阵转换到立方体(齐次)空间中,这个立方体的中心位于原点,边长为2。这个立方体构成的空间被叫做规范视域体。齐次是指坐标需要除以它的第四个分量才是真正的笛卡尔坐标。关于齐次坐标的更多信息,可以参考任意你喜欢的计算机图形学书籍。

剪切空间下的坐标的x分量和y分量被我们用来访问Shadowmap。坐标的z分量可以用来和Shadowmap中的值进行比较。但是,使用它们之前,我们还要对将它们的值变换到0到1之间(代替-1到1的范围),并且还需要进行透视除法。

我们需要将坐标的x,y和z分量转换到Shadowmap使用的0到1的范围(对于位于光源椎体中的点)。OpenGL的深度值的范围在0到1之间(通常来说)。0代表点位于透视椎体的近平面,1代表点位于透视椎体的远平面。使用坐标的z分量和深度缓冲中的数据进行比较前,应该对它进行适当的转换。

剪切坐标(进行透视除法之后)的z分量的范围为-1到1。视口变换将这个范围转换到0到1。我们可以调用glDepthRange函数设置其它范围,比如0到100。

为了访问纹理,我们需要将坐标的x分量和y分量转换到0到1之间。

我们使用下面的矩阵来完成这一转换。

这个矩阵可以将坐标的x,y和z分量的范围转换到0到1(进行透视除法后)。设一矩阵为B,光源的视图矩阵为 \textbf{V}_l ,光源的投影矩阵为 \textbf{P}_l ,世界坐标系下的点的坐标为 \textbf{W} ,那么我们可以使用下面的式子将这个坐标转换到可以用来访问Shadowmap的齐次坐标空间。

\textbf{Q}=\textbf{B}\textbf{P}_l\textbf{V}_l\textbf{W}

最后,我们将Q的前三个分量除以第四个分量完成透视除法。这一步,将齐次坐标转换到笛卡尔坐标,使用透视投影矩阵时,这一步是必须的。

接下来,我们定义一个阴影矩阵 \textbf{S} ,它包含了模型矩阵 \textbf{M} ,从而使我们可以直接从模型坐标转换到齐次坐标。

\textbf{Q}=\textbf{S}\textbf{C}

为了保持清晰简单,我们这里使用的是最基础的Shadowmap算法。所以,渲染的结果可能并不十分令人满意。可能会出现明显的走样。但结合一些反走样技术后,它仍能产生不错的结果,我们将在后面讨论这些提升效果的技术。

实现

我们设置顶点位置location为0,法线location为1。然后需要设置Phong着色模型相关的Uniform变量,以及我们使用的变换矩阵。最后我们需要设置矩阵ShadowMatrix来完成模型坐标到Shadowmap坐标的转换(也就是之前提到的 \textbf{S} 矩阵)。

Uniform变量Shadowmap是Shadowmap纹理的句柄,我们设置它为0来对应使用的0号纹理单元。

为了实现阴影效果,我们先创建一个帧缓冲来进行Shadowmap纹理的渲染,然后实现其它需要的着色器:

1. 在OpenGL程序中创建一个帧缓冲对象,并将帧缓冲对象的句柄存储在变量shadowFBO中。深度缓冲需要绑定一个纹理对象。我们可以使用下面的代码来完成这些工作:

GLfloat border[]={1.0f,0.0f,0.0f,0.0f};
//The shadow maptexture
GLuint depthTex;
glGenTextures(1,&depthTex);
glBindTexture(GL_TEXTURE_2D,depthTex);
glTexImage2D(GL_TEXTURE_2D,0,GL_DEPTH_COMPONENT,shadowMapWidth,shadowMapHeight,0,
						GL_DEPTH_COMPONENT,GL_UNSIGNED_BYTE,NULL);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_BORDER);
glTexParameterfv(GL_TEXTURE_2D,GL_TEXTURE_BORDER_COLOR,border);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_MODE,GL_COMPARE_REF_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_FUNC,GL_LESS);
//Assign the shadow map to texture channel 0
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,depthTex);
//Create and set up the FBO
glGenFramebuffers(1,&shadowFBO);
glBindFramebuffer(GL_FRAMEBUFFER,shadowFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,GL_TEXTURE_2D,depthTex,0);
GLenum drawBuffers[]={GL_NONE};
glDrawBuffers(1,drawBuffers);
// Revert to the default framebuffer for now
glBindFramebuffer(GL_FRAMEBUFFER,0);

2. 使用下面的代码作为顶点着色器:

#version 400
layout (location=0) in vec3 VertexPosition;
layout (location=1) in vec3 VertexNormal;
out vec3 Normal;
out vec3 Position;
// Coordinate to be used for shadow map lookup out vec4 ShadowCoord;
uniform mat4 ModelViewMatrix;
uniform mat3 NormalMatrix;
uniform mat4 MVP;
uniform mat4 ShadowMatrix;
void main()
	Position = (ModelViewMatrix *vec4(VertexPosition,1.0)).xyz;
	Normal = normalize( NormalMatrix * VertexNormal );
	// ShadowMatrix converts from modeling coordinates
	// to shadow map coordinates.
	ShadowCoord =ShadowMatrix * vec4(VertexPosition,1.0);
	gl_Position = MVP * vec4(VertexPosition,1.0);
}

3. 使用下面的代码作为片段着色器:

#version 400
// Declare any uniforms needed for
// the Phong shading model
uniform sampler2DShadow ShadowMap;
in vec3 Position;
in vec3 Normal;
in vec4 ShadowCoord;
layout (location = 0) out vec4 FragColor;
vec3 phongModelDiffAndSpec()
	// Compute only the diffuse and specular components of
	// the Phong shading model.
subroutine void RenderPassType();
subroutine uniform RenderPassType RenderPass;
subroutine (RenderPassType)
void shadeWithShadow()
	vec3 ambient = ;
	// compute ambient component here
	vec3 diffAndSpec = phongModelDiffAndSpec();
	// Do the shadow-map look-up
	float shadow = textureProj(ShadowMap, ShadowCoord);
	//If the fragment is in shadow, use ambient light only.
	FragColor = vec4(diffAndSpec * shadow + ambient, 1.0);
subroutine (RenderPassType)
void recordDepth()