LearnOpenGL——点光源阴影笔记

news2024/11/28 13:40:50

LearnOpenGL——点光源阴影笔记

  • 点光源阴影
    • 一、生成深度立方体贴图
      • 1. 创建立方体贴图
      • 2. 光空间的变换
      • 3. 深度着色器
    • 二、万向阴影贴图
    • 三、PCF

点光源阴影

点光阴影(也叫万向阴影贴图(Omnidirectional Shadow Maps,OSM))适用于点光源,生成所有方向上的阴影,以前也叫做万向阴影贴图。点光源发出的光线向四面八方扩散,因此需要从多个方向考虑深度信息,普通的2D深度贴图无法满足。我们将会使用立方体贴图

一、生成深度立方体贴图

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

多次渲染场景会非常的耗性能。几何着色器允许我们使用一次渲染过程来建立深度立方体贴图。

首先,创建一个立方体贴图

GLuint depthCubemap;
glGenTextures(1, &depthCubemap);

然后生成立方体的每个面,将其作为2D深度纹理图像

const GLuint SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
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);

由于我们将使用一个几何着色器,它允许我们把所有面在一个过程渲染,我们可以使用glFramebufferTexture直接把立方体贴图附加成帧缓冲的深度附件:

glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubemap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

万向阴影贴图有两个渲染阶段:

  • 首先我们生成深度贴图
// 1. first render to depth cubemap
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
  • 然后我们正常使用深度贴图渲染,在场景中创建阴影。
// 2. then render scene as normal with shadow mapping (using depth cubemap)
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
RenderScene();

2. 光空间的变换

6个面需要6个变换矩阵。每个光空间的变换矩阵包含了投影和视图矩阵。对于投影矩阵来说,我们这次用透视投影矩阵。

我们将fov设置为90度,是因为可以保证视野足够大到可以填满立方体贴图每个面,立方体贴图的所有面都能与其他面在边缘对齐。因为投影矩阵在每个方向上并不会改变,我们可以在6个变换矩阵中重复使用

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::lookAt创建6个观察方向,每个都按顺序注视着立方体贴图的的一个方向:右、左、上、下、近、远。我们创建了6个视图矩阵,把它们乘以投影矩阵,来得到6个不同的光空间变换矩阵。glm::lookAt的target参数是它注视的立方体贴图的面的一个方向。

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

3. 深度着色器

为了把值渲染到深度立方体贴图,我们将需要3个着色器:顶点和像素着色器,以及一个它们之间的几何着色器。几何着色器是负责将所有世界空间的顶点变换到6个不同的光空间的着色器。因此顶点着色器简单地将顶点变换到世界空间,然后直接发送到几何着色器:

#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中。
  • 然后我们通过把面的光空间变换矩阵×gl_Position,将每个世界空间顶点变换到相关的光空间,生成每个三角形。
#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位置和光源位置之间的线性距离。这里我们把fragment和光源之间的距离,映射到0到1的范围,作为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;
}

使用这些着色器渲染场景,立方体贴图附加的帧缓冲对象激活以后,你会得到一个完全填充的深度立方体贴图,以便于进行第二阶段的阴影计算。

二、万向阴影贴图

glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.Use();  
// ... send uniforms to shader (including light's far_plane value)
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
// ... bind other textures
RenderScene();

因为顶点着色器不再需要将他的位置向量变换到光空间,所以我们可以去掉FragPosLightSpace变量

#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;

void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0f);
    vs_out.FragPos = vec3(model * vec4(position, 1.0));
    vs_out.Normal = transpose(inverse(mat3(model))) * normal;
    vs_out.TexCoords = texCoords;
}

片段着色器的Blinn-Phong光照代码和我们之前阴影相乘的结尾部分一样

#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;

float ShadowCalculation(vec3 fragPos)
{
    [...]
}

void main()
{           
    vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;
    vec3 normal = normalize(fs_in.Normal);
    vec3 lightColor = vec3(0.3);
    // 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 = ShadowCalculation(fs_in.FragPos);                      
    vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;    

    FragColor = vec4(lighting, 1.0f);
}

对于ShaderCalculation函数,与普通的阴影计算不同,我们将fragment的光空间位置换成了fragment的位置,我们将使用一个方向向量来采样立方体深度贴图

float ShadowCalculation(vec3 fragPos)
{
    // Get vector between fragment position and light position
    vec3 fragToLight = fragPos - lightPos;
    // Use the light to fragment vector to sample from the depth map    
    float closestDepth = texture(depthMap, fragToLight).r;
    // It is currently in linear range between [0,1]. Re-transform back to original value
    closestDepth *= far_plane;
    // Now get current linear depth as the length between the fragment and light position
    float currentDepth = length(fragToLight);
    // Now test for shadows
    float bias = 0.05; 
    float shadow = currentDepth -  bias > closestDepth ? 1.0 : 0.0;

    return shadow;
}

