Learn OpenGL 32 PBR光照

光照

在本章节中,我们把重点放在将之前讨论的理论转化为实际的渲染器,这个渲染器将使用直接的(或解析的)光源:比如点光源,定向灯或聚光灯。

我们先来看看上一个章提到的反射方程的最终版:

我们大致上清楚这个反射方程在干什么,但我们仍然留有一些迷雾尚未揭开。比如说我们究竟将怎样表示场景上的辐照度(Irradiance), 辐射率(Radiance) L我们知道辐射率L(在计算机图形领域中)表示光源的辐射通量(Radiant flux),或光源在给定立体角ω下发出的光能。在我们的情况下,不妨假设立体角ω无限小,这样辐射度就表示光源在一条光线或单个方向向量上的辐射通量。

基于以上的知识,我们如何将其转化为之前的教程中所积累的一些光照知识呢? 那么想象一下,我们有一个点光源(一个在所有方向都具有相同亮度的光源),它的辐射通量为用RGB表示为(23.47, 21.31, 20.79)。该光源的辐射强度(Radiant Intensity)等于其在所有出射光线的辐射通量。 然而,当我们为一个表面上的特定的点p着色时,在其半球领域ΩΩ的所有可能的入射方向上,只有一个入射方向向量ωi直接来自于该点光源。 假设我们在场景中只有一个光源,位于空间中的某一个点,因而对于p点的其他可能的入射光线方向上的辐射率为0:

如果从一开始,我们就假设点光源不受光线衰减(光照强度会随着距离变暗)的影响,那么无论我们把光源放在哪,入射光线的辐射率总是一样的(除去入射角cosθ对辐射率的影响之外)。 这是因为无论我们从哪个角度观察它,点光源总具有相同的辐射强度,我们可以有效地将其辐射强度建模为其辐射通量: 一个常量向量(23.47, 21.31, 20.79)

然而,辐射率也需要将位置p作为输入,正如所有现实的点光源都会受光线衰减影响一样,点光源的辐射强度应该根据点p所在的位置和光源的位置以及他们之间的距离而做一些缩放。 因此,根据原始的辐射方程,我们会根据表面法向量n和入射角度wi来缩放光源的辐射强度。

在实现上来说:对于直接点光源的情况,辐射率函数L先获取光源的颜色值, 然后光源和某点p的距离衰减,接着按照n⋅wi缩放,但是仅仅有一条入射角为wi的光线打在点p上, 这个wi同时也等于在p点光源的方向向量。写成代码的话会是这样:

vec3  lightColor  = vec3(23.47, 21.31, 20.79);
vec3  wi          = normalize(lightPos - fragPos);
float cosTheta    = max(dot(N, Wi), 0.0);
float attenuation = calculateAttenuation(fragPos, lightPos);
float radiance    = lightColor * attenuation * cosTheta;

除了一些术语上的差异外,这段代码对你们来说应该很熟悉:这正是我们一直以来怎么计算漫反射光照的!当涉及到直接光照(direct lighting)时,辐射率的计算方式和我们之前计算只有一个光源照射在物体表面的时候非常相似。

请注意,这个假设成立的条件是点光源体积无限小,相当于在空间中的一个点。如果我们认为该光源是具有体积的,它的辐射率会在不只一个入射光方向上非零。

对于其它类型的从单点发出来的光源我们类似地计算出辐射率。比如,定向光(directional light)拥有恒定的wi而不会有衰减因子;而一个聚光灯光源则没有恒定的辐射强度,其辐射强度是根据聚光灯的方向向量来缩放的。

这也让我们回到了对于表面的半球领域(hemisphere)ΩΩ的积分∫∫上。由于我们事先知道的所有贡献光源的位置,因此对物体表面上的一个点着色并不需要我们尝试去求解积分。我们可以直接拿光源的(已知的)数目,去计算它们的总辐照度,因为每个光源仅仅只有一个方向上的光线会影响物体表面的辐射率。这使得PBR对直接光源的计算相对简单,因为我们只需要有效地遍历所有有贡献的光源。而当我们之后把环境照明也考虑在内的IBL教程中,我们就必须采取积分去计算了,这是因为光线可能会在任何一个方向入射。

一个PBR表面模型

现在让我们开始写片段着色器来实现上述的PBR模型吧~ 首先我们需要把PBR相关的输入放进片段着色器。

#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
in vec3 WorldPos;
in vec3 Normal;

