✠OpenGL-8-阴影
Posted itzyjr
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了✠OpenGL-8-阴影相关的知识,希望对你有一定的参考价值。
投影阴影
一种很适合在地平面上绘制阴影,又相对不需要太大计算代价的方法,叫作投影阴影(projective shadows)
。
给定一个位于(xL, yL, zL)的点光源、一个需要渲染的物体以及一个投射阴影的平面,可以通过生成一个变换矩阵,将物体上的点(xw, yw, zw)变换为相应阴影在平面上的点(xs, 0, zs)。之后将其生成的“阴影多边形”绘制出来,通常使用暗色物体与地平面纹理混合作为其纹理,如下图所示。
使用投影阴影进行投射的优点是它的高效和易于实现。但是,它仅适用于平坦表面——这种方法无法投射阴影于曲面或其他物体。即使如此,它仍然适用于有室外场景并对性能要求较高的应用,很多游戏中的场景都属于这类。
阴影体
先找到被物体阴影覆盖的阴影体,之后减少视体与阴影体相交部分中的多边形的颜色强度。如下图展示了阴影体中的立方体,因此,立方体绘制时会更暗。
阴影体
的优点在于其高度准确,比起其他方法来更不容易产生伪影。但是,计算出阴影体以及每个多边形是否在其中这件事,即使对于现代 GPU 来说,计算代价也很大。几何着色器可以用于计算阴影体, 模板缓冲区可以用于判断像素是否在阴影体内。有些显卡对于特定的阴影体操作优化提供了硬件支持。
阴影贴图
阴影贴图
是用于投射阴影最实用也最流行的方法之一。虽然它并不总是像阴影体一样准确(且通常伴随着讨厌的伪影),但阴影贴图实现起来更简单,可以在各种情况下使用,并享有强大的硬件支持。
阴影贴图基于一个非常简明的想法: 光线无法看到的任何东西都在阴影中。也就是说,如果对象#1 阻挡光到达对象#2,等同于光不能“看到”对象#2。
这个想法的强大之处在于我们已经有了方法来确定物体是否可以被“看到”——使用Z缓冲区的隐藏面消除算法(HSR),如Blog“✠OpenGL-2-图像管线”-像素操作(隐藏面消除、Z-Buffer算法) 所述。因此,计算阴影的策略是,暂时将摄像机移动到光的位置,应用 Z 缓冲区 HSR 算法,然后使用生成的深度信息来计算阴影。
因此,渲染场景需要两轮:第 1 轮从灯光的角度渲染场景(但实际上没有将其绘制到屏幕上),第 2 轮从摄像机的角度渲染场景。第 1 轮的目的是从光的角度生成 Z 缓冲区。完成第 1 轮之后,我们需要保留 Z 缓冲区并使用它来帮助我们在第 2 轮生成阴影。第 2 轮实际绘制场景。
- (第 1 轮)从灯光的位置渲染场景。然后,对于每个像素,深度缓冲区包含光与最近的对象之间的距离。
- (中间步骤)将深度缓冲区复制到单独的“阴影缓冲区”。
- (第 2 轮)正常渲染场景。对于每个像素,在阴影缓冲区中查找相应的位置。如果相机到渲染点的距离大于从阴影缓冲区检索到的值, 则在该像素处绘制的对象离光线的距离,比离光线最近的对象更远,因此该像素处于阴影中。
当发现像素处于阴影中时,我们需要使其更暗。一种简单而有效的方法是仅渲染其环境光,忽略其漫反射和镜面反射分量。
上述方法通常被称为“阴影缓冲区”。而当我们在第二步中,将深度缓冲区复制到纹理中,则称为“阴影贴图”。当纹理对象用于储存阴影深度信息时,我们称其为阴影纹理,OpenGL 通过 sampler2DShadow
类型支持阴影纹理
。这样,我们就可以利用片段着色器中纹理单元和采样器变量(即“纹理贴图”)的硬件支持功能,在第 2 轮快速执行深度查找。我们现在修改的策略是:
- (第 1 轮)与之前相同;
- (中间步骤)将深度缓冲区的内容复制进纹理对象;
- (第 2 轮)与之前相同,不过阴影缓冲区变为阴影纹理。
阴影贴图(第1轮)——从光源位置“绘制”物体
在第一步中,我们首先将相机移动到灯光的位置然后渲染场景。我们的目标不是在显示器上实际绘制场景,而是完成足够的渲染过程以正确填充深度缓冲区。因此,没有必要为像素生成颜色,我们的第一遍将仅使用顶点着色器,但片段着色器不执行任何操作。
当然,移动相机需要构建适当的观察矩阵。根据场景的内容,我们需要在光源处依合适的方向来看场景。通常,我们希望此方向朝向最终在第 2 轮中呈现的区域。
这个方向通常依场景而定——在我们的场景中,我们通常会将相机从光源指向原点。
第 1 轮中有几个需要处理的重要细节。
- 配置缓冲区和阴影纹理。
- 禁用颜色输出。
- 从光源到视野中的物体构建一个 LookAt 矩阵。
- 启用 GLSL 第 1 轮着色器程序,该程序仅包含下图中的简单顶点着色器,准备接收 MVP 矩阵。在这种情况下, MVP 矩阵将包括对象的模型矩阵 M、前一步中计算的 LookAt 矩阵(作为观察矩阵 V)(LookAt矩阵描述如Blog:“✠OpenGL-3-数学基础”-变换矩阵),以及透视矩阵 P。我们将该 MVP 矩阵称为“shadowMVP”,因为它是基于光而不是相机的观察点。
// 顶点着色器
#version 430
layout(location = 0) int vec3 vertPos;
uniform mat4 shadowMVP;
void main() {
gl_Position = shadowMVP * vec4(vertPos, 1.0);
}
// 片段着色器
void main() {}
由于实际上没有显示来自光源的视图,因此第 1 轮着色器程序的片段着色器不会执行任何操作。
- 为每个对象创建 shadowMVP 矩阵,并调用 glDrawArrays()。第 1 轮中不需要包含纹理或光照,因为对象不会渲染到屏幕上。
阴影贴图(中间步骤)——将Z缓冲区复制到纹理
OpenGL 提供了两种将 Z 缓冲区深度数据放入纹理单元的方法。
第一种方法是生成空阴影纹理,然后使用命令 glCopyTexImage2D()
将活动深度缓冲区复制到阴影纹理中。
第二种方法是在第 1 轮中构建一个“自定义帧缓冲区
”(而不是使用默认的 Z 缓冲区),并使用命令 glFrameBufferTexture()
将阴影纹理附加到它上面。 OpenGL 在 3.0 版中引入该命令,以进一步支持阴影纹理。使用这种方法时,无须将 Z 缓冲区“复制”到纹理中,因为缓冲区已经附加了纹理,深度信息由 OpenGL 自动放入纹理中。我们将在实现中使用这种方法。
glFrameBufferTexture(GLenum target, GLenum attachment, GLuint texture, GLint level)
附加纹理对象的LV作为帧缓冲区对象的逻辑缓冲区。
target:指定除glNamedFramebufferTexture之外的所有命令的帧缓冲区绑定到的目标,如常量GL_FRAMEBUFFER。
attachment:指定帧缓冲区的附着点,如常量GL_DEPTH_ATTACHMENT。
texture:指定要附加的现有纹理对象的名称,如一个GLuint类型的纹理ID。
level:指定要附加的纹理对象的mipmap级别,默认为0。
命令将纹理对象的选定mipmap级别或图像附加为指定帧缓冲区对象的逻辑缓冲区之一。纹理不能附加到默认的“绘制”和“读取”帧缓冲区,因此它们不是该命令的有效目标。
void glDrawBuffer(GLenum buf);
指定要绘制到的颜色缓冲区。
buf:将颜色写入帧缓冲区时,它们将写入指定的颜色缓冲区。以下值之一可用于默认帧缓冲区:
GL_NONE——不写入颜色缓冲区。
GL_FRONT——只写入左前和右前颜色缓冲区。如果没有右前颜色缓冲区,则只写入左前颜色缓冲区。
GL_FRONT_LEFT——只写入左前颜色缓冲区。
GL_BACK_RIGHT——只写入右后颜色缓冲区。
…
对于单缓冲上下文,初始值为GL_FRONT,对于双缓冲上下文,初始值为GL_BACK。
void glBindFramebuffer(GLenum target, GLuint framebuffer);
将帧缓冲区绑定到帧缓冲区目标。
target:指定绑定操作的帧缓冲区目标。
framebuffer:指定要绑定的帧缓冲区对象的名称。
glBindFramebuffer将名为framebuffer的framebuffer对象绑定到target指定的framebuffer目标。目标必须是GL_DRAW_FRAMEBUFFER, GL_READ_FRAMEBUFFER 或 GL_FRAMEBUFFER。如果帧缓冲区对象绑定到GL_DRAW_FRAMEBUFFER 或 GL_READ_FRAMEBUFFER,则它将分别成为渲染或回读操作的目标,直到将其删除或将另一个帧缓冲区绑定到相应的绑定点。调用glBindFramebuffer并将target设置为GL_FRAMEBUFFER会将framebuffer绑定到read和draw framebuffer目标。framebuffer是以前从调用glGenFramebuffers返回的framebuffer对象的名称,或为零以中断framebuffer对象到目标的现有绑定。
阴影贴图(第2轮)——渲染带阴影的场景
从纹理贴图尝试查找像素时,情况比较复杂。 OpenGL 相机使用[−1…+ 1]坐标空间,而纹理贴图使用[0…1]空间。常见的解决方案是构建一个额外的矩阵变换,通常称为 B(“偏离”, biases),它将用于从摄像机空间到纹理空间的转换。
得到 B 的过程很简单——先缩放为 1/2,再平移 1/2。矩阵 B 如下:
B
=
[
0.5
0
0
0.5
0
0.5
0
0.5
0
0
0.5
0.5
0
0
0
1
]
B= \\left[ \\begin{matrix} 0.5& 0& 0& 0.5 \\\\ 0& 0.5& 0& 0.5 \\\\ 0& 0& 0.5& 0.5 \\\\ 0& 0& 0& 1 \\end{matrix} \\right]
B=⎣⎢⎢⎡0.500000.500000.500.50.50.51⎦⎥⎥⎤
之后将 B 合并入 shadowMVP 矩阵以备在第 2 轮中使用,如下:
s
h
a
d
o
w
M
V
P
=
[
B
]
[
s
h
a
d
o
w
M
V
P
(
p
a
s
s
1
)
]
shadowMVP = [B][shadowMVP_{(pass1)}]
shadowMVP=[B][shadowMVP(pass1)]
假设我们使用阴影纹理附加到我们的自定义帧缓冲区的方法, OpenGL 提供了一些相对简单的工具,用于确定绘制对象时,像素是否处于阴影中。
- 构建变换矩阵 B,用于从光照空间转换到纹理空间[更合适在 init()中进行]。
- 启用阴影纹理以进行查找。
- 启用颜色输出。
- 启用 GLSL 第 2 轮渲染程序,包含顶点着色器和片段着色器。
- 根据摄像机位置(正常)为正在绘制的对象构建 MVP 矩阵。
- 构建 shadowMVP2 矩阵(包含 B 矩阵)——着色器将需要用它查找阴影纹理中的像素坐标。
- 将生成的矩阵变换发送到着色器统一变量。
- 像往常一样启用包含顶点、法向量和纹理坐标(如果使用)的缓冲区。
- 调用 glDrawArrays()。
除了渲染任务外,顶点和片段着色器还需要额外承担一些任务。
- 顶点着色器将顶点位置从相机空间转换为光照空间, 并将结果坐标发送到顶点属性中的片段着色器,以便对它们进行插值。这样片段着色器可以从阴影纹理中检索正确的值。
- 片段着色器调用
textureProj()
函数, 该函数返回 0 或 1, 指示像素是否处于阴影中。如果它在阴影中,则着色器通过剔除其漫反射和镜面反射分量来输出更暗的像素。
float textureProj(sampler2DShadow sampler, vec4 P, [float bias]);
使用projection执行纹理查找。它专门用于投影纹理访问的。
sampler:指定将从中检索texel的纹理绑定到的采样器。
P:指定纹理采样的纹理坐标。
bias:指定要在详细等级计算期间应用的可选偏移。
OpenGL Reference:
P - Specifies the texture coordinates at which texture will be sampled.
The texture coordinates consumed from P, not including the last component of P, are divided by the last component of P.
即,vec4类型点P的使用是:除了最后一个分量自身外,其他分量都要除以最后一个分量。
也即,使用纹理查找时,它的纹理坐标各分量会除以最后一个分量,【然后才】访问纹理。
结果产生的在阴影形式中(in the shadow forms)的P的第三个分量用作Dref。
计算完这些值后,纹理查找将按texture()函数中的方式进行。
textureProj()函数用于从阴影纹理中查找值,它类似于我们之前看到的 texture(),其区别是除了 textureProj()函数使用 vec4 来索引纹理而不是通常的 vec2。
由于像素坐标是vec4,因此需要将其投影到 2D 纹理空间上,以便在阴影纹理贴图中查找深度值。
齐次纹理坐标——vec4类型纹理坐标
齐次纹理坐标(homogeneous texture coordinates)
的概念对大多数人来说比较陌生,纹理坐标一般是二维的,如果是体纹理,其纹理坐标也只是三维的。齐次纹理坐标的出现是为了和三维顶点的齐次坐标相对应, 因为本质上,投影纹理坐标是通过三维顶点的齐次坐标计算得到的。
齐次纹理坐标通常表示为(s, t, r, q),以区别于物体位置齐次坐标(x, y, z, w) 。一维纹理常用s坐标表示,二维纹理常用(s, t) 坐标表示,目前忽略r坐标,q 坐标的作用与齐次坐标点中的 w 坐标非常类似,一般值为1,主要用于建立齐次坐标。
// 顶点着色器
#version 430
...
out vec4 shadow_coord;
uniform mat4 shadowMVP2;
layout(location = 0) in vec3 vertPos;
void main() {
...
shadow_coord = shadowMVP2 * vec4(vertPos, 1.0);
gl_Position = proj_matrix * mv_matrix * vec4(vertPos, 1.0);
}
渲染的像素和阴影纹理中的值的深度比较
首先,从顶点着色器开始,在模型空间中使用顶点坐标,我们将其与shadowMVP2 相乘以生成阴影纹理坐标,这些坐标对应于投影到光照空间中的顶点坐标,是之前从光源的视角生成的。经过插值后的3D光照空间坐标 (x, y, z) 在片段着色器中使用如下:z 分量表示从光到像素的距离, (x, y) 分量用于检索存储在2D阴影纹理中的深度信息。将该检索的值(到最靠近光的物体的距离)与 z 进行比较。该比较产生“二元”结果,告诉我们我们正在渲染的像素是否比【最接近光的物体】离光更远(即像素是否处于阴影中)。
假设光源位置以视觉空间坐标表示。
如果我们在 OpenGL 中使用前面介绍过的 glFrameBufferTexture()并启用深度测试,然后使用片段着色器的 sampler2DShadow 和 textureProj(),所渲染的结果将完全满足我们的需求。即 textureProj()将输出 0.0 或 1.0,具体取决于深度比较。基于此值,当像素离光源比离光源最近的物体更远时,我们可以在片段着色器中忽略漫反射和镜面反射分量,从而有效地创建阴影。
如下图,[有光照无阴影的场景(左)] 和 [有光照有阴影的场景(右)]:
如果是自身的阴影,就正常的光照模型就处理了,正常的渲染。
如果是被其他模型遮挡,就得专门算法针对处理了。
所以,关键就是要判断本模型是否被其他模型所遮挡,如被遮挡,就忽略遮挡部位漫反射和镜面反射分量;否则,就使用标准光照模型。
// 片段着色器
#version 430
...
in vec4 shadow_coord;
layout (binding = 0) uniform sampler2DShadow shTex;
void main() {
...
float notInShadow = textureProj(shTex, shadow_coord);
fragColor = globalAmbient * materail.ambient + light.ambient * material.ambient;
if (notInShadow == 1.0) {
fragColor += light.diffuse * material.diffuse * max(dob(L, N), 0.0) + light.specular * material.specular * pow(max(dot(H, N), 0.0), material.shininess * 3.0);
}
}
背面剔除与阴影的区别
这个区别其实很明显,“背面剔除”是在由眼睛视线方向的一条射线穿过物体,产生多于一个点的交点,在所有交点中,取离屏幕最近的点展示,其他的点不去渲染。同理,对于线、面也是这样。
而阴影是可以看到的,光照模型本身就会产生阴暗面,这是由光照算法决定的。阴影是一个物体遮挡另一物体所产生的一种暗面效果。
如上图,坐标点P和Q在同一指向相机的射线上,试想如果点P是一个孔,那么就可以直接看到点Q了,所以点P的存在遮挡了点Q。要相机空间中,背景剔除就是点P的深度值小于点Q的深度值,点Q被剔除。
点M在球体的阴暗面,点N在投影阴影上面,这是相机可以直接看到的。
阴影贴图示例及分析
main.cpp
...
void passOne(void);
void passTwo(void);
#define numVAOs 1
#define numVBOs 5
GLuint renderingProgram1, renderingProgram2;
glm::vec3 torusLoc(1.6f, 0.0f, -0.3f);
glm::vec3 pyrLoc(-1.0f, 0.1f, 0.3f);
glm::vec3 cameraLoc(0.0f, 0.2f, 6.0f);
glm::vec3 lightLoc(-3.8f, 2.2f, 1.1f);
// 白光
float globalAmbient[4] = { 0.7f, 0.7f, 0.7f, 1.0f };
float lightAmbient[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
float lightDiffuse[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
float lightSpecular[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
// 黄金材质(gold material)
float* gMatAmb = Util::goldAmbient();
float* gMatDif = Util::goldDiffuse();
float* gMatSpe = Util::goldSpecular();
float gMatShi = Util::goldShininess();
// 铜材质(bronze material)
float* bMatAmb = Util::bronzeAmbient();
float* bMatDif = Util::bronzeDiffuse();
float* bMatSpe = Util::bronzeSpecular();
float bMatShi = Util::bronzeShininess();
// 正被渲染的使用的光照的环境光、漫反射、镜面反射分量,
// 材质环境光、漫反射、镜面反射分量。4个元素即R/G/B/A。
float thisAmb[4], thisDif[4], thisSpe[4], matAmb[4], matDif[4], matSpe[4];
// 正被渲染的使用的光照的闪光度,材质闪光度
float thisShi, matShi;
// 阴影相关
int scSizeX, scSizeY;
GLuint shadowTex, shadowBuffer;
glm::mat4 lightVmatrix;
glm::mat4 lightPmatrix;
glm::mat4 shadowMVP1;
glm::mat4 shadowMVP2;
glm::mat4 b;
// variable allocation for display
GLuint mvLoc, projLoc, nLoc, sLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, mMat, mvMat, invTrMat;
glm::vec3 currentLightPos, transformed;
float lightPos[3];
GLuint globalAmbLoc, ambLoc, diffLoc, specLoc, posLoc, mambLoc, mdiffLoc, mspecLoc, mshiLoc;
// 用于构建LookAt矩阵,之后将光源替换相机,完成“光照空间”
glm::vec3 origin(0.0f, 0.0f, 0.0f);
glm::vec3 up(0.0f, 1.0f, 0.0f);
void installLights(int renderingProgram, glm::mat4 vMatrix) {
// 转换到【光照空间】
transformed = glm::vec3(vMatrix * glm::vec4(currentLightPos, 1.0));
lightPos[0] = transformed.x;
lightPos[1] = transformed.y;
lightPos[2] = transformed.z;
matAmb[0] = thisAmb[0]; matAmb[1] = thisAmb[1]; matAmb[2] = thisAmb[2]; matAmb[3] = thisAmb[3];
matDif[0] = thisDif[0]; matDif[1] = thisDif[1]; matDif[2] = thisDif[2]; matDif[3] = thisDif[3];
matSpe[0] = thisSpe[0]; matSpe[1] = thisSpe[1]; matSpe[2] = thisSpe[2]; matSpe[3] = thisSpe[3];
matShi = thisShi;
// get the locations of the light and material fields in the shader
globalAmbLoc = glGetUniformLocation(renderingProgram, "globalAmbient");
ambLoc = glGetUniformLocation(renderingProgram, "light.ambient");
diffLoc = glGetUniformLocation(renderingProgram, "light.diffuse");
specLoc = glGetUniformLocation(renderingProgram, "light.specular");
posLoc = glGetUniformLocation(renderingProgram, "light.position");
mambLoc = glGetUniformLocation(renderingProgram, "material.ambient");
mdiffLoc = glGetUniformLocation(renderingProgram, "material.diffuse");
mspecLoc = glGetUniformLocation(renderingProgram, "material.specular");
mshiLoc = glGetUniformLocation(renderingProgram, "material.shininess");
// set the uniform light and material values in the shader
glProgramUniform4fv(renderingProgram, globalAmbLoc, 1, globalAmbient);
glProgramUniform4fv(renderingProgram, ambLoc, 1, lightAmbient);
glProgramUniform4fv(renderingProgram, diffLoc, 1, lightDiffuse);
glProgramUniform4fv(renderingProgram, specLoc, 1, lightSpecular);
glProgramUniform3fv(renderingProgram, posLoc, 1, lightPos);
glProgramUniform4fv(renderingProgram, mambLoc, 1, matAmb);
glProgramUniform4fv(renderingProgram, mdiffLoc, 1, matDif);
glProgramUniform4fv(renderingProgram, mspecLoc, 1, matSpe);
glProgramUniform1f(renderingProgram, mshiLoc, matShi);
}
void setupVertices(void) {
.以上是关于✠OpenGL-8-阴影的主要内容,如果未能解决你的问题,请参考以下文章
Cg入门17:Fragment shader - 片段级光照(添加阴影)