其中closestDepth *= far_plane;意思是,因为上一句使用fragToLight进行采样,closesDepth在0-1的范围值,我们先要将其转换到0到far_plane的范围

三、PCF

PCF或称Percentage-closer filtering允许我们通过对fragment位置周围过滤多个样本,并对结果平均化。
以下是简单的PCF过滤器。

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

以上实现方法可能需要过多的样本,导致效率不高。为了优化,可以使用一个预定义的方向数组sampleOffsetDirections,这些方向足够分散(尽可能均匀地分布在空间中,并且每个方向都尽可能地与其他方向不同,避免方向之间过于接近),可以减少样本数量同时保持阴影质量。

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)
);
float shadow = 0.0;
float bias = 0.15;
int samples = 20;
float viewDistance = length(viewPos - fragPos);
//float diskRadius = 0.05;
float diskRadius = (1.0 + (viewDistance / far_plane)) / 25.0;
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定义了采样的范围,可以基于观察者里一个fragment的距离来改变diskRadius;这样我们就能根据观察者的距离来增加偏移半径了,当距离更远的时候阴影更柔和,更近了就更锐利。
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2060294.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【乐吾乐大屏可视化组态编辑器】动画按顺序播放

动画按顺序播放 在线使用&#xff1a;https://v.le5le.com/ 如案例所示&#xff0c;通过连线去串联一组动画图元&#xff0c;动画按照顺序向后执行。 ① 首先给每个图元都配置动画&#xff0c;注意这里的动画播放次数一定要配置有限个&#xff08;这里配置都是1次&#xff0…

AI在医学领域:FEDMEKI平台实现在隐私约束下将医学知识整合到基础模型

基础模型已在众多领域掀起了一场革命性的变革&#xff0c;它们在处理多样化模态和复杂任务方面展现出了卓越的能力。以GPT-3和LLaMA为例&#xff0c;这些模型在众多应用场景中均表现出色。其成功的核心在于接触并学习海量的训练数据&#xff0c;从而深入洞察不同领域。借助这些…

【python与java的区别-04(文件流)】

一、文件和目录的操作 1、IO流&#xff08;Stream&#xff09; 通过“流”的形式允许计算机程序使用相同的方式来访问不同的流入/流出源。Stream是从起源&#xff08;source&#xff09;到接收(sink)的有序数据。我们把输入/输出源对比成“水桶”&#xff0c;那么流就是“管道…

企业给排水乙级资质续期:人才储备与补充计划

企业给排水乙级资质续期过程中&#xff0c;人才储备与补充计划是至关重要的环节。以下是一个详细的人才储备与补充计划&#xff0c;旨在帮助企业顺利应对资质续期挑战&#xff1a; 一、人才储备计划 1. 提前规划与预测 政策分析&#xff1a;密切关注住建部门或相关权威机构发…

VirtualBox和VMware的虚拟机ip配置为同一网段不使用wlan的网卡(vulnhub打靶前期准备)

打靶前期准备工作&#xff0c;virtualbox和VMware之间的网络互通&#xff08;即同一个网段下非wlan网卡的设置&#xff09; 首先在打靶的时候因为vulnhub的靶机都是使用的virtualbox的虚拟机&#xff0c;但是我的kali已经用了很久了一直使用的是VMware&#xff0c;突然转换使用…

面试必刷——二叉树习题/面试题详解

&#xff08;1&#xff09;检查两棵树是否相同 题目链接&#xff1a; . - 力扣&#xff08;LeetCode&#xff09;. - 备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems…

品质更进阶 长安马自达MAZDA EZ-6通关中国“热极”

继在中国规格最高的重庆垫江测试场完成驾控试炼后&#xff0c;8月20日-22日&#xff0c;长安马自达MAZDA EZ-6“众测先享官—品质更进阶”在中国“热极”吐鲁番再次拉开帷幕。针对电动车用户最关心的酷暑天气用车痛点场景&#xff0c;由长安马自达工程师团队携手用户代表、权威…

云微客短视频矩阵获客有多容易?低成本获客备受好评

数字化时代&#xff0c;短视频矩阵已经成为企业获客的重要渠道之一&#xff0c;云微客短视频矩阵系统为企业解决在短视频营销中的账号搭建、内容生产、账号运营、低成本引流等难题。 短视频矩阵是一种基于抖音、快手、小红书、视频号等短视频平台&#xff0c;通过批量剪辑、批量…

Linux shell编程学习笔记72:tr命令——集合转换工具

