LearnOpenGL-笔记-其十

article/2025/7/31 13:25:25

今天我们要进入高级光照的环节了:

Advanced Lighting

之前的学习中,我们的光照模型采用的是比较简单的phong光照模型,也就是光照强度由环境光加上漫反射光加上镜面反射组成。

用一张图足以解释:

就这么简单,针对夹角过大的部分,我们的点积为0,这个时候我们会粗暴地将这部分镜面反射的光照强度设置为0,于是导致了上图中的现象。

采取半程向量和法线的夹角而不是光源与反射光线的夹角的好处就是,我们的夹角永远不会大于90度了,这样避免了突兀的阴影出现的情况。半程向量的获取方法确实非常简单,我们只需要把光线向量和观察向量相加之后归一化即可。

vec3 lightDir   = normalize(lightPos - FragPos);
vec3 viewDir    = normalize(viewPos - FragPos);
vec3 halfwayDir = normalize(lightDir + viewDir);

这个是GLSL的代码。

float spec = pow(max(dot(normal, halfwayDir), 0.0), shininess);
vec3 specular = lightColor * spec;

剩下的就是点乘并保证不小于0即可。

我们主要在片元着色器上实现这一步骤:

#version 330 core
out vec4 FragColor;in VS_OUT {vec3 FragPos;vec3 Normal;vec2 TexCoords;
} fs_in;uniform sampler2D floorTexture;
uniform vec3 lightPos;
uniform vec3 viewPos;
uniform bool blinn;void main()
{           vec3 color = texture(floorTexture, fs_in.TexCoords).rgb;// ambientvec3 ambient = 0.05 * color;// diffusevec3 lightDir = normalize(lightPos - fs_in.FragPos);vec3 normal = normalize(fs_in.Normal);float diff = max(dot(lightDir, normal), 0.0);vec3 diffuse = diff * color;// specularvec3 viewDir = normalize(viewPos - fs_in.FragPos);vec3 reflectDir = reflect(-lightDir, normal);float spec = 0.0;if(blinn){vec3 halfwayDir = normalize(lightDir + viewDir);  spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);}else{vec3 reflectDir = reflect(-lightDir, normal);spec = pow(max(dot(viewDir, reflectDir), 0.0), 8.0);}vec3 specular = vec3(0.3) * spec; // assuming bright white light colorFragColor = vec4(ambient + diffuse + specular, 1.0);
}

如果我们打开布林-冯开关,我们的镜面反射光的计算公式会发生变化,会额外计算一个半程向量然后与法线点乘。

大体上还是可以看出区别的。

Gamma Correction

既然说到所谓的伽马矫正,那么我们当然优先得知道什么是伽马值。

所谓的伽马值,本质上其实是一个亮度和信号电压的映射关系,更准确地说,这个亮度是我们人眼感受到的亮度。

简单地说就是,我们的伽马矫正是基于我们的显示器的空间来做,而显示器本身的亮度/颜色配置并不是一个线性的配置,中间亮度被压缩,在非线性的空间内进行非线性变换就会导致问题。伽马校正的本质,是通过数学补偿显示器的非线性,确保物理计算在线性空间正确,而最终输出适配显示器的伽马曲线。

OpenGL中内建得有gamma矫正的帧缓冲,我们可以直接使用。

OpenGL中默认设备的伽马值为2.2,我们在OpenGL中开启GL_FRAMEBUFFER_SRGB即可。

第二种做法则是我们手动的去乘以伽马值的倒数次幂,这样的话我们必须手动地对所有片元着色器使用这个伽马矫正方法。同时我们还要注意,自己手动实现伽马矫正还有一些额外的细节需要注意。

我们的纹理是基于显示器的RGB空间来实现的,如果我们采用手动伽马矫正,那么虽然我们的显示器亮度颜色空间变成了线性空间,但是我们的纹理就会收到影响变成完全不同的效果。

OpenGL也为我们做好了这个东西,我们用GL_SRGB来使用纹理即可。

