在第7章中,我们学会了如何为3D场景添加光照。但是,我们并没有真的添加光线,而是模拟光照在物体上的效果——使用ADS模型——并相应地调整这些物体的绘制方式。
当我们用这种方法照亮同一个场景中的多个物体时,它的局限性就体现出来了。考虑图8.1所示的场景,其中包含了砖块纹理环面以及地平面(地平面是一个巨大立方体的顶部)。
一眼望去我们的场景好像没问题。但是,仔细观察会发现有什么重要的东西没有出现。具体来说,就是我们没有办法分辨出环面距离它下方纹理立方体的距离。环面究竟是浮在立方体上面呢,还是放置在立方体顶部呢?
我们无法回答这个问题的原因正是因为场景中缺乏阴影。我们期望看到阴影,因为大脑需要通过阴影,才能针对我们所看到的物体以及他们的位置关系构建完整的心理模型。
考虑图8.2所示的同样的场景,不过添加了阴影。现在就很明显了,左图中环面放在地平面上;而右图中,环面则浮于其上。
为了给3D场景添加阴影,人们设计了许多有趣的方法。其中一种很适合在地平面上(如图8.1所示)绘制阴影,又相对不需要太大计算代价的方法,叫作投影阴影(projective shadows)。给定一个位于(XL,YL ,ZL)的点光源、一个需要渲染的物体以及一个投射阴影的平面,可以通过生成一个变换矩阵,将物体上的点(XW,YW,ZW)变换为相应阴影在平面上的点(XS,0,ZS)。之后将其生成的“阴影多边形”绘制出来,通常使用暗色物体与地平面纹理混合作为其纹理,如图8.3所示。
使用投影阴影进行投射的优点是它的高效和易于实现。但是,它仅适用于平坦表面——这种方法无法投射阴影于曲面或其他物体。即使如此,它仍然适用于有室外场景并对性能要求较高的应用,很多游戏中的场景都属于这类。
这个方法先找到被物体阴影覆盖的阴影体,之后减少视体与阴影体相交部分中的多边形的颜色强度。图8.4展示了阴影体中的立方体,因此,立方体绘制时会更暗。
阴影体的优点在于其高度准确,比起其他方法来更不容易产生伪影。但是,计算出阴影体以及每个多边形是否在其中这件事,即使对于现代GPU来说,计算代价也很大。
几何着色器可以用于计算阴影体,模板缓冲区[1]可以用于判断像素是否在阴影体内。有些显卡对于特定的阴影体操作优化提供了硬件支持。
阴影贴图是用于投射阴影最实用也最流行的方法之一。虽然它并不总是像阴影体一样准确(且通常伴随着讨厌的伪影),但阴影贴图实现起来更简单,可以在各种情况下使用,并享有强大的硬件支持。
对学生来说,通常在3D图形课程中最难实现的技术之一就是阴影贴图。着色器程序本质上很难调试,阴影贴图需要几个组件和着色器模块的完美协调。请注意,通过使用前面2.2节中描述的调试工具,可以极大地促进阴影贴图的成功实现。
阴影贴图基于一个非常简明的想法:光线无法看到的任何东西都在阴影中。也就是说,如果对象#1阻挡光到达对象#2,等同于光不能“看到”对象#2。
因此,计算阴影的策略是,暂时将摄像机移动到光的位置,应用Z缓冲区HSR算法,然后使用生成的深度信息来计算阴影。
因此,渲染场景需要两轮:第1轮从灯光的角度渲染场景(但实际上没有将其绘制到屏幕上),第2轮从摄像机的角度渲染场景。第1轮的目的是从光的角度生成Z缓冲区。完成第1轮之后,我们需要保留Z缓冲区并使用它来帮助我们在第2轮生成阴影。第2轮实际绘制场景。
我们的策略可以更加精炼。
(第1轮)从灯光的位置渲染场景。然后,对于每个像素,深度缓冲区包含光与最近的对象之间的距离。 将深度缓冲区复制到单独的“阴影缓冲区”。
(第2轮)正常渲染场景。对于每个像素,在阴影缓冲区中查找相应的位置。如果相机到渲染点的距离大于从阴影缓冲区检索到的值,则在该像素处绘制的对象离光线的距离,比离光线最近的对象更远,因此该像素处于阴影中。
当发现像素处于阴影中时,我们需要使其更暗。一种简单而有效的方法是仅渲染其环境光,忽略其漫反射和镜面反射分量。
上述方法通常被称为“阴影缓冲区”。而当我们在第二步中,将深度缓冲区复制到纹理中,则称为“阴影贴图”。当纹理对象用于储存阴影深度信息时,我们称其为阴影纹理,OpenGL通过 sampler2DShadow类型支持阴影纹理(稍后讨论)。这样,我们就可以利用片段着色器中纹理单元和采样器变量(即“纹理贴图”)的硬件支持功能,在第2轮快速执行深度查找。我们现在修改的策略是:
(第1轮)与之前相同; 将深度缓冲区的内容复制进纹理对象;
(第2轮)与之前相同,不过阴影缓冲区变为阴影纹理。 现在我们来实现这些步骤。
在第一步中,我们首先将相机移动到灯光的位置然后渲染场景。 我们的目标不是在显示器上实际绘制场景,而是完成足够的渲染过程以正确填充深度缓冲区。因此,没有必要为像素生成颜色,我们的第一遍将仅使用顶点着色器,但片段着色器不执行任何操作。
当然,移动相机需要构建适当的观察矩阵。根据场景的内容,我们需要在光源处依合适的方向来看场景。通常,我们希望此方向朝向最终在第2轮中呈现的区域。
这个方向通常依场景而定——在我们的场景中,我们通常会将相机从光源指向原点。
第1轮中有几个需要处理的重要细节。
为每个对象创建shadowMVP矩阵,并调用glDrawArrays()
。第1轮中不需要包含纹理或光照,因为对象不会渲染到屏幕上。
OpenGL提供了两种将Z缓冲区深度数据放入纹理单元的方法。第一种方法是生成空阴影纹理,然后使用命令glCopyTexImage2D()
将活动深度缓冲区复制到阴影纹理中。
第二种方法是在第1轮中构建一个“自定义帧缓冲区”(而不是使用默认的Z缓冲区),并使用命令glFrameBufferTexture()
将阴影纹理附加到它上面。OpenGL在3.0版中引入该命令,以进一步支持阴影纹理。使用这种方法时,无须将Z缓冲区“复制”到纹理中,因为缓冲区已经附加了纹理,深度信息由OpenGL自动放入纹理中。我们将在实现中使用这种方法。
第2轮中的大部分内容与我们在第7章中看到的类似,即我们在这里渲染完整的场景及其中的所有物体,以及光照、材质和装饰场景中物体的纹理。同时,我们还需要添加必要的代码,以确定每个像素是否在阴影中。
第2轮的一个重要特征是它使用了两个MVP矩阵。一个是将对象坐标转换为屏幕坐标的标准MVP矩阵(如我们之前的大多数示例所示)。 另一个是在第1轮中生成的shadowMVP矩阵,用于从光源的角度进行渲染——现在将在第2轮中用于从阴影纹理中查找深度信息。
在第2轮中,从纹理贴图尝试查找像素时,情况比较复杂。OpenGL相机使用[−1…+ 1]坐标空间,而纹理贴图使用[0…1]空间。常见的解决方案是构建一个额外的矩阵变换,通常称为B,它将用于从摄像机空间到纹理空间的转换(或“偏离”,biases,因此名称)。得到的B过程很简单——先缩放为1/2,再平移1/2。
矩阵B如下:
之后将B合并入shadowMVP矩阵以备在第2轮中使用,如下:
假设我们使用阴影纹理附加到我们的自定义帧缓冲区的方法, OpenGL提供了一些相对简单的工具,用于确定绘制对象时,像素是否处于阴影中。以下是第二阶段处理的详细信息摘要。
除了渲染任务外,顶点和片段着色器还需要额外承担一些任务。
阴影贴图是一种常见任务,因此GLSL为其提供了一种特殊类型的采样器变量,称为sampler2DShadow
(如前所述),可以附加到C++ / OpenGL应用程序中的阴影纹理。textureProj()
函数用于从阴影纹理中查找值,它类似于我们之前在第5章中看到的texture(),其区别是除了textureProj()函数使用vec3来索引纹理而不是通常的vec2。由于像素坐标是vec4,因此需要将其投影到2D纹理空间上,以便在阴影纹理贴图中查找深度值。正如我们将在下面看到的,textureProj()完成了这些功能。
顶点着色器和片段着色器代码的其余部分实现了Blinn-Phong着色。这些着色器如图8.6和图8.7所示,并增加了阴影贴图的代码。
让我们更仔细地研究一下如何使用OpenGL来执行正在渲染的像素和阴影纹理中的值之间的深度比较。首先,从顶点着色器开始,在模型空间中使用顶点坐标,我们将其与shadowMVP2相乘以生成阴影纹理坐标,这些坐标对应于投影到光照空间中的顶点坐标,是之前从光源的视角生成的。经过插值后的(3D)光照空间坐标(x,y,z)在片段着色器中使用如下。z分量表示从光到像素的距离。(x,y)分量用于检索存储在(2D)阴影纹理中的深度信息。将该检索的值(到最靠近光的物体的距离)与z进行比较。该比较产生“二元”结果,告诉我们正在渲染的像素是否比最接近光的物体离光更远(即像素是否处于阴影中)。
假设光源位置以视觉空间坐标表示。
与顶点着色器相同的结构体和统一变量。
如果我们在OpenGL中使用前面介绍过的glFrameBufferTexture()
并启用深度测试,然后使用片段着色器(见图8.7)的 sampler2DShadow
和textureProj()
,所渲染的结果将完全满足我们的 需求。即textureProj()
将输出0.0或1.0,具体取决于深度比较。基 此值,当像素离光源比离光源最近的物体更远时,我们可以在片段着色器中忽略漫反射和镜面反射分量,从而有效地创建阴影。概述如图 8.8所示。
我们现在准备构建C++ / OpenGL应用程序以使用上述着色器。
考虑图8.9中包含环面和金字塔的场景。位置光源放置在左侧(注意镜面高光)。
金字塔应该在环面上投下阴影。
为了阐明示例的开发,我们的第一步是将第1轮渲染到屏幕以确保它正常工作。为此,我们将临时添加一个简单的片段着色器(它不会包含在最终版本中)并在第1轮中仅输出一种固定颜色(如红色);例 如:
让我们假设场景的原点位于图的中心在金字塔和环面之间。在第1轮中,我们将相机放在光源的位置(图8.10中的左图)并指向(0,0,0)。然后我们用红色绘制对象,它会产生如图8.10(见彩插) 右图所示的输出。注意金字塔顶部附近的环面——这个制高点附近的环面部分位于金字塔后面。
包含光照与阴影贴图的完整第2轮C++/OpenGL代码见程序8.1。
vert1Shader.glsl
#version 430
layout (location=0) in vec3 vertPos;
uniform mat4 shadowMVP;
void main(void)
{ gl_Position = shadowMVP * vec4(vertPos,1.0);
}
vert2Shader.glsl
#version 430
layout (location=0) in vec3 vertPos;
layout (location=1) in vec3 vertNormal;
out vec3 vNormal, vLightDir, vVertPos, vHalfVec;
out vec4 shadow_coord;
struct PositionalLight
{ vec4 ambient, diffuse, specular;
vec3 position;
};
struct Material
{ vec4 ambient, diffuse, specular;
float shininess;
};
uniform vec4 globalAmbient;
uniform PositionalLight light;
uniform Material material;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;
uniform mat4 shadowMVP;
layout (binding=0) uniform sampler2DShadow shadowTex;
void main(void)
{ //output the vertex position to the rasterizer for interpolation
vVertPos = (mv_matrix * vec4(vertPos,1.0)).xyz;
//get a vector from the vertex to the light and output it to the rasterizer for interpolation
vLightDir = light.position - vVertPos;
//get a vertex normal vector in eye space and output it to the rasterizer for interpolation
vNormal = (norm_matrix * vec4(vertNormal,1.0)).xyz;
// calculate the half vector (L+V)
vHalfVec = (vLightDir-vVertPos).xyz;
shadow_coord = shadowMVP * vec4(vertPos,1.0);
gl_Position = proj_matrix * mv_matrix * vec4(vertPos,1.0);
}
frag1Shader.glsl
#version 430
void main(void) {}
frag2Shader.glsl
#version 430
in vec3 vNormal, vLightDir, vVertPos, vHalfVec;
in vec4 shadow_coord;
out vec4 fragColor;
struct PositionalLight
{ vec4 ambient, diffuse, specular;
vec3 position;
};
struct Material
{ vec4 ambient, diffuse, specular;
float shininess;
};
uniform vec4 globalAmbient;
uniform PositionalLight light;
uniform Material material;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;
uniform mat4 shadowMVP;
layout (binding=0) uniform sampler2DShadow shadowTex;
void main(void)
{ vec3 L = normalize(vLightDir);
vec3 N = normalize(vNormal);
vec3 V = normalize(-vVertPos);
vec3 H = normalize(vHalfVec);
float inShadow = textureProj(shadowTex, shadow_coord);
fragColor = globalAmbient * material.ambient
+ light.ambient * material.ambient;
if (inShadow != 0.0)
{ fragColor += light.diffuse * material.diffuse * max(dot(L,N),0.0)
+ light.specular * material.specular
* pow(max(dot(H,N),0.0),material.shininess*3.0);
}
}
程序8.1 阴影贴图
#include
#include
#include
#include
#include
#include
#include
#include
#include "Torus.h"
#include "Utils.h"
#include "ImportedModel.h"
using namespace std;
void passOne(void);
void passTwo(void);
float toRadians(float degrees) { return (degrees * 2.0f * 3.14159f) / 360.0f; }
#define numVAOs 1
#define numVBOs 5
GLuint renderingProgram1, renderingProgram2;
GLuint vao[numVAOs];
GLuint vbo[numVBOs];
ImportedModel pyramid("pyr.obj");// 定义金字塔
Torus myTorus(0.6f, 0.4f, 48);// 定义环面
int numPyramidVertices, numTorusVertices, numTorusIndices;
// 环面、金字塔、相机和光源的位置
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 };
// 金字塔的黄金材质
float* gMatAmb = Utils::goldAmbient();
float* gMatDif = Utils::goldDiffuse();
float* gMatSpe = Utils::goldSpecular();
float gMatShi = Utils::goldShininess();
// 环面的青铜材质
float* bMatAmb = Utils::bronzeAmbient();
float* bMatDif = Utils::bronzeDiffuse();
float* bMatSpe = Utils::bronzeSpecular();
float bMatShi = Utils::bronzeShininess();
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;
// 在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;
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) {
// 与之前的例子相同。这个函数用来创建VAO和VBO
// 之后将环面及金字塔的顶点与法向量读入缓冲区
numPyramidVertices = pyramid.getNumVertices();
std::vector<glm::vec3> vert = pyramid.getVertices();
std::vector<glm::vec3> norm = pyramid.getNormals();
std::vector<float> pyramidPvalues;
std::vector<float> pyramidNvalues;
for (int i = 0; i < numPyramidVertices; i++) {
pyramidPvalues.push_back((vert[i]).x);
pyramidPvalues.push_back((vert[i]).y);
pyramidPvalues.push_back((vert[i]).z);
pyramidNvalues.push_back((norm[i]).x);
pyramidNvalues.push_back((norm[i]).y);
pyramidNvalues.push_back((norm[i]).z);
}
numTorusVertices = myTorus.getNumVertices();
numTorusIndices = myTorus.getNumIndices();
std::vector<int> ind = myTorus.getIndices();
vert = myTorus.getVertices();
norm = myTorus.getNormals();
std::vector<float> torusPvalues;
std::vector<float> torusNvalues;
for (int i = 0; i < numTorusVertices; i++) {
torusPvalues.push_back(vert[i].x);
torusPvalues.push_back(vert[i].y);
torusPvalues.push_back(vert[i].z);
torusNvalues.push_back(norm[i].x);
torusNvalues.push_back(norm[i].y);
torusNvalues.push_back(norm[i].z);
}
glGenVertexArrays(1, vao);
glBindVertexArray(vao[0]);
glGenBuffers(numVBOs, vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBufferData(GL_ARRAY_BUFFER, torusPvalues.size() * 4, &torusPvalues[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBufferData(GL_ARRAY_BUFFER, pyramidPvalues.size() * 4, &pyramidPvalues[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);
glBufferData(GL_ARRAY_BUFFER, torusNvalues.size() * 4, &torusNvalues[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, vbo[3]);
glBufferData(GL_ARRAY_BUFFER, pyramidNvalues.size() * 4, &pyramidNvalues[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[4]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, ind.size() * 4, &ind[0], GL_STATIC_DRAW);
}
void setupShadowBuffers(GLFWwindow* window) {
glfwGetFramebufferSize(window, &width, &height);
scSizeX = width;
scSizeY = height;
// 创建自定义帧缓冲区
glGenFramebuffers(1, &shadowBuffer);
// 创建阴影纹理并让它存储深度信息
glGenTextures(1, &shadowTex);
glBindTexture(GL_TEXTURE_2D, shadowTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32,
scSizeX, scSizeY, 0, GL_DEPTH_COMPONENT, GL_FLOAT, 0);
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_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
// may reduce shadow border artifacts
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
void init(GLFWwindow* window) {
renderingProgram1 = Utils::createShaderProgram("./vert1Shader.glsl", "./frag1Shader.glsl");
renderingProgram2 = Utils::createShaderProgram("./vert2Shader.glsl", "./frag2Shader.glsl");
glfwGetFramebufferSize(window, &width, &height);
aspect = (float)width / (float)height;
pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
setupVertices();
setupShadowBuffers(window);
b = glm::mat4(
0.5f, 0.0f, 0.0f, 0.0f,
0.0f, 0.5f, 0.0f, 0.0f,
0.0f, 0.0f, 0.5f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f);
}
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// display()函数分别管理第1轮需要使用的自定义帧缓冲区
// 以及第2轮需要使用的阴影纹理初始化过程。阴影相关新功能已高亮
void display(GLFWwindow* window, double currentTime) {
glClear(GL_DEPTH_BUFFER_BIT);
glClear(GL_COLOR_BUFFER_BIT);
currentLightPos = glm::vec3(lightLoc);
// 从光源视角初始化视觉矩阵以及透视矩阵,以便在第1轮中使用
lightVmatrix = glm::lookAt(currentLightPos, origin, up);// 从光源到原点的矩阵
lightPmatrix = glm::perspective(toRadians(60.0f), aspect, 0.1f, 1000.0f);
// 使用自定义帧缓冲区,将阴影纹理附着到其上
glBindFramebuffer(GL_FRAMEBUFFER, shadowBuffer);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, shadowTex, 0);
// 关闭绘制颜色,同时开启深度计算
glDrawBuffer(GL_NONE);
glEnable(GL_DEPTH_TEST);
glEnable(GL_POLYGON_OFFSET_FILL); // for reducing
glPolygonOffset(2.0f, 4.0f); // shadow artifacts
passOne();
// 使用显示缓冲区,并重新开启绘制
glDisable(GL_POLYGON_OFFSET_FILL); // artifact reduction, continued
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, shadowTex);
glDrawBuffer(GL_FRONT);// 重新开启绘制颜色
passTwo();
}
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// 接下来是第1轮和第2轮的代码
// 这些代码和之前的大体相同
// 与阴影相关的新增代码已高亮
void passOne(void) {
// renderingProgram1包含了第1轮中的顶点着色器和片段着色器
glUseProgram(renderingProgram1);
// draw the torus
// 接下来的代码段通过从光源角度渲染环面获得深度缓冲区
mMat = glm::translate(glm::mat4(1.0f), torusLoc);
// 轻微旋转以便查看
mMat = glm::rotate(mMat, toRadians(25.0f), glm::vec3(1.0f, 0.0f, 0.0f));
// 我们从光源角度绘制,因此使用光源的P、V矩阵
shadowMVP1 = lightPmatrix * lightVmatrix * mMat;
sLoc = glGetUniformLocation(renderingProgram1, "shadowMVP");
glUniformMatrix4fv(sLoc, 1, GL_FALSE, glm::value_ptr(shadowMVP1));
// 在第1轮中我们只需要环面的顶点缓冲区,而不需要它的纹理或法向量
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glClear(GL_DEPTH_BUFFER_BIT);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[4]);// vbo[4]包含环面索引
glDrawElements(GL_TRIANGLES, numTorusIndices, GL_UNSIGNED_INT, 0);
// 对金字塔做同样的处理(但不清除GL_DEPTH_BUFFER_BIT)
// 金字塔没有索引,因此我们使用glDrawArrays()而非glDrawElements()
mMat = glm::translate(glm::mat4(1.0f), pyrLoc);
mMat = glm::rotate(mMat, toRadians(30.0f), glm::vec3(1.0f, 0.0f, 0.0f));
mMat = glm::rotate(mMat, toRadians(40.0f), glm::vec3(0.0f, 1.0f, 0.0f));
shadowMVP1 = lightPmatrix * lightVmatrix * mMat;
glUniformMatrix4fv(sLoc, 1, GL_FALSE, glm::value_ptr(shadowMVP1));
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glDrawArrays(GL_TRIANGLES, 0, numPyramidVertices);
}
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
void passTwo(void) {
glUseProgram(renderingProgram2);// 第2轮顶点着色器和片段着色器
// 绘制环面,这次我们需要加入光照、材质、法向量等
// 同时我们需要为相机空间以及光照空间都提供MVP变换
mvLoc = glGetUniformLocation(renderingProgram2, "mv_matrix");
projLoc = glGetUniformLocation(renderingProgram2, "proj_matrix");
nLoc = glGetUniformLocation(renderingProgram2, "norm_matrix");
sLoc = glGetUniformLocation(renderingProgram2, "shadowMVP");
// 环面是黄铜材质
thisAmb[0] = bMatAmb[0]; thisAmb[1] = bMatAmb[1]; thisAmb[2] = bMatAmb[2]; // bronze
thisDif[0] = bMatDif[0]; thisDif[1] = bMatDif[1]; thisDif[2] = bMatDif[2];
thisSpe[0] = bMatSpe[0]; thisSpe[1] = bMatSpe[1]; thisSpe[2] = bMatSpe[2];
thisShi = bMatShi;
vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraLoc.x, -cameraLoc.y, -cameraLoc.z));
// 轻微旋转以便查看
mMat = glm::translate(glm::mat4(1.0f), torusLoc);
mMat = glm::rotate(mMat, toRadians(25.0f), glm::vec3(1.0f, 0.0f, 0.0f));
currentLightPos = glm::vec3(lightLoc);
installLights(renderingProgram2, vMat);
// 构建相机视角环面的MV矩阵
mvMat = vMat * mMat;
invTrMat = glm::transpose(glm::inverse(mvMat));
// 构建光源视角环面的MV矩阵
shadowMVP2 = b * lightPmatrix * lightVmatrix * mMat;
glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
glUniformMatrix4fv(nLoc, 1, GL_FALSE, glm::value_ptr(invTrMat));
glUniformMatrix4fv(sLoc, 1, GL_FALSE, glm::value_ptr(shadowMVP2));
// 初始化环面顶点和法向量缓冲区()
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);// 环面顶点
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);// 环面法向量
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(1);
glClear(GL_DEPTH_BUFFER_BIT);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[4]);
glDrawElements(GL_TRIANGLES, numTorusIndices, GL_UNSIGNED_INT, 0);
// 对黄金金字塔重复同样步骤
// draw the pyramid
thisAmb[0] = gMatAmb[0]; thisAmb[1] = gMatAmb[1]; thisAmb[2] = gMatAmb[2]; // gold
thisDif[0] = gMatDif[0]; thisDif[1] = gMatDif[1]; thisDif[2] = gMatDif[2];
thisSpe[0] = gMatSpe[0]; thisSpe[1] = gMatSpe[1]; thisSpe[2] = gMatSpe[2];
thisShi = gMatShi;
mMat = glm::translate(glm::mat4(1.0f), pyrLoc);
mMat = glm::rotate(mMat, toRadians(30.0f), glm::vec3(1.0f, 0.0f, 0.0f));
mMat = glm::rotate(mMat, toRadians(40.0f), glm::vec3(0.0f, 1.0f, 0.0f));
currentLightPos = glm::vec3(lightLoc);
installLights(renderingProgram2, vMat);
mvMat = vMat * mMat;
invTrMat = glm::transpose(glm::inverse(mvMat));
shadowMVP2 = b * lightPmatrix * lightVmatrix * mMat;
glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
glUniformMatrix4fv(nLoc, 1, GL_FALSE, glm::value_ptr(invTrMat));
glUniformMatrix4fv(sLoc, 1, GL_FALSE, glm::value_ptr(shadowMVP2));
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vbo[3]);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(1);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glDrawArrays(GL_TRIANGLES, 0, numPyramidVertices);
}
void window_size_callback(GLFWwindow* win, int newWidth, int newHeight) {
aspect = (float)newWidth / (float)newHeight;
glViewport(0, 0, newWidth, newHeight);
pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
}
int main(void) {
if (!glfwInit()) { exit(EXIT_FAILURE); }
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
GLFWwindow* window = glfwCreateWindow(800, 800, "Chapter8 - program1", NULL, NULL);
glfwMakeContextCurrent(window);
if (glewInit() != GLEW_OK) { exit(EXIT_FAILURE); }
glfwSwapInterval(1);
glfwSetWindowSizeCallback(window, window_size_callback);
init(window);
while (!glfwWindowShouldClose(window)) {
display(window, glfwGetTime());
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_SUCCESS);
}
程序8.1展示了与之前详述过的第1轮、第2轮着色器交互部分的 C++ / OpenGL应用程序。之前已经展示过的模块,如读取着色器、编译着色器、构建模型及相关缓冲区、在着色器中初始化位置光源ADS特性以及进行透视矩阵和LookAt矩阵计算等,这些模块同之前一样。
虽然我们已经实现了为场景添加阴影的所有基本要求,但运行程序8.1会产生错杂的结果,如图8.11所示。
好消息是我们的金字塔现在在环面上投下阴影!坏消息则是,这种成功伴随着严重的伪影。有许多波浪线覆盖在场景中的表面。这是阴影贴图的常见副作用,称为阴影痤疮(也称为阴影斑块,shadow acne)或错误的自阴影。
阴影痤疮是由深度测试期间的舍入误差引起的。在阴影纹理中查找深度信息时计算的纹理坐标通常与实际坐标不完全匹配。因此,从阴影纹理中查找到的深度值可能并非当前渲染中像素的深度,而是相邻像素的深度。如果相邻像素在更远位置,则当前像素会被错误地显示为阴影。
阴影痤疮也会由纹理贴图和深度计算之间的精度差引起。这也可能导致舍入误差,并造成对像素是否处于阴影中的误判。
幸运的是,阴影痤疮很容易修复。由于阴影痤疮通常发生在没有阴影的表面上,这里有个简单的技巧,在第1轮中将每个像素稍微移向光源,之后在第2轮将它们移回原位。通常,这么做足以补偿各类舍入误差。在我们的实现中简单地在display()函数中调用glPolygonOffset()
即可,如图8.12所示(突出显示部分)。
将这几行代码添加到display()函数,可以显著改善程序的输出,如图8.13所示。还要注意,随着伪影的消失,现在可以看到环面的内圆在其自身上显示了一个正确的小阴影。
虽然修复阴影痤疮很容易,但有时修复会引起新的伪影。在第1轮之前移动对象的“技巧”有时会导致在对象阴影中出现间隙。图8.14显示了一个这样的例子。这种伪影通常被称为“Peter Panning”,因 为有时它会导致静止物体的阴影与物体底部分离的问题(从而使物体的阴影部分与阴影的其余部分分离,让人想起詹姆斯·马修·巴利笔下的角色Peter Pan[PP16])。修复此伪影需要调整glPolygonOffset() 的参数。如果它们太小,就会出现阴影痤疮;如果太大,则会出现 Peter Panning。
在实现阴影贴图时可能会发生许多其他伪影。如重复阴影,因为在第1轮(存入阴影缓冲区时)渲染的场景区域与第2轮中渲染的场景区域不同(来自不同的观察位置)。这种差异可能导致在第2轮中渲染的场景中,某些区域尝试使用范围[0…1]之外的特征坐标来访问阴影纹理。回想一下,在这种情况下默认行为是GL_REPEAT,因此,这可能 导致错误的重复阴影。
一种可能的解决方案是将以下代码行添加到setupShadowBuffers(),将纹理换行模式设置为“夹紧到边缘”。
这样纹理边缘以外的值会被限制为边缘处的值(而非重复)。注意,这种方法自身也有可能造成伪影,即当阴影纹理的边缘处存在阴影时,截取边缘可能产生延伸到场景边缘的“阴影条”。
另一种常见错误是锯齿状阴影边缘。当投射的阴影明显大于阴影缓冲区可以准确表示的阴影时,就有可能出问题。通常,这取决于场景中物体和灯光的位置。尤其当光源在距离物体较远时,更容易发生。一个例子如图8.15所示。
消除锯齿状阴影边缘就没有处理之前的伪影那么简单了。一种技术是在第1轮期间将光位置移动到更接近场景的位置,然后在第2轮放回原始位置。另一种常用的有效方法则是我们将在下面讨论的“柔和阴影”方法之一。
目前我们所展示的阴影生成方法都仅限于生成硬阴影,即带锐边的阴影。但是,现实世界中出现的大多数阴影都是柔和阴影。它们的边缘都会发生不同程度的模糊。在本节中,我们将探讨现实世界中柔 和阴影的外观,然后描述在OpenGL中模拟它们的常用算法。消除锯齿 状阴影边缘并不像处理之前的伪影那么简单。
柔和阴影的成因有很多,同时也有许多类型的柔和阴影。通常在自然界中产生柔和阴影原因是,真实世界的光源很少是点光源——它们常常是区域光源。另一个原因是材料和表面的缺陷积累,以及物体本身通过其自身的反射特性产生环境光的作用。
图8.16展示了物体向桌面投射柔和阴影的照片示例。注意,这不 是计算机渲染的3D场景,而是真实的照片,是本书作者之一在家中拍摄的。
对于图8.16中的阴影,有两点需要注意。
光源本身的维度会导致柔和阴影。如图8.17所示,光源上各处会投射出略微不同的阴影。各种阴影不同的区域称为半影(penumbra),包括阴影边缘的柔和区域。
有多种方法可以用来模拟半影效果以在软件中生成柔和阴影。最简单也最常见的一种方法叫作百分比邻近滤波(Percentage Closer Filtering,PCF)。在PCF中,我们对单个点周围的几个位置的阴影纹 理进行采样,以估计附近位置在阴影中的百分比。根据附近位置在阴 影中的数量,对正在渲染的像素的光照分量进行修改。整个计算可以在片段着色器中完成,所以我们只需要对其中的代码进行修改。PCF还 可用于减少锯齿线伪影。
在研究实际的PCF算法之前,我们先看一个类似的简单示例来展示 PCF的目标。考虑图8.18中所示的输出片段(像素)集,其颜色由片段着色器计算。
假设深色像素处于阴影中,这是阴影贴图计算的结果。假设我们可以访问相邻的像素信息,而不是简单地如图所示渲染像素(即包括或不包括漫反射和镜面反射分量),这样我们就可以看到有多少相邻像素处于阴影中。例如,考虑图8.19(见彩插)中以黄色突出显示的特定像素,根据图8.18,该像素不在阴影中。
在高亮像素的9个像素邻域中,3个像素处于阴影中而6个像素处于阴影外。因此,渲染像素的颜色可以被计算为该像素处的环境光分量 加上漫反射和镜面反射分量的9/6,这样会使像素一定程度(但不是完 全)变亮。在整个网格中重复此过程将会产生图8.20所示的像素颜色。注意,对于那些邻域完全位于阴影中(或阴影外)的像素,生成的颜色与标准阴影贴图相同。
与上例不同的是,在PCF的实现中,不是对渲染像素临近区域内的每个像素进行采样。这有两个原因:
(a)我们想在片段着色器中执行此计算,但片段着色器无法访问其他像素;
(b)获得足够宽的半影效果(例如,10~20像素宽)将需要为每个被渲染的像素采样数百个附近的像素。
PCF解决了以下两个问题。首先,我们不试图访问附近的像素,而 是在阴影贴图中对附近的纹素进行采样。片段着色器可以执行此操作,因为虽然它无法访问附近像素的值,但它可以访问整个阴影贴图。其次,为了获得足够宽的半影效果,需要对附近一定数量的阴影贴图纹素进行采样,每个采样的纹素都距离所渲染像素的纹素一定距离。
半影的宽度和采样点数可以根据场景和性能要求调整。例如,图 8.21所示PCF生成的图像是,每个像素的亮度是通过对64个不同的纹素 进行采样确定的,它们与像素的纹素距离各不相同。
柔和阴影的准确度或平滑度取决于所采样附近纹素的数量。因此,在性能和质量之间需要权衡——采样点越多,效果越好,但计算开销也越多。场景的复杂性和给定应用所需的帧率对于阴影可实现的 质量有着相应的限制。每像素采样64个点(如图8.21所示)通常是不切实际的。
一种用于实现PCF的常见算法是对每个像素附近的4个纹素进行采样,其中样本通过指定从像素对应纹素的偏移量选择。对于每个像素,我们都需要改变偏移量,并用新的偏移量确定采样的4个纹素。用交错方式改变偏移量的方法被称为抖动,它旨在使得柔和阴影边界不会由于采样点不足看起来“结块”。
一种常见的方法是假设有4种不同偏移模式,每次取其中一种——我们可以通过计算像素的glFragCoord mod 2
来选择当前像素的偏移模式。之前有提到,glFragCoord
是vec2类型,包含像素位置的x,y坐标。因此,mod计算的结果有4种可能的值:(0,0)、(0,1)、(1,0)或 (1,1)。我们使用glFragCoord mod 2
的结果来从纹素空间(即阴影贴图)4种不同偏移模式中选择一种。
偏移模式通常在x和y方向上指定,具有−1.5,−0.5,+0.5和+1.5的不同组合(也可以根据需要进行缩放)。更具体来说,由 glFragCoord mod 2
计算得到的每种情况的4种常用偏移模式是:
Sx和Sy指的是与正在渲染的像素相对应的阴影贴图中的位置( Sx,Sy),在本章的代码示例中标识为shadow_coord。这4种偏移模式如图8.22所示(见彩插),每种情况都以不同的颜色显示。在每种情况下,对应于正被渲染的像素的纹素位于该情况的图的原点。请注意,当在图8.23(见彩插)中一起显示时,偏移的交错/抖动很明显。
让我们来针对特定像素看看整个计算过程。假设正在渲染的像素位于glFragCoord =(48,13)。首先我们确定像素在阴影贴图的4个采样点。为此,我们将计算vec2(48,13) mod 2,等于(0,1)。因此我们选择(0,1)所对应的偏移,在图8.22中以绿色显示,并且在阴影贴图对相应的点进行采样(假设没有指定偏移的缩放量),得到:
(shadow_coord.x–1.5, shadow_coord.y+0.5)
(shadow_coord.x–1.5, shadow_coord.y–1.5)
(shadow_coord.x+0.5, shadow_coord.y+0.5)
(shadow_coord.x+0.5, shadow_coord.y–1.5)
(回想一下,shadow_coord是阴影贴图中与正在渲染的像素相对 应的纹素的位置——在图8.22和图8.23中显示为白色圆圈)。
接下来,对我们选取的这4个点分别调用textureProj(),在每种情况下都返回0.0或1.0,具体取决于该采样点是否在阴影中。将4个结果相加并除以4.0,就可以确定阴影中采样点的百分比。然后将此百分比用作乘数,确定渲染当前像素时要应用的漫反射和镜面反射分量。
尽管采样尺寸很小——每个像素只有4个样本——这种抖动方法通常可以产生好得惊人的柔和阴影。图8.24是使用4像素抖动PCF生成的。虽然它不如之前图8.21所示的64点采样版本好,但渲染速度要快得多。
在下一节中,我们对GLSL片段着色器进行编码,实现4采样抖动的 PCF柔和阴影以及之前展示的64采样PCF柔和阴影。
如前所述,柔和阴影计算可以完全在片段着色器中完成。程序8.2 展示了片段着色器代码,取代图8.7中的片段着色器。添加的PCF相关代码已突出显示。
程序8.2 百分比邻近滤波(PCF)
vert1shader.glsl
#version 430
layout (location=0) in vec3 vertPos;
uniform mat4 shadowMVP;
void main(void)
{ gl_Position = shadowMVP * vec4(vertPos,1.0);
}
vert2shader.glsl
#version 430
layout (location=0) in vec3 vertPos;
layout (location=1) in vec3 vertNormal;
out vec3 varyingNormal, varyingLightDir, varyingVertPos, varyingHalfVec;
out vec4 shadow_coord;
struct PositionalLight
{ vec4 ambient, diffuse, specular;
vec3 position;
};
struct Material
{ vec4 ambient, diffuse, specular;
float shininess;
};
uniform vec4 globalAmbient;
uniform PositionalLight light;
uniform Material material;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;
uniform mat4 shadowMVP;
layout (binding=0) uniform sampler2DShadow shadowTex;
void main(void)
{ //output the vertex position to the rasterizer for interpolation
varyingVertPos = (mv_matrix * vec4(vertPos,1.0)).xyz;
//get a vector from the vertex to the light and output it to the rasterizer for interpolation
varyingLightDir = light.position - varyingVertPos;
//get a vertex normal vector in eye space and output it to the rasterizer for interpolation
varyingNormal = (norm_matrix * vec4(vertNormal,1.0)).xyz;
// calculate the half vector (L+V)
varyingHalfVec = (varyingLightDir-varyingVertPos).xyz;
shadow_coord = shadowMVP * vec4(vertPos,1.0);
gl_Position = proj_matrix * mv_matrix * vec4(vertPos,1.0);
}
frag1shader.glsl
#version 430
void main(void) {}
frag2shader.glsl
#version 430
in vec3 varyingNormal, varyingLightDir, varyingVertPos, varyingHalfVec;
in vec4 shadow_coord;
out vec4 fragColor;
struct PositionalLight
{ vec4 ambient, diffuse, specular;
vec3 position;
};
struct Material
{ vec4 ambient, diffuse, specular;
float shininess;
};
uniform vec4 globalAmbient;
uniform PositionalLight light;
uniform Material material;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;
uniform mat4 shadowMVP;
layout (binding=0) uniform sampler2DShadow shadowTex;
// 从shadow_coord返回距离(x,y)处的纹素的阴影深度值
// shadow_coord是阴影贴图中与正在渲染的当前像素相对应的位置
float lookup(float x, float y)
{ float t = textureProj(shadowTex, shadow_coord + vec4(x * 0.001 * shadow_coord.w,
y * 0.001 * shadow_coord.w,
-0.01, 0.0));//第三个参数(-0.01)是用于消除阴影痤疮的偏移量
return t;
}
void main(void)
{ float shadowFactor=0.0;
vec3 L = normalize(varyingLightDir);
vec3 N = normalize(varyingNormal);
vec3 V = normalize(-varyingVertPos);
vec3 H = normalize(varyingHalfVec);
// -----此部分生成一个4采样抖动的柔和阴影
float swidth = 2.5;//可调整的阴影扩散量
vec2 o = mod(floor(gl_FragCoord.xy), 2.0) * swidth;
shadowFactor += lookup(-1.5*swidth + o.x, 1.5*swidth - o.y);
shadowFactor += lookup(-1.5*swidth + o.x, -0.5*swidth - o.y);
shadowFactor += lookup( 0.5*swidth + o.x, 1.5*swidth - o.y);
shadowFactor += lookup( 0.5*swidth + o.x, -0.5*swidth - o.y);
shadowFactor = shadowFactor / 4.0;// shadowFactor是4个采样点的平均值
// // ----- 取消本节注释以生成64采样的高分辨率柔和阴影
/* float width = 2.5;// 可调整的阴影扩散量
float endp = width * 3.0 + width/2.0;
for (float m=-endp ; m<=endp ; m=m+width)
{ for (float n=-endp ; n<=endp ; n=n+width)
{ shadowFactor += lookup(m,n);
} }
shadowFactor = shadowFactor / 64.0;
*/
// this would produce normal hard shadows
// shadowFactor = lookup(0.0, 0.0);
vec4 shadowColor = globalAmbient * material.ambient
+ light.ambient * material.ambient;
vec4 lightedColor = light.diffuse * material.diffuse * max(dot(L,N),0.0)
+ light.specular * material.specular
* pow(max(dot(H,N),0.0),material.shininess*3.0);
fragColor = vec4((shadowColor.xyz + shadowFactor*(lightedColor.xyz)),1.0);
}
main.cpp
#include
#include
#include
#include
#include
#include
#include
#include
#include "Torus.h"
#include "Utils.h"
#include "ImportedModel.h"
using namespace std;
void passOne(void);
void passTwo(void);
float toRadians(float degrees) { return (degrees * 2.0f * 3.14159f) / 360.0f; }
#define numVAOs 1
#define numVBOs 5
GLuint renderingProgram1, renderingProgram2;
GLuint vao[numVAOs];
GLuint vbo[numVBOs];
ImportedModel pyramid("pyr.obj");
Torus myTorus(0.6f, 0.4f, 48);
int numPyramidVertices, numTorusVertices, numTorusIndices;
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);
// white light
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 = Utils::goldAmbient();
float* gMatDif = Utils::goldDiffuse();
float* gMatSpe = Utils::goldSpecular();
float gMatShi = Utils::goldShininess();
// bronze material
float* bMatAmb = Utils::bronzeAmbient();
float* bMatDif = Utils::bronzeDiffuse();
float* bMatSpe = Utils::bronzeSpecular();
float bMatShi = Utils::bronzeShininess();
float thisAmb[4], thisDif[4], thisSpe[4], matAmb[4], matDif[4], matSpe[4];
float thisShi, matShi;
// shadow stuff
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;
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) {
// pyramid definition
numPyramidVertices = pyramid.getNumVertices();
std::vector<glm::vec3> vert = pyramid.getVertices();
std::vector<glm::vec3> norm = pyramid.getNormals();
std::vector<float> pyramidPvalues;
std::vector<float> pyramidNvalues;
for (int i = 0; i < numPyramidVertices; i++) {
pyramidPvalues.push_back((vert[i]).x);
pyramidPvalues.push_back((vert[i]).y);
pyramidPvalues.push_back((vert[i]).z);
pyramidNvalues.push_back((norm[i]).x);
pyramidNvalues.push_back((norm[i]).y);
pyramidNvalues.push_back((norm[i]).z);
}
// torus definition
numTorusVertices = myTorus.getNumVertices();
numTorusIndices = myTorus.getNumIndices();
std::vector<int> ind = myTorus.getIndices();
vert = myTorus.getVertices();
norm = myTorus.getNormals();
std::vector<float> torusPvalues;
std::vector<float> torusNvalues;
for (int i = 0; i < numTorusVertices; i++) {
torusPvalues.push_back(vert[i].x);
torusPvalues.push_back(vert[i].y);
torusPvalues.push_back(vert[i].z);
torusNvalues.push_back(norm[i].x);
torusNvalues.push_back(norm[i].y);
torusNvalues.push_back(norm[i].z);
}
glGenVertexArrays(1, vao);
glBindVertexArray(vao[0]);
glGenBuffers(numVBOs, vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBufferData(GL_ARRAY_BUFFER, torusPvalues.size() * 4, &torusPvalues[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBufferData(GL_ARRAY_BUFFER, pyramidPvalues.size() * 4, &pyramidPvalues[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);
glBufferData(GL_ARRAY_BUFFER, torusNvalues.size() * 4, &torusNvalues[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, vbo[3]);
glBufferData(GL_ARRAY_BUFFER, pyramidNvalues.size() * 4, &pyramidNvalues[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[4]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, ind.size() * 4, &ind[0], GL_STATIC_DRAW);
}
void setupShadowBuffers(GLFWwindow* window) {
glfwGetFramebufferSize(window, &width, &height);
scSizeX = width;
scSizeY = height;
glGenFramebuffers(1, &shadowBuffer);
glGenTextures(1, &shadowTex);
glBindTexture(GL_TEXTURE_2D, shadowTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32,
scSizeX, scSizeY, 0, GL_DEPTH_COMPONENT, GL_FLOAT, 0);
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_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
// may reduce shadow border artifacts
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
void init(GLFWwindow* window) {
renderingProgram1 = Utils::createShaderProgram("./vert1Shader.glsl", "./frag1Shader.glsl");
renderingProgram2 = Utils::createShaderProgram("./vert2Shader.glsl", "./frag2Shader.glsl");
glfwGetFramebufferSize(window, &width, &height);
aspect = (float)width / (float)height;
pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
setupVertices();
setupShadowBuffers(window);
b = glm::mat4(
0.5f, 0.0f, 0.0f, 0.0f,
0.0f, 0.5f, 0.0f, 0.0f,
0.0f, 0.0f, 0.5f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f);
}
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
void display(GLFWwindow* window, double currentTime) {
glClear(GL_DEPTH_BUFFER_BIT);
glClear(GL_COLOR_BUFFER_BIT);
currentLightPos = glm::vec3(lightLoc);
lightVmatrix = glm::lookAt(currentLightPos, origin, up);
lightPmatrix = glm::perspective(toRadians(60.0f), aspect, 0.1f, 1000.0f);
glBindFramebuffer(GL_FRAMEBUFFER, shadowBuffer);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, shadowTex, 0);
glDrawBuffer(GL_NONE);
glEnable(GL_DEPTH_TEST);
glEnable(GL_POLYGON_OFFSET_FILL); // for reducing
glPolygonOffset(2.0f, 4.0f); // shadow artifacts
passOne();
glDisable(GL_POLYGON_OFFSET_FILL); // artifact reduction, continued
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, shadowTex);
glDrawBuffer(GL_FRONT);
passTwo();
}
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
void passOne(void) {
glUseProgram(renderingProgram1);
// draw the torus
mMat = glm::translate(glm::mat4(1.0f), torusLoc);
mMat = glm::rotate(mMat, toRadians(25.0f), glm::vec3(1.0f, 0.0f, 0.0f));
shadowMVP1 = lightPmatrix * lightVmatrix * mMat;
sLoc = glGetUniformLocation(renderingProgram1, "shadowMVP");
glUniformMatrix4fv(sLoc, 1, GL_FALSE, glm::value_ptr(shadowMVP1));
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glClear(GL_DEPTH_BUFFER_BIT);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[4]);
glDrawElements(GL_TRIANGLES, numTorusIndices, GL_UNSIGNED_INT, 0);
// draw the pyramid
mMat = glm::translate(glm::mat4(1.0f), pyrLoc);
mMat = glm::rotate(mMat, toRadians(30.0f), glm::vec3(1.0f, 0.0f, 0.0f));
mMat = glm::rotate(mMat, toRadians(40.0f), glm::vec3(0.0f, 1.0f, 0.0f));
shadowMVP1 = lightPmatrix * lightVmatrix * mMat;
glUniformMatrix4fv(sLoc, 1, GL_FALSE, glm::value_ptr(shadowMVP1));
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glDrawArrays(GL_TRIANGLES, 0, numPyramidVertices);
}
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
void passTwo(void) {
glUseProgram(renderingProgram2);
mvLoc = glGetUniformLocation(renderingProgram2, "mv_matrix");
projLoc = glGetUniformLocation(renderingProgram2, "proj_matrix");
nLoc = glGetUniformLocation(renderingProgram2, "norm_matrix");
sLoc = glGetUniformLocation(renderingProgram2, "shadowMVP");
// draw the torus
thisAmb[0] = bMatAmb[0]; thisAmb[1] = bMatAmb[1]; thisAmb[2] = bMatAmb[2]; // bronze
thisDif[0] = bMatDif[0]; thisDif[1] = bMatDif[1]; thisDif[2] = bMatDif[2];
thisSpe[0] = bMatSpe[0]; thisSpe[1] = bMatSpe[1]; thisSpe[2] = bMatSpe[2];
thisShi = bMatShi;
vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraLoc.x, -cameraLoc.y, -cameraLoc.z));
mMat = glm::translate(glm::mat4(1.0f), torusLoc);
mMat = glm::rotate(mMat, toRadians(25.0f), glm::vec3(1.0f, 0.0f, 0.0f));
currentLightPos = glm::vec3(lightLoc);
installLights(renderingProgram2, vMat);
mvMat = vMat * mMat;
invTrMat = glm::transpose(glm::inverse(mvMat));
shadowMVP2 = b * lightPmatrix * lightVmatrix * mMat;
glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
glUniformMatrix4fv(nLoc, 1, GL_FALSE, glm::value_ptr(invTrMat));
glUniformMatrix4fv(sLoc, 1, GL_FALSE, glm::value_ptr(shadowMVP2));
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(1);
glClear(GL_DEPTH_BUFFER_BIT);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[4]);
glDrawElements(GL_TRIANGLES, numTorusIndices, GL_UNSIGNED_INT, 0);
// draw the pyramid
thisAmb[0] = gMatAmb[0]; thisAmb[1] = gMatAmb[1]; thisAmb[2] = gMatAmb[2]; // gold
thisDif[0] = gMatDif[0]; thisDif[1] = gMatDif[1]; thisDif[2] = gMatDif[2];
thisSpe[0] = gMatSpe[0]; thisSpe[1] = gMatSpe[1]; thisSpe[2] = gMatSpe[2];
thisShi = gMatShi;
mMat = glm::translate(glm::mat4(1.0f), pyrLoc);
mMat = glm::rotate(mMat, toRadians(30.0f), glm::vec3(1.0f, 0.0f, 0.0f));
mMat = glm::rotate(mMat, toRadians(40.0f), glm::vec3(0.0f, 1.0f, 0.0f));
currentLightPos = glm::vec3(lightLoc);
installLights(renderingProgram2, vMat);
mvMat = vMat * mMat;
invTrMat = glm::transpose(glm::inverse(mvMat));
shadowMVP2 = b * lightPmatrix * lightVmatrix * mMat;
glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
glUniformMatrix4fv(nLoc, 1, GL_FALSE, glm::value_ptr(invTrMat));
glUniformMatrix4fv(sLoc, 1, GL_FALSE, glm::value_ptr(shadowMVP2));
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vbo[3]);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(1);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glDrawArrays(GL_TRIANGLES, 0, numPyramidVertices);
}
void window_size_callback(GLFWwindow* win, int newWidth, int newHeight) {
aspect = (float)newWidth / (float)newHeight;
glViewport(0, 0, newWidth, newHeight);
pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
}
int main(void) {
if (!glfwInit()) { exit(EXIT_FAILURE); }
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
GLFWwindow* window = glfwCreateWindow(800, 800, "Chapter8 - program2", NULL, NULL);
glfwMakeContextCurrent(window);
if (glewInit() != GLEW_OK) { exit(EXIT_FAILURE); }
glfwSwapInterval(1);
glfwSetWindowSizeCallback(window, window_size_callback);
init(window);
while (!glfwWindowShouldClose(window)) {
display(window, glfwGetTime());
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_SUCCESS);
}
程序8.2中展示的片段着色器包含4采样和64采样的PCF柔和阴影的代码。为了更方便进行采样,我们需要定义lookup()函数。在 lookup()函数中调用GLSL函数textureProj(),从而在阴影纹理中以指定偏移量(ox,oy)进行查找。偏移量需要乘以1 / windowsize,这里我们简单地假设窗口大小为1000像素×1000像素,将乘数硬编码为0.001。[2]
4样本抖动的计算代码在main()函数中高亮显示,其实现遵循一 节中描述的算法。同时添加了一个比例因子swidth,用于调整阴影边缘的“柔和”区域的大小。
64采样代码以注释形式出现在后面。可以通过取消64采样代码注 释并注释4采样代码以使用64采样。在64采样代码中,swidth比例因子用作嵌套循环中的步长,其采样距离正被渲染的像素的不同距离处的点。例如,当使用代码中的swidth值(2.5)时,程序将沿着每个轴在两个方向上以1.25、3.75、6.25和8.25的距离选择采样点——然后根据窗口大小进行缩放(如前所述)并用作纹理坐标采样阴影纹理。在这么多采样的情况下,通常不需要使用抖动来获得更好的结果。
图8.25展示了我们运行的环面/金字塔阴影贴图示例,它将PCF柔 和阴影与程序8.2中的片段着色器相结合,分别使用了4采样和64采样 的方法。swidth的选值取决于场景;对于环面/金字塔示例,它的值为2.5,而对于之前的图8.21中显示的海豚示例,swidth的值为8.0。
在本章中,我们仅给出了3D图形中阴影世界的最基本介绍。在更复杂的场景中,即便使用本章提供的基础阴影贴图方法,也可能需要进行进一步的研究。
例如,当场景中的某些对象拥有纹理的情况下,添加阴影时必须确保片段着色器正确区分阴影纹理和其他纹理。一种简单的方法是将它们绑定到不同的纹理单元,例如:
然后,C++ / OpenGL应用程序可以通过它们的绑定值来引用两个采样器。
当场景使用多个灯光时,则需要多个阴影纹理——每个光源需要一个阴影纹理。此外,每个光源都需要单独执行第1轮渲染,并在第2 轮渲染中合并结果。
尽管我们在阴影贴图的每个阶段都使用了透视投影,但值得注意的是,当光源是远距离光源和定向光源而非我们使用的位置光时,正射投影通常才是首选。
生成真实的阴影在计算机图形学中仍然是一个活跃而又复杂的领域,其中提出的许多技术超出了本书的范畴。我们鼓励对更多细节感兴趣的读者研究更专业的资源。
8.7.3小节包含一个GLSL函数的例子(除了“main”)。与在C语言中一样,必须在调用它们之前(或“上方”)定义函数,否则必须提供前向声明。在该示例中则不需要前向声明,因为函数定义在调用代码上方。