基于物理的实时渲染 -- PBR

news2024/9/27 21:29:30

简介

PBR,或者用更通俗一些的称呼是指基于物理的渲染(Physically Based Rendering),它指的是一些在不同程度上都基于与现实世界的物理原理更相符的基本理论所构成的渲染技术的集合。正因为基于物理的渲染目的便是为了使用一种更符合物理学规律的方式来模拟光线,因此这种渲染方式与我们原来的Phong或者Blinn-Phong光照算法相比总体上看起来要更真实一些。除了看起来更好些以外,由于它与物理性质非常接近,因此我们(尤其是美术师们)可以直接以物理参数为依据来编写表面材质,而不必依靠粗劣的修改与调整来让光照效果看上去正常。使用基于物理参数的方法来编写材质还有一个更大的好处,就是不论光照条件如何,这些材质看上去都会是正确的,而在非PBR的渲染管线当中有些东西就不会那么真实了。

这种渲染方式需要遵循以下三个条件:

  • 基于微平面(Microfacet)的表面模型。
  • 能量守恒。
  • 应用基于物理的BRDF。

一、 微平面模型

所有的PBR技术都基于微平面理论。这项理论认为,达到微观尺度之后任何平面都可以用被称为微平面(Microfacets)的细小镜面来进行描绘。根据平面粗糙程度的不同,这些细小镜面的取向排列可以相当不一致:
在这里插入图片描述
产生的效果就是:一个平面越是粗糙,这个平面上的微平面的排列就越混乱。这些微小镜面这样无序取向排列的影响就是,当我们特指镜面光/镜面反射时,入射光线更趋向于向完全不同的方向发散(Scatter)开来,进而产生出分布范围更广泛的镜面反射。而与之相反的是,对于一个光滑的平面,光线大体上会更趋向于向同一个方向反射,造成更小更锐利的反射:
在这里插入图片描述
我们可以基于一个平面的粗糙度来计算出某个向量的方向与微平面平均取向方向一致的概率。这个向量便是位于光线向量LigthDir和视线向量ViewDir之间的中间向量(Halfway Vector)。H向量等于标准化后的(L向量+V向量)即:H=normalize(L+V)。

二、 能量守恒

微平面近似法使用了这样一种形式的能量守恒(Energy Conservation):出射光线的能量永远不能超过入射光线的能量(发光面除外)。当一束光线碰撞到一个表面的时候,它就会分离成一个折射部分和一个反射部分。反射部分就是会直接反射开来而不会进入平面的那部分光线,这就是我们所说的镜面光照。而折射部分就是余下的会进入表面并被吸收的那部分光线,这也就是我们所说的漫反射光照。

反射光与折射光它们二者之间是互斥的关系。无论何种光线,其被材质表面所反射的能量将无法再被材质吸收。因此,诸如折射光这样的余下的进入表面之中的能量正好就是我们计算完反射之后余下的能量。我们按照能量守恒的关系,首先计算镜面反射部分,它的值等于入射光线被反射的能量所占的百分比。然后折射光部分就可以直接由镜面反射部分计算得出。

三、 反射率方程

在这里插入图片描述
该函数是个连续函数所以我们需要对其进行离散化,离散化后的各个符号代表的含义如下:p表示观察点、ωo表示观察方向即出射方向、ωi表示入射方向、L表示光源颜色方程、fr表示双向反射分布函数。

3.1 辐射率方程

这个方程表示的是,一个拥有辐射通量Φ的光源在单位面积A,单位立体角ω上的辐射出的总能量:
在这里插入图片描述
如果我们把立体角ω和面积A看作是无穷小的,那么我们就能用辐射率来表示单束光线穿过空间中的一个点的通量。这样我们可以计算得出作用于单个(片段)点上的单束光线的辐射率,我们实际上把立体角ω转变为方向向量ωi然后把面A转换为点p。这样我们可以直接在着色器中使用辐射率来计算单束光线对每个片段的作用了。

