openGL系列文章目录


文章目录

前言

为了给3D 场景添加阴影,人们设计了许多有趣的方法。其中一种很适合在地平面上(如
图8.1 所示)绘制阴影,又相对不需要太大计算代价的方法,叫作投影阴影(projective
shadows)。给定一个位于(XL,YL,ZL)的点光源、一个需要渲染的物体以及一个投射阴影的
平面,可以通过生成一个变换矩阵,将物体上的点(XW,YW,ZW)变换为相应阴影在平面上的
点(XS,0,ZS)。之后将其生成的“阴影多边形”绘制出来,通常使用暗色物体与地平面纹理
混合作为其纹理,如图1所示。

openGL阴影实现(硬阴影)_2d
图1

使用投影阴影进行投射的优点是它的高效和易于实现。但是,它仅适用于平坦表面——
这种方法无法投射阴影于曲面或其他物体。即使如此,它仍然适用于有室外场景并对性能
要求较高的应用,很多游戏中的场景都属于这类。

一、阴影体

Franklin C. Crow 在1977 年提出了另一个重要的方法,这个方法先找到被物体阴影覆盖
的阴影体,之后减少视体与阴影体相交部分中的多边形的颜色强度。图2 展示了阴影体中
的立方体,因此,立方体绘制时会更暗。
阴影体的优点在于其高度准确,比起其他方法来更不容易产生伪影。但是,计算出阴影
体以及每个多边形是否在其中这件事,即使对于现代GPU 来说,计算代价也很大。几何着
色器可以用于计算阴影体,模板缓冲区①可以用于判断像素是否在阴影体内。有些显卡对于
特定的阴影体操作优化提供了硬件支持。
openGL阴影实现(硬阴影)_着色器_02
图2

二、阴影贴图

阴影贴图是用于投射阴影最实用也最流行的方法之一。虽然它并不总是像阴影体一样准
确(且通常伴随着讨厌的伪影),但阴影贴图实现起来更简单,可以在各种情况下使用,并
享有强大的硬件支持。
如果我们不在这里澄清前一段中的“更简单”这个词,那将是我们的疏忽。虽然阴影贴
图比阴影体(在概念和实践中)更简单,但它绝不“简单”!对学生来说,通常在3D 图形
课程中最难实现的技术之一就是阴影贴图。着色器程序本质上很难调试,阴影贴图需要几
个组件和着色器模块的完美协调
阴影贴图基于一个非常简明的想法:光线无法看到的任何东西都在阴影中。也就是说,
如果对象#1 阻挡光到达对象#2,等同于光不能“看到”对象#2。
这个想法的强大之处在于我们已经有了方法来确定物体是否可以被“看到”——使用Z
缓冲区的隐藏面消除算法(HSR),如2.1.7 节所述。因此,计算阴影的策略是,暂时将摄像
机移动到光的位置,应用Z 缓冲区HSR 算法,然后使用生成的深度信息来计算阴影。
因此,渲染场景需要两轮:第1 轮从灯光的角度渲染场景(但实际上没有将其绘制到屏
幕上),第2 轮从摄像机的角度渲染场景。第1 轮的目的是从光的角度生成Z 缓冲区。完成
第1 轮之后,我们需要保留Z 缓冲区并使用它来帮助我们在第2 轮生成阴影。第2 轮实际
绘制场景。
我们的策略可以更加精炼。
􀀠 (第1 轮)从灯光的位置渲染场景。然后,对于每个像素,深度缓冲区包含光与最
近的对象之间的距离。
􀀠 将深度缓冲区复制到单独的“阴影缓冲区”。
􀀠 (第2 轮)正常渲染场景。对于每个像素,在阴影缓冲区中查找相应的位置。如果
相机到渲染点的距离大于从阴影缓冲区检索到的值,则在该像素处绘制的对象离光
线的距离,比离光线最近的对象更远,因此该像素处于阴影中。
当发现像素处于阴影中时,我们需要使其更暗。一种简单而有效的方法是仅渲染其环境
光,忽略其漫反射和镜面反射分量。
上述方法通常被称为“阴影缓冲区”。而当我们在第二步中,将深度缓冲区复制到纹理
中,则称为“阴影贴图”。当纹理对象用于储存阴影深度信息时,我们称其为阴影纹理,
OpenGL 通过sampler2DShadow 类型支持阴影纹理(稍后讨论)。这样,我们就可以利用片
段着色器中纹理单元和采样器变量(即“纹理贴图”)的硬件支持功能,在第2 轮快速执行
深度查找。我们现在修改的策略是:
􀀠 (第1 轮)与之前相同;
􀀠 将深度缓冲区的内容复制进纹理对象;
􀀠 (第2 轮)与之前相同,不过阴影缓冲区变为阴影纹理。
现在我们来实现这些步骤。

