【Overload游戏引擎细节分析】PBR材质Shader---完结篇
PBR基于物理的渲染可以实现更加真实的效果,其Shader值得分析一下。但PBR需要较多的基础知识,不适合不会OpenGL的朋友。
一、PBR理论
PBR指基于物理的渲染,其理论较多,需要的基础知识也较多,我在这就不再写一遍了,具体可以参看:
LearnOpenGL PBR理论-英文 或者 LearnOpenGL PBR理论-中文
Overload也提供了这种材料,借助贴图可以实现非常真实的材质效果。下面这个例子的贴图来自LearnOpenGL,大家可以自己去下载。
二、PBR Shader分析
顶点着色器
#shader vertex
#version 430 corelayout (location = 0) in vec3 geo_Pos;
layout (location = 1) in vec2 geo_TexCoords;
layout (location = 2) in vec3 geo_Normal;
layout (location = 3) in vec3 geo_Tangent;
layout (location = 4) in vec3 geo_Bitangent;/* Global information sent by the engine */
layout (std140) uniform EngineUBO
{mat4 ubo_Model;mat4 ubo_View;mat4 ubo_Projection;vec3 ubo_ViewPos;float ubo_Time;
};/* Information passed to the fragment shader */
out VS_OUT
{vec3 FragPos;vec3 Normal;vec2 TexCoords;mat3 TBN;flat vec3 TangentViewPos;vec3 TangentFragPos;
} vs_out;void main()
{vs_out.TBN = mat3(normalize(vec3(ubo_Model * vec4(geo_Tangent, 0.0))),normalize(vec3(ubo_Model * vec4(geo_Bitangent, 0.0))),normalize(vec3(ubo_Model * vec4(geo_Normal, 0.0))));mat3 TBNi = transpose(vs_out.TBN);vs_out.FragPos = vec3(ubo_Model * vec4(geo_Pos, 1.0));vs_out.Normal = normalize(mat3(transpose(inverse(ubo_Model))) * geo_Normal);vs_out.TexCoords = geo_TexCoords;vs_out.TangentViewPos = TBNi * ubo_ViewPos;vs_out.TangentFragPos = TBNi * vs_out.FragPos;gl_Position = ubo_Projection * ubo_View * vec4(vs_out.FragPos, 1.0);
}
顶点着色器基本与standard材质一致,这里就不再分析了,具体可看standard材质Shader
片元着色器:
#shader fragment
#version 430 core/** 模型视图矩阵、摄像机位置,使用UBO传入 */
/* Global information sent by the engine */
layout (std140) uniform EngineUBO
{mat4 ubo_Model;mat4 ubo_View;mat4 ubo_Projection;vec3 ubo_ViewPos;float ubo_Time;
};/* 顶点着色器的输出 */
/* Information passed from the fragment shader */
in VS_OUT
{vec3 FragPos;vec3 Normal;vec2 TexCoords;mat3 TBN;flat vec3 TangentViewPos;vec3 TangentFragPos;
} fs_in;/* 光源数据用SSBO传入 */
/* Light information sent by the engine */
layout(std430, binding = 0) buffer LightSSBO
{mat4 ssbo_Lights[];
};out vec4 FRAGMENT_COLOR;uniform sampler2D u_AlbedoMap; // 反照率贴图
uniform sampler2D u_MetallicMap; // 金属度贴图
uniform sampler2D u_RoughnessMap; // 粗糙度贴图
uniform sampler2D u_AmbientOcclusionMap; // 环境光遮蔽贴图
uniform sampler2D u_NormalMap; // 法线贴图
uniform vec4 u_Albedo = vec4(1.0); // 反照率系数,控制反照率贴图的权重
uniform vec2 u_TextureTiling = vec2(1.0, 1.0);
uniform vec2 u_TextureOffset = vec2(0.0, 0.0);
uniform bool u_EnableNormalMapping = false; // 是否使用法线贴图
uniform float u_HeightScale = 0.0;
uniform float u_Metallic = 1.0; // 金属度
uniform float u_Roughness = 1.0; // 粗糙度const float PI = 3.14159265359;// 计算法向分布函数D,使用Trowbridge-Reitz GGX
float DistributionGGX(vec3 N, vec3 H, float roughness)
{float a = roughness*roughness;float a2 = a*a;float NdotH = max(dot(N, H), 0.0);float NdotH2 = NdotH*NdotH;float num = a2;float denom = (NdotH2 * (a2 - 1.0) + 1.0);denom = PI * denom * denom;return num / denom;
}float GeometrySchlickGGX(float NdotV, float roughness)
{float r = (roughness + 1.0);float k = (r*r) / 8.0;float num = NdotV;float denom = NdotV * (1.0 - k) + k;return num / denom;
}// Smith’s method
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{float NdotV = max(dot(N, V), 0.0);float NdotL = max(dot(N, L), 0.0);float ggx2 = GeometrySchlickGGX(NdotV, roughness);float ggx1 = GeometrySchlickGGX(NdotL, roughness);return ggx1 * ggx2;
}// 菲涅尔项,使用Fresnel-Schlick方程
vec3 fresnelSchlick(float cosTheta, vec3 F0)
{return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}/* 将32位数字变成RGBA颜色 */
vec3 UnPack(float p_Target)
{return vec3(// CPU传入的数据是0-255,转换成0-1.0float((uint(p_Target) >> 24) & 0xff) * 0.003921568627451,float((uint(p_Target) >> 16) & 0xff) * 0.003921568627451,float((uint(p_Target) >> 8) & 0xff) * 0.003921568627451);
}bool PointInAABB(vec3 p_Point, vec3 p_AabbCenter, vec3 p_AabbHalfSize)
{return(p_Point.x > p_AabbCenter.x - p_AabbHalfSize.x && p_Point.x < p_AabbCenter.x + p_AabbHalfSize.x &&p_Point.y > p_AabbCenter.y - p_AabbHalfSize.y && p_Point.y < p_AabbCenter.y + p_AabbHalfSize.y &&p_Point.z > p_AabbCenter.z - p_AabbHalfSize.z && p_Point.z < p_AabbCenter.z + p_AabbHalfSize.z);
}/*光照衰减系数,LearnOpenGL中有具体公式*/
float LuminosityFromAttenuation(mat4 p_Light)
{const vec3 lightPosition = p_Light[0].rgb;const float constant = p_Light[0][3];const float linear = p_Light[1][3];const float quadratic = p_Light[2][3];const float distanceToLight = length(lightPosition - fs_in.FragPos);const float attenuation = (constant + linear * distanceToLight + quadratic * (distanceToLight * distanceToLight));return 1.0 / attenuation;
}/* 盒状环境光 */
vec3 CalcAmbientBoxLight(mat4 p_Light)
{const vec3 lightPosition = p_Light[0].rgb;const vec3 lightColor = UnPack(p_Light[2][0]);const float intensity = p_Light[3][3];const vec3 size = vec3(p_Light[0][3], p_Light[1][3], p_Light[2][3]);return PointInAABB(fs_in.FragPos, lightPosition, size) ? lightColor * intensity : vec3(0.0);
}/* 球状环境光 */
vec3 CalcAmbientSphereLight(mat4 p_Light)
{const vec3 lightPosition = p_Light[0].rgb;const vec3 lightColor = UnPack(p_Light[2][0]);const float intensity = p_Light[3][3];const float radius = p_Light[0][3];return distance(lightPosition, fs_in.FragPos) <= radius ? lightColor * intensity : vec3(0.0);
}void main()
{vec2 texCoords = u_TextureOffset + vec2(mod(fs_in.TexCoords.x * u_TextureTiling.x, 1), mod(fs_in.TexCoords.y * u_TextureTiling.y, 1));vec4 albedoRGBA = texture(u_AlbedoMap, texCoords) * u_Albedo; // Albedo反照率贴图数据vec3 albedo = pow(albedoRGBA.rgb, vec3(2.2)); // 这种反照率处理方式与LearOpenGL一致float metallic = texture(u_MetallicMap, texCoords).r * u_Metallic; // 金属度float roughness = texture(u_RoughnessMap, texCoords).r * u_Roughness; // 粗糙度float ao = texture(u_AmbientOcclusionMap, texCoords).r; // 环境光遮蔽AOvec3 normal;if (u_EnableNormalMapping) // 是否使用法线贴图{normal = texture(u_NormalMap, texCoords).rgb; // 法线贴图的原始值normal = normalize(normal * 2.0 - 1.0); // 法线贴图矢量坐标范围变成-1到1normal = normalize(fs_in.TBN * normal); // 变换到全局坐标系下}else{normal = normalize(fs_in.Normal); // 使用顶点着色器输出的法线}vec3 N = normalize(normal); vec3 V = normalize(ubo_ViewPos - fs_in.FragPos); // 计算视线方向vec3 F0 = vec3(0.04); F0 = mix(F0, albedo, metallic); // 插值方式得到平面的基础反射率F0// reflectance equationvec3 Lo = vec3(0.0);vec3 ambientSum = vec3(0.0); // 环境光结果for (int i = 0; i < ssbo_Lights.length(); ++i) {// 两种环境光灯光if (int(ssbo_Lights[i][3][0]) == 3){ambientSum += CalcAmbientBoxLight(ssbo_Lights[i]);}else if (int(ssbo_Lights[i][3][0]) == 4){ambientSum += CalcAmbientSphereLight(ssbo_Lights[i]);}else{// calculate per-light radiance// 光源方向vec3 L = int(ssbo_Lights[i][3][0]) == 1 ? -ssbo_Lights[i][1].rgb : normalize(ssbo_Lights[i][0].rgb - fs_in.FragPos);vec3 H = normalize(V + L);// 半程向量float distance = length(ssbo_Lights[i][0].rgb - fs_in.FragPos);float lightCoeff = 0.0; // 最终到片元处的光强系数 switch(int(ssbo_Lights[i][3][0])){case 0:lightCoeff = LuminosityFromAttenuation(ssbo_Lights[i]) * ssbo_Lights[i][3][3]; // 点光源要考虑随距离衰减break;case 1:lightCoeff = ssbo_Lights[i][3][3]; // 方向光无衰减break;// 聚光灯的计算case 2:const vec3 lightForward = ssbo_Lights[i][1].rgb;const float cutOff = cos(radians(ssbo_Lights[i][3][1]));const float outerCutOff = cos(radians(ssbo_Lights[i][3][1] + ssbo_Lights[i][3][2]));const vec3 lightDirection = normalize(ssbo_Lights[i][0].rgb - fs_in.FragPos);const float luminosity = LuminosityFromAttenuation(ssbo_Lights[i]);/* Calculate the spot intensity */const float theta = dot(lightDirection, normalize(-lightForward)); const float epsilon = cutOff - outerCutOff;const float spotIntensity = clamp((theta - outerCutOff) / epsilon, 0.0, 1.0);lightCoeff = luminosity * spotIntensity * ssbo_Lights[i][3][3];break;}vec3 radiance = UnPack(ssbo_Lights[i][2][0]) * lightCoeff;// cook-torrance brdffloat NDF = DistributionGGX(N, H, roughness); // 法线分布函数float G = GeometrySmith(N, V, L, roughness); // 几何函数vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); // 菲涅尔项vec3 kS = F;vec3 kD = vec3(1.0) - kS;kD *= 1.0 - metallic;vec3 numerator = NDF * G * F;float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0);vec3 specular = numerator / max(denominator, 0.001);// add to outgoing radiance Lofloat NdotL = max(dot(N, L), 0.0);Lo += (kD * albedo / PI + specular) * radiance * NdotL; }}vec3 ambient = ambientSum * albedo * ao;// 环境光最终贡献vec3 color = ambient + Lo; // 环境光与cook-torrance模型累加// HDR色调映射color = color / (color + vec3(1.0));// gamma 矫正color = pow(color, vec3(1.0/2.2)); FRAGMENT_COLOR = vec4(color, albedoRGBA.a); // alpha使用反照率贴图
}
Fragment Shader大体分为三部分:
- 从贴图中获取反照率、金属度、粗糙度、法线数据
- 计算灯光光照,环境光灯光只影响环境光;方向光、聚光灯、点光源会影响光强lightCoeff,最终的光照使用cook-torrance模型进行计算,公式可以参考LearnOpenGL
- 最后进行环境光与PBR模型结果进行叠加,并进行色调映射与gamma矫正,这里使用的公式在LearnOpenGL中都有的
总结:
这个PBR Shader整体上与LearnOpenGL中的理论一致,看完LearnOpenGL之后再看这个Shader就比较简单了。
完结总结:
写的这里,这个专栏暂时告一段落了,主要分析了Overload的Render模块,其他的包括UI、物理引擎、音频等模块没有涉及。Overload是一个Demo性质的游戏引擎,其渲染涉只涉及最基础的渲染方式,是对OpenGL简单封装,远远满足不了实际游戏开发需求,只能作为渲染引擎入门。
另外,这个专栏的文章只聚焦一些细节,对应架构涉及很少,因为本人发现架构方面的文章参考性不大,一旦一个软件定型架构方面的改动很困难,读了软件架构的文章也很难在工作中用上。故单纯只介绍一个技术点反而可能拿来直接使用。最后希望能对大家有所帮助!
相关文章:

【Overload游戏引擎细节分析】PBR材质Shader---完结篇
PBR基于物理的渲染可以实现更加真实的效果,其Shader值得分析一下。但PBR需要较多的基础知识,不适合不会OpenGL的朋友。 一、PBR理论 PBR指基于物理的渲染,其理论较多,需要的基础知识也较多,我在这就不再写一遍了&…...

C++设计模式_18_State 状态模式
State和Memento被归为“状态变化”模式。 文章目录 1. “状态变化”模式1.1 典型模式 2. 动机 (Motivation)3. 代码演示State 状态模式3.1 常规方式3.2 State 状态模式 4. 模式定义5. 结构( Structure )6. 要点总结7. 其他参考 1. “状态变化”模式 在组件构建过程中…...

详解final, abstract, interface关键字
一.final关键字 1.final关键字介绍 ——final关键字可以去修饰类、方法、属性和局部变量 2.final关键字的作用 1)final修饰类,这个类不能被其他类继承 2)final修饰方法,方法不能被重写 3)final修饰属性,属…...
统计特殊四元组
题记: 给你一个 下标从 0 开始 的整数数组 nums ,返回满足下述条件的 不同 四元组 (a, b, c, d) 的 数目 : nums[a] nums[b] nums[c] nums[d] ,且a < b < c < d 示例 1: 输入: nums [1,2,3…...

腾讯云轻量应用服务器“镜像”怎么选择合适?
腾讯云轻量应用服务器镜像怎么选择?如果是用来搭建网站可以选择宝塔Linux面板腾讯云专享版,镜像系统根据实际使用来选择,腾讯云百科txybk.com来详细说下腾讯云轻量应用服务器镜像的选择方法: 腾讯云轻量应用服务器镜像选择 轻量…...
Ruby模块和程序组织
和类一样,模块是一组方法和常量的集合。 和类不同,模块没有实例,取而代之的是可以将特殊模块的功能添加到一个类或者指定对象之中。 Class类是Module类的一个子类,因此每一个类对象也是一个模块对象 一、模块创建和基础应用 编写…...
14、SpringCloud -- WebSocket 实时通知用户
目录 实时通知用户需求:代码:前端:后端:WebSocket创建 websocket-server 服务添加依赖:配置 yml 和 启动类:前端:后端代码:注意:测试:总结:实时通知用户 需求: 用户订单秒杀成功之后,对用户进行秒杀成功通知。 弹出个提示框来提示。 代码: 前端:...

智能井盖传感器推荐,万宾科技助力城市信息化建设
随着科技产品更新换代进程加快,人工智能在人们日常生活之中逐渐普及开来,深入人们生活的方方面面,影响城市基础设施建设工程。例如在大街小巷之中的井盖作为城市基础建设的一个重要部分,一旦出现松动倾斜或凸起等异常问题…...

3D模型格式转换工具HOOPS Exchange对工业级3D产品HOOPS的支持与应用
一、概述 HOOPS Exchange是一套高性能模型转换软件库,可以给软件提供强大的模型的导入和导出功能,我们可以将其单独作为转换工具使用,也可以将其集成到自己的软件中。 同样,HOOPS 的其它产品,也离不开HOOPS Exchange…...

table 表体滚动, 表头、表尾固定
在开发报表中,如果报表数据行过多页面无法全部显示,或者内容溢出div,需要把表头和表尾固定表体滚动这样就可以在页面上全部显示,并且不会溢出div 效果:最终实现效果 代码:<!DOCTYPE html> <html&g…...

第57篇-某钩招聘网站加密参数分析【2023-10-31】
声明:该专栏涉及的所有案例均为学习使用,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!如有侵权,请私信联系本人删帖! 文章目录 一、前言二、网站分析1.X-S-HEADER参数2.请求参数data3.响应机密值data一、前言 网址: aHR0cHM6Ly93d3cubGFnb3UuY29t…...

C语言数据结构之数据结构入门
目录 数据结构介绍 数据结构发展史 何为算法 数据结构基础 基本概念和术语 四大逻辑结构(Logic Structure) 数据类型 理解复杂度概念 时间空间复杂度定义 度量时间复杂度的方法 程序运行时的内存与地址 编程预备 数据结构介绍 数据结构发展…...

如何知道服务器的某个端口是否打开
1、telnet 命令:telnet ip port,port即端口,我们一般最常见的命令就是telnet,但是telnet使用的是tcp协议,换句话说telnet只能检测tcp的这个端口打开了没 若是端口打开,会出现下列信息 失败的是这个 如…...

【ICCV‘23】One-shot Implicit Animatable Avatars with Model-based Priors
文章目录 前置知识 前置知识 1)SMPL模型 \quad SMPL这类方法只建模穿很少衣服的人体(裸体模型),它只能刻画裸体角色的动画,并不能刻画穿衣服的人体的动画 2)data-efficient \quad 这个词推荐用ÿ…...