ps:事实上,当涉及到辐射率时,我们通常关心的是所有投射到点pp上的光线的总和,而这个和就称为辐射照度或者辐照度,辐照度公式如下。
在这里插入图片描述

vec3  lightColor  = vec3(23.47f, 21.31f, 20.79f);//我们一般用RGB代替辐射通量Φ
vec3  wi          = normalize(lightPos - fragWorldPos);//入射方向
float cosTheta    = max(dot(n, wi), 0.0f);
float attenuation = calculateAttenuation(fragWorldPos, lightPos);//光线衰减
float radiance    = lightColor * attenuation * cosTheta;

3.2 BRDF双向反射分布函数

它接受入射(光)方向ωi,出射(观察)方向ωo,平面法线n以及一个用来表示微平面粗糙程度的参数a作为函数的输入参数。BRDF可以近似的求出每束光线对一个给定了材质属性的平面上最终反射出来的光线所作出的贡献程度。举例来说,如果一个平面拥有完全光滑的表面(比如镜面),那么对于所有的入射光线ωi(除了一束以外)而言BRDF函数都会返回0.0 ,只有一束与出射光线ωo拥有相同(被反射)角度的光线会得到1.0这个返回值。

我们一般使用Cook-Torrance BRDF模型:
在这里插入图片描述
这里的kd是入射光线中被折射部分的能量所占的比率,而ks是被反射部分的比率。BRDF的左侧表示的是漫反射部分,这里用flambert来表示。它被称为Lambertian漫反射,这和我们之前在漫反射着色中使用的常数因子类似,用如下的公式来表示:
在这里插入图片描述
c表示表面颜色,除以ππ是为了对漫反射光进行标准化。

RDF的镜面反射部分要稍微更高级一些,它的形式如下所示:
在这里插入图片描述
其中D表示正态分布函数、F表示菲涅尔方程、G表示几何函数。

  • 正态分布函数:估算在受到表面粗糙度的影响下,取向方向与中间向量一致的微平面的数量。这是用来估算微平面的主要函数。
  • 几何函数:描述了微平面自成阴影的属性。当一个平面相对比较粗糙的时候,平面表面上的微平面有可能挡住其他的微平面从而减少表面所反射的光线。
  • 菲涅尔方程:菲涅尔方程描述的是在不同的表面角下表面所反射的光线所占的比率

a、正态分布函数
假设给定向量h,如果我们的微平面中有35%与向量h取向一致,则正态分布函数或者说NDF将会返回0.35。
在这里插入图片描述
h表示中间向量,n表示法向量,α表示表面粗糙程度。

float DistributionGGX(vec3 N, vec3 H, float roughness)
{
    float a      = roughness*roughness;//这里不是很懂为什么要把从main中传递过来的粗糙度进行乘方处理
    float a2     = a*a;
    float NdotH  = max(dot(N, H), 0.0);
    float NdotH2 = NdotH*NdotH;
	
    float nom   = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;
	
    return nom / denom;
}

b、几何函数:从统计学上近似的求得了微平面间相互遮蔽的比率,这种相互遮蔽会损耗光线的能量。
在这里插入图片描述
几何函数采用一个材料的粗糙度参数作为输入参数,粗糙度较高的表面其微平面间相互遮蔽的概率就越高。我们将要使用的几何函数是GGX与Schlick-Beckmann近似的结合体,因此又称为Schlick-GGX:
在这里插入图片描述
为了有效的估算几何部分,需要将观察方向(几何遮蔽(Geometry Obstruction))和光线方向向量(几何阴影(Geometry Shadowing))都考虑进去。我们可以使用史密斯法(Smith’s method)来把两者都纳入其中:
在这里插入图片描述

float GeometrySchlickGGX(float NdotV, float roughness)
{
    float r = (roughness + 1.0);
    float k = (r*r) / 8.0;//计算K,这里是直接光照

    float nom   = NdotV;
    float denom = NdotV * (1.0 - k) + k;
	
    return nom / 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;
}

