目录

  • 写在前面
  • 阴影映射原理简介
  • 封装 Camera 类
  • 帧缓冲
  • 阴影映射
    • 准备工作
    • 创建帧缓冲与深度纹理附件
    • 从光源方向进行渲染
    • 正常地渲染场景
  • 如何查找bug(⚠重要)
    • 多纹理传送
    • 查看深度纹理数据
    • 检验光源处的相机是否配置正确
    • 纹理环绕方式
  • 完整代码
    • c++
    • 着色器
      • shadow着色器
      • debug着色器
      • 正常渲染的着色器

写在前面

今天是 2021 年的元旦。不管 2020 怎么说,都翻篇了,需要看向一个新的前方。新年快乐!


书接上文:OpenGL学习(八)phong光照模型

上次我们讲到通过 phong 光照模型进行简单光照特效的绘制。今天我们使用阴影映射技术,来进一步绘制阴影,使得场景的光照效果更加逼真。

注:
事实上,这是我第 n 次写这个内容了。。。

第一次是:从零开始编写minecraft光影包(1)基础阴影绘制

第二次是:深大计算机图形学大作业之虚拟场景建模

第 3 到 n-1 次是帮别人改阴影映射的 bug

第 n 次则是这篇博客。

已经,品鉴的足够多次了。。。

另:
为何如侠客行一般反复品鉴阴影映射,却推迟到现在才出博客?
因为期末了在补别的科目的 ddl,元旦滚回家好不容易有点时间,才来更新一下
我爬我是菜鸡 Orz

阴影映射原理简介

阴影映射是一种渲染阴影的算法。最早在 1978 年被提出。因为其能以较小的代价模拟真实世界的阴影,比如将阴影投影到任何平面上。因此阴影映射是现代计算机游戏中,最常用的绘制阴影的方法,没有之一。

阴影映射的水非常深,有非常多的技术可以用来优化这一过程,比如 PCF,穹式投影,硬件深度比较,基于法线的偏移,peter panning… 但是我们今天只讨论最简单的阴影映射。

阴影映射的原理十分简单,就是通过从光源方向进行一次渲染,得到深度图,在正常的渲染中,通过该深度图来判断当前点是否在阴影之中。

步骤如下:

  1. 获取当前像素 p 到光源的距离 currentDepth
  2. 通过深度图,获取距离光源最近的点的深度 closestDepth
  3. 如果 closestDepth < currentDepth 说明 p 被遮挡,p 在阴影中

图解如下:

更加细节的代码实现:

  1. 从光源方向进行一次渲染,得到深度纹理
  2. 将当前点坐标,从世界坐标系下,变换到光源坐标系下(通过 view,projection 变换和透视除法)
  3. 根据变换后的坐标,取深度纹理中的数据,即 closestDepth
  4. 比较 closestDepth 和 currentDepth,判断是否在阴影中

思路还就内个思路。难点在于我们如何通过从光源方向的渲染,来获取深度图。这才是最容易出 bug 的地方。

封装 Camera 类

我们总共要进行两次渲染,第一次渲染从光源方向进行,进而完成深度图的绘制,第二次渲染从正常的相机视角进行,进行场景的绘制。

这意味着我们要重复进行两次相机矩阵的计算,并且使用两套全局变量,对相机进行管理:

...// 相机参数
glm::vec3 cameraPosition(0, 0, 0);      // 相机位置
glm::vec3 cameraDirection(0, 0, -1);    // 相机视线方向
glm::vec3 cameraUp(0, 1, 0);            // 世界空间下竖直向上向量
float pitch = 0.0f;
float roll = 0.0f;
float yaw = 0.0f;
// 视界体参数
float left = -1, right = 1, bottom = -1, top = 1, zNear = 0.01, zFar = 100.0;...// 计算欧拉角以确定相机朝向
cameraDirection.x = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
cameraDirection.y = sin(glm::radians(pitch));
cameraDirection.z = -cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 相机看向z轴负方向// 传视图矩阵
glm::mat4 view = glm::lookAt(cameraPosition, cameraPosition + cameraDirection, cameraUp);
GLuint vlocation = glGetUniformLocation(program, "view");
glUniformMatrix4fv(vlocation, 1, GL_FALSE, glm::value_ptr(view));// 传投影矩阵
glm::mat4 projection = glm::perspective(glm::radians(70.0f), (GLfloat)windowWidth / (GLfloat)windowHeight, zNear, zFar);
GLuint plocation = glGetUniformLocation(program, "projection");
glUniformMatrix4fv(plocation, 1, GL_FALSE, glm::value_ptr(projection));

这是非常麻烦的,于是我们需要封装一个 Camera 类,帮助我们管理相机相关的操作。回想一个相机需要那些属性?

  • 首先我们需要相机的位置和朝向,以确定相机的模视变换矩阵

  • 我们还需要一组欧拉角来以 FPS 相机的形式,确定相机的朝向

  • 随后我们需要指定投影的相关参数,比如 left, right, bottom, top, zNear, zFar, fovy, aspect,并且根据这些参数,定制一个投影矩阵。

需求很明确了,我们可以给出 Camera 类的结构:

注:
因为懒得开头文件,和上次的 Mesh,Model 类一样,我们直接写在 .cpp 里面了

class Camera
{public:// 相机参数glm::vec3 position = glm::vec3(0, 0, 0);    // 位置glm::vec3 direction = glm::vec3(0, 0, -1);  // 视线方向glm::vec3 up = glm::vec3(0, 1, 0);          // 上向量,固定(0,1,0)不变float pitch = 0.0f, roll = 0.0f, yaw = 0.0f;    // 欧拉角float fovy = 70.0f, aspect = 1.0, zNear = 0.01, zFar = 100; // 透视投影参数float left = -1.0, right = 1.0,top = 1.0,bottom = -1.0; // 正交投影参数Camera() {}// 视图变换矩阵glm::mat4 getViewMatrix(bool useEulerAngle = true){if (useEulerAngle)  // 使用欧拉角更新相机朝向{direction.x = cos(glm::radians(pitch)) * sin(glm::radians(yaw));direction.y = sin(glm::radians(pitch));direction.z = -cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 相机看向z轴负方向}return glm::lookAt(position, position + direction, up);}// 投影矩阵glm::mat4 getProjectionMatrix(bool usePerspective = true){if (usePerspective) // 透视投影{return glm::perspective(glm::radians(fovy), aspect, zNear, zFar);}return glm::ortho(left, right, bottom, top, zNear, zFar);}
};

其中 getViewMatrix 用于返回视图变换矩阵,传入的参数是 “是否使用欧拉角更新相机朝向”,而 getProjectionMatrix 用于返回投影矩阵,传入的参数决定是否使用透视投影。

接着我们创建相机对象:

// 相机
Camera camera;          // 正常渲染
Camera shadowCamera;    // 从光源方向渲染

于是我们非常简单的就可以进行相机矩阵的计算:

// 传视图矩阵
glUniformMatrix4fv(glGetUniformLocation(program, "view"), 1, GL_FALSE, glm::value_ptr(camera.getViewMatrix()));
// 传投影矩阵
glUniformMatrix4fv(glGetUniformLocation(program, "projection"), 1, GL_FALSE, glm::value_ptr(camera.getProjectionMatrix()));

在鼠标的回调函数中,我们直接更新相机的参数(欧拉角)以决定相机朝向即可:

// 鼠标运动函数
void mouse(int x, int y)
{// 调整旋转camera.yaw += 35 * (x - float(windowWidth) / 2.0) / windowWidth;camera.yaw = glm::mod(camera.yaw + 180.0f, 360.0f) - 180.0f;    // 取模范围 -180 ~ 180camera.pitch += -35 * (y - float(windowHeight) / 2.0) / windowHeight;camera.pitch = glm::clamp(camera.pitch, -89.0f, 89.0f);glutWarpPointer(windowWidth / 2.0, windowHeight / 2.0);glutPostRedisplay();    // 重绘
}

而键盘回调函数则是同理,我们更新 camera.position 即可。详细代码见下文

帧缓冲

帧缓冲是一个抽象的屏幕。在之前的 OpenGL 学习中,我们的绘制命令总是直接绘制到屏幕上面。

阴影映射需要我们先从光源方向(对应 shadowCamera 对象)对场景进行绘制。而我们需要显示的内容则是需要由正常的相机来完成(对应 camera 对象),这就发生了冲突。

通俗点说,我们无法利用一张画纸,画两张画。于是便需要引入帧缓冲的概念。

帧缓冲是一个抽象的屏幕,或者说一种抽象的管理显存的方式。一个帧缓冲是由一系列 “画纸” 组成的,这些画纸叫做 “附件” (attachment) 。这些附件通常都是纹理。因为在 OpenGL 中,纹理既可以被当作被写入的对象,也可以被当作读的对象。

其中一个帧缓冲可以有多个颜色附件和一个深度附件,但是默认最终只有 0 号帧缓冲会被输出到屏幕。

帧缓冲的概念非常重要。在之后的博客,我们会手把手的实现延迟渲染管线。还要和它打交道。

在着色器中使用:

gl_FragData[x]

即可向当前 draw call 对应的帧缓冲的第 x 号颜色附件进行写入。

同时我们也可以通过

uniform sampler2D tex;...color.rgb = texture2D(tex, texcoord.st).rgb;

来从附件(也就是纹理)中读取被其他着色器绘制好的数据。

使用如下的代码可以创建并绑定一块帧缓冲,一旦帧缓冲绑定之后,任何的 draw call 都会输出到当前的帧缓冲。

GLuint frameBufferObject;...glGenFramebuffers(1, &frameBufferObject);

而通过

glFramebufferTexture2D(GL_FRAMEBUFFER, 附件类型, GL_TEXTURE_2D, 纹理对象, 0);

函数则可以将某个纹理对象,作为某个附件,添加到当前帧缓冲中。

阴影映射

我们正式开始进行阴影映射代码的编写!

这一部分的代码分为三个步骤,分别是:

  1. 变量准备
  2. 创建帧缓冲与深度纹理
  3. 从光源方向进行场景绘制
  4. debug 绘制

准备工作

因为要从光源方向进行场景的渲染,我们需要两个相机:

// 相机
Camera camera;          // 正常渲染
Camera shadowCamera;    // 从光源方向渲染...// 正交投影参数配置 -- 视界体范围 -- 调整到场景一般大小即可
shadowCamera.left = -20;
shadowCamera.right = 20;
shadowCamera.bottom = -20;
shadowCamera.top = 20;
shadowCamera.position = glm::vec3(0, 4, 15);

此外,我们需要三组着色器,其中 shadow 和 debug 着色器分别负责从光源方向的渲染,和输出深度纹理以 debug。

// 着色器程序对象
GLuint program;
GLuint debugProgram;    // 调试用
GLuint shadowProgram;   // 绘制阴影的着色器程序对象...// 生成着色器程序对象
program = getShaderProgram("shaders/fshader.fsh", "shaders/vshader.vsh");
shadowProgram = getShaderProgram("shaders/shadow.fsh", "shaders/shadow.vsh");
debugProgram = getShaderProgram("shaders/debug.fsh", "shaders/debug.vsh");

注:
其实 shadow 着色器仅仅是顶点着色器在工作。
因为不用输出颜色,片元着色器不用写任何代码,除了 void main{}

最后我们需要帧缓冲和其深度纹理附件:

// 光源与阴影参数
int shadowMapResolution = 1024;             // 阴影贴图分辨率
GLuint shadowMapFBO;                        // 从光源方向进行渲染的帧缓冲
GLuint shadowTexture;                       // 阴影纹理

创建帧缓冲与深度纹理附件

在了解到阴影映射的算法原理之后,我们需要从光源方向进行一次绘制。我们需要额外的一块绘制区域(帧缓冲),同时我们将深度信息输出到深度缓存。

在 init 部分应该有如下的代码:

// 创建shadow帧缓冲
glGenFramebuffers(1, &shadowMapFBO);
// 创建阴影纹理
glGenTextures(1, &shadowTexture);
glBindTexture(GL_TEXTURE_2D, shadowTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, shadowMapResolution, shadowMapResolution, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// 将阴影纹理绑定到 shadowMapFBO 帧缓冲
glBindFramebuffer(GL_FRAMEBUFFER, shadowMapFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowTexture, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

从光源方向进行渲染

我们创建了对应的帧缓冲,接下来我们在 display 中 bind 该缓冲并且调用 draw call 即可进行绘制:

// 从光源方向进行渲染
glUseProgram(shadowProgram);
glBindFramebuffer(GL_FRAMEBUFFER, shadowMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
glViewport(0, 0, shadowMapResolution, shadowMapResolution);// 光源看向世界坐标原点
shadowCamera.direction = glm::normalize(glm::vec3(0, 0, 0) - shadowCamera.position);
// 传视图矩阵
glUniformMatrix4fv(glGetUniformLocation(shadowProgram, "view"), 1, GL_FALSE, glm::value_ptr(shadowCamera.getViewMatrix(false)));
// 传投影矩阵
glUniformMatrix4fv(glGetUniformLocation(shadowProgram, "projection"), 1, GL_FALSE, glm::value_ptr(shadowCamera.getProjectionMatrix(false)));// 从光源方向进行绘制
for (auto m : models)
{m.draw(shadowProgram);
}

而我们的 shadow 系列着色器也是十分简单。我们首先在顶点着色器完成 mvp 变换,而片元着色器不用输出任何像素。下面是顶点着色器 shadow.vsh 代码:

#version 330 corelayout (location = 0) in vec3 vPosition;uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;void main()
{gl_Position = projection * view * model * vec4(vPosition, 1.0);
}

片段着色器 shadow.fsh :

#version 330 corevoid main()
{             // gl_FragDepth = gl_FragCoord.z;
}

正常地渲染场景

这部分的代码和往常无异。只是除了相机 camera 对象的 v,p 变换矩阵,光源的位置信息等 uniform 变量以外,我们需要额外传入至少两个 uniform 变量:

  1. 从光源方向渲染得到的深度纹理
  2. 转换到光源坐标系的 v,p 变换矩阵,通过 shadowCamera 对象获取

才能完成阴影映射的整个过程。

注:
这里其实可以将 v 和 p 矩阵直接乘起来,这样我们只需要传递一个矩阵即可
下文使用的就是这种思路,我们传递一个 shadowVP 矩阵即可

// 正常滴渲染
glUseProgram(program);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glViewport(0, 0, windowWidth, windowHeight);// 传视图矩阵
glUniformMatrix4fv(glGetUniformLocation(program, "view"), 1, GL_FALSE, glm::value_ptr(camera.getViewMatrix()));
// 传投影矩阵
glUniformMatrix4fv(glGetUniformLocation(program, "projection"), 1, GL_FALSE, glm::value_ptr(camera.getProjectionMatrix()));// 传递矩阵: 转换到光源坐标的变换矩阵
glm::mat4 shadowVP = shadowCamera.getProjectionMatrix(false) * shadowCamera.getViewMatrix(false);
glUniformMatrix4fv(glGetUniformLocation(program, "shadowVP"), 1, GL_FALSE, glm::value_ptr(shadowVP));// 传深度纹理
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, shadowTexture);
glUniform1i(glGetUniformLocation(program, "shadowtex"), 1);// 传递光源位置
glUniform3fv(glGetUniformLocation(program, "lightPos"), 1, glm::value_ptr(shadowCamera.position));
// 传递相机位置
glUniform3fv(glGetUniformLocation(program, "cameraPos"), 1, glm::value_ptr(camera.position));// 正常绘制
for (auto m : models)
{m.draw(program);
}

c++ 部分的代码告一段落。


片元着色器(fshader.fsh 文件)在上一篇博客的基础上,也要做对应的修改。我们添加 shadowMapping 函数以返回一个片元是否在阴影之中。

shadowMapping 函数也分为如下几个步骤:

  1. 将当前片元(像素)的世界坐标转换到光源坐标系下
  2. 通过当前像素在光源坐标系下的坐标,对深度纹理 tex 进行采样
  3. 根据采样得到距离摄像机最近的深度 closestDepth 并且和当前的深度进行比对

没什么复杂的,对吧

注:
注意到下面代码,判断深度大小时 +0.005 是因为我们的深度缓冲精度不够,如果直接判断会出现条纹状的阴影

引自:learn OpenGL
此外,除了常数偏移,还有基于法线的偏移等奇技淫巧,这里不做讨论

下面是 shadowMapping 函数的代码:

...uniform sampler2D shadowtex;  // shadow纹理
uniform mat4 shadowVP;  // 转换到光源坐标的变换矩阵...float shadowMapping(sampler2D tex, mat4 shadowVP, vec4 worldPos) {// 转换到光源坐标vec4 lightPos = shadowVP * worldPos;lightPos = vec4(lightPos.xyz/lightPos.w, 1.0);lightPos = lightPos*0.5 + 0.5;// 计算shadowmappingfloat closestDepth = texture2D(tex, lightPos.xy).r;    // shadowmap中最近点的深度float currentDepth = lightPos.z;    // 当前点的深度float isInShadow = (currentDepth>closestDepth+0.005) ? (1.0) : (0.0);return isInShadow;
}

其中 main 函数也是一样要修改。如果一个点不在阴影之中,我们计算其 phong 光照,否则在阴影中的点只有环境光:

...void main()
{fColor.rgb =  texture2D(texture, texcoord).rgb;float isInShadow = shadowMapping(shadowtex, shadowVP, vec4(worldPos, 1.0));if(isInShadow==0) {fColor.rgb *= phong(worldPos, cameraPos, lightPos, normal);} else {fColor.rgb *= 0.3;  // only ambient}
}

当然你也可以不使用 phong 光照。我们直接把处于阴影之中的片元涂黑即可。下面展示一种更加简单的方案:

...void main()
{fColor.rgb =  texture2D(texture, texcoord).rgb;float isInShadow = shadowMapping(shadowtex, shadowVP, vec4(worldPos, 1.0));if(isInShadow!=0) {fColor.rgb *= 0.3;  // 涂黑他}
}

如果顺利,那么我们会得到如下的结果。我们用一只鸭子模型来跟随光源以表示光源的位置。此外,通过按键可以控制光源的移动:

如果它不顺利呢?我们就让他顺利起来!

如何查找bug(⚠重要)

唔。。。。其实原本的计划中,我没有打算编写这一部分,但是因为最近被疯狂问怎么改阴影映射的 bug,也遇到了很多特殊情况,自己也积累了很多经验。于是分享出来

多纹理传送

如果你使用了读取 obj 的方式绘制物体,那么物体有一个纹理,阴影贴图又是一张纹理,我们要使用多纹理的方式进行纹理传送,否则无法正确的获取到深度贴图中的数据!

在 glsl 中,最多允许我们有 16 个纹理,我们传送纹理,也可以像传送一个 vec3 变量一样简单。在 glew 中,我们通过 glUniform1i 函数来进行纹理的传送。

注:
虽然纹理的变量类型是 sampler2D,但是为了和 c++ 端(或者别的语言)统一接口,使用 int 作为引用也是一种合理的解决方案

此外,因为 OpenGL 是状态机,传送纹理命令只会传送当前的纹理。我们在传送纹理之前,还要激活一个纹理单元,并且绑定当前纹理,最后才是传送。下面的代码给出了一个实例:

代码如下:

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, 纹理对象0);
glUniform1i(glGetUniformLocation(program, "0号纹理samlper2D的变量名称"), 0);glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, 纹理对象1);
glUniform1i(glGetUniformLocation(program, "1号纹理samlper2D的变量名称"), 1);glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, 纹理对象2);
glUniform1i(glGetUniformLocation(program, "2号纹理samlper2D的变量名称"), 2);glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, 纹理对象3);
glUniform1i(glGetUniformLocation(program, "3号纹理samlper2D的变量名称"), 3);

查看深度纹理数据

因为阴影映射的核心就是从光源方向渲染得到的深度纹理。我们首当其冲地查看深度纹理的数据是否正确。只有保证了这一点,才能进行后续的编码。

注 意 到 深度缓冲是一张纹理:

GLuint shadowTexture;                       // 阴影纹理

纹理即是图片。那么怎么将这张纹理以图像的形式输出呢?因为 OpenGL 并非阳间产物,没有提供诸如 imshow() 等方法进行 cpu 级别的图像输出,我们只能曲线救国。

