综合研究基于物理的渲染算法Unity3D[Translation]

基于物理的渲染算法
有关 Unity3D 的概括研究

来说说基于物理的渲染。
基于物理的渲染(PBR)近几年非常火爆。
Unity 5, 虚幻引擎 4, 寒霜引擎, 甚至是 ThreeJS, 更多的游戏引擎在使用它。
很多 3D 模型工作室在根据 Marmoset Toolbag 和 Allegorithmic Substance Suite等人气工具转换成 “PBR Pipeline”。
现在没几个不熟悉管线的美术,但是了解管线在后台是怎么运行的,很少有引擎和美术知道。
我这个指南分解(分析) PBR 渲染 (shading),是想让初学者方便理解 PBR。
让我们开始吧。

PBR的物理特征

在过去的 3 ~ 40 年,我们对周围世界的理解和科学上 / 数学上它是怎么运作的都有很多进步。
而且这种理解在渲染技术领域也有了很大的发展。多位灵敏的研究者对光,视野,表面正常形态,然后这三个是怎么互相作用的有着深刻的结论。这种发展大部分是围绕 BRDF (双向反射率分布函数)和固有能量保存的想法为中心形成的。​如果想理解光和观测点跟Surface是怎么相互作用的,首先要理解Surface本身。
光照射到完全光滑的表面几乎可以在表面形成完整的反射。
光和我们叫做粗糙表面的东西相互作用的时候,他的反射不是类似的,这可以用微表面 (microfacets)的存在说明。

我们看物件的时候,要假设他的表面不是完全光滑的,而是用很小的面组成的。每个物件都是完整的 Specular reflection 。
这个微表面(microfacets)通过光滑表面的法线,具有分布的法线。
微表面法线的不同程度是根据表面粗糙度定的。
表面越粗糙,高光损失的可能性就越大。
所以粗糙的表面有更大更模糊的斑点。
光滑的表面上,光的反射比之前更完整时可以压缩反射高光。

再回到 BRDF…

双向反射率分布函数 (Bridirectional Reflectance Distribution Function, BRDF)是说明表面反射率的函数。
有各种 BRDF 模型 / 算法,其中大部分不是基于物理的算法。
基于物理的 BRDF需要能量守恒。
‘能量守恒 (Energy Conservation)’是说从地表反射的总光量,比地表收到的总量少。
表面反射的光不能比之前讨论的所有微表面相互作用前更强烈。

编写 PBR Shader : nut,bolt 和光滑的表面
PBR Shader 属性

大部分 PBR Shading 模型上通常是用某种形态影响几种相同属性。
现代 PBR 思路上最重要的两种属性是光滑 (Smoothness) 和金属性 (Metallic)。
这两个值都是在 0..1 之间的时候最好起效。
编写 PBR Shader 的方法有多个,其中一部分为了更多类似迪士尼 PBR 管线的效果可以使用 BRDF 模型。各个效果由特定属性引出。
如果在 Unity 里没有细读 Writing Shaders相关我的页面,这可以成为你可以集中细读的时间。
在 Unity Shader 里定义 public 变数。那些以后会添加。
属性下方有Shader初始化结构。
以后添加更多功能的时候会参考 #pragma 指令。

Vertex Program

Vertex 程序跟 Unity 里的编写 Shade r相关自学书上生成的类似。
我们需要的核心因素是有关 vertex 的 normal, tangent 和 bi-tangent 信息。
也就是说这些一定要包含在 Vertex Program里。

Fragment Program

fragment 程序里,在算法上要定义之后可以使用的变量Set:

Roughness

下面的方法上会重新处理粗糙度。 我做这个的理由是个人喜好。

发现下方重新处理的粗糙度在视觉上的结果更分明。

float roughness = 1- (_Glossiness * _Glossiness);   // 1 - smoothness*smoothness
roughness = roughness * roughness;

Metallic