uniform vec3 camPos;

uniform vec3  albedo;
uniform float metallic;
uniform float roughness;
uniform float ao;

我们把通用的顶点着色器的输出作为输入的一部分。另一部分输入则是物体表面模型的一些材质参数。

然后在片段着色器的开始部分我们做一下任何光照算法都需要做的计算:

void main()
{
    vec3 N = normalize(Normal); 
    vec3 V = normalize(camPos - WorldPos);
    [...]
}

直接光照

在本教程的例子中我们会采用总共4个点光源来直接表示场景的辐照度。为了满足反射率方程,我们循环遍历每一个光源,计算他们各自的辐射率然后求和,接着根据BRDF和光源的入射角来缩放该辐射率。我们可以把循环当作在物体的半球领域内对所有直接光源求积分。首先我们来计算一些可以预计算的光照变量:

vec3 Lo = vec3(0.0);
for(int i = 0; i < 4; ++i) 
{
    vec3 L = normalize(lightPositions[i] - WorldPos);
    vec3 H = normalize(V + L);

    float distance    = length(lightPositions[i] - WorldPos);
    float attenuation = 1.0 / (distance * distance);
    vec3 radiance     = lightColors[i] * attenuation; 
    [...]  

由于我们在线性空间内计算光照(我们会在着色器的最后进行Gamma校正),我们使用在物理上更为准确的平方倒数作为衰减。

相对于物理上正确来说,你可能仍然想使用常量,线性或者二次衰减方程(他们在物理上相对不准确),却可以为您提供在光的能量衰减更多的控制。

然后,对于每一个光源我们都想计算完整的 Cook-Torrance specular BRDF项:

首先我们想计算的是镜面反射和漫反射之间的比值,或者说与表面折射的光线相比,它反射了多少光线。 我们从上一个教程知道可以使用菲涅尔方程计算(注意这里用的clamp是为了避免黑点):

vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}  

Fresnel-Schlick近似法接收一个参数F0,被称为0°入射角的反射率,或者说是直接(垂直)观察表面时有多少光线会被反射。 这个参数F0会因为材料不同而不同,而且对于金属材质会带有颜色。在PBR金属流中我们简单地认为大多数的绝缘体在F0为0.04的时候看起来视觉上是正确的,对于金属表面我们根据反射率特别地指定F0。 因此代码上看起来会像是这样:

vec3 F0 = vec3(0.04); 
F0      = mix(F0, albedo, metallic);
vec3 F  = fresnelSchlick(max(dot(H, V), 0.0), F0);

可以看到,对于非金属表面F0始终为0.04。对于金属表面,我们根据初始的F0和表现金属属性的反射率进行线性插值。

我们已经算出F, 剩下的项就是计算法线分布函数D和几何遮蔽函数G了。

在直接PBR光照着色器中D和G的计算代码类似于:

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

这里比较重要的是和理论章节相比,我们直接把粗糙度(roughness)作为参数传给了上述函数;通过这种方式,我们可以针对每一个不同的项对粗糙度做一些修改。根据迪士尼公司给出的观察以及后来被Epic Games公司采用的光照模型,在几何遮蔽函数和法线分布函数中采用粗糙度的平方会让光照看起来更加自然。

现在两个函数都给出了定义,在计算反射的循环中计算NDF和G项就变得很简单:

float NDF = DistributionGGX(N, H, roughness);       
float G   = GeometrySmith(N, V, L, roughness);       

这样我们就凑够了足够的项来计算Cook-Torrance BRDF:

vec3 nominator    = NDF * G * F;
float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001; 
vec3 specular     = nominator / denominator;  

注意我们在分母项中加了一个0.001为了避免出现除零错误。

现在我们终于可以计算每个光源在反射率方程中的贡献值了!因为菲涅尔方程直接给出了kS, 我们可以使用F表示所有打在物体表面上的镜面反射光的贡献。 从kS我们很容易计算折射的比值kD:

vec3 kS = F;
vec3 kD = vec3(1.0) - kS;

kD *= 1.0 - metallic;   

我们可以看作kS表示光能中被反射的能量的比例, 而剩下的光能会被折射, 比值即为kD。更进一步来说,因为金属不会折射光线,因此不会有漫反射。所以如果表面是金属的,我们会把系数kD变为0。 这样,我们终于集齐所有变量来计算我们出射光线的值:

    const float PI = 3.14159265359;

    float NdotL = max(dot(N, L), 0.0);        
    Lo += (kD * albedo / PI + specular) * radiance * NdotL;
}