我们通过渲染一个 2 x 2 的正方形,这个正方形的大小刚好覆盖整个屏幕。将目标纹理贴上去,我们可以间接地输出纹理中的图像信息!

于是我们添加如下的全局变量,即一个模型对象,然后在 init 部分中,我们将一个正方形加入该模型对象即可:

Model screen;   // 渲染一个四方形做屏幕...// 生成一个四方形做荧幕 -- 用以显示纹理中的数据
Mesh msquare;
msquare.vertexPosition = { glm::vec3(-1, -1, 0), glm::vec3(1, -1, 0), glm::vec3(-1, 1, 0), glm::vec3(1, 1, 0) };
msquare.vertexTexcoord = { glm::vec2(0, 0), glm::vec2(1, 0), glm::vec2(0, 1), glm::vec2(1, 1) };
msquare.index = { 0,1,2,2,1,3 };
msquare.bindData();
screen.meshes.push_back(msquare);

为了打印对应纹理中的信息,我们还需要准备一组着色器,名叫 debug,专门负责输出纹理中的信息:

GLuint debugProgram;    // 调试用...debugProgram = getShaderProgram("shaders/debug.fsh", "shaders/debug.vsh");

其中着色器的代码如下。我们不需要进行任何矩阵乘法和变换,因为输入的顶点本来就是覆盖了整个屏幕的正方形。顶点着色器代码如下:

#version 330 core// 顶点着色器输入
layout (location = 0) in vec3 vPosition;
layout (location = 1) in vec2 vTexcoord;// 传给片元着色器的变量
out vec2 texcoord;void main()
{gl_Position = vec4(vPosition, 1.0);texcoord = vTexcoord;
}

注:
这段代码我们使用 layout (location = …) 的方式指定传入的顶点属性
这意味着不同的 c++ 代码在传送时,位置有所不同。直接 copy 该代码可能无法正常运行。。。
可以参考我之前的博客:OpenGL学习(七)通过assimp库读取多种格式的模型
以查看详细的绑定方式,或者看图:

片元着色器代码如下。我们直接输出纹理即可:

#version 330 corein vec2 texcoord;
out vec4 fColor;// 纹理数据
uniform sampler2D shadowtex;    // shadow纹理void main()
{       fColor = vec4(vec3(texture2D(shadowtex, texcoord).r*0.5+0.5), 1);
}

最后我们在 display 中,完成场景渲染之后,利用 debug 着色器追加一次渲染即可:

// debug着色器输出一个四方形以显示深度纹理中的数据
glDisable(GL_DEPTH_TEST);   // 需要取消深度测试以保证其覆盖在原画面上
glUseProgram(debugProgram);
glViewport(0, 0, windowWidth / 3, windowHeight / 3);// 传深度纹理
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, shadowTexture);
glUniform1i(glGetUniformLocation(debugProgram, "shadowtex"), 1);// 绘制
screen.draw(debugProgram);
glEnable(GL_DEPTH_TEST);

如果顺利的话,画面的左下角将浮现出一个子窗口负责输出深度纹理的数据:

检验光源处的相机是否配置正确

从光源处进行渲染,摄像机的设置也很重要,我们要保证:

  1. 相机总是看向对的方向
  2. 相机使用正交投影
  3. 相机的视野范围足够宽,能够容纳整个场景

相机看向错误的方向,比如天空,我们会得到一张全白的深度图。尽量避免这种情况的发生!

我们默认相机总是看向场景的原点,也就是世界坐标 (0,0,0):

// 光源看向世界坐标原点
shadowCamera.direction = glm::normalize(glm::vec3(0, 0, 0) - shadowCamera.position);

我们可以通过将正常渲染中的 view 和 projection 矩阵换成光源相机的变换矩阵,这样我们可以直接从光源视角观看整个场景:


效果如下:

从光源方向观察是很有必要的,保证你的变换矩阵没有差错!


此外,如果不使用正交投影,那么最后得到的深度图将会是几乎一片白色,因为透视投影的 z 是非线性的!如果 z 坐标差异非常小,那么将不利于我们的硬件进行深度比较,因此使用正交投影是有必要的。

如图为使用透视投影进行深度纹理的绘制,可以看到因为 z 是非线性的,值大多都在 0.99 - 1.0 范围.所以左下角的深度纹理几乎全为白色,而我们的深度比较则失效,不能正确的绘制阴影:

最后,需要保证摄像机的范围足够宽,能够容纳整个场景,否则边缘处将会出现严重的 bug 。

纹理环绕方式

唔。。。这似乎是一个经常性的问题。如果你的光源方向的摄像机,视野范围不够大,并且你的深度纹理的环绕方式设置为 GL_REPEAT 的话,就会出现如下的情况:


这是因为如果纹理坐标超出 1.0 那么他会对 1.0 取模,然后重新采样纹理。因为相机视野不够大,不能覆盖整个场景,场景中超出光源相机的部分,就会重复采样同一张纹理,导致错误。


解决方案也十分简单:

  1. 给你的光源相机一个大范围的视野,大到足够覆盖整个场景
  2. 将纹理环绕方式改为 GL_CLAMP_TO_EDGE

如图:

完整代码

c++