阴影贴图(第1 轮)——从光源位置“绘制”物体

在第一步中,我们首先将相机移动到灯光的位置然后渲染场景。我们的目标不是在显示
器上实际绘制场景,而是完成足够的渲染过程以正确填充深度缓冲区。因此,没有必要为
像素生成颜色,我们的第一遍将仅使用顶点着色器,但片段着色器不执行任何操作。
当然,移动相机需要构建适当的观察矩阵。根据场景的内容,我们需要在光源处依合适
的方向来看场景。通常,我们希望此方向朝向最终在第2 轮中呈现的区域。
这个方向通常依场景而定——在我们的场景中,我们通常会将相机从光源指向原点。
第1 轮中有几个需要处理的重要细节。
􀀠 配置缓冲区和阴影纹理。
􀀠 禁用颜色输出。
􀀠 从光源到视野中的物体构建一个LookAt 矩阵。
􀀠 启用GLSL 第1 轮着色器程序,该程序仅包含图8.5 中的简单顶点着色器,准备接
收MVP 矩阵。在这种情况下,MVP 矩阵将包括对象的模型矩阵M、前一步中计算
的LookAt 矩阵(作为观察矩阵V),以及透视矩阵P。我们将该MVP 矩阵称为
“shadowMVP”,因为它是基于光而不是相机的观察点。由于实际上没有显示来自光
源的视图,因此第1 轮着色器程序的片段着色器不会执行任何操作。
openGL阴影实现(硬阴影)_openGL阴影带伪影_03
图3
􀀠 为每个对象创建shadowMVP 矩阵,并调用glDrawArrays()。第1 轮中不需要包含
纹理或光照,因为对象不会渲染到屏幕上。

阴影贴图(中间步骤)——将Z 缓冲区复制到纹理

OpenGL 提供了两种将Z 缓冲区深度数据放入纹理单元的方法。第一种方法是生成空阴
影纹理,然后使用命令glCopyTexImage2D()将活动深度缓冲区复制到阴影纹理中。
第二种方法是在第1 轮中构建一个“自定义帧缓冲区”(而不是使用默认的Z 缓冲区),
并使用命令glFrameBufferTexture()将阴影纹理附加到它上面。OpenGL 在3.0 版中引入该命
令,以进一步支持阴影纹理。使用这种方法时,无须将Z 缓冲区“复制”到纹理中,因为
缓冲区已经附加了纹理,深度信息由OpenGL 自动放入纹理中。我们将在实现中使用这种
方法。

阴影贴图(第2 轮)——渲染带阴影的场景