最终的结果Lo,或者说是出射光线的辐射率,实际上是反射率方程的在半球领域ΩΩ的积分的结果。但是我们实际上不需要去求积,因为对于所有可能的入射光线方向我们知道只有4个方向的入射光线会影响片段的着色。因为这样,我们可以直接循环N次计算这些入射光线的方向(N也就是场景中光源的数目)。

剩下的工作就是加一个环境光照项给Lo,然后我们就拥有了片段的最后颜色:

vec3 ambient = vec3(0.03) * albedo * ao;
vec3 color   = ambient + Lo;  

线性空间和HDR渲染

直到现在,我们假设的所有计算都在线性的颜色空间中进行的,因此我们需要在着色器最后做伽马矫正。 在线性空间中计算光照是非常重要的,因为PBR要求所有输入都是线性的,如果不是这样,我们就会得到不正常的光照。另外,我们希望所有光照的输入都尽可能的接近他们在物理上的取值,这样他们的反射率或者说颜色值就会在色谱上有比较大的变化空间。Lo作为结果可能会变大得很快(超过1),但是因为默认的低动态范围(LDR)而取值被截断。所以在伽马矫正之前我们采用色调映射使Lo从LDR的值映射为HDR的值。

color = color / (color + vec3(1.0));
color = pow(color, vec3(1.0/2.2)); 

这里我们采用的色调映射方法为Reinhard 操作,使得我们在伽马矫正后可以保留尽可能多的辐照度变化。 我们没有使用一个独立的帧缓冲或者采用后期处理,所以我们需要直接在每一步光照计算后采用色调映射和伽马矫正。

采用线性颜色空间和HDR在PBR渲染管线中非常重要。如果没有这些操作,几乎是不可能正确地捕获到因光照强度变化的细节,这最终会导致你的计算变得不正确,在视觉上看上去非常不自然。

完整的直接光照PBR着色器

现在剩下的事情就是把做好色调映射和伽马矫正的颜色值传给片段着色器的输出,然后我们就拥有了自己的直接光照PBR着色器。 为了完整性,这里给出了完整的代码:

#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
in vec3 WorldPos;
in vec3 Normal;

// material parameters
uniform vec3  albedo;
uniform float metallic;
uniform float roughness;
uniform float ao;

// lights
uniform vec3 lightPositions[4];
uniform vec3 lightColors[4];

uniform vec3 camPos;

const float PI = 3.14159265359;

float DistributionGGX(vec3 N, vec3 H, float roughness);
float GeometrySchlickGGX(float NdotV, float roughness);
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness);
vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness);

void main()
{       
    vec3 N = normalize(Normal);
    vec3 V = normalize(camPos - WorldPos);

    vec3 F0 = vec3(0.04); 
    F0 = mix(F0, albedo, metallic);

    // reflectance equation
    vec3 Lo = vec3(0.0);
    for(int i = 0; i < 4; ++i) 
    {
        // calculate per-light radiance
        vec3 L = normalize(lightPositions[i] - WorldPos);
        vec3 H = normalize(V + L);
        float distance    = length(lightPositions[i] - WorldPos);
        float attenuation = 1.0 / (distance * distance);
        vec3 radiance     = lightColors[i] * attenuation;        

        // 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 nominator    = NDF * G * F;
        float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001; 
        vec3 specular     = nominator / denominator;

        // add to outgoing radiance Lo
        float NdotL = max(dot(N, L), 0.0);                
        Lo += (kD * albedo / PI + specular) * radiance * NdotL; 
    }   

    vec3 ambient = vec3(0.03) * albedo * ao;
    vec3 color = ambient + Lo;

    color = color / (color + vec3(1.0));
    color = pow(color, vec3(1.0/2.2));  

    FragColor = vec4(color, 1.0);
}  

如果我们采用这个着色器,加上4个点光源和一些球体,同时令这些球体的金属性(metallic)和粗糙度(roughness)沿垂直和水平方向分别变化,我们会得到这样的结果:

(上述图片)从下往上球体的金属性从0.0变到1.0, 从左到右球体的粗糙度从0.0变到1.0。你可以看到仅仅改变这两个值,显示的效果会发生巨大的改变!

下面是十行十列的效果:

带贴图的PBR

