XDRender_ShaderMode_StandardLit(1) 物理渲染-默认PBR-实现(1)

XDRender_ShaderMode_StandardLit(1) 物理渲染-默认PBR-实现(1)

@白袍小道

image-20201022152123188

image-20201022152215489

前言


理论部分可以参考PRT部分、RealTimeRender,网上大部分资料.

其中相互联系图-参考

https://www.cnblogs.com/BaiPao-XD/p/13751667.html

这里特别推荐

毛星云:

https://zhuanlan.zhihu.com/p/56967462

以及博主关于法线分布函数和几何分布的总结.

文刀秋二:

https://zhuanlan.zhihu.com/p/20091064

正文


这里作为实现部分,主要是考虑三件事:

1、HLSL的结构,BRDF的相关数据结构,以及传递和组织这些数据

2、关于菲涅尔反射、法线分布函数、几何分布函数、积分处理部分

3、公式的演化、特性、以及实现和组织部分

先放出一个数据流图

一、理论引进


简化公式

第一个大公式类

image-20201022154453238

参数说明:

image-20201022154521486

第二个大公式类

image-20201022154946194

拆开后

image-20201022154901051

image-20201022154650268

对于每一个细节部分实现中会单独说明和处理, 抱着敬畏和大胆的步伐,开始实现.

光和介质的交互

​ 当一束光线入射到物体表面时,由于物体表面与空气两种介质之间折射率的快速变化,光线会发生反射和折射:

  • 反射(Reflection)。光线在两种介质交界处的直接反射即镜面反射(Specular)。金属的镜面反射颜色为三通道的彩色,而非金属的镜面反射颜色为单通道的单色。
  • 折射(Refraction)。从表面折射入介质的光,会发生吸收(absorption)和散射(scattering),而介质的整体外观由其散射和吸收特性的组合决定,其中:
  • 散射(Scattering)。折射率的快速变化引起散射,光的方向会改变(分裂成多个方向),但是光的总量或光谱分布不会改变。散射最终被视作的类型与观察尺度有关:
  • 次表面散射(Subsurface Scattering)。观察像素小于散射距离,散射被视作次表面散射
  • 漫反射(Diffuse)。观察像素大于散射距离,散射被视作漫反射
  • 透射(Transmission)。入射光经过折射穿过物体后的出射现象。透射为次表面散射的特例。
  • 吸收(Absorption)。具有复折射率的物质区域会引起吸收,具体原理是光波频率与该材质原子中的电子振动的频率相匹配。复折射率(complex number)的虚部(imaginary part)确定了光在传播时是否被吸收(转换成其他形式的能量)。发生吸收的介质的光量会随传播的距离而减小(如果吸收优先发生于某些波长,则可能也会改变光的颜色),而光的方向不会因为吸收而改变。任何颜色色调通常都是由吸收的波长相关性引起的。

二、实现过程


假定最最最理想的情况

这里实现我们先假设一大堆条件,避免分散注意.比如多直接光,环境光照, 光衰减, 透射,暂时不守恒,只包含Forward方式等等大量的条件.

我们的最简化参数: 粗超度, 基础颜色, 金属度

流程

入口:这里是Shader部分, 通过填写LightMode启用,(LightMode被OpoPass识别)

其实我们也可以将直接光的数据在前面的阶段(比如ConfigLightPass, Deff的话是先搞出光照需要所有纹理数据,再等到后续直接进行对纹理处理)传递.当然这是

第一步: XDRender/ShaderMode/DefaultLit.Shader

第二步: 进入到构建必要数据

a、Surface:这里是将Shader数据处理为统一, 比如一些用BaseColor纹理,一些不用.这样我们到这里统一搞出Color.

#ifdef  _DIFFCUSE_MAP
	half4 albedoAlpha = _BaseColor * 		SAMPLE_TEXTURE2D(_DiffcuseMap,sampler_DiffcuseMap, input.baseUV);
#else
	half4 albedoAlpha = _BaseColor;
#endif
outSurfaceData.alpha = albedoAlpha.r;
outSurfaceData.albedo = albedoAlpha.rgb;
float3 rmao_sample = SAMPLE_TEXTURE2D(_RouAOMap,sampler_RouAOMap, input.baseUV).b * _OcclusionStrength;

outSurfaceData.metallic = rmao_sample.r;
outSurfaceData.specular = _SpecColor;
outSurfaceData.smoothness = 1.0-rmao_sample.g;

b、Input:顶点属性相关