关于息肉检测和识别项目的总结
前言 整体的思路:首先息肉数据集分为三类: 1.正常细胞 2. 增生性息肉 3. 肿瘤要想完成这个任务,首先重中之重是分割任务,分割结果的好坏, 当分割结果达到一定的准确度后,开始对分割后的结果进行下游分类…...

Jetson Xavier NX FFmpeg支持硬件编解码
最近在用Jetson Xavier NX板子做视频处理,但是CPU进行视频编解码,效率比较地下。 于是便考虑用硬解码来对视频进行处理。 通过jtop查看,发现板子是支持 NVENC硬件编解码的。 1、下载源码 因为需要对ffmpeg进行打补丁修改,因此需要编译两份源码 1.1、编译jetson-ffmpeg …...

518抽奖软件,为什么说比别的抽奖软件更美观精美?
518抽奖软件简介 518抽奖软件,518我要发,超好用的年会抽奖软件,简约设计风格。 包含文字号码抽奖、照片抽奖两种模式,支持姓名抽奖、号码抽奖、数字抽奖、照片抽奖。(www.518cj.net) 精致美观功能 字体平滑无锯齿图片放大后清晰…...
React的组件学习
React的组件学习 参考资料:https://zh-hans.react.dev/learn/your-first-component 一、定义组件 export default function Profile() {return (<imgsrc"https://i.imgur.com/MK3eW3Am.jpg"alt"Katherine Johnson"/>) }以下是构建组件…...