PBR shader里使用 Metallic的时候要小心的很多。
可以知道任何算法都不说明那个算法。所以我们用其他形式完全包含了。
金属材料是电介质 (非金属,也就是金属 = 0) 或者是决定是不是金属 (Metal = 1) 材料的控制值。
也就是说,为了我们的金属度值用正确的方式影响到Shader,我们要用扩散的颜色连接这些,诱导出我们的反射颜色。
金属不会显示扩散反射(Diffuse),所以有完全反射的 Albedo,实际的反射颜色会反映物体的表面去变更。
参考以下 :

float3 diffuseColor = _Color.rgb * (1-_Metallic) ;
float3 specColor = lerp(_SpecularColor.rgb, _Color.rgb, _Metallic * 0.5);

添加到Unity里在材质时要使用Shader代码制作白色个体。
通过属性Shun, 变量section的变量, Helper 函数section的 Helper 函数,算法section的算法和 Fragments section的Shader代码实现扩张shader。

构成 PBR SHADER Pt.1 :法线分布函数 (Specular Function)
法线分布函数是什么?

Normal Distribution Function是构成 BRDF shader的三个核心因素中的一种。
NDF统计说明表面的微面正常分布。根据用途 NDF的作用是调整反射 (明暗)亮度的权重函数。重点是要以几何学特征想NDF。
可以开始在Shader添加算法,视觉化 NDF 生成的效果。我们想做的第一件事情是制作算法。为了视觉化算法,override 并返还 Float 类型的任意值。

float3 SpecularDistribution = specColor;

//the algorithm implementations will go here

return float4(float3(1,1,1) * SpecularDistribution.rgb,1);

下面的 section 形式如下。在算法section里写算法后,在上面说明过的位置实现算法。实现新算法的时候,把上面激活的算法注释处理的话,出来的结果效果就只是基于现在没有注释处理的算法 。不要担心,我们之后在整理这个 Unity 内部会提供便于算法间转换的方法,不需要再现注释了。用简单的 Blinn-Phong 方式开始吧。

Blinn-Phong NDF

float BlinnPhongNormalDistribution(float NdotH, float specularpower, float speculargloss){
    float Distribution = pow(NdotH,speculargloss) * specularpower;
    Distribution *= (2+specularpower) / (2*3.1415926535);
    return Distribution;
}

Phong specularity的 Blinn 近似是由 Phong Specular Model的优化做成的。Blinn(图形研究人员名字), 他决定比起计算每个frame的光反射,做法线Vector和半Vector的内积会更快。算法上 Blinn比 Phong更柔和,产出的结果更多样化。Blinn-Phong 虽然不算是正确的物理算法,但是可以生成符合特定游戏表现意图的高光。
算法section放上方算法,碎片section放下方代码。

SpecularDistribution *=  BlinnPhongNormalDistribution(NdotH, _Glossiness,  max(1,_Glossiness * 40));

在 Shader 指定 Glossiness 值,个体会显示为表现普通分布 (Specularity)的白色强调,剩余部分表现为黑色。是方便测试 Shader 的构造,但上面的 40是为了提供高范围函数,但不是说适合所有人。

Phong NDF

float PhongNormalDistribution(float RdotV, float specularpower, float speculargloss){
    float Distribution = pow(RdotV,speculargloss) * specularpower;
    Distribution *= (2+specularpower) / (2*3.1415926535);
    return Distribution;
}

Phong 算法是非物理算法,但会生成比上方 Blinn 近似更精确的结果。下面是实现案例。

SpecularDistribution *=  PhongNormalDistribution(RdotV, _Glossiness, max(1,_Glossiness * 40));

跟 Blinn-Phong 方式一样, * 40 只能用于测试。

Beckman NDF

Beckman Normal Distribution 功能是更高级的功能,是考虑粗糙度值的。

