【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岁男艺人蒋某某因肺癌去世,引发关注。他们都没有吸烟习惯,那么他们为什么会得肺癌呢?浙大二院呼吸内科副主任医师兰芬说,现在年轻人熬夜、加班导致身体过劳,在劳累情况下身…...

利用最小二乘法找圆心和半径
#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...

8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...

家政维修平台实战20:权限设计
目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系,主要是分成几个表,用户表我们是记录用户的基础信息,包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题,不同的角色…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...

如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...

IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)
文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...
Typeerror: cannot read properties of undefined (reading ‘XXX‘)
最近需要在离线机器上运行软件,所以得把软件用docker打包起来,大部分功能都没问题,出了一个奇怪的事情。同样的代码,在本机上用vscode可以运行起来,但是打包之后在docker里出现了问题。使用的是dialog组件,…...
现有的 Redis 分布式锁库(如 Redisson)提供了哪些便利?
现有的 Redis 分布式锁库(如 Redisson)相比于开发者自己基于 Redis 命令(如 SETNX, EXPIRE, DEL)手动实现分布式锁,提供了巨大的便利性和健壮性。主要体现在以下几个方面: 原子性保证 (Atomicity)ÿ…...