c、菲涅尔方程:
该方程描述的是被反射的光线对比光线被折射的部分所占的比率,这个比率会随着我们观察的角度不同而不同。当光线碰撞到一个表面的时候,菲涅尔方程会根据观察角度告诉我们被反射的光线所占的百分比。利用这个反射比率和能量守恒原则,我们可以直接得出光线被折射的部分以及光线剩余的能量。
当垂直观察的时候,任何物体或者材质表面都有一个基础反射率(Base Reflectivity),但是如果以一定的角度往平面上看的时候所有反光都会变得明显起来。你可以自己尝试一下,用垂直的视角观察你自己的木制/金属桌面,此时一定只有最基本的反射性。但是如果你从近乎90度(译注:应该是指和法线的夹角)的角度观察的话反光就会变得明显的多。如果从理想的角度观察,所有的平面理论上来说都能完全的反射光线。这种现象因菲涅尔而闻名,并体现在了菲涅尔方程之中。
菲涅尔方程是一个相当复杂的方程式,不过幸运的是菲涅尔方程可以用Fresnel-Schlick近似法求得近似解:
在这里插入图片描述
Fo表示平面基础反射率,N为法线向量,V为观察向量。

vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness)
{
    return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
}  

void main()
{
	vec3 F0 = vec3(0.04);//平面的基础反射率
    F0      = mix(F0, albedo, metallic);//根据材质的反射率和金属程度计算较为准确的反射率
    vec3 F  = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);//菲涅尔方程计算出被反射光线的百分比
}

最终方程:
在这里插入图片描述

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

// 材质参数
uniform vec3 albedo;//反射率
uniform float metallic;//金属程度
uniform float roughness;//粗糙程度
uniform float ao;//

// 灯属性
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 a      = roughness*roughness;
    float a2     = a*a;
    float NdotH  = max(dot(N, H), 0.0);
    float NdotH2 = NdotH*NdotH;
	
    float nom   = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;
	
    return nom / denom;
}

float GeometrySchlickGGX(float NdotV, float roughness)
{
    float r = (roughness + 1.0);
    float k = (r*r) / 8.0;

    float nom   = NdotV;
    float denom = NdotV * (1.0 - k) + k;
	
    return nom / 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;
}

//菲涅尔方程
vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness)
{
    return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
}  

void main()
{		
    vec3 N = normalize(Normal);//得到法线向量
    vec3 V = normalize(camPos - WorldPos);//得到观察方向向量

    vec3 F0 = vec3(0.04);//平面的基础反射率
    F0      = mix(F0, albedo, metallic);//根据材质的反射率和金属程度计算较为准确的反射率
    vec3 F  = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);//菲涅尔方程计算出被反射光线的百分比
	
    vec3 kS = F;
    vec3 kD = vec3(1.0) - kS;
    kD *= 1.0 - metallic;	  
           
    // 反射比方程式
    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;//衰减后的灯光     
        
        // cook-torrance brdf
		//双向反射分布函数,它接受入射(光)方向ωi,出射(观察)方向ωo,
		//平面法线nn以及一个用来表示微平面粗糙程度的参数aa作为函数的输入参数。
		//BRDF可以近似的求出每束光线对一个给定了材质属性的平面上最终反射出来的光线所作出的贡献程度。
		float NDF = DistributionGGX(N, H, roughness);//正态分布函数,这是用来估算微平面的主要函数。
        float G   = GeometrySmith(N, V, L, roughness);//几何函数,从统计学上近似的求得了微平面间相互遮蔽的比率
        
		//BRDF镜面反射部分
        vec3 nominator    = NDF * G * F;//分子
        float denominator = 4 * max(dot(V, N), 0.0) * max(dot(L, N), 0.0) + 0.001;//分母 
        vec3 brdf = nominator / denominator;
            
        float NdotL = max(dot(N, L), 0.0);                
        Lo += (kD * albedo / PI + brdf) * 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));  //gamma矫正
   
    FragColor = vec4(color, 1.0);
}  

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

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