把我们系统扩展成可以接受纹理作为参数可以让我们对物体的材质有更多的自定义空间:

[...]
uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;

void main()
{
    vec3 albedo     = pow(texture(albedoMap, TexCoords).rgb, 2.2);
    vec3 normal     = getNormalFromNormalMap();
    float metallic  = texture(metallicMap, TexCoords).r;
    float roughness = texture(roughnessMap, TexCoords).r;
    float ao        = texture(aoMap, TexCoords).r;
    [...]
}

不过需要注意的是一般来说反射率(albedo)纹理在美术人员创建的时候就已经在sRGB空间了,因此我们需要在光照计算之前先把他们转换到线性空间。一般来说,环境光遮蔽贴图(ambient occlusion maps)也需要我们转换到线性空间。不过金属性(Metallic)和粗糙度(Roughness)贴图大多数时间都会保证在线性空间中。

只是把之前的球体的材质性质换成纹理属性,就在视觉上有巨大的提升:

注意金属表面会在场景中看起来有点黑,因为他们没有漫反射。它们会在考虑环境镜面光照的时候看起来更加自然,不过这是我们下一个教程的事情了。

相比起在网上找到的其他PBR渲染结果来说,尽管在视觉上不算是非常震撼,因为我们还没考虑到基于图片的光照(IBL),但我们现在也算是有了一个基于物理的渲染器了,虽然还没考虑IBL,但你会发现你的光照看起来更加真实了。

最终效果:

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

UI自动化测试用例管理平台搭建

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 用到的工具&#xff1a;python3 django2 mysql RabbitMQ celery selenium python3和selen…

Linux系统编程--信号

1、信号&#xff08;一&#xff09; 1.1、什么是中断 1.2、中断分类 1.3、信号 1.4、信号与中断 1.5、信号名称 1.6、进程对信号的三种响应 1.7、signal函数&#xff1a;注册信号 signal(SIGINT, handler);返回值是SIGINT所对应的处理程序 再调用一下signal(SIGINT, handler2…

QT数据类型和容器用法

Qt库提供了基于通用模板的容器类, 这些类可用于存储指定类型的数据项&#xff0c;Qt中这些容器类的设计比STL容器更轻&#xff0c;更安全且更易于使用。容器类也都是隐式共的&#xff0c;它们是可重入的&#xff0c;并且已针对速度/低内存消耗和最小的内联代码扩展进行了优化&a…

(二)windows配置JDK环境

1、安装包下载地址,官网:Java Archive | Oracle 长期稳定支持版本8、11、17、21 选择一个需要下载的连接点进去: 在下载列表中根据操作系统选择不同的下载包: 注意:部分版本下载需要先登录后才可以下载。

Vit Transformer

一 VitTransformer 介绍 vit : An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale 论文是基于Attention Is All You Need&#xff0c;由于图像数据和词数据数据格式不一样&#xff0c;经典的transformer不能处理图像数据&#xff0c;在视觉领域的应…

C++电子宠物商店

一、功能描述 店内有不同类型的电子宠物 1.每种电子宠物能通过显示出来的文本提出需要或表示情绪如&#xff1a;饿、渴、饱涨、困、不舒服、高兴、生气、伤心、绝望、无聊等。 2.店员用户通过键盘操作“饲养”电子宠物&#xff0c;给它实施喂饭、喂水、带它上厕所、陪它玩耍、…

3.27作业

1、完成下面类 #include <iostream> #include <cstring> using namespace std;class myString { private:char *str; //记录c风格的字符串int size; //记录字符串的实际长度 public://无参构造myString():size(10){str new char[size]; …

凯撒加密.

题目描述 给定一个单词&#xff0c;请使用凯撒密码将这个单词加密 凯撒密码是一种替换加密的技术&#xff0c;单词中的所有字母都在字母表上向后偏移3位后被替换成密文。即a变为 d&#xff0c;变为e.&#xff0c;w 变为z&#xff0c;2 变为a&#xff0c;y变为 6&#xff0c;z变…

javaWeb项目-大学生体质测试管理系统功能介绍

项目关键技术 开发工具&#xff1a;IDEA 、Eclipse 编程语言: Java 数据库: MySQL5.7 框架&#xff1a;ssm、Springboot 前端&#xff1a;Vue、ElementUI 关键技术&#xff1a;springboot、SSM、vue、MYSQL、MAVEN 数据库工具&#xff1a;Navicat、SQLyog 1、JSP技术 JSP(Jav…