// std c++
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <map>
#include <sstream>
#include <iostream>// glew glut
#include <GL/glew.h>
#include <GL/freeglut.h>// glm
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>// SOIL
#include <SOIL2/SOIL2.h>// assimp
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>// --------------------- end of include --------------------- //class Mesh
{public:// OpenGL 对象GLuint vao, vbo, ebo;GLuint diffuseTexture;  // 漫反射纹理// 顶点属性std::vector<glm::vec3> vertexPosition;std::vector<glm::vec2> vertexTexcoord;std::vector<glm::vec3> vertexNormal;// glDrawElements 函数的绘制索引std::vector<int> index;Mesh() {}void bindData(){// 创建顶点数组对象glGenVertexArrays(1, &vao); // 分配1个顶点数组对象glBindVertexArray(vao);     // 绑定顶点数组对象// 创建并初始化顶点缓存对象 这里填NULL 先不传数据glGenBuffers(1, &vbo);glBindBuffer(GL_ARRAY_BUFFER, vbo);glBufferData(GL_ARRAY_BUFFER,vertexPosition.size() * sizeof(glm::vec3) +vertexTexcoord.size() * sizeof(glm::vec2) +vertexNormal.size() * sizeof(glm::vec3),NULL, GL_STATIC_DRAW);// 传位置GLuint offset_position = 0;GLuint size_position = vertexPosition.size() * sizeof(glm::vec3);glBufferSubData(GL_ARRAY_BUFFER, offset_position, size_position, vertexPosition.data());glEnableVertexAttribArray(0);   // 着色器中 (layout = 0) 表示顶点位置glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (GLvoid*)(offset_position));// 传纹理坐标GLuint offset_texcoord = size_position;GLuint size_texcoord = vertexTexcoord.size() * sizeof(glm::vec2);glBufferSubData(GL_ARRAY_BUFFER, offset_texcoord, size_texcoord, vertexTexcoord.data());glEnableVertexAttribArray(1);   // 着色器中 (layout = 1) 表示纹理坐标glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*)(offset_texcoord));// 传法线GLuint offset_normal = size_position + size_texcoord;GLuint size_normal = vertexNormal.size() * sizeof(glm::vec3);glBufferSubData(GL_ARRAY_BUFFER, offset_normal, size_normal, vertexNormal.data());glEnableVertexAttribArray(2);   // 着色器中 (layout = 2) 表示法线glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, (GLvoid*)(offset_normal));// 传索引到 eboglGenBuffers(1, &ebo);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);glBufferData(GL_ELEMENT_ARRAY_BUFFER, index.size() * sizeof(GLuint), index.data(), GL_STATIC_DRAW);glBindVertexArray(0);}void draw(GLuint program){glBindVertexArray(vao);// 传纹理glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, diffuseTexture);glUniform1i(glGetUniformLocation(program, "texture"), 0);// 绘制glDrawElements(GL_TRIANGLES, this->index.size(), GL_UNSIGNED_INT, 0);glBindVertexArray(0);}
};class Model
{public:std::vector<Mesh> meshes;std::map<std::string, GLuint> textureMap;glm::vec3 translate = glm::vec3(0, 0, 0), rotate = glm::vec3(0, 0, 0), scale = glm::vec3(1, 1, 1);Model() {}void load(std::string filepath){Assimp::Importer import;const aiScene* scene = import.ReadFile(filepath, aiProcess_Triangulate | aiProcess_FlipUVs);// 异常处理if (!scene || scene->mFlags == AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode){std::cout << "读取模型出现错误: " << import.GetErrorString() << std::endl;exit(-1);}// 模型文件相对路径std::string rootPath = filepath.substr(0, filepath.find_last_of('/'));// 循环生成 meshfor (int i = 0; i < scene->mNumMeshes; i++){// 引用当前meshmeshes.push_back(Mesh());Mesh& mesh = meshes.back();// 获取 assimp 的读取到的 aimesh 对象aiMesh* aimesh = scene->mMeshes[i];// 我们将数据传递给我们自定义的meshfor (int j = 0; j < aimesh->mNumVertices; j++){// 顶点glm::vec3 vvv;vvv.x = aimesh->mVertices[j].x;vvv.y = aimesh->mVertices[j].y;vvv.z = aimesh->mVertices[j].z;mesh.vertexPosition.push_back(vvv);// 法线vvv.x = aimesh->mNormals[j].x;vvv.y = aimesh->mNormals[j].y;vvv.z = aimesh->mNormals[j].z;mesh.vertexNormal.push_back(vvv);// 纹理坐标: 如果存在则加入。assimp 默认可以有多个纹理坐标 我们取第一个(0)即可glm::vec2 vv(0, 0);if (aimesh->mTextureCoords[0]){vv.x = aimesh->mTextureCoords[0][j].x;vv.y = aimesh->mTextureCoords[0][j].y;}mesh.vertexTexcoord.push_back(vv);}// 如果有材质,那么传递材质if (aimesh->mMaterialIndex >= 0){// 获取当前 aimesh 的材质对象aiMaterial* material = scene->mMaterials[aimesh->mMaterialIndex];// 获取 diffuse 贴图文件路径名称 我们只取1张贴图 故填 0 即可aiString aistr;material->GetTexture(aiTextureType_DIFFUSE, 0, &aistr);std::string texpath = aistr.C_Str();texpath = rootPath + '/' + texpath;   // 取相对路径// 如果没生成过纹理,那么生成它if (textureMap.find(texpath) == textureMap.end()){// 生成纹理GLuint tex;glGenTextures(1, &tex);glBindTexture(GL_TEXTURE_2D, tex);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_MIRRORED_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);int textureWidth, textureHeight;unsigned char* image = SOIL_load_image(texpath.c_str(), &textureWidth, &textureHeight, 0, SOIL_LOAD_RGB);glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidth, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);   // 生成纹理delete[] image;textureMap[texpath] = tex;}// 传递纹理mesh.diffuseTexture = textureMap[texpath];}// 传递面片索引for (GLuint j = 0; j < aimesh->mNumFaces; j++){aiFace face = aimesh->mFaces[j];for (GLuint k = 0; k < face.mNumIndices; k++){mesh.index.push_back(face.mIndices[k]);}}mesh.bindData();}}void draw(GLuint program){// 传模型矩阵glm::mat4 unit(    // 单位矩阵glm::vec4(1, 0, 0, 0),glm::vec4(0, 1, 0, 0),glm::vec4(0, 0, 1, 0),glm::vec4(0, 0, 0, 1));glm::mat4 scale = glm::scale(unit, this->scale);glm::mat4 translate = glm::translate(unit, this->translate);glm::mat4 rotate = unit;    // 旋转rotate = glm::rotate(rotate, glm::radians(this->rotate.x), glm::vec3(1, 0, 0));rotate = glm::rotate(rotate, glm::radians(this->rotate.y), glm::vec3(0, 1, 0));rotate = glm::rotate(rotate, glm::radians(this->rotate.z), glm::vec3(0, 0, 1));// 模型变换矩阵glm::mat4 model = translate * rotate * scale;GLuint mlocation = glGetUniformLocation(program, "model");    // 名为model的uniform变量的位置索引glUniformMatrix4fv(mlocation, 1, GL_FALSE, glm::value_ptr(model));   // 列优先矩阵for (int i = 0; i < meshes.size(); i++){meshes[i].draw(program);}}
};class Camera
{public:// 相机参数glm::vec3 position = glm::vec3(0, 0, 0);    // 位置glm::vec3 direction = glm::vec3(0, 0, -1);  // 视线方向glm::vec3 up = glm::vec3(0, 1, 0);          // 上向量,固定(0,1,0)不变float pitch = 0.0f, roll = 0.0f, yaw = 0.0f;    // 欧拉角float fovy = 70.0f, aspect = 1.0, zNear = 0.01, zFar = 100; // 透视投影参数float left = -1.0, right = 1.0,top = 1.0,bottom = -1.0; // 正交投影参数Camera() {}// 视图变换矩阵glm::mat4 getViewMatrix(bool useEulerAngle = true){if (useEulerAngle)  // 使用欧拉角更新相机朝向{direction.x = cos(glm::radians(pitch)) * sin(glm::radians(yaw));direction.y = sin(glm::radians(pitch));direction.z = -cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 相机看向z轴负方向}return glm::lookAt(position, position + direction, up);}// 投影矩阵glm::mat4 getProjectionMatrix(bool usePerspective = true){if (usePerspective) // 透视投影{return glm::perspective(glm::radians(fovy), aspect, zNear, zFar);}return glm::ortho(left, right, bottom, top, zNear, zFar);}
};// ---------------------------- end of class definition ---------------------------- //// 模型
std::vector<Model> models;  // 场景
Model screen;   // 渲染一个四方形做屏幕// 着色器程序对象
GLuint program;
GLuint debugProgram;    // 调试用
GLuint shadowProgram;   // 绘制阴影的着色器程序对象// 相机
Camera camera;          // 正常渲染
Camera shadowCamera;    // 从光源方向渲染// 光源与阴影参数
int shadowMapResolution = 1024;             // 阴影贴图分辨率
GLuint shadowMapFBO;                        // 从光源方向进行渲染的帧缓冲
GLuint shadowTexture;                       // 阴影纹理// glut与交互相关变量
int windowWidth = 512;  // 窗口宽
int windowHeight = 512; // 窗口高
bool keyboardState[1024];   // 键盘状态数组 keyboardState[x]==true 表示按下x键// --------------- end of global variable definition --------------- //// 读取文件并且返回一个长字符串表示文件内容
std::string readShaderFile(std::string filepath)
{std::string res, line;std::ifstream fin(filepath);if (!fin.is_open()){std::cout << "文件 " << filepath << " 打开失败" << std::endl;exit(-1);}while (std::getline(fin, line)){res += line + '\n';}fin.close();return res;
}// 获取着色器对象
GLuint getShaderProgram(std::string fshader, std::string vshader)
{// 读取shader源文件std::string vSource = readShaderFile(vshader);std::string fSource = readShaderFile(fshader);const char* vpointer = vSource.c_str();const char* fpointer = fSource.c_str();// 容错GLint success;GLchar infoLog[512];// 创建并编译顶点着色器GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertexShader, 1, (const GLchar**)(&vpointer), NULL);glCompileShader(vertexShader);glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);   // 错误检测if (!success){glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);std::cout << "顶点着色器编译错误\n" << infoLog << std::endl;exit(-1);}// 创建并且编译片段着色器GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragmentShader, 1, (const GLchar**)(&fpointer), NULL);glCompileShader(fragmentShader);glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);   // 错误检测if (!success){glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);std::cout << "片段着色器编译错误\n" << infoLog << std::endl;exit(-1);}// 链接两个着色器到program对象GLuint shaderProgram = glCreateProgram();glAttachShader(shaderProgram, vertexShader);glAttachShader(shaderProgram, fragmentShader);glLinkProgram(shaderProgram);// 删除着色器对象glDeleteShader(vertexShader);glDeleteShader(fragmentShader);return shaderProgram;
}// 鼠标滚轮函数
void mouseWheel(int wheel, int direction, int x, int y)
{// zFar += 1 * direction * 0.1;glutPostRedisplay();    // 重绘
}// 鼠标运动函数
void mouse(int x, int y)
{// 调整旋转camera.yaw += 35 * (x - float(windowWidth) / 2.0) / windowWidth;camera.yaw = glm::mod(camera.yaw + 180.0f, 360.0f) - 180.0f;    // 取模范围 -180 ~ 180camera.pitch += -35 * (y - float(windowHeight) / 2.0) / windowHeight;camera.pitch = glm::clamp(camera.pitch, -89.0f, 89.0f);glutWarpPointer(windowWidth / 2.0, windowHeight / 2.0);glutPostRedisplay();    // 重绘
}// 键盘回调函数
void keyboardDown(unsigned char key, int x, int y)
{keyboardState[key] = true;
}
void keyboardDownSpecial(int key, int x, int y)
{keyboardState[key] = true;
}
void keyboardUp(unsigned char key, int x, int y)
{keyboardState[key] = false;
}
void keyboardUpSpecial(int key, int x, int y)
{keyboardState[key] = false;
}
// 根据键盘状态判断移动
void move()
{float cameraSpeed = 0.035f;// 相机控制if (keyboardState['w']) camera.position += cameraSpeed * camera.direction;if (keyboardState['s']) camera.position -= cameraSpeed * camera.direction;if (keyboardState['a']) camera.position -= cameraSpeed * glm::normalize(glm::cross(camera.direction, camera.up));if (keyboardState['d']) camera.position += cameraSpeed * glm::normalize(glm::cross(camera.direction, camera.up));if (keyboardState[GLUT_KEY_CTRL_L]) camera.position.y -= cameraSpeed;if (keyboardState[' ']) camera.position.y += cameraSpeed;// 光源位置控制if (keyboardState['i']) shadowCamera.position.x += cameraSpeed;if (keyboardState['I']) shadowCamera.position.x -= cameraSpeed;if (keyboardState['o']) shadowCamera.position.y += cameraSpeed;if (keyboardState['O']) shadowCamera.position.y -= cameraSpeed;glutPostRedisplay();    // 重绘
}// 初始化
void init()
{// 生成着色器程序对象program = getShaderProgram("shaders/fshader.fsh", "shaders/vshader.vsh");shadowProgram = getShaderProgram("shaders/shadow.fsh", "shaders/shadow.vsh");debugProgram = getShaderProgram("shaders/debug.fsh", "shaders/debug.vsh");// ------------------------------------------------------------------------ // // 读取 obj 模型Model tree1 = Model();tree1.translate = glm::vec3(2.5, 0, 2);tree1.scale = glm::vec3(0.0025, 0.0025, 0.0025);tree1.load("models/tree/tree02.obj");models.push_back(tree1);Model tree2 = Model();tree2.translate = glm::vec3(10, 0, 7);tree2.scale = glm::vec3(0.0015, 0.0015, 0.0015);tree2.load("models/tree/tree02.obj");models.push_back(tree2);Model plane = Model();plane.translate = glm::vec3(0, -1.1, 0);plane.scale = glm::vec3(10, 10, 10);plane.rotate = glm::vec3(0, 0, 0);plane.load("models/plane/plane.obj");models.push_back(plane);// 光源位置标志物Model vlight = Model();vlight.translate = glm::vec3(1, 0, -1);vlight.rotate = glm::vec3(-90, 0, 0);vlight.scale = glm::vec3(0.03, 0.03, 0.03);vlight.load("models/duck/12248_Bird_v1_L2.obj");models.push_back(vlight);// ------------------------------------------------------------------------ // // 生成一个四方形做荧幕 -- 用以显示纹理中的数据Mesh msquare;msquare.vertexPosition = { glm::vec3(-1, -1, 0), glm::vec3(1, -1, 0), glm::vec3(-1, 1, 0), glm::vec3(1, 1, 0) };msquare.vertexTexcoord = { glm::vec2(0, 0), glm::vec2(1, 0), glm::vec2(0, 1), glm::vec2(1, 1) };msquare.index = { 0,1,2,2,1,3 };msquare.bindData();screen.meshes.push_back(msquare);// ------------------------------------------------------------------------ // // 正交投影参数配置 -- 视界体范围 -- 调整到场景一般大小即可shadowCamera.left = -20;shadowCamera.right = 20;shadowCamera.bottom = -20;shadowCamera.top = 20;shadowCamera.position = glm::vec3(0, 4, 15);// 创建shadow帧缓冲glGenFramebuffers(1, &shadowMapFBO);// 创建阴影纹理glGenTextures(1, &shadowTexture);glBindTexture(GL_TEXTURE_2D, shadowTexture);glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, shadowMapResolution, shadowMapResolution, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);// 将阴影纹理绑定到 shadowMapFBO 帧缓冲glBindFramebuffer(GL_FRAMEBUFFER, shadowMapFBO);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowTexture, 0);glDrawBuffer(GL_NONE);glReadBuffer(GL_NONE);glBindFramebuffer(GL_FRAMEBUFFER, 0);// ------------------------------------------------------------------------ // glEnable(GL_DEPTH_TEST);  // 开启深度测试glClearColor(1.0, 1.0, 1.0, 1.0);   // 背景颜色
}// 显示回调函数
void display()
{move(); // 移动控制 -- 控制相机位置// 最后一个物体作为光源位置的标志物models.back().translate = shadowCamera.position + glm::vec3(0, 0, 2);// ------------------------------------------------------------------------ // // 从光源方向进行渲染glUseProgram(shadowProgram);glBindFramebuffer(GL_FRAMEBUFFER, shadowMapFBO);glClear(GL_DEPTH_BUFFER_BIT);glViewport(0, 0, shadowMapResolution, shadowMapResolution);// 光源看向世界坐标原点shadowCamera.direction = glm::normalize(glm::vec3(0, 0, 0) - shadowCamera.position);// 传视图矩阵glUniformMatrix4fv(glGetUniformLocation(shadowProgram, "view"), 1, GL_FALSE, glm::value_ptr(shadowCamera.getViewMatrix(false)));// 传投影矩阵glUniformMatrix4fv(glGetUniformLocation(shadowProgram, "projection"), 1, GL_FALSE, glm::value_ptr(shadowCamera.getProjectionMatrix(false)));// 从光源方向进行绘制for (auto m : models){m.draw(shadowProgram);}// ------------------------------------------------------------------------ // // 正常滴渲染glUseProgram(program);glBindFramebuffer(GL_FRAMEBUFFER, 0);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glViewport(0, 0, windowWidth, windowHeight);// 传视图矩阵glUniformMatrix4fv(glGetUniformLocation(program, "view"), 1, GL_FALSE, glm::value_ptr(camera.getViewMatrix()));// 传投影矩阵glUniformMatrix4fv(glGetUniformLocation(program, "projection"), 1, GL_FALSE, glm::value_ptr(camera.getProjectionMatrix()));/*// debug用 -- 从光源方向渲染场景 -- 测试正交投影是否正确glUniformMatrix4fv(glGetUniformLocation(program, "view"), 1, GL_FALSE, glm::value_ptr(shadowCamera.getViewMatrix(false)));glUniformMatrix4fv(glGetUniformLocation(program, "projection"), 1, GL_FALSE, glm::value_ptr(shadowCamera.getProjectionMatrix(false)));*/// 传递矩阵: 转换到光源坐标的变换矩阵glm::mat4 shadowVP = shadowCamera.getProjectionMatrix(false) * shadowCamera.getViewMatrix(false);glUniformMatrix4fv(glGetUniformLocation(program, "shadowVP"), 1, GL_FALSE, glm::value_ptr(shadowVP));// 传深度纹理glActiveTexture(GL_TEXTURE1);glBindTexture(GL_TEXTURE_2D, shadowTexture);glUniform1i(glGetUniformLocation(program, "shadowtex"), 1);// 传递光源位置glUniform3fv(glGetUniformLocation(program, "lightPos"), 1, glm::value_ptr(shadowCamera.position));// 传递相机位置glUniform3fv(glGetUniformLocation(program, "cameraPos"), 1, glm::value_ptr(camera.position));// 正常绘制for (auto m : models){m.draw(program);}// ------------------------------------------------------------------------ // // debug着色器输出一个四方形以显示深度纹理中的数据glDisable(GL_DEPTH_TEST);   // 需要取消深度测试以保证其覆盖在原画面上glUseProgram(debugProgram);glViewport(0, 0, windowWidth / 3, windowHeight / 3);// 传深度纹理glActiveTexture(GL_TEXTURE1);GLuint test = models.back().textureMap.begin()->second;glBindTexture(GL_TEXTURE_2D, shadowTexture);glUniform1i(glGetUniformLocation(debugProgram, "shadowtex"), 1);// 绘制screen.draw(debugProgram);glEnable(GL_DEPTH_TEST);// ------------------------------------------------------------------------ // glutSwapBuffers();                  // 交换缓冲区
}// -------------------------------- main -------------------------------- //int main(int argc, char** argv)
{glutInit(&argc, argv);              // glut初始化glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);glutInitWindowSize(windowWidth, windowHeight);// 窗口大小glutCreateWindow("8 - shadowMapping"); // 创建OpenGL上下文#ifdef __APPLE__
#elseglewInit();
#endifinit();// 绑定鼠标移动函数 -- //glutMotionFunc(mouse);  // 左键按下并且移动glutPassiveMotionFunc(mouse);   // 鼠标直接移动//glutMouseWheelFunc(mouseWheel); // 滚轮缩放// 绑定键盘函数glutKeyboardFunc(keyboardDown);glutSpecialFunc(keyboardDownSpecial);glutKeyboardUpFunc(keyboardUp);glutSpecialUpFunc(keyboardUpSpecial);glutDisplayFunc(display);           // 设置显示回调函数 -- 每帧执行glutMainLoop();                     // 进入主循环return 0;
}

着色器

shadow着色器

顶点:

#version 330 corelayout (location = 0) in vec3 vPosition;uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;void main()
{gl_Position = projection * view * model * vec4(vPosition, 1.0);
}

片元:

#version 330 corevoid main()
{             // gl_FragDepth = gl_FragCoord.z;
}

debug着色器

顶点:

#version 330 core// 顶点着色器输入
layout (location = 0) in vec3 vPosition;
layout (location = 1) in vec2 vTexcoord;// 传给片元着色器的变量
out vec2 texcoord;void main()
{gl_Position = vec4(vPosition, 1.0);texcoord = vTexcoord;
}

片元:

#version 330 corein vec2 texcoord;
out vec4 fColor;// 纹理数据
uniform sampler2D shadowtex;    // shadow纹理void main()
{       fColor = vec4(vec3(texture2D(shadowtex, texcoord).r*0.5+0.5), 1);
}

正常渲染的着色器

顶点:

#version 330 core// 顶点着色器输入
layout (location = 0) in vec3 vPosition;
layout (location = 1) in vec2 vTexcoord;
layout (location = 2) in vec3 vNormal;out vec3 worldPos;
out vec2 texcoord;
out vec3 normal;uniform mat4 model;         // 模型变换矩阵
uniform mat4 view;          // 模型变换矩阵
uniform mat4 projection;    // 模型变换矩阵void main()
{gl_Position = projection * view * model * vec4(vPosition, 1.0);// 传递到片段着色器texcoord = vTexcoord;   worldPos = (model * vec4(vPosition, 1.0)).xyz;normal = (model * vec4(vNormal, 0.0)).xyz;
}

片元:

#version 330 corein vec3 worldPos;   // 当前片元的世界坐标
in vec2 texcoord;   // 纹理坐标
in vec3 normal;     // 法向量out vec4 fColor;    // 片元输出像素的颜色uniform sampler2D texture;  // 纹理图片
uniform sampler2D shadowtex;    // shadow纹理uniform mat4 shadowVP;  // 转换到光源坐标的变换矩阵uniform vec3 lightPos;  // 光源位置
uniform vec3 cameraPos; // 相机位置float shadowMapping(sampler2D tex, mat4 shadowVP, vec4 worldPos) {// 转换到光源坐标vec4 lightPos = shadowVP * worldPos;lightPos = vec4(lightPos.xyz/lightPos.w, 1.0);lightPos = lightPos*0.5 + 0.5;// 计算shadowmappingfloat closestDepth = texture2D(tex, lightPos.xy).r;   // shadowmap中最近点的深度float currentDepth = lightPos.z;    // 当前点的深度float isInShadow = (currentDepth>closestDepth+0.005) ? (1.0) : (0.0);return isInShadow;
}float phong(vec3 worldPos, vec3 cameraPos, vec3 lightPos, vec3 normal)
{vec3 N = normalize(normal);vec3 V = normalize(worldPos - cameraPos);vec3 L = normalize(worldPos - lightPos);vec3 R = reflect(L, N);float ambient = 0.3;float diffuse = max(dot(N, -L), 0) * 0.7;float specular = pow(max(dot(-R, V), 0), 50.0) * 1.1;return ambient + diffuse + specular;
}void main()
{fColor.rgb =  texture2D(texture, texcoord).rgb;float isInShadow = shadowMapping(shadowtex, shadowVP, vec4(worldPos, 1.0));if(isInShadow==0) {fColor.rgb *= phong(worldPos, cameraPos, lightPos, normal);} else {fColor.rgb *= 0.3;  // only ambient}
}

OpenGL学习(九)阴影映射(shadowMapping)相关推荐

  1. OpenGL基础52:阴影映射(上)

    参考于:https://learnopengl.com/#!Advanced-Lighting/Shadows/Shadow-Mapping 一.游戏中的阴影 阴影是光线被阻挡的结果,当一个光源的光线 ...

  2. Android OpenGL ES 学习(九) – 坐标系统和实现3D效果

    OpenGL 学习教程 Android OpenGL ES 学习(一) – 基本概念 Android OpenGL ES 学习(二) – 图形渲染管线和GLSL Android OpenGL ES 学 ...

  3. [转载] [OpenGL] shadow mapping(实时阴影映射)

    参考链接: Java中静态函数的阴影(方法隐藏) 转载原创:ZJU_fish1996   http://blog.csdn.net/zju_fish1996/article/details/51932 ...

  4. OpenGL基础53:阴影映射(下)

    接上文:OpenGL基础52:阴影映射(上) 五.阴影失真 按照上文的计算的结果,一个很明显的问题是:对于参与计算深度贴图的物体,其表面可以看到这样的栅格状的阴影,这种常见的错误表现也叫做阴影失真(S ...

  5. [OpenGL] shadow mapping(实时阴影映射)

    source:原文地址 code:点击可以直接下载源代码 1978年,Lance Williams在其发表的论文<Casting curved shadows on curved surface ...

  6. openGL实现阴影映射(Shadow Mapping)

    openGL系列文章目录 文章目录 openGL系列文章目录 前言 阴影映射 阴影映射原理 二.使用步骤 显示效果 源码下载 参考 前言 阴影是光线被阻挡的结果:当一个光源的光线由于其他物体的阻挡不能 ...

  7. OpenGL学习(十)天空盒

    目录 写在前面 天空盒简介 创建立方体贴图 渲染一个立方体 立方体贴图着色器 开始绘制天空盒 完整代码 着色器 c++ 写在前面 上一篇博客回顾:OpenGL学习(九)阴影映射(shadowMappi ...

  8. opengl 深度详解_一步步学OpenGL(23) -《阴影贴图1》

    教程 23 阴影贴图1 原文: http://ogldev.atspace.co.uk/www/tutorial23/tutorial23.html CSDN完整版专栏: https://blog.c ...

  9. 【我的OpenGL学习进阶之旅】OpenGL ES 3.0新功能

    目录 1.1 纹理 1.2 着色器 1.3 几何形状 1.4 缓冲区对象 1.5 帧缓冲区 OpenGL ES 2.0 开创了手持设备可编程着色器的时代,在驱动大量设备的游戏.应用程序和用户接口中获得 ...

最新文章

  1. 旋转遍历矩阵 Spiral Matrix
  2. python软件怎么用-Python如何创建应用程序
  3. 框架下cookie的使用_为什么自动化运维系统越来越多使用都RESTful API?
  4. 启明云端分享| 86盒串口屏烧录说明
  5. oracle错误 904,ORACLE 导出错误 EXP-00008: 遇到 Oracle 错误 904
  6. 加载gif动图_GIF生成神器——ScreenToGif
  7. 【高校宿舍管理系统】第十一章 学生系统
  8. 经典线程同步总结 关键段 事件 互斥量 信号量
  9. iPhone手机获取uuid 方法
  10. 新概念系列之《Part 1 Lesson 137 A pleasant dream》
  11. 人工合成生命的最新进展比AI还快
  12. 麦考利久期公式(c语言实现)
  13. HTML期末学生作业~HTML+CSS+JavaScript仿猫眼电影在线网站
  14. 域名、dns、服务器、IP、主机名(写的好)
  15. POJ2187-最远点对-旋转卡壳(怎么开心怎么读)
  16. Win32 Disk Imager Error 5: Access is Denied 解决方案
  17. mysql里all什么意思_mysql中all的用法是什么
  18. Problems and Solutions
  19. 七海的java学习笔记(八)
  20. 刷Trailhead笔记- 用aura component创建app

热门文章

  1. 整理了下这三天【面试】遇到的让人心惊胆颤的难题。
  2. Mac Docker Desktop “Mounts denied: EOF.“解决方法
  3. 快速了解位运算符——与()、非(~)、或(|)、异或(^)
  4. java unsafe park_java – Unsafe.park vs Object.wait
  5. libreoffice转换文档的方法(支持各平台各版本的libreoffice)
  6. 耶鲁大学的心态 ,送给正在奋斗的人!
  7. ”舌上有龙泉,杀人不见血,生而为人,需得择善而行”
  8. 直播|BIA Separations 和元生物两位大咖关于质粒DNA的制造工艺和质量控制
  9. 他升职加薪,竟然是因为这样舔狗?
  10. 诸葛io接口调用学习