unity-shader-PBR基于物理渲染

转载:https://blog.csdn.net/yangxuan0261/article/details/88749969

 

title: unity-shader-PBR基于物理渲染
categories: Unity3d-Shader
tags: [unity, shader, pbr, ta]
date: 2019-03-07 10:13:21
comments: false
PBR : Physically based rendering, 基于物理的渲染。 暂时只有直接光照部分。

前篇
learnopengl 中文站的 PBR 知识, 包含 PBR 所有的方程使用姿势, 引起极度舒适.
pbr 理论 - https://learnopengl-cn.github.io/07 PBR/01 Theory/
光照 - https://learnopengl-cn.github.io/07 PBR/02 Lighting/
英文原版官网 : https://learnopengl.com/PBR/Lighting
Unity Shader学习笔记(31)基于物理的渲染技术(PBS)、BRDF - http://gad.qq.com/article/detail/38388
Unity PBR Workflow-2-PBR in Unity - https://www.youtube.com/watch?v=Q0G1SMaeEwI
Create PBR Surface In Unity 5 - https://www.youtube.com/watch?v=qH63_l8DVfI
PBR材质]锈迹材质全流程实例:Blender-》SP-》UE4 - https://zhuanlan.zhihu.com/p/34764973
探究PBR的两种流程以及Unity中的PBS - http://richbabe.top/2018/06/25/探究PBR的两种流程以及Unity中的PBS/
Unity的PBR扩展——皮毛材质 - https://zhuanlan.zhihu.com/p/57897827
Unity3D手游项目的总结和思考(2) - 角色渲染 ( 实现pbr的shader, 带溶解等效果 ) - https://blog.csdn.net/qq18052887/article/details/80375546
关于PBR贴图的那些事儿 - http://www.tenjoyedu.com/news/99.html
浅谈PBR在手游开发中的适用性 - https://blog.uwa4d.com/archives/TechSharing_109.html
什么是PBR - http://geekfaner.com/unity/blog5_PBR.html
猴子都能看懂的PBR - https://zhuanlan.zhihu.com/p/33464301
unity standard shader ( 深度好文 ) - http://geekfaner.com/unity/blog16_UnityStandardShader.html
PBR - http://geekfaner.com/unity/blog5_PBR.html
PBR物理渲染的基本原理及与传统渲染的异同 ( good ) - https://www.vive.com/cn/forum/2128
(good) substance 的 pbr-guide - https://academy.allegorithmic.com/courses/the-pbr-guide-part-1
中文翻译版
模型贴图师宝典:PBR 综合指南 - Vol1《光与物质:创建PBR纹理的实践指南》
模型贴图师宝典:PBR 综合指南 - Vol2《光与物质:创建PBR纹理的实践指南》
浅墨的游戏编程
【基于物理的渲染(PBR)白皮书】(一) 开篇:PBR核心知识体系总结与概览
【基于物理的渲染(PBR)白皮书】(二) PBR核心理论与渲染光学原理总结
【基于物理的渲染(PBR)白皮书】(三)迪士尼原则的BRDF与BSDF相关总结
PBR 工作流
PBR工作流(笔记) - https://www.jianshu.com/p/67321efbdaac
PBR概述 - https://www.cnblogs.com/guanzz/p/7416790.html
pbr的三个条件
判断一种PBR光照模型是否是基于物理的,必须满足以下三个条件(不用担心,我们很快就会了解它们的):

基于微平面(Microfacet)的表面模型。
能量守恒。
应用基于物理的BRDF。
漫反射 与 镜面反射
​ 平面散射光有两种:

进入平面的部分( 漫反射 )
从平面出去的部分( 镜面反射 )
光与平面的交互一种是平面直接反射出去的部分,我们成为 镜面反射,这部分其实就是美术常用的 specular 。另一部分是传到物质内部,经过折射被物质吸收的部分,或者内部进行散射,一些散射的光会最终重新返回平面折射出来,我们称为 漫反射,也就是 diffuse 。

漫反射光 被物质吸收并散射后,会成为不同波长的光。前面我们说过,真实的物理世界是没有颜色属性的,颜色只是人眼的感知。正是漫反射光被人眼感知后,“赋予”了物体颜色。例如物体吸收了蓝色以外的光,那物体就是蓝色的,所以可以说漫反射决定了物体基本的颜色,这也是为什么我们用diffuse贴图来表达纹理。(传统的渲染方式,由于全部使用漫反射贴图这一张贴图来模拟光的信息,所以一般情况下,我们会将AO,也就是环境光遮罩,直接绘制在diffuse上,但是对于PBR来说,由于整个引擎渲染使用的都是物理渲染管线。所以在绘制diffuse贴图时,AO信息可以不画,或者极少。)

能量守恒
能量守恒,是PBR与传统渲染最大的区别。所谓能量守恒,简单的理解就是镜面反射出去的光与漫反射出去的光,加起来总量不能超过入射的光量,也就是 diffuse+specular 不能大于1。如果你希望你的材质有较高的反射率,那就要同时降低漫反射的强度。在真实的物理世界中也是如此。