我们在这里渲染完整的场景及
其中的所有物体,以及光照、材质和装饰场景中物体的纹理。同时,我们还需要添加必要
的代码,以确定每个像素是否在阴影中。
第2 轮的一个重要特征是它使用了两个MVP 矩阵。一个是将对象坐标转换为屏幕坐标
的标准MVP 矩阵(如我们之前的大多数示例所示)。另一个是在第1 轮中生成的shadowMVP
矩阵,用于从光源的角度进行渲染——现在将在第2 轮中用于从阴影纹理中查找深度信息。
在第2 轮中,从纹理贴图尝试查找像素时,情况比较复杂。OpenGL 相机使用[−1…+ 1]
坐标空间,而纹理贴图使用[0…1]空间。常见的解决方案是构建一个额外的矩阵变换,通常
称为B,它将用于从摄像机空间到纹理空间的转换(或“偏离”,biases,因此名称)。得到
B 的过程很简单——先缩放为1/2,再平移1/2。
openGL阴影实现(硬阴影)_贴图_04
之后将B 合并入shadowMVP 矩阵以备在第2 轮中使用,如下:
( 1) shadowMVP2 [ ][shadowMVP ] pass = B
假设我们使用阴影纹理附加到我们的自定义帧缓冲区的方法,OpenGL 提供了一些相对
简单的工具,用于确定绘制对象时,像素是否处于阴影中。以下是第二阶段处理的详细信
息摘要。
􀀠 构建变换矩阵B,用于从光照空间转换到纹理空间[更合适在init()中进行]。
􀀠 启用阴影纹理以进行查找。
􀀠 启用颜色输出。
􀀠 启用GLSL 第2 轮渲染程序,包含顶点着色器和片段着色器。
􀀠 根据摄像机位置(正常)为正在绘制的对象构建MVP 矩阵。
􀀠 构建shadowMVP2 矩阵(包含B 矩阵,如前所述)——着色器将需要用它查找阴
影纹理中的像素坐标。
􀀠 将生成的矩阵变换发送到着色器统一变量。
􀀠 像往常一样启用包含顶点、法向量和纹理坐标(如果使用)的缓冲区。
􀀠 调用glDrawArrays()。
除了渲染任务外,顶点和片段着色器还需要额外承担一些任务。
􀀠 顶点着色器将顶点位置从相机空间转换为光照空间,并将结果坐标发送到顶点属性
中的片段着色器,以便对它们进行插值。这样片段着色器可以从阴影纹理中检索正
确的值。
􀀠 片段着色器调用textureProj()函数,该函数返回0 或1,指示像素是否处于阴影中(所
涉及的机制将在后面解释)。如果它在阴影中,则着色器通过剔除其漫反射和镜面
反射分量来输出更暗的像素。
阴影贴图是一种常见任务,因此GLSL 为其提供了一种特殊类型的采样器变量,称为
sampler2DShadow(如前所述),可以附加到C++ / OpenGL 应用程序中的阴影纹理。
textureProj()函数用于从阴影纹理中查找值,它类似于我们之前在第5 章中看到的texture(),
其区别是除了textureProj()函数使用vec3 来索引纹理而不是通常的vec2。由于像素坐标是
vec4,因此需要将其投影到2D 纹理空间上,以便在阴影纹理贴图中查找深度值。正如我们
将在下面看到的,textureProj()为完成了这些功能。
顶点着色器和片段着色器代码的其余部分实现了Blinn-Phong 着色。

代码

#include "glew/glew.h"
#include "glfw/glfw3.h"
#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"
#include "glm/gtc/type_ptr.hpp"
#include "camera.h"
#include "Utils.h"
#include "Torus.h"
#include "ImportedModel.h"
#include <iostream>
#include <string>
#include <fstream>

using namespace std;

void passOne(void);
void passTwo(void);



static const int screen_width = 1920;
static const int screen_height = 1080;

static const float pai = 3.14159265f;
float toRadins(float degree) { return (degree * 2.f * pai) / (float)360.f; }

static const int numVAOs = 1;
static const int numVBOs = 5;
GLuint vao[numVAOs] = { 0 };
GLuint vbo[numVBOs] = { 0 };

GLuint renderingProgram1 = 0, renderingProgram2 = 0;

ImportedModel pyramid("pyr.obj");
Camera myCamera(glm::vec3(0.f, 2.f, 3.f));
Torus myTorus(0.6f, 0.4f, 48);
int numPyramidVertices = 0, numTorusVertices = 0, numTorusIndices = 0;

glm::vec3 torusLoc(1.6f, 0.f, -0.3f);
glm::vec3 pyrLoc(-1.f, 0.1f, 0.3f);
glm::vec3 cameraLoc(0.f, 0.2f, 6.0f);
glm::vec3 lightLoc(-3.8f, 2.2f, 1.1f);

float amt = 0.f;

// white light
float globalAmbient[4] = { 0.7f, 0.7f, 0.7f, 1.f };
float lightAmbient[4] = { 0.f, 0.f, 0.f, 1.f };
float lightDiffuse[4] = { 1.0f };
float lightSpecular[4] = { 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] = { 0.f }, thisDif[4] = { 0.f }, thisSpe[4] = { 0.f }, matAmb[4] = { 0.f }, matDif[4] = { 0.f }, matSpe[4] = { 0.f };
float thisShi = 0.f, matShi = 0.f;

