unity-shader-PBR基于物理渲染

88 篇文章 8 订阅
31 篇文章 7 订阅

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, 基于物理的渲染。 暂时只有直接光照部分。


前篇


PBR 工作流


pbr的三个条件

判断一种PBR光照模型是否是基于物理的,必须满足以下三个条件(不用担心,我们很快就会了解它们的):

  1. 基于微平面(Microfacet)的表面模型。
  2. 能量守恒。
  3. 应用基于物理的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; // 能量守恒

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

当α非常接近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);
F0 材料对应值表

shader 中的 F0 参数根据物体的材料属性填入对应的值会显得更接近于真实世界.


G - 几何函数

ka ( 粗糙度 ) 求得

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


为了有效的估算几何部分,需要将观察方向(几何遮蔽(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

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

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

kD *= 1.0 - metallic; // metallic 参与

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

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

比较重要的是我们没有把 kS 乘进去我们的反射率方程中,这是因为我们已经在 specualr BRDF 中乘了菲涅尔系数F了,因为 kS 等于 F,因此我们不需要再乘一次。

TODO: 上面这句话不太理解。

这篇文章提到说是原公式是 不妥当 的 - ( 猴子都能看懂的PBR - https://zhuanlan.zhihu.com/p/33464301 )

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


完整的直接光照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); // 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);
}  
  • 8
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蝶泳奈何桥.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值