我们来用一个实例体现出有无伽马矫正的区别:

unsigned int loadTexture(const char* path, bool gammaCorrection);

我们在这里将是否开启伽马矫正传进函数中。

这个函数的具体写法是:

unsigned int loadTexture(char const* path, bool gammaCorrection)
{unsigned int textureID;glGenTextures(1, &textureID);int width, height, nrComponents;unsigned char* data = stbi_load(path, &width, &height, &nrComponents, 0);if (data){GLenum internalFormat;GLenum dataFormat;if (nrComponents == 1){internalFormat = dataFormat = GL_RED;}else if (nrComponents == 3){internalFormat = gammaCorrection ? GL_SRGB : GL_RGB;dataFormat = GL_RGB;}else if (nrComponents == 4){internalFormat = gammaCorrection ? GL_SRGB_ALPHA : GL_RGBA;dataFormat = GL_RGBA;}glBindTexture(GL_TEXTURE_2D, textureID);glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, dataFormat, GL_UNSIGNED_BYTE, data);glGenerateMipmap(GL_TEXTURE_2D);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_MIPMAP_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);stbi_image_free(data);}else{std::cout << "Texture failed to load at path: " << path << std::endl;stbi_image_free(data);}return textureID;
}

现在我们来看具体的片元着色器如何修改:

#version 330 core
out vec4 FragColor;in VS_OUT {vec3 FragPos;vec3 Normal;vec2 TexCoords;
} fs_in;uniform sampler2D floorTexture;uniform vec3 lightPositions[4];
uniform vec3 lightColors[4];
uniform vec3 viewPos;
uniform bool gamma;vec3 BlinnPhong(vec3 normal, vec3 fragPos, vec3 lightPos, vec3 lightColor)
{// diffusevec3 lightDir = normalize(lightPos - fragPos);float diff = max(dot(lightDir, normal), 0.0);vec3 diffuse = diff * lightColor;// specularvec3 viewDir = normalize(viewPos - 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;    // simple attenuationfloat max_distance = 1.5;float distance = length(lightPos - fragPos);float attenuation = 1.0 / (gamma ? distance * distance : distance);diffuse *= attenuation;specular *= attenuation;return diffuse + specular;
}void main()
{           vec3 color = texture(floorTexture, fs_in.TexCoords).rgb;vec3 lighting = vec3(0.0);for(int i = 0; i < 4; ++i)lighting += BlinnPhong(normalize(fs_in.Normal), fs_in.FragPos, lightPositions[i], lightColors[i]);color *= lighting;if(gamma)color = pow(color, vec3(1.0/2.2));FragColor = vec4(color, 1.0);
}

除了之前实现的布林-冯光照模型以外,我们最大的改进就是根据外部传入的bool值伽马来决定是否开启伽马矫正,true的话就让颜色值除以一个2.2即可。

显然:有伽马矫正的图的光线分布更柔合,更偏向中间值,而无伽马矫正的光线看起来更极端。归根结底,还是因为无伽马矫正的场景的光线强度和颜色的曲线不够平缓,较小的亮度变化也会导致巨大的颜色落差。

Shadow Mapping

目前是没有完美的阴影算法的,阴影贴图是其中一种方法来模拟阴影的实现。

简单地说,我们的阴影映射的思路就是从光源处出发进行深度测试,比较场景中各个物体的深度值来判断阴影。

我们来看具体的代码:

const unsigned int SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
unsigned int depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);
// create depth texture
unsigned int depthMap;
glGenTextures(1, &depthMap);
glBindTexture(GL_TEXTURE_2D, depthMap);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, SHADOW_WIDTH, SHADOW_HEIGHT, 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_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// attach depth texture as FBO's depth buffer
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

效果如图:

这样我们就完成了基于光源的深度测试,我们现在在此基础上进行阴影渲染:

// 1. 渲染深度贴图
simpleDepthShader.use();
simpleDepthShader.setMat4("lightSpaceMatrix", lightSpaceMatrix);
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);glClear(GL_DEPTH_BUFFER_BIT);glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, woodTexture);renderScene(simpleDepthShader);
glBindFramebuffer(GL_FRAMEBUFFER, 0);// 2. 用深度贴图渲染主场景(有阴影)
shader.use();
shader.setInt("diffuseTexture", 0);
shader.setInt("shadowMap", 1);
...
renderScene(shader);

这时候我们就实现了基本的阴影渲染了,这个渲染的过程就是我们先根据之前生成的深度贴图获取到各个片元到摄像机的深度值,然后我们再正常进行渲染的过程,如果这个片元处于阴影之下(深度值更大)我们就把该片元的亮度减小,也就是形成阴影的效果,这样我们就能实现基本的阴影效果了,但是这个效果很“硬”,也就是边缘会产生锯齿。

如何生成所谓的“软”阴影呢?这个时候我们就要多一些对阴影本身的处理:阴影偏移(Bias)和PCF软阴影(多点采样模糊)技术。

// 计算偏移
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
// PCF软阴影
float shadow = 0.0;
vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
for(int x = -1; x <= 1; ++x)
{for(int y = -1; y <= 1; ++y){float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r; shadow += currentDepth - bias > pcfDepth  ? 1.0 : 0.0;        }    
}
shadow /= 9.0;

效果如图:

Point Shadows

之前我们提到的阴影贴图有关的内容都是基于平行光假设来做的,但是显然我们还要考虑其他光照模型的阴影如点光源:
算法和定向阴影映射差不多:我们从光的透视图生成一个深度贴图,基于当前fragment位置来对深度贴图采样,然后用储存的深度值和每个fragment进行对比,看看它是否在阴影中。定向阴影映射和万向阴影映射的主要不同在于深度贴图的使用上。

对于深度贴图,我们需要从一个点光源的所有渲染场景,普通2D深度贴图不能工作;如果我们使用立方体贴图会怎样?因为立方体贴图可以储存6个面的环境数据,它可以将整个场景渲染到立方体贴图的每个面上,把它们当作点光源四周的深度值来采样。

// 片元着色器
float ShadowCalculation(vec3 fragPos)
{vec3 fragToLight = fragPos - lightPos;float closestDepth = texture(depthMap, fragToLight).r;closestDepth *= far_plane;float currentDepth = length(fragToLight);float bias = 0.05;float shadow = currentDepth -  bias > closestDepth ? 1.0 : 0.0;        return shadow;
}

换句话说,针对点光源和平行光源的阴影贴图实现最大的差异就是我们如何去使用深度贴图:对于平行光或者说定向光来说,我们每个像素只用考虑一个方向的深度;但是对于点光源来说,每个像素实际的接收到光照的方向不同,因此需要一个更全面的量化深度的方式:

其实思路也很简单:我们用六张深度贴图组成一个立方体深度贴图即可,这样每个深度贴图都能获取一个深度值,这样的话针对这六个方向我们也能分别生成定向光一样的阴影,从而达到效果如图:

当然,我们依然可以进行软阴影的升级:

float shadow = 0.0;
float bias = 0.15;
int samples = 20;
float viewDistance = length(viewPos - fragPos);
float diskRadius = (1.0 + (viewDistance / far_plane)) / 25.0;
for(int i = 0; i < samples; ++i)
{float closestDepth = texture(depthMap, fragToLight + gridSamplingDisk[i] * diskRadius).r;closestDepth *= far_plane;if(currentDepth - bias > closestDepth)shadow += 1.0;
}
shadow /= float(samples);


http://www.hkcw.cn/article/KbkuIqffin.shtml

相关文章

大话软工笔记—分离原理

1. 基本概念 1.1 定义 分离原理&#xff0c;将研究对象中符合相同设计方法的同类项进行分离、归集。 1.2 作用 分离原理的主要作用是&#xff1a; &#xff08;1&#xff09;降低了需求分析与软件设计的复杂程度。 &#xff08;2&#xff09;提出了对企业构成内容按照设计…