这里的 ‘粗糙度’ 在工程学上意味着 ‘表面粗糙度(Surface Roughness)’。
形成在金属表面的周期短,振幅较小的不规则 凹凸的大小。 触针式的测量机,测量面的垂直断面显示的轮廓,从竖向或者横下放大记录的断面曲线得出表面粗糙度。
KS B 0161里表面粗糙度是用以下三个方法规定的。
最高 (Rmax)
十漸平均粗糙度 (Rz)
中心线平均粗糙度(Ra)
不仅仅是粗糙度,跟我们的正常方向和反方向间的内积(dot product)一起,我们能近似出穿过表面的法线分布。这个算法的实现非常简单。

float BeckmannNormalDistribution(float roughness, float NdotH)
{
    float roughnessSqr = roughness*roughness;
    float NdotHSqr = NdotH*NdotH;
    return max(0.000001,(1.0 / (3.1415926535*roughnessSqr*NdotHSqr*NdotHSqr))
* exp((NdotHSqr-1)/(roughnessSqr*NdotHSqr)));
}

需要注意的是 Beckman 模型物件表面的控制方式。上图就能知道, Beckman 模型使用柔和的值 Highlight 极具强化到某个特定点的速度会变慢。就跟表面光滑增加一样, Reflection Highlight 一起调,做出美术上比较不错的柔和的值。这个 Behavior有利于初期粗糙度里的粗糙的金属,增加柔和度后对塑料也很好。

SpecularDistribution *=  BeckmannNormalDistribution(roughness, NdotH);

Gaussian NDF

Gaussian Normal Distribution 模型跟其他模型比用的频率不多。因为设到更高的柔和值的时候,会生成比想要的更柔和的倾斜面高光。在艺术观点看的时候,这是可取的,但是对实际物理特征还需论争。

float GaussianNormalDistribution(float roughness, float NdotH)
{
    float roughnessSqr = roughness*roughness;
	float thetaH = acos(NdotH);
    return exp(-thetaH*thetaH/roughnessSqr);
}

这个算法的实现跟使用表面粗糙度和普通half vector的内积的其他普通分布函数的实现是类似的。

SpecularDistribution *=  GaussianNormalDistribution(roughness, NdotH);

GGX NDF

GGX不是现在普遍适用的,但的确是受欢迎的算法中的一个。现代应用软件大部分是依赖于 BRDF 部分功能。GGX是 Bruce Walter和 Kenneth Torrance开发的。这篇论文里使用的函数都是普遍使用的函数中的一部分。

float GGXNormalDistribution(float roughness, float NdotH)
{
    float roughnessSqr = roughness*roughness;
    float NdotHSqr = NdotH*NdotH;
    float TanNdotHSqr = (1-NdotHSqr)/NdotHSqr;
    return (1.0/3.1415926535) * sqr(roughness/(NdotHSqr * (roughnessSqr + TanNdotHSqr)));
}
SpecularDistribution *=  GGXNormalDistribution(roughness, NdotH);

GGX 算法的反射高光非常坚固,但是通过圆球表面后还是维持柔和的分布。这是很好的案例,来说明为什么 GGX 算法通过金属表面复制反射的扭曲是好的。

Trowbridge-Reitz NDF

Trowbridge-Reitz接近法和GGX研发自同一个论文,得出的结果与GGX算法非常相似。比较显著的差异是物体极端边缘位置高光比GGX更加平滑。GGX比锐角有着更加粗糙的衰减。

float TrowbridgeReitzNormalDistribution(float NdotH, float roughness){
    float roughnessSqr = roughness*roughness;
    float Distribution = NdotH*NdotH * (roughnessSqr-1.0) + 1.0;
    return roughnessSqr / (3.1415926535 * Distribution*Distribution);
}

SpecularDistribution *=  TrowbridgeReitzNormalDistribution(NdotH, roughness);

一般Trowbridge-Reitz公式依存于粗糙度和法线矢量以及半矢量的点积(dot product)。

Ward Anisotropic NDF