相关文章

如何申请代码签名证书

代码签名证书也是数字证书的一种&#xff0c;其主要作用是对可执行脚本、软件代码和内容进行数字签名的数字证书。代码签名证书用于验证开发者身份真实性、保护代码的完整性。用户下载软件时&#xff0c;能通过数字签名验证软件来源&#xff0c;确认软件、代码没有被非法篡改或…

2024 年广西职业院校技能大赛高职组《云计算应用》赛项赛题第 1 套

#需要资源或有问题的&#xff0c;可私博主&#xff01;&#xff01;&#xff01; #需要资源或有问题的&#xff0c;可私博主&#xff01;&#xff01;&#xff01; #需要资源或有问题的&#xff0c;可私博主&#xff01;&#xff01;&#xff01; 某企业根据自身业务需求&#…

数据结构和算法:树

二叉树 与链表类似&#xff0c;二叉树的基本单元是节点&#xff0c;每个节点包含值、左子节点引用和右子节点引用。 /* 二叉树节点结构体 */ struct TreeNode {int val; // 节点值TreeNode *left; // 左子节点指针TreeNode *right; // 右子节点指针TreeNode(int x) : val(x),…

项目管理证书有何用?这些PMP考试机会一定要抓住

项目管理证书有何用&#xff1f;这些PMP考试机会一定要抓住&#xff01; PMP认证的中文全称是“项目管理专业人士资格认证”&#xff0c;是目前国际上声誉较高并且含金量比较高的项目管理证书之一&#xff0c;本人有幸考过&#xff0c;也通过PMP认证成功转岗&#xff0c;应该也…

每日一题 --- 反转链表[力扣][Go]

反转链表 题目&#xff1a;206. 反转链表 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1]示例 2&#xff1a; 输入&#xff1a;head [1,2] 输出&a…

mybatis实践篇(二)

自定义类型处理器&#xff08;TypeHandler&#xff09; 实体 package com.wyl.mybatis.entity;import java.util.Date;/*** Description 省市区-字典* Author wuyilong* Date 2024-02-26*/public class FullCity {private static final long serialVersionUID 1L;/*** 主键I…

python之(19)CPU性能分析常见工具

Python之(19)CPU性能分析常见工具 Author: Once Day Date: 2024年3月24日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 全系列文章可参考专栏:Python开发_Once-Day的博客…

车载测试 UDS诊断 CANoe使用(线下实操项目)

本周末2天的时间&#xff0c;可以线下带大家对车载项目&#xff1a; uds诊断进行实操训练和CANoe工具的灵活使用 本博主从事新能源汽车的研发部&#xff0c;主要是嵌入式方面的&#xff0c;对车载测试的底层逻辑非常熟悉。 需要项目或者CANoe工具实操的可以关注并私信我

【数据结构和算法初阶(C语言)】二叉树的链式结构--前、中、后序遍历实现详解,节点数目计算及oj题目详解---二叉树学习日记③

1.二叉树的链式存储 二叉树的链式存储结构是指&#xff0c;用链表来表示一棵二叉树&#xff0c;即用链来指示元素的逻辑关系。 通常的方法是 链表中每个结点由三个域组成&#xff0c;数据域和左右指针域&#xff0c;左右指针分别用来给出该结点左孩子和右孩子所 在的链结点的存…

[自研开源] MyData v0.7.5 更新日志

开源地址&#xff1a;gitee | github 详细介绍&#xff1a;MyData 基于 Web API 的数据集成平台 部署文档&#xff1a;用 Docker 部署 MyData 使用手册&#xff1a;MyData 使用手册 试用体验&#xff1a;https://demo.mydata.work 交流Q群&#xff1a;430089673 介绍 MyData …