// shadow stuff
int scSizeX = 0, scSizeY = 0;
GLuint shadowTex = 0, shadowBuffer = 0;
glm::mat4 lightVMatrix(1.f);
glm::mat4 lightPMatrix(1.f);
glm::mat4 shadowMVP1(1.f);
glm::mat4 shadowMVP2(1.f);
glm::mat4 b(1.f);

// variable allocation for display
GLuint mvLoc = 0, projLoc = 0, nLoc = 0, sLoc = 0;
int width = 0, height = 0;
float aspect = 0.f;
glm::mat4 pMat(1.f), vMat(1.f), mMat(1.f), mvMat(1.f), invTrMat(1.f), rMat(1.f);
glm::vec3 currentLightPos(0.f), transformed(0.f);
float lightPos[3] = { 0.f };
GLuint globalAmbLoc(0), ambLoc(0), difLoc(0), speLoc(0), posLoc(0), mAmbLoc(0), mDifLoc(0), mSpecLoc(0), mShiLoc(0);
glm::vec3 origin(0.f);
glm::vec3 up(0.f, 1.f, 0.f);

Camera camera(glm::vec3(0.f, 1.f, 4.f));
GLboolean keys[1024] = { GL_FALSE };
GLboolean b_firstMouse = GL_TRUE;
float deltaTime = 0.f;

float lastFrame = 0.f;
float lastLocX = 0.f;
float lastLocY = 0.f;


void window_size_callback(GLFWwindow* window, int newWidth, int newHeight)
{
glfwGetFramebufferSize(window, &newWidth, &newHeight);

aspect = (float)newWidth / (float)newHeight;

glViewport(0, 0, newWidth, newHeight);
pMat = glm::perspective(glm::radians(45.f), aspect, 0.01f, 1000.f);
}

void do_movement()
{
if (keys[GLFW_KEY_W])
{
camera.ProcessKeyboard(FORWARD, deltaTime);
}
if (keys[GLFW_KEY_S])
{
camera.ProcessKeyboard(BACKWARD, deltaTime);
}
if (keys[GLFW_KEY_A])
{
camera.ProcessKeyboard(LEFT, deltaTime);
}
if (keys[GLFW_KEY_D])
{
camera.ProcessKeyboard(RIGHT, deltaTime);
}
/*if (keys[GLFW_KEY_ESCAPE])
{
glfwSetWindowShouldClose(window, GL_TRUE);
}*/
}

void key_press_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
if ((key == GLFW_KEY_ESCAPE) && (action == GLFW_PRESS))
{
glfwSetWindowShouldClose(window, GL_TRUE);
}
if (action == GLFW_PRESS)
{
keys[key] = GLFW_TRUE; //这里一定一定不能写成“==“,否则 按键WSAD按键失效!!!!!!!
}
else if (action == GLFW_RELEASE)
{
keys[key] = GLFW_FALSE; //这里一定一定不能写成“==“,否则 按键WSAD按键失效!!!!!!!
}
}

void mouse_move_callback(GLFWwindow* window, double xPos, double yPos)
{
if (b_firstMouse)
{
lastLocX = xPos;
lastLocY = yPos;
b_firstMouse = GL_FALSE;
}

float xOffset = xPos - lastLocX;
float yOffset = lastLocY - yPos;
lastLocX = xPos;
lastLocY = yPos;

camera.ProcessMouseMovement(xOffset, yOffset);

}

void mouse_scroll_callback(GLFWwindow* window, double xPos, double yPos)
{
camera.ProcessMouseScroll(yPos);
}



void installLights(int renderingProgram, glm::mat4 vMatrix)
{
transformed = glm::vec3(vMatrix * glm::vec4(currentLightPos, 1.f));
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");
difLoc = glGetUniformLocation(renderingProgram, "light.diffuse");
speLoc = glGetUniformLocation(renderingProgram, "light.specular");
posLoc = glGetUniformLocation(renderingProgram, "light.position");
mAmbLoc = glGetUniformLocation(renderingProgram, "material.ambient");
mDifLoc = 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, difLoc, 1, lightDiffuse);
glProgramUniform4fv(renderingProgram, speLoc, 1, lightSpecular);
glProgramUniform3fv(renderingProgram, posLoc, 1, lightPos);
glProgramUniform4fv(renderingProgram, mAmbLoc, 1, matAmb);
glProgramUniform4fv(renderingProgram, mDifLoc, 1, matDif);
glProgramUniform4fv(renderingProgram, mSpecLoc, 1, matSpe);
glProgramUniform1f(renderingProgram, mShiLoc, matShi);
}