float WardAnisotropicNormalDistribution(float anisotropic, float NdotL, float NdotV, float NdotH, float HdotX, float HdotY)
{
    float aspect = sqrt(1.0h-anisotropic * 0.9h);
    float X = max(.001, sqr(1.0-_Glossiness)/aspect) * 5;
 	float Y = max(.001, sqr(1.0-_Glossiness)*aspect) * 5;
    float exponent = -(sqr(HdotX/X) + sqr(HdotY/Y)) / sqr(NdotH);
    float Distribution = 1.0 / (4.0 * 3.14159265 * X * Y * sqrt(NdotL * NdotV));
    Distribution *= exp(exponent);
    return Distribution;
}

SpecularDistribution *=  WardAnisotropicNormalDistribution(_Anisotropic,NdotL, NdotV, NdotH, dot(halfDirection, i.tangentDir), dot(halfDirection,  i.bitangentDir));

Ward接近方式的非各同向性与Trowbridge-Reitz方法非常不同。Specular 高光在表面越柔和就会更加柔和、消失的也更加迅速。和Trowbridge-Reitz方法一样,Ward Algorithm需要tangent和Bit tangent数据,但使用的不光是一般内积和光的内积,还有正常内积及我们观点里的内积。

一起结合PBR Shader Pt.2:几何学上的阴影函数
何谓几何学的阴影算法?

几何学上的阴影功能用于解释microfacet自身阴影现象导致的光线减弱。此近似值模拟给定的支点上microfacet互相遮挡或者光线在多个微领域反射的概率。这个概率中,光线在到达点之前就失去了能量。
想要准确生成GSF,需要对粗糙度进行采样,决定microfacet的分布。如果有不含粗糙度的几种功能,虽然能获取可靠的结果,但与采样粗糙度功能相比,他们并不适合许多情况。Geometric Shadowing Function是保留BRDF能量所必需的。
没有GSF的话,BRDF能够反射比接受的光线能量更多的光线能量。
microfacet BRDF方程式的核心部分与活性表面(根据从L到V的光能反射表面区域覆盖的面积)和微表面的总面积之间的比例有关。
不考虑Shadowing和Masking的情况,活性区域可能超出整体区域,也可能导致BRDF无法保存能量,有时会导致性能太耗。

float GeometricShadow = 1;
//the algorithm implementations will go here
 return float4(float3(1,1,1) * GeometricShadow,1);

为了预览GSF函数,我们在正规分布函数上添上这个代码。格式的工作方式与NDF功能的实现方法非常相似。

Implicit GSF

含蓄的GSF是Geometric Shadowing的基本逻辑。

float ImplicitGeometricShadowingFunction (float NdotL, float NdotV)
{
	float Gs =  (NdotL*NdotV);       
	return Gs;
}
GeometricShadow *= ImplicitGeometricShadowingFunction (NdotL, NdotV);

通过将法线与光的内部相乘,然后将法线与视点的内部相乘,我们精确地描述了光是如何影响基于我们观点的物体的表面的。

Ashikhmin-Shirley GSF

设计成与异方性的正规分布函数一起使用的Ashikhmin-Shirley GSF,可以为异方性效果提供一个好的基础。

float AshikhminShirleyGSF (float NdotL, float NdotV, float LdotH)
{
	float Gs = NdotL*NdotV/(LdotH*max(NdotL,NdotV));
	return  (Gs);
}

GeometricShadow *= AshikhminShirleyGSF (NdotL, NdotV, LdotH);

如同左边看到的,这个Model生成的微表面(microfacet)的影子是非常微妙的。

Ashikhmin-Premoze GSF

Ashikhmin-Premoze GSF与Ashikhmin-Shirley方式不同,设计成要与各同向性的NDF一起使用。和Ashikhmin-Shirley同样的是,这个是一个非常微妙的GSF。

float AshikhminPremozeGeometricShadowingFunction (float NdotL, float NdotV)
{
	float Gs = NdotL*NdotV/(NdotL+NdotV - NdotL*NdotV);
	return  (Gs);
}
GeometricShadow *= AshikhminPremozeGeometricShadowingFunction (NdotL, NdotV);

Duer GSF