(ES6)前端八股文修炼Day2

1. let const var 的区别 var&#xff1a; var 是在 ES5 中引入的声明变量的关键字。 具有函数作用域&#xff0c;而不是块作用域&#xff0c;这意味着使用 var 声明的变量在函数内部是可见的。 变量可以被重复声明&#xff0c;而且变量的值可以在声明前使用&#xff0c;这可能…

一文带你看懂甘特图,项目进度、资源分配清清楚楚

带大家看懂一个甘特图&#xff0c;我们打开一个zz-plan 的甘特图&#xff0c;左边是任务栏&#xff0c;右边是进度条&#xff0c;上面这个是时间&#xff0c;下面是一个整个项目的一个状态&#xff0c;任务、工时、周期和进度。 这一列颜色灰色的表示是当天&#xff0c;我们从…

Linux常用命令和基础命令合集---非常推荐

非常好用的Linux通用基础常用命令和高级命令。 下载链接&#xff1a;https://download.csdn.net/download/lishihuijava/89026399

2024腾讯云服务器2核4G、4核8G、8核16G配置最新收费标准

腾讯云服务器多少钱一年&#xff1f;61元一年起。2024年最新腾讯云服务器优惠价格表&#xff0c;腾讯云轻量2核2G3M服务器61元一年、2核2G4M服务器99元一年可买三年、2核4G5M服务器165元一年、3年756元、轻量4核8M12M服务器646元15个月、4核16G10M配置32元1个月、312元一年、8核…

城市内涝水文水动力模型软件

原文链接&#xff1a;城市内涝水文水动力模型软件https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247598401&idx3&sn0c4c86b3a5d09a75b8f07e6fad81aa9c&chksmfa8200a6cdf589b0970a6854869e8e3a9f132fe40a19977863c091cbcf6d9786f067e0c5651e&token1…

Autosar MCAL配置——ADC

文章目录 前言一、创建Adc硬件单元二、创建、配置Adc通道1.根据电路原理图,有多少ADC采样引脚,就创建多少ADC采样通道。2.配置Adc通道3.配置ADC组4.配置ADC扫描组三、配置ADC通用设置前言 ADC,即Analogue Digital Converter缩写。简单来说,它是将输入的模拟信号转换为数字…

[数据结构]二叉树的建立与遍历(递归)

一、二叉树的遍历与建立 首先我们拥有如下二叉树: 要了解二叉树遍历,我们得先了解二叉树的三种遍历方式:前序遍历,中序遍历,后序遍历 1.前序遍历 前序遍历:根,左子树,右子树 遍历的结果就是:1 2 4 8 N N 9 N N 5 10 N N 11 N N 3 6 N N 7 N N 2.中序遍历 中序遍历:左子树…

SpringCloud-记

目录 什么是SpringCloud 什么是微服务 SpringCloud的优缺点 SpringBoot和SpringCloud的区别 RPC 的实现原理 RPC是什么 eureka的自我保护机制 Ribbon feigin优点 Ribbon和Feign的区别 什么是SpringCloud Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发…

day29|leetcode|C++|491. 非递减子序列|46. 全排列|47. 全排列 II

Leetcode 491. 非递减子序列 链接&#xff1a;491. 非递减子序列 thought: 设 stack 中最后一个值的位置为 last。如果 stack 为空&#xff0c;则 last -1。 设当前正在处理的位置为 pos。如果在 nums 的子区间 [last1, pos) 中&#xff0c;存在和 nums[pos] 相同的值&…

智慧公厕:跨界融合,打造智慧城市新名片

随着城市化进程的不断加快&#xff0c;公共厕所建设成为一个亟待解决的问题。传统的公厕存在着管理繁琐、卫生差、服务不到位等一系列问题&#xff0c;与城市发展的节奏不协调。为此&#xff0c;推进新型智慧公厕建设成为了一个重要的解决方案。智慧公厕的建设需要推进技术融合…