0 前言 在大数据时代&#xff0c;我们要面对大量数据&#xff0c;有时需要对数据进行整理和转换。 在Linux中&#xff0c;我们可以使用 tr命令来整理和转换数据&#xff0c;也可以进行简单的加解密。 1 tr命令 的帮助信息&#xff0c;功能&#xff0c;格式&#xff0c;选项和…

图片展示时等比例缩放

通过object-fit进行图片等比例缩放 object-fit 属性有以下几种值&#xff1a; contain&#xff1a;图片等比例缩放以完全填充容器&#xff0c;同时保持图像的宽高比。 cover&#xff1a;图片等比例缩放以完全填充容器&#xff0c;但可能会裁剪图片。 fill&#xff1a;图片拉伸以…

AR 眼镜之-系统应用音效-实现方案

目录 &#x1f4c2; 前言 AR 眼镜系统版本 系统应用音效 1. &#x1f531; 技术方案 1.1 技术方案概述 1.2 实现方案 1&#xff09;初始化 2&#xff09;播放音效 3&#xff09;释放资源 2. &#x1f4a0; 播放音效 2.1 静音不播放 2.2 获取音效默认音量 3. ⚛️ …

一文通透mamba2:力证Transformer are SSM——从SSM、半可分矩阵、SSD到mamba2

前言 实话说&#xff0c;过去一两月一直忙着我司两大类项目的推进 一类是正在逐一上线基于大模型的论文翻译、论文审稿、论文对话、论文修订/润色、论文idea提炼等等一类是正在抓紧做面向一个个工厂的具身智能机器人的解决方案&#xff0c;且很快会分别在我司在各地的办公室(…

day06_算法训练

一. Stream流 1.1 Stream流概述 概念: jdk1.8以后提供的新的API, 主要用于批量操作数据(集合的另外一种操作方式),代码非常简洁 流式处理思想: 2.2 Stream对象获取 1.单列集合的Stream流对象获取 2.双列集合的Stream流对象获取 3.数组的Stream流对象获取 4.散装数据的St…

数据结构day03(栈 Stack 顺序栈、链式栈 )内含具体详细代码实现

目录 【1】栈 Stack 1》栈的定义 2》顺序栈 2》链式栈 4》顺序栈的链式栈的区别 【1】栈 Stack 1》栈的定义 栈&#xff1a;是只允许在一端进行插入或删除的线性表&#xff0c;首先栈是一种线性表&#xff0c;但限定这种线性表只能在某一端进行插入和删除操作。 栈顶&…

《Python编程:从入门到实践》笔记(一)

一、字符串 1.修改字符串大小写 title()以首字母大写的方式显示每个单词&#xff0c;即将每个单词的首字母都改为大写&#xff0c;其他的改为小写。 upper()将字母都改为大写&#xff0c;lower()将字母都改为小写。 2.合并(拼接)字符串 Python使用加号()来合并字符串。这种合…

Java—认识异常 ( ̄▽ ̄)~*

目录&#xff1a; 一、异常的概念和体系结构&#xff1a; 1、异常的概念&#xff1a; 2、异常的体系&#xff1a; 3、异常的分类&#xff1a; 二、异常的处理&#xff1a; 1、防御式编程&#xff1a; 1&#xff09;、 事前防御型(LBYL) &#xff1a; 2&#xff09;、事后…

C语言典型例题47

《C程序设计教程&#xff08;第四版&#xff09;——谭浩强》 习题3.7 输入4个整数&#xff0c;要求按照从小到大的顺序输出 4个数之间进行比较&#xff0c;冒泡排序最最最详细过程&#xff0c;如果想更改为任意数之间相互比较&#xff0c;只需要修改两个地方&#xff08;数组大…

cocoscreator怪物实时寻路AI

这章主要介绍怪物AI 1&#xff1a;环境 cococreator2.4.* 2&#xff1a;规则 当前规则很简单&#xff0c;就是跳上&#xff0c;跳下 一个土块 3&#xff1a;上代码 // Learn cc.Class: // - https://docs.cocos.com/creator/manual/en/scripting/class.html // Learn Attri…

C++智能指针配合STL模板类

代码 #include <unordered_map> #include <set> #include <memory> class ResID { public:using SP std::shared_ptr<ResID>;ResID() default;ResID(const std::string& id, const std::string& type): m_id(id), m_type(type){}public:~Re…

SAM 2——视频和图像实时实例分割的全新开源模型

引言 源码地址&#xff1a;https://github.com/facebookresearch/segment-anything-2 过去几年&#xff0c;人工智能领域在文本处理的基础人工智能方面取得了显著进步&#xff0c;这些进步改变了从客户服务到法律分析等各个行业。然而&#xff0c;在图像处理方面&#xff0c;我…