LearnOpenGL学习笔记—高级光照 03(2):点阴影
【项目地址:点击这里这里这里】
本节对应官网学习内容:点阴影
1 万向阴影贴图
上个笔记我们学到了如何使用阴影映射技术创建动态阴影。
虽然效果不错,但它只适合定向光,因为阴影只是在单一定向光源下生成的。
深度(阴影)贴图生成自定向光的视角,所以它也叫定向阴影映射。
- 本节我们的焦点是在各种方向生成动态阴影。这个技术可以适用于点光源,生成所有方向上的阴影。
这个技术叫做点光阴影,过去的名字是万向阴影贴图(omnidirectional shadow maps)技术。
本节代码基于前面的阴影映射教程。
算法和定向阴影映射差不多:我们从光的透视图生成一个深度贴图,基于当前fragment位置来对深度贴图采样,然后用储存的深度值和每个fragment进行对比,看看它是否在阴影中。
定向阴影映射和万向阴影映射的主要不同在于深度贴图的使用上。
对于深度贴图,我们需要从一个点光源的所有渲染场景,普通2D深度贴图显然是不能做到的。
但是如果我们使用立方体贴图会怎样?
因为立方体贴图可以储存6个面的环境数据,它可以将整个场景渲染到立方体贴图的每个面上,把它们当作点光源四周的深度值来采样。
生成后的深度立方体贴图被传递到光照的片段着色器,它会用一个方向向量来采样立方体贴图,从而得到当前的fragment的深度(从光的透视图)。
期间大部分复杂的事情已经在阴影映射教程中有所阐述,算法只是在深度立方体贴图生成上稍微复杂一点。
1.1 生成深度立方体贴图
为创建一个能体现一个点光周围的深度值的立方体贴图,我们必须渲染场景6次:每次一个面。
显然渲染场景6次需要6个不同的视图矩阵,每次把一个不同的立方体贴图面附加到帧缓冲对象上,然后渲染场景。
举个例子来说比如这样:
for(int i = 0; i < 6; i++)
{
GLuint face = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i;
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, face, depthCubemap, 0);
BindViewMatrix(lightViewMatrices[i]);
RenderScene();
}
这会很耗费性能,因为每做一个深度贴图需要进行很多渲染调用。
于是我们将转而使用另外的一个小技巧来做这件事,几何着色器允许我们使用一次渲染过程来建立深度立方体贴图。
下面是我们的步骤(略去在这之前的相机宣告,输入检测,开窗,建立shader,设置材质,宣告光源等步骤)
首先,建立深度贴图的FBO
// 设置深度贴图的FBO
GLuint depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);
而后建立立方体贴图
GLuint depthCubemap;
glGenTextures(1, &depthCubemap);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
为生成这个立方体贴图的每个面,它们会作为深度值的2D纹理图像
const GLuint SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
for (GLuint i = 0; i < 6; ++i)
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_DEPTH_COMPONENT, SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
同时设置合适的纹理参数
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
我们本来是要渲染场景六次,每次渲染到不同的面上,从而得到这个立方体贴图,但是我们会在之后用几何着色器,它会让我们只在一个过程里渲染。
所以我们这直接把这个立方体贴图作为帧缓冲的深度附件
// 把立方体贴图附加成帧缓冲的深度附件
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubemap, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "Framebuffer not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
在生成万象阴影贴图时,我们会有两个阶段:
一个阶段是生成深度贴图,我们把光源看到的视角渲染到深度贴图里
第二个阶段是使用深度贴图渲染,从场景中创建阴影。
抽象来看代码会是这个结构:
// 1. 渲染到深度贴图
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 2. 渲染阴影
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
RenderScene();
这个过程和之前的阴影映射一样,虽然这次是立方体贴图的深度纹理。
那么接下来我们还是需要计算出合适的变换矩阵,来渲染六个方向的深度贴图
1.2 光空间的变换
与阴影映射教程类似,我们将需要光空间的变换矩阵T,但是这次是每个面都有一个。
每个光空间的变换矩阵包含了投影和视图矩阵。
对于投影矩阵来说,我们将使用透视投影矩阵;因为光源代表一个空间中的点,所以透视投影矩阵更有意义。
每个光空间变换矩阵使用同样的投影矩阵:
GLfloat aspect = (GLfloat)SHADOW_WIDTH / (GLfloat)SHADOW_HEIGHT;
GLfloat near = 1.0f;
GLfloat far = 25.0f;
glm::mat4 shadowProj = glm::perspective(glm::radians(90.0f), aspect, near, far);
这里glm::perspective的视野参数,设置为90度。
90度我们才能保证视野足够大到可以合适地填满立方体贴图的一个面,立方体贴图的所有面都能与其他面在边缘对齐。
因为投影矩阵在每个方向上并不会改变,我们可以在6个变换矩阵中重复使用。
之后我们要为每个方向提供一个不同的视图矩阵。
用glm::lookAt创建6个观察方向,每个都按顺序注视着立方体贴图的的一个方向:右、左、上、下、近、远:
std::vector<glm::mat4> shadowTransforms;
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(1.0, 0.0, 0.0), glm::vec3(0.0, -1.0, 0.0)));
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(-1.0, 0.0, 0.0), glm::vec3(0.0, -1.0, 0.0)));
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(0.0, 1.0, 0.0), glm::vec3(0.0, 0.0, 1.0)));
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(0.0, -1.0, 0.0), glm::vec3(0.0, 0.0, -1.0)));
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(0.0, 0.0, 1.0), glm::vec3(0.0, -1.0, 0.0)));
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(0.0, 0.0, -1.0), glm::vec3(0.0, -1.0, 0.0)));
这里我们创建了6个视图矩阵,把它们乘以投影矩阵,来得到6个不同的光空间变换矩阵。
glm::LookAt函数需要一个原始位置、一个目标位置和表示世界空间中的上向量的向量。
这些变换矩阵发送到着色器渲染到立方体贴图里。
1.3 深度着色器
为了把值渲染到深度立方体贴图,我们将需要3个着色器:顶点和像素着色器,以及一个它们之间的几何着色器。
顶点着色器简单地将顶点变换到世界空间,然后直接发送到几何着色器:
#version 330 core
layout (location = 0) in vec3 position;
uniform mat4 model;
void main()
{
gl_Position = model * vec4(position, 1.0);
}
紧接着几何着色器以3个三角形的顶点作为输入,它还有一个光空间变换矩阵的uniform数组。
几何着色器接下来会负责将顶点变换到光空间
几何着色器有一个内建变量叫做gl_Layer,它指定发散出基本图形送到立方体贴图的哪个面。
当不管它时,几何着色器就会像往常一样把它的基本图形发送到输送管道的下一阶段,但当我们更新这个变量就能控制每个基本图形将渲染到立方体贴图的哪一个面。
我们输入一个三角形,输出总共6个三角形(6*3顶点,所以总共18个顶点)。
在main函数中,我们遍历立方体贴图的6个面,我们每个面指定为一个输出面,把这个面的interger(整数)存到gl_Layer。
然后,我们通过把面的光空间变换矩阵乘以FragPos,将每个世界空间顶点变换到相关的光空间,生成每个三角形。
注意,我们还要将最后的FragPos变量发送给像素着色器,我们需要计算一个深度值。
#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices=18) out;
uniform mat4 shadowMatrices[6];
out vec4 FragPos; // FragPos from GS (output per emitvertex)
void main()
{
for(int face = 0; face < 6; ++face)
{
gl_Layer = face; // built-in variable that specifies to which face we render.
for(int i = 0; i < 3; ++i) // for each triangle's vertices
{
FragPos = gl_in[i].gl_Position;
gl_Position = shadowMatrices[face] * FragPos;
EmitVertex();
}
EndPrimitive();
}
}
这次我们将计算自己的深度,这个深度就是每个fragment位置和光源位置之间的线性距离。计算自己的深度值使得之后的阴影计算更加直观。
#version 330 core
in vec4 FragPos;
uniform vec3 lightPos;
uniform float far_plane;
void main()
{
// get distance between fragment and light source
float lightDistance = length(FragPos.xyz - lightPos);
// map to [0;1] range by dividing by far_plane
lightDistance = lightDistance / far_plane;
// write this as modified depth
gl_FragDepth = lightDistance;
}
像素着色器将来自几何着色器的FragPos、光的位置和视锥的远平面值作为输入。
这里我们把fragment和光源之间的距离,映射到0到1的范围,把它写入为fragment的深度值。
使用这些着色器渲染场景,然后在立方体贴图附加的帧缓冲对象激活以后,我们会得到一个完全填充的深度立方体贴图,以便于进行第二阶段的阴影计算。
1.4 生成万向阴影贴图
所有事情都做好了,是时候来渲染万向阴影(Omnidirectional Shadow)了。
尽管这次我们绑定的深度贴图是一个立方体贴图,并且将光的投影的远平面发送给了着色器,但是这个过程和定向阴影映射教程是相似的。
抽象地用代码表示
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.Use();
// 。。。传递uniform
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
// ... 绑定其他uniform
RenderScene();
这里的RenderScene函数含义是在一个大立方体房间中渲染一些立方体,它们散落在大立方体各处,光源在场景中央。
顶点着色器和像素着色器和原来的阴影映射着色器大部分都一样:不同之处是在光空间中像素着色器不再需要一个fragment位置,现在我们可以使用一个方向向量采样深度值。
为了大立方体内被照亮,我们会翻转法线。
顶点着色器:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords;
out vec2 TexCoords;
out VS_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoords;
} vs_out;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
uniform int reverse_normals;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
vs_out.FragPos = vec3(model * vec4(position, 1.0));
if(reverse_normals==0){
vs_out.Normal = transpose(inverse(mat3(model))) * normal;
}else{
vs_out.Normal = -(transpose(inverse(mat3(model))) * normal);
}
vs_out.TexCoords = texCoords;
}
片段着色器:
#version 330 core
out vec4 FragColor;
in VS_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoords;
} fs_in;
uniform sampler2D diffuseTexture;
uniform samplerCube depthMap;
uniform vec3 lightPos;
uniform vec3 viewPos;
uniform float far_plane;
uniform bool shadows;
float ShadowCalculation(vec3 fragPos)
{
// 得到fragment的位置与光的位置之间的向量
vec3 fragToLight = fragPos - lightPos;
// 使用这个向量作为一个方向向量去对立方体贴图进行采样
float closestDepth = texture(depthMap, fragToLight).r;
// closestDepth值现在在0到1的范围内,把他乘以far_plane,将其转换回0到far_plane的范围
closestDepth *= far_plane;
// 获取当前fragment和光源之间的深度值,使用fragToLight的长度来获取它
float currentDepth = length(fragToLight);
//将两个深度值对比一下,看看哪一个更接近
float bias = 0.05; // 包含一个阴影偏移,避免阴影失真
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
//return closestDepth;
return shadow;
}
void main()
{
vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;
vec3 normal = normalize(fs_in.Normal);
vec3 lightColor = vec3(1.0);
// Ambient
vec3 ambient = 0.3 * color;
// Diffuse
vec3 lightDir = normalize(lightPos - fs_in.FragPos);
float diff = max(dot(lightDir, normal), 0.0);
vec3 diffuse = diff * lightColor;
// Specular
vec3 viewDir = normalize(viewPos - fs_in.FragPos);
vec3 reflectDir = reflect(-lightDir, normal);
float spec = 0.0;
vec3 halfwayDir = normalize(lightDir + viewDir);
spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);
vec3 specular = spec * lightColor;
// Calculate shadow
float shadow = shadows ? ShadowCalculation(fs_in.FragPos) : 0.0;
vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;
//FragColor = vec4(vec3( ShadowCalculation(fs_in.FragPos)/far_plane),1.0);
FragColor = vec4(lighting, 1.0f);
}
- 有一些细微的不同:光照代码一样,但我们现在有深度贴图的uniform变量samplerCube
shadowCalculation函数用fragment的位置作为它的参数,取代了光空间的fragment位置。
我们现在还要引入光的视锥的远平面值,后面我们会需要它。
像素着色器的最后,我们计算出阴影元素,当fragment在阴影中时它是1.0,不在阴影中时是0.0。
我们使用计算出来的阴影元素去影响光照的diffuse和specular元素。
1.5 阴影结果
有了这些着色器,我们已经能得到非常好的阴影效果了,这次从一个点光源所有周围方向上都有阴影。
有一个位于场景中心的点光源,看起来会像这样:
1.6 显示立方体贴图深度缓冲
一个简单的把深度缓冲显示出来的技巧是,在ShadowCalculation函数中计算标准化的closestDepth变量,把变量显示为:
FragColor = vec4(vec3(closestDepth / far_plane), 1.0);
结果是一个灰度场景,每个颜色代表着场景的线性深度值:
2 PCF
由于万向阴影贴图基于传统阴影映射的原则,它也继承了由解析度产生的非真实感。
如果放大就会看到锯齿边了。
PCF或称Percentage-closer filtering允许我们通过对fragment位置周围过滤多个样本,并对结果平均化。
如果我们用和前面教程同样的那个简单的PCF过滤器,并加入第三个维度,就是这样的:
float ShadowCalculation(vec3 fragPos)
{
// 得到fragment的位置与光的位置之间的向量
vec3 fragToLight = fragPos - lightPos;
// 使用这个向量作为一个方向向量去对立方体贴图进行采样
float closestDepth = texture(depthMap, fragToLight).r;
// closestDepth值现在在0到1的范围内,把他乘以far_plane,将其转换回0到far_plane的范围
closestDepth *= far_plane;
// 获取当前fragment和光源之间的深度值,使用fragToLight的长度来获取它
float currentDepth = length(fragToLight);
//将两个深度值对比一下,看看哪一个更接近
float shadow = 0.0;
float bias = 0.05;
float samples = 4.0;
float offset = 0.1;
for(float x = -offset; x < offset; x += offset / (samples * 0.5))
{
for(float y = -offset; y < offset; y += offset / (samples * 0.5))
{
for(float z = -offset; z < offset; z += offset / (samples * 0.5))
{
float closestDepth = texture(depthMap, fragToLight + vec3(x, y, z)).r;
closestDepth *= far_plane; // Undo mapping [0;1]
if(currentDepth - bias > closestDepth)
shadow += 1.0;
}
}
}
shadow /= (samples * samples * samples);
//return closestDepth;
return shadow;
}
这段代码和我们传统的阴影映射没有多少不同。
这里我们根据样本的数量动态计算了纹理偏移量,我们在三个轴向采样三次,最后对子样本进行平均化。
现在阴影看起来更加柔和平滑了,由此得到更加真实的效果:
然而,samples设置为4.0,每个fragment我们会得到总共64个样本,这太多了
大多数这些采样都是多余的,与其在原始方向向量附近处采样,不如在采样方向向量的垂直方向进行采样更有意义。
可是,没有方式能够指出哪一个子方向是多余的。
有个技巧可以使用,用一个偏移量方向数组,它们差不多都是分开的,每一个指向完全不同的方向,剔除彼此接近的那些子方向。下面就是一个有着20个偏移方向的数组:
vec3 sampleOffsetDirections[20] = vec3[]
(
vec3( 1, 1, 1), vec3( 1, -1, 1), vec3(-1, -1, 1), vec3(-1, 1, 1),
vec3( 1, 1, -1), vec3( 1, -1, -1), vec3(-1, -1, -1), vec3(-1, 1, -1),
vec3( 1, 1, 0), vec3( 1, -1, 0), vec3(-1, -1, 0), vec3(-1, 1, 0),
vec3( 1, 0, 1), vec3(-1, 0, 1), vec3( 1, 0, -1), vec3(-1, 0, -1),
vec3( 0, 1, 1), vec3( 0, -1, 1), vec3( 0, -1, -1), vec3( 0, 1, -1)
);
然后我们把PCF算法与从sampleOffsetDirections得到的样本数量进行适配,使用它们从立方体贴图里采样。这么做的好处是与之前的PCF算法相比,我们需要的样本数量变少了。
float shadow = 0.0;
float bias = 0.15;
int samples = 20;
float viewDistance = length(viewPos - fragPos);
float diskRadius = 0.05;
for(int i = 0; i < samples; ++i)
{
float closestDepth = texture(depthMap, fragToLight + sampleOffsetDirections[i] * diskRadius).r;
closestDepth *= far_plane; // Undo mapping [0;1]
if(currentDepth - bias > closestDepth)
shadow += 1.0;
}
shadow /= float(samples);
这里我们把一个偏移量添加到指定的diskRadius中,它在fragToLight方向向量周围从立方体贴图里采样。
另一个在这里可以应用的有意思的技巧是,我们可以基于观察者里一个fragment的距离来改变diskRadius;这样我们就能根据观察者的距离来增加偏移半径了,当距离更远的时候阴影更柔和,更近了就更锐利。
float diskRadius = (1.0 + (viewDistance / far_plane)) / 25.0;
我们添加到每个样本的bias(偏移)高度依赖于上下文,总是要根据场景进行微调的。
使用几何着色器来生成深度贴图不会一定比每个面渲染场景6次更快。
使用几何着色器有它自己的性能局限,在第一个阶段使用它可能获得更好的性能表现。这取决于环境的类型,以及特定的显卡驱动等等,所以如果很关心性能,就要确保对两种方法有大致了解,然后选择对场景来说更高效的那个。
3 代码
shader代码都已在上面附上
以下列出main.cpp的代码
#include <iostream>
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include "Shader.h"
#include "Camera.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <vector>
//检测窗口变化的回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
//鼠标移动镜头
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
//滚轮缩放
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
//检测输入
void processInput(GLFWwindow *window);
//导入图
unsigned int LoadImageToGPU(const char* filename, GLint internalFormat, GLenum format);
//渲染场景
void RenderScene(const Shader* shader);
void RenderCube();
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
#pragma region Camera Declare
//建立camera
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
Camera camera(cameraPos, cameraTarget, cameraUp);
#pragma endregion
#pragma region Input Declare
//移动用
float deltaTime = 0.0f; // 当前帧与上一帧的时间差
float lastFrame = 0.0f; // 上一帧的时间
void processInput(GLFWwindow* window) {
//看是不是按下esc键 然后退出
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
glfwSetWindowShouldClose(window, true);
}
//更流畅点的摄像机系统
if (deltaTime != 0) {
camera.cameraPosSpeed = 5 * deltaTime;
}
//camera前后左右根据镜头方向移动
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.PosUpdateForward();
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.PosUpdateBackward();
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.PosUpdateLeft();
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.PosUpdateRight();
if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS)
camera.PosUpdateUp();
if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS)
camera.PosUpdateDown();
}
float lastX;
float lastY;
bool firstMouse = true;
unsigned int planeVAO;
//鼠标控制镜头方向
void mouse_callback(GLFWwindow* window, double xpos, double ypos) {
if (firstMouse == true)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float deltaX, deltaY;
float sensitivity = 0.05f;
deltaX = (xpos - lastX)*sensitivity;
deltaY = (ypos - lastY)*sensitivity;
lastX = xpos;
lastY = ypos;
camera.ProcessMouseMovement(deltaX, deltaY);
};
//缩放
float fov = 45.0f;
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
if (fov >= 1.0f && fov <= 45.0f)
fov -= yoffset;
if (fov <= 1.0f)
fov = 1.0f;
if (fov >= 45.0f)
fov = 45.0f;
}
#pragma endregion
int main() {
#pragma region Open a Window
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// Open GLFW Window
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "My OpenGL Game", NULL, NULL);
if (window == NULL)
{
printf("Open window failed.");
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
// 关闭鼠标显示
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// 回调函数监听鼠标
glfwSetCursorPosCallback(window, mouse_callback);
// 回调函数监听滚轮
glfwSetScrollCallback(window, scroll_callback);
// 每当窗口改变大小就会启动
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// Init GLEW
glewExperimental = true;
if (glewInit() != GLEW_OK)
{
printf("Init GLEW failed.");
glfwTerminate();
return -1;
}
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
GLboolean shadows = true;
GLuint woodTexture;
GLuint planeVAO;
#pragma endregion
#pragma region Init Shader Program
// 所需要的shader
Shader* shader = new Shader("point_shadows.vert", "point_shadows.frag");
Shader* simpleDepthShader = new Shader("point_shadows_depth.vert", "point_shadows_depth.frag", "point_shadows_depth.geom");
#pragma endregion
// 设置材质
shader->use();
glUniform1i(glGetUniformLocation(shader->ID, "diffuseTexture"), 0);
glUniform1i(glGetUniformLocation(shader->ID, "depthMap"), 1);
woodTexture = LoadImageToGPU("wood.jpg", GL_RGB, GL_RGB);
// 宣告光源
glm::vec3 lightPos(0.0f, 0.0f, 0.0f);
// 设置深度贴图的FBO
GLuint depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);
// 创建深度贴图
GLuint depthCubemap;
glGenTextures(1, &depthCubemap);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
const GLuint SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
for (GLuint i = 0; i < 6; ++i)
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_DEPTH_COMPONENT, SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
// 把立方体贴图附加成帧缓冲的深度附件
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubemap, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "Framebuffer not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 渲染循环
while (!glfwWindowShouldClose(window))
{
// 显示相关的数据
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// 检测输入
processInput(window);
// 0. 创建六个面光空间变换的矩阵
GLfloat aspect = (GLfloat)SHADOW_WIDTH / (GLfloat)SHADOW_HEIGHT;
GLfloat near = 1.0f;
GLfloat far = 25.0f;
glm::mat4 shadowProj = glm::perspective(glm::radians(90.0f), aspect, near, far);
std::vector<glm::mat4> shadowTransforms;
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(1.0, 0.0, 0.0), glm::vec3(0.0, -1.0, 0.0)));
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(-1.0, 0.0, 0.0), glm::vec3(0.0, -1.0, 0.0)));
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(0.0, 1.0, 0.0), glm::vec3(0.0, 0.0, -1.0)));
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(0.0, -1.0, 0.0), glm::vec3(0.0, 0.0, -1.0)));
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(0.0, 0.0, 1.0), glm::vec3(0.0, -1.0, 0.0)));
shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(0.0, 0.0, -1.0), glm::vec3(0.0, -1.0, 0.0)));
// 1. 渲染到深度贴图
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
simpleDepthShader->use();
for (GLuint i = 0; i < 6; ++i)
glUniformMatrix4fv(glGetUniformLocation(simpleDepthShader->ID, ("shadowMatrices[" + std::to_string(i) + "]").c_str()), 1, GL_FALSE, glm::value_ptr(shadowTransforms[i]));
glUniform1f(glGetUniformLocation(simpleDepthShader->ID, "far_plane"), far);
glUniform3f(glGetUniformLocation(simpleDepthShader->ID, "lightPos"),lightPos.x, lightPos.y, lightPos.z);
RenderScene(simpleDepthShader);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 2. 正常渲染
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader->use();
glm::mat4 projection = glm::perspective(glm::radians(fov), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
glUniformMatrix4fv(glGetUniformLocation(shader->ID, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
glUniformMatrix4fv(glGetUniformLocation(shader->ID, "view"), 1, GL_FALSE, glm::value_ptr(view));
// Set light uniforms
glUniform3f(glGetUniformLocation(shader->ID, "lightPos"),lightPos.x, lightPos.y, lightPos.z);
glUniform3f(glGetUniformLocation(shader->ID, "viewPos"),camera.Position.x, camera.Position.y, camera.Position.z);
glUniform1i(glGetUniformLocation(shader->ID, "shadows"), shadows);
glUniform1f(glGetUniformLocation(shader->ID, "far_plane"), far);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, woodTexture);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
RenderScene(shader);
// Swap the buffers
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
void RenderScene(const Shader * shader)
{
// Room cube
glm::mat4 model;
model = glm::scale(model, glm::vec3(14.0));
glUniformMatrix4fv(glGetUniformLocation(shader->ID, "model"), 1, GL_FALSE, glm::value_ptr(model));
glDisable(GL_CULL_FACE); // Note that we disable culling here since we render 'inside' the cube instead of the usual 'outside' which throws off the normal culling methods.
glUniform1i(glGetUniformLocation(shader->ID, "reverse_normals"), 1); // A small little hack to invert normals when drawing cube from the inside so lighting still works.
RenderCube();
glUniform1i(glGetUniformLocation(shader->ID, "reverse_normals"), 0); // And of course disable it
glEnable(GL_CULL_FACE);
// Cubes
model = glm::mat4();
model = glm::translate(model, glm::vec3(3.0f, 2.0f, 0));
glUniformMatrix4fv(glGetUniformLocation(shader->ID, "model"), 1, GL_FALSE, glm::value_ptr(model));
RenderCube();
model = glm::mat4();
model = glm::translate(model, glm::vec3(-3.0f, -2.0f, 0));
glUniformMatrix4fv(glGetUniformLocation(shader->ID, "model"), 1, GL_FALSE, glm::value_ptr(model));
RenderCube();
model = glm::mat4();
model = glm::translate(model, glm::vec3(3.0f, 0, 2.0f));
glUniformMatrix4fv(glGetUniformLocation(shader->ID, "model"), 1, GL_FALSE, glm::value_ptr(model));
RenderCube();
model = glm::mat4();
model = glm::translate(model, glm::vec3(-3.0f, 0, -2.0f));
glUniformMatrix4fv(glGetUniformLocation(shader->ID, "model"), 1, GL_FALSE, glm::value_ptr(model));
RenderCube();
model = glm::mat4();
model = glm::translate(model, glm::vec3(0, -2.0f, -3.0f));
glUniformMatrix4fv(glGetUniformLocation(shader->ID, "model"), 1, GL_FALSE, glm::value_ptr(model));
RenderCube();
model = glm::mat4();
model = glm::translate(model, glm::vec3(0, 2.0f, 3.0f));
glUniformMatrix4fv(glGetUniformLocation(shader->ID, "model"), 1, GL_FALSE, glm::value_ptr(model));
RenderCube();
}
// RenderCube() Renders a 1x1 3D cube in NDC.
GLuint cubeVAO = 0;
GLuint cubeVBO = 0;
void RenderCube()
{
// Initialize (if necessary)
if (cubeVAO == 0)
{
GLfloat vertices[] = {
// Back face
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, // Bottom-left
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, // top-right
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, // bottom-right
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, // top-right
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, // bottom-left
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,// top-left
// Front face
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom-left
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, // bottom-right
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, // top-right
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, // top-right
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, // top-left
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom-left
// Left face
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-right
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top-left
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-left
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-left
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // bottom-right
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-right
// Right face
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-left
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-right
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top-right
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-right
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-left
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // bottom-left
// Bottom face
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, // top-right
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f, // top-left
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,// bottom-left
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, // bottom-left
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, // bottom-right
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, // top-right
// Top face
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,// top-left
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom-right
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, // top-right
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom-right
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,// top-left
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f // bottom-left
};
glGenVertexArrays(1, &cubeVAO);
glGenBuffers(1, &cubeVBO);
// Fill buffer
glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// Link vertex attributes
glBindVertexArray(cubeVAO);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
// Render Cube
glBindVertexArray(cubeVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
//确保视口与新的窗口尺寸匹配;注意宽度和高度将大于屏幕显示上指定的尺寸
glViewport(0, 0, width, height);
}
//加载一般的图片
unsigned int LoadImageToGPU(const char* filename, GLint internalFormat, GLenum format) {
unsigned int TexBuffer;
glGenTextures(1, &TexBuffer);
glBindTexture(GL_TEXTURE_2D, TexBuffer);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
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_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
// 加载并生成纹理
int width, height, nrChannel;
unsigned char *data = stbi_load(filename, &width, &height, &nrChannel, 0);
if (data) {
glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
printf("Failed to load texture");
}
stbi_image_free(data);
return TexBuffer;
}