nputData.viewDirectionWS = normalize(_WorldSpaceCameraPos.xyz - input.positionWS.xyz);
	inputData.positionWS = input.positionWS;
	inputData.normalWS = normalWS;
	inputData.bakedGI = SAMPLE_GI(lightmapUV, input.vertexSH, inputData.normalWS);

第三步:组织BRDF的数据结构

第四步:

BRDF必要数据

1、这里我们就不考虑两种PBR数据流区别,先就简单把数据传过来

2、我们先简单传递,然后在下一步过程中去处理金属和电解质的特殊性.

注意一下,HLSL可以类似多态的函数写法,还可以建立多个InitializeBRDFData辅助与宏来处理多种数据结构. 减少不断增加.

这里还可以YY一下, 就一种Float4 * X结构去组织

inline void InitializeBRDFData(half3 albedo, half metallic, half3 specular, half smoothness, half alpha, 
    out BRDFData outBRDFData)
{

//http://richbabe.top/2018/06/25/%E6%8E%A2%E7%A9%B6PBR%E7%9A%84%E4%B8%A4%E7%A7%8D%E6%B5%81%E7%A8%8B%E4%BB%A5%E5%8F%8AUnity%E4%B8%AD%E7%9A%84PBS/

    outBRDFData.diffuse = albedo;
    outBRDFData.specular = specular;
    outBRDFData.grazing = saturate(smoothness + 1 - metallic);
    
    
    outBRDFData.perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(smoothness);
    outBRDFData.roughness = max(PerceptualRoughnessToRoughness(outBRDFData.perceptualRoughness), HALF_MIN);
    outBRDFData.roughness2 = outBRDFData.perceptualRoughness * outBRDFData.perceptualRoughness;
    outBRDFData.lerpRoughness = lerp(0.01, 1.0, outBRDFData.perceptualRoughness);
    outBRDFData.metallic = metallic;
    outBRDFData.clearcoat = 0;
    outBRDFData.clearcoatgloss = 0;
    outBRDFData.secondcolor=0;

BRDF计算

这里我们只关注BSDF中的BRDF的直接光照部分,其中光照按照介质的不同对光照会做不同的分化影响

0 基础参数计算

主要是计算 :法向量(宏观非亚像素),入射点光照方向, 射点视野, 半角方向(这里注意一般来说主要使用的NH来作为后续几个函数的因变量, 推导过程网上已经大量这里就不详细说明)几个向量的叉积.

注意:这里我们就会注意到是在采样点进行的计算, 所以这几个参数不是物理意义上的点,偏差出现.但整体相对可接受.

float3 halfVector = normalize(lightDir + viewDir); //半角向量
float nl = max(saturate(dot(normal, lightDir)), 0.000001); //防止除0
float vh = max(saturate(dot(viewDir, halfVector)), 0.000001);
float lh = max(saturate(dot(lightDir, halfVector)), 0.000001);
float nh = max(saturate(dot(normal, halfVector)), 0.000001);
float nv = max(saturate(dot(normal, viewDir)), 0.000001); 

为何是半角?

URL:

1 确定漫反射和镜面反射的比例

这里我们假定光线在介质交界处(默认为空气到其他),只发生了漫反射和镜面反射,两者合起来辐射度为入射处光线辐射度,从而满足一定意义上的能量守恒原则.

所以我们需要对这个比例进行计算,反过来说于分配辐射度.

L=Kd^Diff + Ks^Spec

其中这两个参数,主要会受到介质的影响.

​ a\金属度(或高光):

金属度 表示了反射时发生镜面反射和漫反射的光线的占比。
Metallic度越大 发生镜面反射的占比越大,漫反射diffuse占比越小,一般金属物体的金属度比较大:70% ~ 100%之间;
Metallic越小 发生镜面反射的占比越小,漫反射diffuse占比越大,一般非金属物质金属度比较小;2% ~ 5%之间,宝石的大概8%;

​ b\菲涅尔反射.

​ (这里注意是金属菲涅而F0是带有颜色,具体我们在后详细)

可参考代码如下:

loat3 F0 = float3(0.04,0.04,0.04); 
//使用mix(lerp)函数利用 metallic对diffuse,最低值做插值,
//metallic越接近0,那么就越接近绝缘体,
//metalness越接近1,那么就越接近金属的Diff。
F0      = lerp(F0, pBRDF.diffuse, pBRDF.metallic);
float3 F = BaseFresnelSchlick(vh,F0);
float3 kS = F;
float3 kD = float3(1.0,1.0,1.0) - kS;
kD *= 1.0 - pBRDF.metallic;

这里我们也可以提前将Diff进行减弱.

​ 先(1.0-0.04 ) - (pBRDF.metallic)(1.0-0.04 ),然后Diff = Diff *这个值来修复Diff

#define kDieletricSpec half4(0.04, 0.04, 0.04, 1.0-0.04) 

half OneMinusReflectivityMetallic(half metallic)
{
    half oneMinusDielectricSpec = kDieletricSpec.a;
    return oneMinusDielectricSpec - metallic * oneMinusDielectricSpec;
}

有了这个比例我们可以先假定已经计算出漫反射和镜面高光反射

 float3 DirectLightResult =  (kD * pBRDF.diffuse + specular) 		* lightColor * nl;
 return DirectLightResult;
///------------直接光照部分--------------------
   vLight = GetMainLight ();
            lightDir= normalize(vLight.direction).xyz;
            lightColor= vLight.color * (1-vLight.distanceAttenuation);
            
            DirectLightResult +=  BRDF01_DirectLight(lightColor,normal,lightDir,viewDir,pBRDFData);
2 菲涅尔反射

定义:根据物理学,麦克斯韦方程组可以在折射率(IOR)变化时计算光的行为。对于空气中通常的物体表面而言,物体的表面便是空气折射率和物体折射率的交界面,对此特殊的折射率交界面而言,麦克斯韦方程组的解被我们称为菲涅尔方程

a\万物皆有菲涅尔效应?

​ 在宏观层面看到的实际上是微观层面微平面菲涅尔效应

​ (表示观察看到的反射光线的量与视角相关的现象,且掠射角度(90度)

​ 下反射率最大)的平均值,即影响菲涅尔效应的关键参数在于每个微平面

​ 的法向量和入射光线的角度,而不是宏观平面的法向量和入射光线的角度

b\其中F0的含义是

​ F0即0度角入射的菲涅尔反射率。任意角度的菲涅尔反射率

b1 简化的公式

​ 可由F0和入射角度计算得出,而上文我们提到过不同的材质(物理意义上)

对算法有一定的区别,比如非金属的F0是一个float,金属的F0是一个float3. 这

样就暗示一个比较重要的问题如何去统一的处理.?总不能if/#if几个吧, 况且

对美术而言很难去规定这个含义并记录.

image-20201023101004427

image-20201023101101742

这里电介质具有相当低的F0值 - 通常为0.06或更低, 故而我们用这个边界值0.04来模拟一下

b2 通过IOR来处理

​ 以上只是一种最简化的求F0, 而光在传播到两种不同介质交界处时,原始光波和新的光波的相速度(Phase Velocity)的比率定义了介质的光学性质,就是折射率(Index of Refraction,IOR)故而我们还有通过IOR来实现.

而这个折射率,我们可以通过查找表( 或者说将表编码:https://link.zhihu.com/?target=https%3A//github.com/QianMo/PBR-White-Paper/raw/master/bonus/%255BPBR-White-Paper%255D%2520PBR-Material-F0-Quick-Reference-Chart.pdf 转为纹理查找等等方式)

(这里补充F0为0度角入射时的菲涅尔反射率。而折射(refracted)到表面中的光量则为为1-F0。)

image-20201023130907263

但我们大部分是空气到其他,故而可以换成

image-20201023132959233

//原始F0求法
inline float3 FresnelF0_IOR(float IOR1, float IOR2)
{
    float IOROffset = IOR2 - IOR1;
    float IORAdd = IOR2 + IOR1;
    IORAdd = IORAdd >0?IORAdd:0.001;
    return Pow2(IOROffset / IORAdd);
}

inline float3 FresnelF0_IOR(float IOR2)
{
    float IOROffset = IOR2 - 1;
    float IORAdd = IOR2 + 1;
    return Pow2(IOROffset / IORAdd);
}

有了F0,我们接下来就可以选则不同的菲尼尔算法来执行计算, 我们选取的法制包括了性能, 和接下来的法线分布、几何分布以及整体的统一性做衡量.

(这里额外说明 : 菲涅尔反射效应,和我们用来做一些效果的菲涅尔效果不要搞混淆,虽然在含义有类似部分, 其中菲涅尔效果等额外效果会在后面节点统一),

以下是一些实现: 这里注意下传入的是VH, 原因可以Google

inline float3 BaseFresnelSchlick(float HdotV, float3 F0)
{
    return F0 + (1 - F0) * Pow5(1 - HdotV);
}		
inline half3 FresnelLerp (half3 F0, half3 F90, half cosA)
{
    half t = Pow5 (1 - cosA);   // ala Schlick interpoliation
    return lerp (F0, F90, t);
}
inline half3 FresnelLerpFast (half3 F0, half3 F90, half cosA)
{
    half t = Pow4 (1 - cosA);
    return lerp (F0, F90, t);
}

以及在间接光部分用的

inline float3 FresnelSchlickRoughness(float cosTheta, float3 F0, float roughness)
{
    return F0 + (max(float3(1, 1, 1) * (1 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
}

以及迪士尼

//传入某个角度的f,得到漫反射的1-f
inline float DSL_SchlickFresnel(float f)
{
    float fd = clamp(1-f, 0, 1);
    return Pow5(fd); 
}

//漫反射的菲尼尔系数。
//没有次表面散射时,我们的F90:掠射角0.5, 垂直角度1
//并且基于粗糙度插值
inline float Fd_burley (float NdotL, float NdotV, float LdotH, float roughness){
    float FL= DSL_SchlickFresnel(NdotL); 
    
    //F0时,NdotV=1. FV=0    Lerp(1, Fd90, FV) =1            
    //F90时,NdotV=0. FV=1     Lerp(1, Fd90, FV) =0
    float FV= DSL_SchlickFresnel(NdotV); 
    float Fd90 = 0.5 + 2.0 * LdotH*LdotH * roughness;
    return  lerp(1, Fd90, FL) * lerp(1, Fd90, FV);
}

3 漫反射

这里选取了DiffR= Diff/PI,当然也有其他公式, 限于篇幅, 这些放在后续单独的章节实现总结.

这里我们假定用最Easy的方式 : diffcuseResult = pBRDF.diffuse. 然后进行KD衰减.

float3 diffcuseResult = Kd * pBRDF.diffuse / PI;
//DSL
float FD90 = 0.5 + 2 * VoH * VoH * Roughness;
float FdV = 1 + (FD90 - 1) * Pow5( 1 - NoV );
float FdL = 1 + (FD90 - 1) * Pow5( 1 - NoL );
return DiffuseColor * ( (1 / PI) * FdV * FdL );
4 镜面反射
NDF:法线分布函数,(这里我们先实现各向同性部分)

a\这个函数让材质有了亚像素级更精细的把控和更科学的定量, 是一种近似拟合. 通过BRDF进行建模,由粗糙度贴图(Roughness Map)配合法线分布函数,提供每亚像素(subpixel)法线信息

b\我们使用半矢量h来表示微观表面法线m,因为仅m = h的表面点的朝向才会将光线l反射到视线v的方向,其他朝向的表面点对BRDF没有贡献(正负相互抵消)

同时出现许多变化: 基本分为形状可变,不变.

​ 这是一个合法的法线分布函数应该具备的特性

image-20201023181218425

image-20201023180116406

总结和推导法线分布函数

Cook Beckmann
// [Beckmann 1963, "The scattering of electromagnetic waves from rough surfaces"]
// src= exp(−tan2(α)/m2) 除以 πm2 cos4(α), 其中 α=arccos(n⋅h) m=a
// 其中−tan2(α) ===> -(1-cos2(α))/cos2(α) =>(cos2(α)-1)/cos2(α)>(nh2-1)/nh2

inline float Distribution_Beckmann(float a2, float nh)
{
    float NoH2 = nh * nh;
    return exp( (NoH2 - 1) / (a2 * NoH2) ) / ( PI * a2 * NoH2 * NoH2 );
}
G:几何函数:遮蔽+阴影

在基于物理的渲染中,几何函数(Geometry Function)是一个0到1之间的标量,描述了微平面自阴影的属性,表示了具有半矢量法线的微平面(microfacet)中,同时被入射方向和反射方向可见(没有被遮挡的)的比例,即

未被遮挡的m= h微表面的百分比

image-20201023180139418

The Visibility Term

其中,在部分游戏引擎和文献中,几何函数G(l,v,h)和分母中的校正因子4(n·l)(n·v)会合并为可见性项(The Visibility Term),Vis项,简称V项

Unity(默认Render)和Unreal

Unity:

UnityStandardBRDF(感兴趣可以去看下这片Paper)

image-20201023192304397

// Ref: http://jcgt.org/published/0003/02/03/paper.pdf
inline float SmithJointGGXVisibilityTerm (float NdotL, float NdotV, float roughness)
{
#if 0
    // Original formulation:
    //  lambda_v    = (-1 + sqrt(a2 * (1 - NdotL2) / NdotL2 + 1)) * 0.5f;
    //  lambda_l    = (-1 + sqrt(a2 * (1 - NdotV2) / NdotV2 + 1)) * 0.5f;
    //  G           = 1 / (1 + lambda_v + lambda_l);

    // Reorder code to be more optimal
    half a          = roughness;
    half a2         = a * a;

    half lambdaV    = NdotL * sqrt((-NdotV * a2 + NdotV) * NdotV + a2);
    half lambdaL    = NdotV * sqrt((-NdotL * a2 + NdotL) * NdotL + a2);

    // Simplify visibility term: (2.0f * NdotL * NdotV) /  ((4.0f * NdotL * NdotV) * (lambda_v + lambda_l + 1e-5f));
    return 0.5f / (lambdaV + lambdaL + 1e-5f);  // This function is not intended to be running on Mobile,
                                                // therefore epsilon is smaller than can be represented by half
#else
    // Approximation of the above formulation (simplify the sqrt, not mathematically correct but close enough)
    float a = roughness;
    float lambdaV = NdotL * (NdotV * (1 - a) + a);
    float lambdaL = NdotV * (NdotL * (1 - a) + a);

#if defined(SHADER_API_SWITCH)
    return 0.5f / (lambdaV + lambdaL + 1e-4f); // work-around against hlslcc rounding error
#else
    return 0.5f / (lambdaV + lambdaL + 1e-5f);
#endif

#endif
}

Unreal:

这里我们注意一下,(引用:https://zhuanlan.zhihu.com/p/81708753)

image-20201023192722110

最后总结: 可以根据拟合程度选取.或近似实现. 这里要求选择合适的微表面轮廓(microsurface profile),从而对G项进行具象化建模。但G对BRDF的整体影响不大,但能保证能量守恒,在我们不断的分化过程会出现一定的比重.

​ 具有相同法线分布但具有不同轮廓(profiles)的微表面导致不同的BRDF

  • 分离的遮蔽阴影型(Separable Masking and Shadowing)

  • 高度相关的遮蔽阴影型(Height-Correlated Masking and Shadowing)

  • 方向相关的遮蔽阴影型(Direction-Correlated Masking and Shadowing)

  • 高度-方向相关遮蔽阴影型(Height-Direction-Correlated Masking and Shadowing)

    基本要求

    • 标量性
    • 对称性
    • 同向可见性
    • 拉伸不变性(Stretch Invariance)

    最后给出一个测试的方法:

    https://github.com/knarkowicz/FurnaceTest, 这部分会在接下来的HLSLLightFunction去实现,

F: 相对的菲涅尔反射方程结果

最后我们简单的实现一下

//法线分布: Input nh
float NDF = DistributionGGX(nh,  pBRDF.roughness);   
//几何: Input Nv, NL 
float G   = GeometrySmith(nv, nl, pBRDF.roughness);    
float3 nominator    = NDF * G * F;
float denominator = 4.0 * nv * nl + 0.001; 
float3 specular     = nominator / denominator; 
5 积分部分

​ 微表面模型的半球积分, 这里其实会涉及到蒙特卡洛积分(离散方式计算积分,概率搞一搞)

 float3 DirectLightResult =  (kD * diffcuseResult + specular) * lightColor * nl;
6 线性空间

​ 还原现实世界方式的光与物质的交互的方式, 其中详细的颜色空间部分在后续说明.

​ 1、我们输入的BaseColor(Diff), 金属度, 粗糙度需要转化到线性空间下, 保证后续的计算全部在线性空间.

​ 2、完成后,需要再次将结果返回到伽马空间, 这里不一定是立马(关键看我们的Pipeline)

7 色调映射

Toonmap: 通过算法,将大于1的部分分散开来, 这样可以保证过亮部分得以在视觉上保留.(具体同理在后续)

8 对部分特性的处理

(具体同理在后续)

三、说明


这里梳理一下相关文件结构

XDArt_RenderLightFunction.hlsl: 函数库

XDArt_RenderLight_BRDF.hlsl : BRDF多个实现,BRDF输入结构定义

XDArt_RenderLight_BSSRDF.hlsl: BSSRDF多个实现,BSSRDF输入结构定义

XDArt_ShaderMode_DefaultLit.hlsl: 内置的多个LitMode实现

接下来就是具体使用, 和扩展

XDArt_ShaderMode_DefaultLit.shader

XDArt_ShaderMode_DefaultLit_RoughAO.shader

XDArt_ShaderMode_DefaultLit_Rough.shader

XDArt_ShaderMode_ClearCoat/Fab/Skin/Mon.shader.......等等

备注

posted @ 2020-10-23 19:49  白袍小道  阅读(296)  评论(0编辑  收藏  举报

白袍小道 DaoZhang_XDZ@163.com - 创建于 8012

窥探道理