【Overload游戏引擎细节分析】PBR材质Shader---完结篇

news2024/11/20 0:42:16

PBR基于物理的渲染可以实现更加真实的效果,其Shader值得分析一下。但PBR需要较多的基础知识,不适合不会OpenGL的朋友。

一、PBR理论

PBR指基于物理的渲染,其理论较多,需要的基础知识也较多,我在这就不再写一遍了,具体可以参看:
LearnOpenGL PBR理论-英文 或者 LearnOpenGL PBR理论-中文

Overload也提供了这种材料,借助贴图可以实现非常真实的材质效果。下面这个例子的贴图来自LearnOpenGL,大家可以自己去下载。
在这里插入图片描述

二、PBR Shader分析

顶点着色器
#shader vertex
#version 430 core

layout (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.0
        float((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; // 环境光遮蔽AO
    vec3 normal;

    if (u_EnableNormalMapping) // 是否使用法线贴图
    {
        normal = texture(u_NormalMap, texCoords).rgb; // 法线贴图的原始值
        normal = normalize(normal * 2.0 - 1.0);   // 法线贴图矢量坐标范围变成-1到1
        normal = 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 equation
    vec3 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 brdf
            float 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 Lo
            float 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大体分为三部分:

  1. 从贴图中获取反照率、金属度、粗糙度、法线数据
  2. 计算灯光光照,环境光灯光只影响环境光;方向光、聚光灯、点光源会影响光强lightCoeff,最终的光照使用cook-torrance模型进行计算,公式可以参考LearnOpenGL
  3. 最后进行环境光与PBR模型结果进行叠加,并进行色调映射与gamma矫正,这里使用的公式在LearnOpenGL中都有的

总结:
这个PBR Shader整体上与LearnOpenGL中的理论一致,看完LearnOpenGL之后再看这个Shader就比较简单了。

完结总结:

写的这里,这个专栏暂时告一段落了,主要分析了Overload的Render模块,其他的包括UI、物理引擎、音频等模块没有涉及。Overload是一个Demo性质的游戏引擎,其渲染涉只涉及最基础的渲染方式,是对OpenGL简单封装,远远满足不了实际游戏开发需求,只能作为渲染引擎入门。
另外,这个专栏的文章只聚焦一些细节,对应架构涉及很少,因为本人发现架构方面的文章参考性不大,一旦一个软件定型架构方面的改动很困难,读了软件架构的文章也很难在工作中用上。故单纯只介绍一个技术点反而可能拿来直接使用。最后希望能对大家有所帮助!

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

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

相关文章

带有强大提醒功能的电脑便签工具

在这个充满节奏感的现代生活中&#xff0c;每一天都需要精确规划和提醒&#xff0c;以确保工作计划得以按时完成。为了做到这一点&#xff0c;你需要一款强大的电脑便签工具&#xff0c;它不仅能让你记录工作计划&#xff0c;还能在关键时刻提醒你。 在电脑上记录工作计划是一…

【Amazon】跨AWS账号资源授权存取访问

文章目录 一、实验框架图二、实验过程说明三、实验演示过程1、在A账号中创建S3存储桶2、在A账号创建S3存储桶访问策略3、在A账号创建信任开发账号的角色4、在B账号为用户添加内联策略5、在B账号中切换角色&#xff0c;以访问A账号中的S3资源 四、实验总结 一、实验框架图 本次…

pgAdmin 4 v7.8 发布,PostgreSQL 开源图形化管理工具

导读pgAdmin 是 PostgreSQL 领先的开源图形化管理工具。pgAdmin 4 旨在满足新手和有经验的 Postgres 用户的需求&#xff0c;提供强大的图形界面&#xff0c;简化了数据库对象的创建、维护和使用。 pgAdmin 开发团队日前发布了 pgAdmin 4 v7.8 版本&#xff0c;这个版本包括 21…

外汇天眼:以下平台牌照被撤销,速度远离!

监管信息早知道&#xff01;外汇天眼将每周定期公布监管牌照状态发生变化的交易商&#xff0c;以供投资者参考&#xff0c;规避投资风险。如果平台天眼评分过高&#xff0c;建议投资者谨慎选择&#xff0c;因为在外汇天眼评分高不代表平台没问题&#xff01; 以下是监管牌照发生…

【地理位置识别】IP归属地应用的特点

IP归属地应用是一类用于确定特定IP地址的地理位置信息&#xff08;通常是城市、地区或国家&#xff09;的工具和服务。以下是IP归属地应用的几个主要特点&#xff1a; 地理位置识别&#xff1a; IP归属地应用主要用于确定IP地址的地理位置。这可以帮助组织更好地了解其网站访问…

CAD需要学c语言嘛?

CAD需要学c语言嘛&#xff1f; AutoCAD 和 C 语言没有关系的。 如果非要说是 AutoCAD 和哪个编程语言有关系&#xff0c;那应该是 VBA, 可以通过 VBA 编程&#xff0c;最近很多小伙伴找我&#xff0c;说想要一些c语言资料&#xff0c;然后我根据自己从业十年经验&#xff0c;熬…

免费提取视频号视频工具有哪些,这个4种方法亲测可用!

很多朋友对视频号都是比较依赖的&#xff0c;另外关于视频号视频下载&#xff0c;一直想找一个关于免费提取视频的方法&#xff0c;今天我就来聊聊该如何使用。 方法一&#xff1a;录屏 在选择需要的设备&#xff0c;并在应用商店搜索录屏工具&#xff0c;或者直接采用手机自…

亚马逊,速卖通,美客多如何打造爆款商品,排名提升榜首

1、产品Listing的完整性 Listing是亚马逊A9算法认识你产品的基础&#xff0c;在发布一条listing的时候&#xff0c;尽可能地做到最好!在准备一条listing之前&#xff0c;一定事先要收集、整理足够多的产品关键词&#xff0c;在优化listing内容的时候填充进去。仔细观察优秀竞品…

《进化优化》第12章 差分进化算法

文章目录 算法流程12.1 基本差分进化算法12.2 差分进化的变种12.2.1 试验向量12.2.2 变异向量12.2.3 比例因子的调整 12.3 离散优化12.3.1 混合整数差分进化12.3.2 离散差分进化 12.4 差分进化与遗传算法 算法流程 12.1 基本差分进化算法 差分进化是为了优化n维连续域中的函数…

人工智能云服务(Alaas)

目录 1、概念介绍 2、人工智能云服务解决了什么问题&#xff1f; 2.1 节约部署成本 2.2 海量数据和机器学习 2.3 降低用户使用人工智能服务的成本 3、人工智能云服务的类型 3.1 公有云 3.2 私有云 3.3 混合云 4、人工智能云服务案例 4.1 微信小程序 “识花君” 4.2…

文件改名,轻松添加前缀顺序编号,文件改名更高效!

您是否曾经需要批量修改文件名&#xff0c;并希望在文件名中添加特定的前缀或顺序编号&#xff1f;现在&#xff0c;我们为您带来了一款全新的文件改名工具&#xff0c;帮助您轻松解决这个问题&#xff01; 第一步&#xff0c;进入文件批量改名高手主页面&#xff0c;在板块栏…

为什么我觉得Rust比C++复杂得多?

为什么我觉得Rust比C复杂得多&#xff1f; Rust自学确实有一定门槛&#xff0c;很多具体问题解决起来搜索引擎也不太帮的上忙&#xff0c;会出现卡住的情况&#xff0c;卡的时间长了就放弃了。最近很多小伙伴找我&#xff0c;说想要一些c语言资料&#xff0c;然后我根据自己从…

MySQL扩展语句和约束条件

MySQL扩展语句 create TABLE if not exists ky32 (id int(4) zerofill primary key auto_inc rement&#xff0c; #表示该字段可以自增长&#xff0c;默认从1开始每条记录会自动递增1name varchar(10) not null,cradid int(10) not null unique key,hobby varchar (50))&#x…

如何找现货黄金代理

虽然现货黄金并不是非常难以掌握的投资品种&#xff0c;但投资新手在刚刚进入这个市场的时候&#xff0c;有很方面的事情都不太了解&#xff0c;需要比较专业的人士从旁提供一些适切的指导&#xff0c;才可以在交易中做到趋利避害&#xff0c;在控制风险的前提下&#xff0c;获…

前端环境的安装 Node npm yarn

一 node npm 1.下载NodeJS安装包 下载地址&#xff1a;Download | Node.js 2.开始安装 打开安装包后&#xff0c;一直Next即可。当然&#xff0c;建议还是修改一下安装位置&#xff0c;NodeJS默认安装位置为 C:\Program Files 3.验证是否安装成功 打开DOS命令界面&#…

Keil Map信息解析

基本功能&#xff1a; 1.在Keil里面&#xff0c;通过App.Map复制所有信息。然后解析剪辑版内容。 2.随意输入一个函数内存地址&#xff0c;即可遍历出该内存地址属于哪个.c或者函数名。或者能遍历出变量。 强化功能&#xff1a; 1.通过Keil5 命令 Save xxxxxxx\1.Hex 0x200173…

一文了解什么是JWT 与sessions

​session 和 JSON Web 令牌 (JWT) 是在调用之间维护此身份验证状态的两种最流行的方法。两者各有利弊&#xff0c;在它们之间进行选择需要了解这些权衡以及它们与应用程序的特定需求之间的关系。 一、基于session的身份验证 在基于session的身份验证&#xff08;也称为基于 c…

GoLong的学习之路(十六)基础工具之Gin框架

Gin框架介绍及使用&#xff0c;这张不用看内容就知道非常重要&#xff0c;重要到什么地步呢&#xff1f;重要到开发java不会Spring全家桶这种概念。 上几篇文章写的是如何构建骨架&#xff0c;经脉。这一章是将血肉注入。 文章目录 Gin框架RESTful API Gin渲染HTML渲染静态文件…

编程实例:钟表维修保养管理系统软件编程,支持拍图留存查询

编程实例&#xff1a;钟表维修保养管理系统软件编程&#xff0c;支持拍图留存查询 软件文件为压缩包文件&#xff0c;将解压后文件夹目录下的程序图标&#xff0c;按鼠标右键 发送到 桌面快捷方式&#xff0c;即可完成安装。如不明白可以咨询客服。 软件特色&#xff1a; …

Qt QWidget、QDialog、QMainWindow的区别

QWidget QWidget是Qt框架中最基础的窗口类&#xff0c;可以理解为用户界面的最基本单元。QWidget类提供了一个空白窗口&#xff0c;可以通过继承该类来创建自定义的窗口类。QWidget类提供了基本的窗口属性和方法&#xff0c;如大小、位置、标题、图标等。 QDialog QDialog是…