void setupVertices(void)
{
// pyramid definition
numPyramidVertices = pyramid.getNumVertices(); //获取金字塔所有的顶点坐标个数
vector<glm::vec3> vert = pyramid.getVertices(); //获取金字塔所有的顶点个数
vector<glm::vec3> norm = pyramid.getNormals(); //获取金字塔所有法线

vector<float> pyramidPValues;
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();
vector<int> ind = myTorus.getIndices();
//这里最好清空一下vert和norm
/*vert.clear();
norm.clear();*/
vert = myTorus.getVertices();
norm = myTorus.getNormals();

vector<float> torusPValues;
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(numVAOs, vao);
glBindVertexArray(vao[0]);

glGenBuffers(numVBOs, vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBufferData(GL_ARRAY_BUFFER, torusPValues.size() * sizeof(float), &torusPValues[0], GL_STATIC_DRAW);

glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBufferData(GL_ARRAY_BUFFER, pyramidPValues.size() * sizeof(float), &pyramidPValues[0], GL_STATIC_DRAW);

glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);
glBufferData(GL_ARRAY_BUFFER, torusNValues.size() * sizeof(float), &torusNValues[0], GL_STATIC_DRAW);

glBindBuffer(GL_ARRAY_BUFFER, vbo[3]);
glBufferData(GL_ARRAY_BUFFER, pyramidNValues.size() * sizeof(float), &pyramidNValues[0], GL_STATIC_DRAW);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[4]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, ind.size() * sizeof(int), &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.vert", "frag1Shader.frag");
renderingProgram2 = Utils::createShaderProgram("vert2Shader.vert", "frag2Shader.frag");

glfwGetFramebufferSize(window, &width, &height);
aspect = (float)width / (float)height;
pMat = glm::perspective(glm::radians(60.f), aspect, 0.001f, 1000.f);

setupVertices();
setupShadowBuffers(window);


//相机坐标空间[-1, -1]变换到纹理坐标空间[0, 1],需要构建B矩阵,先缩放1/2,在平移1/2
b = glm::mat4(
0.5f, 0.f, 0.f, 0.f,
0.f, 0.5f, 0.f, 0.f,
0.f, 0.f, 0.5f, 0.f,
0.5f, 0.5f, 0.5f, 1.f);

//
}

void display(GLFWwindow* window, double currentTime)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glClearColor(0.f, 0.5f, 1.f, 1.f);

deltaTime = currentTime - lastFrame;
lastFrame = currentTime;

do_movement();

//这句必须要有,否则鼠标中键失效
pMat = glm::perspective(camera.Zoom, aspect, 0.01f, 1000.f);


vMat = camera.GetViewMatrix();
currentLightPos = glm::vec3(lightLoc);

mvMat = vMat * mMat;
invTrMat = glm::transpose(glm::inverse(mvMat));

lightVMatrix = glm::lookAt(currentLightPos, origin, up);
lightPMatrix = glm::perspective(glm::radians(60.f), aspect, 0.01f, 1000.f);

glBindFramebuffer(GL_FRAMEBUFFER, shadowBuffer);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, shadowTex, 0);

glDrawBuffer(GL_NONE);
//GL_DEPTH_TEST | GL_POLYGON_OFFSET_FILL写到一起有问题,为什么?
//glEnable(GL_POLYGON_OFFSET_FILL);//为减少伪影
glEnable(GL_DEPTH_TEST);
glEnable(GL_POLYGON_OFFSET_FILL);
//设置用于计算深度值的比例和单位
glPolygonOffset(2.f, 4.f); //伪影减少

passOne();

glDisable(GL_POLYGON_OFFSET_FILL); //伪影减少

glBindFramebuffer(GL_FRAMEBUFFER, 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, shadowTex);

glDrawBuffer(GL_FRONT);

passTwo();
}