uni-app配置微信开发者工具
一、配置微信开发者工具路径 工具->设置->运行配置->小程序运行配置->微信开发者工具路径 二、微信开发者工具开启服务端口...

肺癌不再是老年病:33岁作家的离世引发关注,有这些情况的请注意
近期,90后网络小说家七月新番和26岁男艺人蒋某某因肺癌去世,引发关注。他们都没有吸烟习惯,那么他们为什么会得肺癌呢?浙大二院呼吸内科副主任医师兰芬说,现在年轻人熬夜、加班导致身体过劳,在劳累情况下身…...
脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)
一、数据处理与分析实战 (一)实时滤波与参数调整 基础滤波操作 60Hz 工频滤波:勾选界面右侧 “60Hz” 复选框,可有效抑制电网干扰(适用于北美地区,欧洲用户可调整为 50Hz)。 平滑处理&…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...

STM32标准库-DMA直接存储器存取
文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA(Direct Memory Access)直接存储器存取 DMA可以提供外设…...

linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...

1.3 VSCode安装与环境配置
进入网址Visual Studio Code - Code Editing. Redefined下载.deb文件,然后打开终端,进入下载文件夹,键入命令 sudo dpkg -i code_1.100.3-1748872405_amd64.deb 在终端键入命令code即启动vscode 需要安装插件列表 1.Chinese简化 2.ros …...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...
今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存
文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...
C++.OpenGL (14/64)多光源(Multiple Lights)
多光源(Multiple Lights) 多光源渲染技术概览 #mermaid-svg-3L5e5gGn76TNh7Lq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3L5e5gGn76TNh7Lq .error-icon{fill:#552222;}#mermaid-svg-3L5e5gGn76TNh7Lq .erro…...

打手机检测算法AI智能分析网关V4守护公共/工业/医疗等多场景安全应用
一、方案背景 在现代生产与生活场景中,如工厂高危作业区、医院手术室、公共场景等,人员违规打手机的行为潜藏着巨大风险。传统依靠人工巡查的监管方式,存在效率低、覆盖面不足、判断主观性强等问题,难以满足对人员打手机行为精…...
日常一水C
多态 言简意赅:就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过,当子类和父类的函数名相同时,会隐藏父类的同名函数转而调用子类的同名函数,如果要调用父类的同名函数,那么就需要对父类进行引用&#…...