扫地机器人苦寻新引擎,大疆们却已攻入腹地

原创 科技新知 前沿科技组 作者丨江篱 编辑丨樱木、九黎 竞争激烈的扫地机器人赛道&#xff0c;迎来了新玩家。 据近日相关报道&#xff0c;大疆扫地机器人产品已开始量产&#xff0c;预计将于6月份发布。消息称大疆研发扫地机器人已超过四年&#xff0c;即将上市的产品是扫…

【Docker管理工具】部署Docker管理面板DweebUI

【Docker管理工具】部署Docker管理面板DweebUI 一、DweebUI介绍1.1 DweebUI 简介1.2 主要特点1.3 使用场景 二、本次实践规划2.1 本地环境规划2.2 本次实践介绍 三、本地环境检查3.1 检查Docker服务状态3.2 检查Docker版本3.3 检查docker compose 版本 四、下载DweebUI镜像五、…

《系统集成项目管理工程师(第三版)》高效学习方法

文章目录 一、学习周期规划(总时长:3-4个月)二、每日学习时间分配表三、重难点突破策略1. 五星必考重点(占分60%+)2. 高频易错点四、高效记忆法(附实例)1. 口诀记忆法2. 联想记忆法3. 对比记忆法五、阶段目标检测六、十大管理高频整理一、十大管理ITTO高频考点表🔥 必…

新松机械臂 2001端口服务的客户端例程

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 源码指引&#xff1a;github源…

HarmonyOS-ArkUI固定样式弹窗(1)

固定样式弹窗指的就是ArkUI中为我们提供的一些具备界面模板性质的弹窗。样式是固定的,我们可以决定在这些模板里输入什么样的内容。常见的有,警告弹窗, 列表选择弹窗, 选择器弹窗,对话框,操作菜单。 下图是本文中要讲到的基类固定样式弹窗,其中选择器弹窗没有包含在内,…

Dify运行本地和在线模型

1、运行本地模型 1.1、下载Ollama 官网下载&#xff1a;Ollama 1.2、安装LLM大模型 Ollama官网搜索模型&#xff0c;复制命令&#xff1a; 在终端中粘贴下载&#xff0c;下载完成后可以输入对话&#xff0c;表示安装成功。 使用命令&#xff1a;ollama list 可以列出已经安装…

manus对比ChatGPT-Deep reaserch进行研究类论文数据分析!谁更胜一筹?

目录 没有账号&#xff0c;只能挑选一个案例 1、manus的效果 Step-1&#xff1a;直接看结果 Step-2&#xff1a;看看其他文件的细节 Step-3&#xff1a;看最终报告 2、Deep reaserch 3、Deep reaserch进行行业分析 总结一下&#xff1a; 大家好这里是学术Anan&#xff…

2025年大一ACM训练-尺取

2025年大一ACM训练-尺取 ​​尺取法&#xff08;Sliding Window&#xff09;&#xff1a; ​​1. 基本概念​​   尺取法&#xff08;又称滑动窗口法&#xff09;是一种​​通过维护窗口的左右边界来高效解决子区间问题​​的算法技巧&#xff0c;常用于&#xff1a;   1…

第十二章 MQTT会话

系列文章目录 第一章 总体概述 第二章 在实体机上安装ubuntu 第三章 Windows远程连接ubuntu 第四章 使用Docker安装和运行EMQX 第五章 Docker卸载EMQX 第六章 EMQX客户端MQTTX Desktop的安装与使用 第七章 EMQX客户端MQTTX CLI的安装与使用 第八章 Wireshark工具的安装与使用 …

【C++】C++入门基础