void passOne()
{
glUseProgram(renderingProgram1);

// draw the torus // 接下来的代码段通过从光源角度渲染环面获得深度缓冲区
mMat = glm::translate(glm::mat4(1.f), torusLoc);

vMat = camera.GetViewMatrix();
// 轻微旋转以便查看
mMat = glm::rotate(mMat, toRadins(56.f), glm::vec3(1.f, 0.f, 0.f));
/*amt += 0.5f;
rMat = glm::rotate(glm::mat4(1.f), glm::radians(45.f), glm::vec3(0.f, 0.f, 1.f));*/

// 我们从光源角度绘制,因此使用光源的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);

// vbo[4] 包含环面索引
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[4]);
glDrawElements(GL_TRIANGLES, numTorusIndices, GL_UNSIGNED_INT, 0);

// draw the pyramid
mMat = glm::translate(glm::mat4(1.f), pyrLoc);
mMat = glm::rotate(mMat, toRadins(30.f), glm::vec3(1.f, 0.f, 0.f));
mMat = glm::rotate(mMat, toRadins(40.f), glm::vec3(0.f, 1.f, 0.f));

shadowMVP1 = lightPMatrix * lightVMatrix * mMat;
glUniformMatrix4fv(sLoc, 1, GL_FALSE, glm::value_ptr(shadowMVP1));
/*glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
glUniformMatrix4fv(nLoc, 1, GL_FALSE, glm::value_ptr(invTrMat));*/

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()
{
glUseProgram(renderingProgram2);

// 绘制环面,这次我们需要加入光照、材质、法向量等
// 同时我们需要为相机空间以及光照空间都提供MVP 变换
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.f), glm::vec3(-cameraLoc.x, -cameraLoc.y, -cameraLoc.z));
vMat = camera.GetViewMatrix();

mMat = glm::translate(glm::mat4(1.f), torusLoc);
// 轻微旋转以便查看
mMat = glm::rotate(mMat, toRadins(45.f), glm::vec3(1.f, 0.f, 0.f));

currentLightPos = glm::vec3(lightLoc);
installLights(renderingProgram2, vMat);

// 构建相机视角环面的MV 矩阵
mvMat = vMat * mMat;
invTrMat = glm::transpose(glm::inverse(mvMat));

// 构建光源视角环面的MV 矩阵
shadowMVP2 = b * lightPMatrix * lightVMatrix * mMat;//必须有,否则渲染不出金字塔和圆环

// 将MV 以及PROJ 矩阵传入相应的统一变量
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]); // 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.f), pyrLoc);
mMat = glm::rotate(mMat, toRadins(30.f), glm::vec3(1.f, 0.f, 0.f));
mMat = glm::rotate(mMat, toRadins(40.f), glm::vec3(0.f, 1.f, 0.f));

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);
}


int main(int argc, char** argv)
{
int glfwState = glfwInit();
if (GLFW_FALSE == glfwState)
{
cout << "GLFW initialize failed,invoke glfwInit()......Error file:" << __FILE__ << "......Error line" << __LINE__ << endl;
glfwTerminate();
exit(EXIT_FAILURE);
}

/*因为我们要使用OpenGL 4.6,所以我们把GLFW_CONTEXT_VERSION_MAJOR和GLFW_CONTEXT_VERSION_MINOR对应的hint都设置为4和6。
因为我们要使用OpenGL核心模式(这个后面会提到更多),所以我们把GLFW_OPENGL_PROFILE对应的hint设置为GLFW_OPENGL_CORE_PROFILE,
表示使用OpenGL核心模式。最后,把GLFW_RESIZABLE对应的hint设置为GLFW_FALSE,表示窗口不允许用户调整大小。
之所以这样做是因为如果允许用户调整大小,大小发生变化后,窗口的绘制区域默认不变(依然是原来窗口的区域),
也就是说窗口上绘制的图像的大小、位置不会发生改变。为了避免这种现象发生,我们就简单地不让用户调整窗口大小
(当然也有更好的方法,就是用GLFW设置一个窗口大小的回调函数,但这样比较简单)。*/
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_CORE_PROFILE, GLFW_OPENGL_PROFILE);

GLFWwindow* window = glfwCreateWindow(screen_width, screen_height, "Shadow mapping artifacts", nullptr, nullptr);
if (!window)
{
cout << "GLFW create window failed,invoke glfwCreateWindow()......Error file:" << __FILE__ << "......Error line:" << __LINE__ << endl;
glfwTerminate();
exit(EXIT_FAILURE);
}