float DuerGeometricShadowingFunction (float3 lightDirection,float3 viewDirection, 
float3 normalDirection,float NdotL, float NdotV)
{
    float3 LpV = lightDirection + viewDirection;
    float Gs = dot(LpV,LpV) * pow(dot(LpV,normalDirection),-4);
    return  (Gs);
}

GeometricShadow *= DuerGeometricShadowingFunction (lightDirection, viewDirection, normalDirection, NdotL, NdotV);

Duer提出了以下GFS函数,以解决在下面使用的Ward GSF函数里面发现的specularity和相关问题。Duer GSF虽然是表现与上面Ashikhmin-Shirley类似的结果,但是更适合各同向性的BRDF以及有着非常些微异方性的BRDF。

Neumann GSF

Neumann – Neumann GSF是适合异方性正规分布的GSF的另一个示例。视野方向或者光线的方向越大,就会生成更多几何阴影。

float NeumannGeometricShadowingFunction (float NdotL, float NdotV)
{
	float Gs = (NdotL*NdotV)/max(NdotL, NdotV);       
	return  (Gs);
}

GeometricShadow *= NeumannGeometricShadowingFunction (NdotL, NdotV);

Kelemen GSF

Kelemen GSF可以提供合适的能量节约GSF。和之前的大部分Model不同,虽然几何阴影的比例不是固定的,但他根据视野角度而变化。这是Cook-Torrance几何阴影函数的极端近似。

Modified-Kelemen GSF

float ModifiedKelemenGeometricShadowingFunction (float NdotV, float NdotL, float roughness)
{
	float c = 0.797884560802865;    // c = sqrt(2 / Pi)
	float k = roughness * roughness * c;
	float gH = NdotV  * k +(1-k);
	return (gH * gH * NdotL);
}

GeometricShadow *=  ModifiedKelemenGeometricShadowingFunction (NdotV, NdotL, roughness );

这是Kelemen Approximation of Cook-Torrance的变形形态。已经被修改为生产粗糙的分散的Kelemen GSF。

Cook-Torrance GSF

float CookTorrenceGeometricShadowingFunction (float NdotL, float NdotV, float VdotH, float NdotH)
{
	float Gs = min(1.0, min(2*NdotH*NdotV / VdotH, 2*NdotH*NdotL / VdotH));
	return  (Gs);
}

GeometricShadow *= CookTorrenceGeometricShadowingFunction (NdotL, NdotV, VdotH, NdotH);

Ward GSF

float WardGeometricShadowingFunction (float NdotL, float NdotV, float VdotH, float NdotH)
{
	float Gs = pow( NdotL * NdotV, 0.5);
	return  (Gs);
}

GeometricShadow *= WardGeometricShadowingFunction (NdotL, NdotV, VdotH, NdotH);

Ward GSF是强化的默示性GSF。Ward使用此方法强化正规分布函数。它可以特别有效的从各个角度来强调表面异方性Band。

Smith Based Geometric Shadowing Functions
Smith为基础的GSF比其他的GSF更加准确,更加被广泛认可,同时考虑到正规分布的粗糙度和形状。该函数的DSF需要处理两个片段才能进行计算。

Walter et all. GSF

Walter et al. NDF是GGX GSF的一般形态,为了能与之共同使用制作了这个函数。
Walter et al. GSF感觉是“虽然对锐角附近或者粗糙表面之外的BSDF(双向散射分布函数)的形成影响很小,但是需要保存能量”。考虑到这一点,我们使用“Roughness”作为GSF的动力,并创建了一个GSF来尊重这一原则。