python笔记进阶--面向对象(2)

目录 1.类和对象&#xff08;实例&#xff09; 1.1对象&#xff08;实例&#xff09; 1.1.1使用对象组织数据 1.1.2类中增加属性 1.2成员方法&#xff08;类&#xff09; 1.2.1类的定义和使用语法 1.2.2成员方法的使用 1.2.3self关键字的作用 1.3类和对象 2&#xff…

Mysql各种日志管理

文章目录 事务日志事务日志的记录过程事务日志类型事务日志的相关变量 错误日志二进制日志功能作用文件的构成日志格式查看日志删除日志 通用日志慢查询日志 Mysql日志记录着数据库在运行过程中的各种操作&#xff0c;帮助管理员定位查找问题。 事务日志 事务日志(Transaction…

方案分享 | 嵌入式指纹方案

随着智能设备的持续发展&#xff0c;指纹识别技术成为了现在智能终端市场和移动支付市场中占有率最高的生物识别技术。凭借高识别率、短耗时等优势&#xff0c;被广泛地运用在智能门锁、智能手机、智能家居等设备上。 上海航芯在2015年进入指纹识别应用领域&#xff0c;自研高性…

MySQL的安装(Linux版)

1.所需要的文件 MySQL.zip 2. 卸载自带的Mysql-libs # 查看是否存在 rpm -qa | grep mariadb# 如果存在则执行命令进行卸载 rpm -e --nodeps mariadb-libs3.在/opt目录下创建MySQL目录并上传所需要的安装包 cd /optmkdir MySQL4.按照编号顺序安装&#xff08;压缩包在解压完…

vs2010打包QT程序

一、环境 win10 、 VS2010 、 qt5.7.1 将代码在release模式下运行 运行完后会在相应的文件夹下生成exe文件&#xff0c;也会将部分dll文件拷贝到release文件夹中 二、生成可执行文件 2.1 选择“文件”->“新建”->”项目“ 2.2 在打开的对框中选择”其他类型项目…

视频素材网有哪些?7个优质无水印素材库推荐

当今社会&#xff0c;视频内容已成为最吸引眼球的媒介之一&#xff0c;无论是社交媒体的短视频还是专业制作的影片&#xff0c;都离不开高质量的视频素材。为了帮助你在茫茫网海中找到宝藏&#xff0c;下面我精选了一系列视频素材网站&#xff0c;不仅提供丰富多样的视频资源&a…

(1) 易经与命运_学习笔记

个人笔记&#xff0c;斟酌阅读 占卦的原理 三个铜板&#xff0c;正面是3&#xff0c;反面2&#xff0c;三个一起转&#xff0c;得出6,7,8,9 数字象6老阴7少阳8少阴9老阳 生数和成数 生数和成数应该说出自《河图》。其中一二三四五为生数&#xff0c;六七八九十为成数。 生…

Typst入门简明教程

文章目录 写在前面对比阅读安装Typst的安装系统路径设置VSCode的设置 用Typst写作节指令图指令表指令公式指令参考文献指令引用指令节的标签图、表、公式的标签参考文献的标签 Typst编译 Typst的<label>写法建议写在最后&#xff1a;Typst、LaTex和Word的比较 写在前面 …

面试经典150题【111-120】

文章目录 面试经典150题【111-120】67.二进制求和190.颠倒二进制位191.位1的个数136.只出现一次的数字137.只出现一次的数字II201.数字范围按位与5.最长回文子串97.交错字符串72.编辑距离221.最大正方形 面试经典150题【111-120】 六道位运算&#xff0c;四道二维dp 67.二进制…

未能加载文件或程序集socutdata或它的某一个依赖项试图加载格式不正确的程序

未能加载文件或程序集socut data或它的某一个依赖项试图加载格式不正确的程序 Socut.Data.dll找不到类型或命名空间名称 把bin目录下面 的socut.data.dll删除就行了 C#报错未能加载文件或程序集socut data或它的某一个依赖项试图加载格式不正确的程序 "/"应用程序…

逐步学习Go-并发通道chan(channel)

概述 Go的Routines并发模型是基于CSP&#xff0c;如果你看过七周七并发&#xff0c;那么你应该了解。 什么是CSP&#xff1f; "Communicating Sequential Processes"&#xff08;CSP&#xff09;这个词组的含义来自其英文直译以及在计算机科学中的使用环境。 CSP…