shader 中由 ks 推导出 kd

vec3 kS = F; // F 是菲涅尔
vec3 kD = vec3(1.0) - kS; // 能量守恒
1
2
BRDF 的 反射率方程 公式


c 表示表面颜色, albedo

入射(光)方向 ωi,出射(观察)方向 ωo

Lo : 看到的颜色最终值, 表示了从 ωo 方向上观察,光线投射到点 p 上反射出来的辐照度。

D : 正态分布函数 ( Normal Distribution Function, 简称 ndf )

F : 菲涅尔方程 ( Fresnel Rquation )

G : 几何函数( Geometry Function )

ks : 也就是 F ( 在实际运算中不用乘以这个 ks, 因为在 DFG 中已经乘过了 )

kd : 1 - ks, 能量守恒

Li : 光的颜色 ( 也就是辐照度, 经过衰减的颜色 )

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

vec3 Lo = vec3(0.0); // 出射光, 也就是看到的物体颜色 (不含环境光)
for(int i = 0; i < 4; ++i) // 4 是光源个数
{
    Lo += (kD * albedo / PI + specular) * radiance * NdotL; // albedo 参与
}
1
2
3
4
5
Cook-Torrance BRDF
Cook-Torrance BRDF的镜面反射部分包含三个函数,此外分母部分还有一个标准化因子 。字母D,F与G分别代表着一种类型的函数,各个函数分别用来近似的计算出表面反射特性的一个特定部分。三个函数分别为正态分布函数(Normal Distribution Function),菲涅尔方程(Fresnel Rquation)和几何函数(Geometry Function):

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


下面公式中的 h 是 半角向量, 也就是 vec3 H = normalize(V + L)

D - 正态分布函数


在这里 h 表示用来与平面上微平面做比较用的 中间向量 ( 也叫半角向量 ) ,而 a 表示表面 粗糙度

float DistributionGGX(vec3 N, vec3 H, float roughness) // 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13


当α非常接近0的时候,光照集中在一点,其他方向会完全看不到光线。

F - 菲涅尔方程


通过预先计算电介质与导体的 F0 值,我们可以对两种类型的表面使用相同的Fresnel-Schlick近似,但是如果是 金属 表面的话就需要对基础 反射率 添加色彩。我们一般是按下面这个样子来实现的:

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

vec3 F0 = vec3(0.04); 
F0      = mix(F0, albedo, metallic); // albedo 参与, metallic 参与
vec3 F  = fresnelSchlick(max(dot(H, V), 0.0), F0);
1
2
3
4
5
6
7
8
F0 材料对应值表
shader 中的 F0 参数根据物体的材料属性填入对应的值会显得更接近于真实世界.

G - 几何函数


k 由 a ( 粗糙度 ) 求得

float GeometrySchlickGGX(float NdotV, float roughness) // 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;
}


1
2
3
4
5
6
7
8
9
10
11
12
为了有效的估算几何部分,需要将观察方向(几何遮蔽(Geometry Obstruction))和光线方向向量(几何阴影(Geometry Shadowing))都考虑进去。我们可以使用史密斯法(Smith’s method)来把两者都纳入其中:

float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) // 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;
}
1
2
3
4
5
6
7
8
9


效果就是粗糙度越大,亮度越低。但视线和光线越接近垂直,受粗糙度的影响就越小

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

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

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

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

    float NdotL = max(dot(N, L), 0.0);        
    Lo += (kD * albedo / PI + specular) * radiance * NdotL; // albedo 参与
}
1
2
3
比较重要的是我们没有把 kS 乘进去我们的反射率方程中,这是因为我们已经在 specualr BRDF 中乘了菲涅尔系数F了,因为 kS 等于 F,因此我们不需要再乘一次。

TODO: 上面这句话不太理解。
这篇文章提到说是原公式是 不妥当 的 - ( 猴子都能看懂的PBR - https://zhuanlan.zhihu.com/p/33464301 )

从官方的源码上看到是没有乘上这个 ks 的

完整的直接光照PBR着色器
参考: 光照 - https://learnopengl-cn.github.io/07 PBR/02 Lighting/
#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); // albedo 参与, metallic 参与, F0 参与

    // 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; // light color 参与     

        // cook-torrance brdf
        float NDF = DistributionGGX(N, H, roughness); // roughness 参与
        float G   = GeometrySmith(N, V, L, roughness); // roughness 参与
        vec3 F    = fresnelSchlick(max(dot(H, V), 0.0), F0);       

        vec3 kS = F;
        vec3 kD = vec3(1.0) - kS;
        kD *= 1.0 - metallic; // metallic 参与

        vec3 nominator    = NDF * G * F;
        float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001; // 分母项中加了一个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; // albedo 参与
    }   

    vec3 ambient = vec3(0.03) * albedo * ao; // 环境光 term, 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);
}  
————————————————
版权声明:本文为CSDN博主「君墨痕」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yangxuan0261/article/details/88749969

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值