本文是小编巩固自身而作&#xff0c;如有错误&#xff0c;欢迎指出&#xff01; 1.C的第一个程序 C兼容C语⾔绝⼤多数的语法&#xff0c;所以C语⾔实现的hello world依旧可以运⾏&#xff0c;C中需要把定义⽂件 代码后缀改为.cpp&#xff0c;vs编译器看到是.cpp就会调⽤C编译…

iEKF的二维应用实例

如果熟悉 EKF 与卡尔曼的推导的话&#xff0c;iEKF 就比较容易理解&#xff0c;关于卡尔曼滤波的推导以及EKF&#xff0c;可以参考以前的文章&#xff1a; 卡尔曼滤波原理&#xff1a;https://blog.csdn.net/a_xiaoning/article/details/130564473?spm1001.2014.3001.5502 E…

[IMX] 10.串行外围设备接口 - SPI

代码链接&#xff1a;GitHub - maoxiaoxian/imx 参考资料&#xff1a; https://zhuanlan.zhihu.com/p/290620901 SPI协议详解 - bujidao1128 - 博客园 SPI总线协议及SPI时序图详解 - Ady Lee - 博客园 目录 1.SPI 简介 2.I.MX6U ECSPI 简介 2.1.控制寄存器 1 - ECSPIx_CO…

评论功能开发全解析:从数据库设计到多语言实现-优雅草卓伊凡

评论功能开发全解析&#xff1a;从数据库设计到多语言实现-优雅草卓伊凡 一、评论功能的核心架构设计 评论功能看似简单&#xff0c;实则涉及复杂的业务逻辑和技术考量。一个完整的评论系统需要支持&#xff1a;内容评论、回复评论、评论点赞、评论排序、敏感词过滤等功能。 …

计算机视觉入门:OpenCV与YOLO目标检测

计算机视觉入门&#xff1a;OpenCV与YOLO目标检测 系统化学习人工智能网站&#xff08;收藏&#xff09;&#xff1a;https://www.captainbed.cn/flu 文章目录 计算机视觉入门&#xff1a;OpenCV与YOLO目标检测摘要引言技术原理对比1. OpenCV&#xff1a;传统图像处理与机器学…

C语言进阶--自定义类型详解(结构体、枚举、联合)

1.结构体 1.1结构体的声明 1.1.1结构的基础知识 结构是一些值的集合&#xff0c;这些值称为成员变量。结构的每个成员可以是不同类型的变量。 1.1.2结构的声明 struct tag {member-list; }variable-list;struct Stu {//学生的属性char name[20];int age; };struct Stu {…

asio之async_result

简介 async_result用来表示异步处理返回类型 async_result 是类模板 type&#xff1a;为类模板中声明的类型&#xff0c;对于不同的类型&#xff0c;可以使用类模板特例化&#xff0c;比如针对use_future

Hash 的工程优势: port range 匹配

昨天和朋友聊到 “如何匹配一个 port range”&#xff0c;觉得挺有意思&#xff0c;简单写篇散文。 回想起十多年前&#xff0c;我移植并优化了 nf-HiPAC&#xff0c;当时还看不上 ipset hash&#xff0c;后来大约七八年前&#xff0c;我又舔 nftables&#xff0c;因为用它可直…

力扣HOT100之动态规划:198. 打家劫舍

这道题之前刷代码随想录的时候做过&#xff0c;这一次直接一遍过了&#xff0c;还是按照动规五部曲&#xff1a; 1.确定dp[i]的含义:将下标为0 ~ i的房子纳入考虑范围时所能取到的最大收益 2.确定递推公式:dp[i] max(dp[i - 2] nums[i], dp[i - 1]); 3.dp数组初始化:dp[0] n…

基于VU37P的高性能采集板卡

基于VU37P的高性能采集板卡是一款最大可提供20路ADC接收通道的高性能采集板卡。每路A/D通道支持1GS/s的采样率&#xff0c;分辨率为14bit&#xff0c;模拟输入带宽可达500MHz&#xff0c;交流耦合&#xff0c;输入阻抗50欧姆。 产品简介 可提供20路ADC接收通道的高性能采集板…