float BeckmanGeometricShadowingFunction (float NdotL, float NdotV, float roughness)
{
    float roughnessSqr = roughness*roughness;
    float NdotLSqr = NdotL*NdotL;
    float NdotVSqr = NdotV*NdotV;

    float calulationL = (NdotL)/(roughnessSqr * sqrt(1- NdotLSqr));
    float calulationV = (NdotV)/(roughnessSqr * sqrt(1- NdotVSqr));

    float SmithL = calulationL < 1.6 ? (((3.535 * calulationL) + (2.181 * calulationL * calulationL))/(1 + (2.276 * calulationL) + (2.577 * calulationL * calulationL))) : 1.0;
    float SmithV = calulationV < 1.6 ? (((3.535 * calulationV) + (2.181 * calulationV * calulationV))/(1 + (2.276 * calulationV) + (2.577 * calulationV * calulationV))) : 1.0;

	float Gs =  (SmithL * SmithV);
	return Gs;
}

GeometricShadow *= BeckmanGeometricShadowingFunction (NdotL, NdotV, roughness);

Walter et al.最初是为与Beckman NDF一起使用而制作的,推测是适合与Phong NDF一起使用的GSF。

float BeckmanGeometricShadowingFunction (float NdotL, float NdotV, float roughness)
{
    float roughnessSqr = roughness*roughness;
    float NdotLSqr = NdotL*NdotL;
    float NdotVSqr = NdotV*NdotV;

    float calulationL = (NdotL)/(roughnessSqr * sqrt(1- NdotLSqr));
    float calulationV = (NdotV)/(roughnessSqr * sqrt(1- NdotVSqr));

    float SmithL = calulationL < 1.6 ? (((3.535 * calulationL) + (2.181 * calulationL * calulationL))/(1 + (2.276 * calulationL) + (2.577 * calulationL * calulationL))) : 1.0;
    float SmithV = calulationV < 1.6 ? (((3.535 * calulationV) + (2.181 * calulationV * calulationV))/(1 + (2.276 * calulationV) + (2.577 * calulationV * calulationV))) : 1.0;

	float Gs =  (SmithL * SmithV);
	return Gs;
}

GeometricShadow *= BeckmanGeometricShadowingFunction (NdotL, NdotV, roughness);

Schlick做了几个在其他Smith GSF里面也可以使用的Smith GSF的近似值。这是Smith GSF的Schlick近似值。

float SchlickGeometricShadowingFunction (float NdotL, float NdotV, float roughness)
{
    float roughnessSqr = roughness*roughness;
	float SmithL = (NdotL)/(NdotL * (1-roughnessSqr) + roughnessSqr);
	float SmithV = (NdotV)/(NdotV * (1-roughnessSqr) + roughnessSqr);
	return (SmithL * SmithV); 
}

GeometricShadow *= SchlickGeometricShadowingFunction (NdotL, NdotV, roughness);

这是Beckman Function的Schlick近似。在光照度上乘以2 / PI的平方根启动。但是我们使用的是0.797884 …..

float SchlickBeckmanGeometricShadowingFunction (float NdotL, float NdotV, float roughness)
{
    float roughnessSqr = roughness*roughness;
    float k = roughnessSqr * 0.797884560802865;

    float SmithL = (NdotL)/ (NdotL * (1- k) + k);
    float SmithV = (NdotV)/ (NdotV * (1- k) + k);

	float Gs =  (SmithL * SmithV);
	return Gs;
}

GeometricShadow *= SchlickBeckmanGeometricShadowingFunction (NdotL, NdotV, roughness);

Schlick-GGX GSF

GGX的 Schlick Approximation只是把我们的光照度值除以2。

float SchlickGGXGeometricShadowingFunction (float NdotL, float NdotV, float roughness)
{
    float k = roughness / 2;

    float SmithL = (NdotL)/ (NdotL * (1- k) + k);
    float SmithV = (NdotV)/ (NdotV * (1- k) + k);

	float Gs =  (SmithL * SmithV);
	return Gs;
}

GeometricShadow *= SchlickGGXGeometricShadowingFunction (NdotL, NdotV, roughness);

一起使用PBR Shader Pt.3 : Fresnel功能