/*此函数使调用线程上的指定窗口的 OpenGL 或 OpenGL ES 上下文成为当前上下文。
一次只能在单个线程上使上下文成为当前上下文,并且每个线程一次只能有一个当前上下文。
在线程之间移动上下文时,必须先使其在旧线程上变为非当前状态,然后再在新线程上变为当前状态。
*/
glfwMakeContextCurrent(window);
glfwSetWindowSizeCallback(window, window_size_callback);
glfwSetCursorPosCallback(window, mouse_move_callback);
glfwSetScrollCallback(window, mouse_scroll_callback);
glfwSetKeyCallback(window, key_press_callback);

int glewState = glewInit();
if (glewState != GLEW_OK)
{
cout << "GLEW initialize failed,invoke glewInit()......Error file:" << __FILE__ << "......Error line:" << __LINE__ << endl;
glfwTerminate();
exit(EXIT_FAILURE);
}

/*此函数设置当前 OpenGL 或 OpenGL ES 上下文的交换间隔,即从调用glfwSwapBuffers开始等待的屏幕更新次数,
然后再交换缓冲区并返回。这有时称为垂直同步、垂直回扫同步或仅vsync。
支持WGL_EXT_swap_control_tear和GLX_EXT_swap_control_tear扩展的上下文也接受负交换间隔,这允许驱动程序立即交换,
即使帧到达有点晚。您可以使用glfwExtensionSupported检查这些扩展。
上下文必须在调用线程上是最新的。在没有当前上下文的情况下调用此函数将导致GLFW_NO_CURRENT_CONTEXT错误。
此功能不适用于 Vulkan。如果您使用 Vulkan 进行渲染,请改为查看交换链的当前模式。
*/
glfwSwapInterval(1);

printf("%s\n", glGetString(GL_SHADING_LANGUAGE_VERSION));//开始初始化过程
const GLubyte* renderer = glGetString(GL_RENDERER);
const GLubyte* vendor = glGetString(GL_VENDOR);
const GLubyte* version = glGetString(GL_VERSION);
const GLubyte* glslVersion = glGetString(GL_SHADING_LANGUAGE_VERSION);
GLint major, minor;
glGetIntegerv(GL_MAJOR_VERSION, &major);
glGetIntegerv(GL_MINOR_VERSION, &minor);
printf("GL Vendor : %s\n", vendor);
printf("GL Renderer : %s\n", renderer);
printf("GL Version (string) : %s\n", version);
printf("GL Version (integer) : %d.%d\n", major, minor);
printf("GLSL Version : %s\n", glslVersion);

glGetError(); // Debug GLEW bug fix


/*因为我们要使用OpenGL 4.6,所以我们把GLFW_CONTEXT_VERSION_MAJOR和GLFW_CONTEXT_VERSION_MINOR对应的hint都设置为4和6。
因为我们要使用OpenGL核心模式(这个后面会提到更多),所以我们把GLFW_OPENGL_PROFILE对应的hint设置为GLFW_OPENGL_CORE_PROFILE,
表示使用OpenGL核心模式。最后,把GLFW_RESIZABLE对应的hint设置为GLFW_FALSE,表示窗口不允许用户调整大小。
之所以这样做是因为如果允许用户调整大小,大小发生变化后,窗口的绘制区域默认不变(依然是原来窗口的区域),
也就是说窗口上绘制的图像的大小、位置不会发生改变。为了避免这种现象发生,我们就简单地不让用户调整窗口大小
(当然也有更好的方法,就是用GLFW设置一个窗口大小的回调函数,但这样比较简单)。*/



/*默认情况下,出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的,
意味着数据在着色器端是不可见的,哪怕数据已经上传到GPU,由glEnableVertexAttribArray启用指定属性,
才可在顶点着色器中访问逐顶点的属性数据。glVertexAttribPointer或VBO只是建立CPU和GPU之间的逻辑连接,
从而实现了CPU数据上传至GPU。但是,数据在GPU端是否可见,即,着色器能否读取到数据,由是否启用了对应的属性决定,
这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU(服务器端)数据。
*/
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

init(window);

while (!glfwWindowShouldClose(window))
{
display(window, glfwGetTime());
glfwSwapBuffers(window);
glfwPollEvents();
}

glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_SUCCESS);

return 0;
}

运行效果

openGL阴影实现(硬阴影)_贴图_05

完成工程下载

​源码下载地址​

参考

计算机图形学编程 使用OpenGL和C++