Fresnel效果是以法国物理学家Augustin-Jean Fresnel的名字来命名的。此效果表示表面上的反射强度随时间点的变化而变化。从锐角角度看,反射角度会增加。为了在我们的Shader中包含Fresnel效果,需要在许多地方使用它。首先,我们必须考虑扩散复古反射,然后考虑BRDF Fresnel的效果。要正确计算Fresnel,必须考虑垂直入射角和锐角。使用下面的粗糙度可以计算可传达Fresnel功能的漫反射Fresnel反射率。要计算此问题,请使用Schlick近似值Fresnel。Fresnel的Schlick近似结构如下:

schlick = x + (1-x) * pow(1-dotProduct,5);

这个函数可以更近似,如下所示。

mix(x,1,pow(1-dotProduct,5)); 

在某些GPU中,此近似值可能更快。可以通过切换上面的x和1来反转近似值。在这里,我们将在下面进行计算Diffuse。

float MixFunction(float i, float j, float x) 
{
	 return  j * x + i * (1.0 - x);
}

float SchlickFresnel(float i)
{
    float x = clamp(1.0-i, 0.0, 1.0);
    float x2 = x*x;
    return x2*x2*x;
}

//normal incidence reflection calculation
float F0 (float NdotL, float NdotV, float LdotH, float roughness)
{
    float FresnelLight = SchlickFresnel(NdotL); 
    float FresnelView = SchlickFresnel(NdotV);
    float FresnelDiffuse90 = 0.5 + 2.0 * LdotH*LdotH * roughness;
    return  MixFunction(1, FresnelDiffuse90, FresnelLight) * MixFunction(1, FresnelDiffuse90, FresnelView);
}

Schlick Fresnel

对于Fresnel方程式Schlick的近似,这可能是方程式最有名的一个近似了。Fresnel效果的近似是在锐角上帮助计算反射率。

float3 SchlickFresnelFunction(float3 SpecularColor,float LdotH)
{
    return SpecularColor + (1 - SpecularColor)* SchlickFresnel(LdotH);
}


FresnelFunction *=  SchlickFresnelFunction(specColor, LdotH);

下一个算法使用传递的特定值,而不是反射颜色。此新值是折射率。IOR是一个无维数,用于表示光穿过表面的速度。若要使用此功能,需要向Shader添加新的属性和变量。

_Ior("Ior",  Range(1,4)) = 1.5

float SchlickIORFresnelFunction(float ior ,float LdotH)
{
    float f0 = pow(ior-1,2)/pow(ior+1, 2);
    return f0 + (1-f0) * SchlickFresnel(LdotH);
}


FresnelFunction *=  SchlickIORFresnelFunction(_Ior, LdotH);

Spherical-Gaussian Fresnel

Spherical-Gaussian Fresnel函数能算出与Schlick ‘s Approximation非常相似的结果。

唯一的差别就是Spherical Gaussian是计算里面派生的。

float SphericalGaussianFresnelFunction(float LdotH,float SpecularColor)
{	
  float power = ((-5.55473 * LdotH) - 6.98316) * LdotH;
  return SpecularColor + (1 - SpecularColor) * pow(2,power);
}

FresnelFunction *= SphericalGaussianFresnelFunction(LdotH, specColor);

一起制作PBR Shader Pt.4:一起导入

结合算法。

现在NDF,GSF以及Fresnel Function有几个版本,因此把他们结合起来就可以看到BRDF PBR Shader的结果。 这样结合算法的形式非常简单。互相相乘之后用4 * NdotL * NdotV去相除。

UnityGI GetUnityGI(float3 lightColor, float3 lightDirection, float3 normalDirection,float3 viewDirection, float3 viewReflectDirection, float attenuation, float roughness, float3 worldPos)
{
 //Unity light Setup ::
    UnityLight light;
    light.color = lightColor;
    light.dir = lightDirection;
    light.ndotl = max(0.0h,dot( normalDirection, lightDirection));
    UnityGIInput d;
    d.light = light;
    d.worldPos = worldPos;
    d.worldViewDir = viewDirection;
    d.atten = attenuation;
    d.ambient = 0.0h;
    d.boxMax[0] = unity_SpecCube0_BoxMax;
    d.boxMin[0] = unity_SpecCube0_BoxMin;
    d.probePosition[0] = unity_SpecCube0_ProbePosition;
    d.probeHDR[0] = unity_SpecCube0_HDR;
    d.boxMax[1] = unity_SpecCube1_BoxMax;
    d.boxMin[1] = unity_SpecCube1_BoxMin;
    d.probePosition[1] = unity_SpecCube1_ProbePosition;
    d.probeHDR[1] = unity_SpecCube1_HDR;
    Unity_GlossyEnvironmentData ugls_en_data;
    ugls_en_data.roughness = roughness;
    ugls_en_data.reflUVW = viewReflectDirection;
    UnityGI gi = UnityGlobalIllumination(d, 1.0h, normalDirection, ugls_en_data );
    return gi;
}
float3 specularity = (SpecularDistribution * FresnelFunction * GeometricShadow) / (4 * (  NdotL * NdotV));

float3 lightingModel = (diffuseColor + specularity);
lightingModel *= NdotL;
float4 finalDiffuse = float4(lightingModel * attenColor,1);
return finalDiffuse;

结合了算法之后,扩散的颜色上可以简单添加上值。实行PBR。

Getting Unity Lighting Information Involved

먼저, Reflection Probe 을 장면에 추가하고 구워냅니다. 그런 다음이 함수를 셰이더에 추가해야합니다.

想在环境里取样,需要在Unity和Shader里面执行几个重要的步骤。首先,Reflection Probe要添加在场景里面并进行烘焙。接下来要将下面的函数添加在Shader里面。

UnityGI GetUnityGI(float3 lightColor, float3 lightDirection, float3 normalDirection,float3 viewDirection, float3 viewReflectDirection, float attenuation, float roughness, float3 worldPos)
{
 //Unity light Setup ::
    UnityLight light;
    light.color = lightColor;
    light.dir = lightDirection;
    light.ndotl = max(0.0h,dot( normalDirection, lightDirection));
    UnityGIInput d;
    d.light = light;
    d.worldPos = worldPos;
    d.worldViewDir = viewDirection;
    d.atten = attenuation;
    d.ambient = 0.0h;
    d.boxMax[0] = unity_SpecCube0_BoxMax;
    d.boxMin[0] = unity_SpecCube0_BoxMin;
    d.probePosition[0] = unity_SpecCube0_ProbePosition;
    d.probeHDR[0] = unity_SpecCube0_HDR;
    d.boxMax[1] = unity_SpecCube1_BoxMax;
    d.boxMin[1] = unity_SpecCube1_BoxMin;
    d.probePosition[1] = unity_SpecCube1_ProbePosition;
    d.probeHDR[1] = unity_SpecCube1_HDR;
    Unity_GlossyEnvironmentData ugls_en_data;
    ugls_en_data.roughness = roughness;
    ugls_en_data.reflUVW = viewReflectDirection;
    UnityGI gi = UnityGlobalIllumination(d, 1.0h, normalDirection, ugls_en_data );
    return gi;
}
float3 specularity = (SpecularDistribution * FresnelFunction * GeometricShadow) / (4 * (  NdotL * NdotV));

float3 lightingModel = (diffuseColor + specularity);
lightingModel *= NdotL;
float4 finalDiffuse = float4(lightingModel * attenColor,1);
return finalDiffuse;

fragment程序内部使用这个函数,可以生成适合环境取样的数据。

为了使用这些值,我们将我们之前放在shader里面的一行或者两行进行替换,像下面看到的那样在最终结果上添加进去。这样就可以通过看到环境数据的方式,在设置属性的时候进行环境取样。

现在,从Unity下拉列表中选择关键词会更改活性算法。因此shader应该要更好。为了应对练习用代码乱七八糟的情况以及能够完美地看到完整的Shader,我在下面准备了Shader代码。

Categories: tutorials

Tagged as:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s