[PBS真的可以为所欲为] Unreal材质模型源码分析

2023-10-17 19:50

本文主要是介绍[PBS真的可以为所欲为] Unreal材质模型源码分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

http://blog.sina.com.cn/s/blog_76d02ce90102xr4x.html


unreal应该算是现在最实用的开源渲染引擎之一了


基于物理的渲染(PBS)用起来真的是为所欲为

这里决定花点时间研究下unreal低层的渲染代码,这里假设读者具有基本的渲染知识,至少知道BRDF是什么

引擎中的着色器代码都在Engine/Shaders中,不同版本之间会有些差别,不过不会差太多就是了

这里使用的版本是4.17

打开Engine/Shaders/Private文件夹,可以看见大量的.usf和.ush文件,这些就是unreal渲染的核心



两者的语法都类似GLSL或HLSL,区别是ush没有入口函数,只能被其他ush或usf来引用,而usf有入口函数,不可被应用。

整个Engine/Shaders/Private中有近300个文件,一行一行解释显然不现实,这篇决定优先分析引擎中material的源码

这部分源码主要在ShadingModels.ush中,会引用其他的BRDF.ush,下面来慢慢分析

首先是下面两个函数:
NormalText Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//  @param  DiffSpecMask  .r:  diffuse,  .g:specular  e.g.  float2(1,1)  for  both,  float2(1,0)  for  diffuse  only
float3  SurfaceShading(  FGBufferData  GBuffer,  float3  LobeRoughness,  float3  LobeEnergy,  float3  L,  float3  V,  half3  N,  uint2  Random  )
{
        switch(  GBuffer.ShadingModelID  )
        {
                case  SHADINGMODELID_UNLIT:
                case  SHADINGMODELID_DEFAULT_LIT:
                case  SHADINGMODELID_SUBSURFACE:
                case  SHADINGMODELID_PREINTEGRATED_SKIN:
                case  SHADINGMODELID_SUBSURFACE_PROFILE:
                case  SHADINGMODELID_TWOSIDED_FOLIAGE:
                        return  StandardShading(  GBuffer.DiffuseColor,  GBuffer.SpecularColor,  LobeRoughness,  LobeEnergy,  L,  V,  );
                case  SHADINGMODELID_CLEAR_COAT:
                        return  ClearCoatShading(  GBuffer,  LobeRoughness,  LobeEnergy,  L,  V,  );
                case  SHADINGMODELID_CLOTH:
                        return  ClothShading(  GBuffer,  LobeRoughness,  LobeEnergy,  L,  V,  );
                case  SHADINGMODELID_EYE:
                        return  EyeShading(  GBuffer,  LobeRoughness,  LobeEnergy,  L,  V,  );
                default:
                        return  0;
        }
}

float3  SubsurfaceShading(  FGBufferData  GBuffer,  float3  L,  float3  V,  half3  N,  float  Shadow,  uint2  Random  )
{
        float3  SubsurfaceColor  ExtractSubsurfaceColor(GBuffer);

        switch(  GBuffer.ShadingModelID  )
        {
                case  SHADINGMODELID_SUBSURFACE:
                        return  SubsurfaceShadingSubsurf ace(  GBuffer,  L,  V,  );
                case  SHADINGMODELID_PREINTEGRATED_SKIN:
                        return  SubsurfaceShadingPreinte gratedSkin(  GBuffer,  L,  V,  );
                case  SHADINGMODELID_TWOSIDED_FOLIAGE:
                        return  SubsurfaceShadingTwoSide d(  SubsurfaceColor,  L,  V,  );
                case  SHADINGMODELID_HAIR:
                        return  HairShading(  GBuffer,  L,  V,  N,  Shadow,  1,  0,  Random  );
                case  SHADINGMODELID_EYE:
                        return  EyeSubsurfaceShading(  GBuffer,  L,  V,  );
                default:
                        return  0;
        }
}

与引擎中的shading model完全一致



从函数中的switch中不难看出用途,这两个函数就定义了不同shading model使用的基础算法和subsurface算法

这两个函数在DeferredLightngCommon.ush中被调用,比如下面
NormalText Code 
1
2
float3  Shading  SurfaceShading(  GBuffer,  LobeRoughness,  1,  L,  V,  N,  Random  NoL;
Shading  +=  SubsurfaceShading(  GBuffer,  L,  V,  N,  1,  Random  );

就是直接把结果线性相加....

SurfaceShading

先看SurfaceShading函数,这个函数表示了material除了subsurface之外的全部内容,可以看出,除了Clear Coat,Cloth , Eye有自己独有的着色算法以外,其他的shading model都用的共通的StandardShading,那么就来看看StandardShading里是什么吧

NormalText Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
float3  StandardShading(  float3  DiffuseColor,  float3  SpecularColor,  float3  LobeRoughness,  float3  LobeEnergy,  float3  L,  float3  V,  half3  )
{
        float  NoL  dot(N,  L);
        float  NoV  dot(N,  V);
        float  LoV  dot(L,  V);
        float  InvLenH  rsqrt(  LoV  );
        float  NoH  saturate(  NoL  NoV  InvLenH  );
        float  VoH  saturate(  InvLenH  InvLenH  LoV  );
        NoL  saturate(NoL);
        NoV  saturate(abs(NoV)  1e-5);

        //  Generalized  microfacet  specular
        float  D_GGX(  LobeRoughness[1],  NoH  LobeEnergy[1];
        float  Vis  Vis_SmithJointApprox(  LobeRoughness[1],  NoV,  NoL  );
        float3  F_Schlick(  SpecularColor,  VoH  );

        float3  Diffuse  Diffuse_Lambert(  DiffuseColor  );
        //float3  Diffuse  Diffuse_Burley(  DiffuseColor,  LobeRoughness[1],  NoV,  NoL,  VoH  );
        //float3  Diffuse  Diffuse_OrenNayar(  DiffuseColor,  LobeRoughness[1],  NoV,  NoL,  VoH  );

        return  Diffuse  LobeEnergy[2]  (D  Vis)  F;
}

基本遵循了epic在siggraph2013上发表的paper[1]上的算法


其中D是D_GGX,G是Vis_SmithJointApprox,F是F_Schlick。具体代码在BRDF.ush中都有。
再加上Lambert的diffuse构成了基础的shading。

Clear Coat比较复杂,有空可能会单独写一篇

Cloth其实是定义了两种不同的BRDF,在这两者间作插值

NormalText Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
float3  ClothShading(  FGBufferData  GBuffer,  float3  LobeRoughness,  float3  LobeEnergy,  float3  L,  float3  V,  half3  )
{
        const  float3  FuzzColor    saturate(GBuffer.CustomData.rgb);
        const  float    Cloth            saturate(GBuffer.CustomData.a);

        float  NoL  dot(N,  L);
        float  NoV  dot(N,  V);
        float  LoV  dot(L,  V);
        float  InvLenH  rsqrt(  LoV  );
        float  NoH  saturate(  NoL  NoV  InvLenH  );
        float  VoH  saturate(  InvLenH  InvLenH  LoV  );
        NoL  saturate(NoL);
        NoV  saturate(abs(NoV)  1e-5);

        //  Diffuse   
        float3  Diffuse  Diffuse_Lambert(  GBuffer.DiffuseColor  );
        float3  Diff  Diffuse  LobeEnergy[2];

        //  Cloth  Asperity  Scattering  Inverse  Beckmann  Layer 
        float3  F1  F_Schlick(  FuzzColor,  VoH  );
        float    D1  D_InvGGX(  LobeRoughness[1],  NoH  );
        float    V1  Vis_Cloth(  NoV,  NoL  );

        float3  Spec1  D1  V1  F1;

        //  Generalized  microfacet  specular
        float3  F2  F_Schlick(  GBuffer.SpecularColor,  VoH  );
        float    D2  D_GGX(  LobeRoughness[1],  NoH  LobeEnergy[1];
        float    V2  Vis_SmithJointApprox(  LobeRoughness[1],  NoV,  NoL  );

        float3  Spec2  D2  V2  F2;

        float3  Spec  lerp(Spec2,  Spec1,  Cloth);

        return  Diff  Spec;
}

Spec2和standShading的高光部分一样,Spec1的D和G则略有不同,用GBuffer中的custom.a做插值,得到最终的输出(Diffuse还是Lambert)

Eye的部分更加简单,就是StandShading去掉Diffuse的部分

NormalText Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
float3  EyeShading(  FGBufferData  GBuffer,  float3  LobeRoughness,  float3  LobeEnergy,  float3  L,  float3  V,  half3  )
{
        float  NoL  dot(N,  L);
        float  NoV  dot(N,  V);
        float  LoV  dot(L,  V);
        float  InvLenH  rsqrt(  LoV  );
        float  NoH  saturate(  NoL  NoV  InvLenH  );
        float  VoH  saturate(  InvLenH  InvLenH  LoV  );
        NoL  saturate(NoL);
        NoV  saturate(abs(NoV)  1e-5);

        //  Generalized  microfacet  specular
        float  D_GGX(  LobeRoughness[1],  NoH  LobeEnergy[1];
        float  Vis  Vis_SmithJointApprox(  LobeRoughness[1],  NoV,  NoL  );
        float3  F_Schlick(  GBuffer.SpecularColor,  VoH  );

        return  Vis  F;
}

不过要注意,NoH和VoH的计算方法和StandShading是不同的。

SubsurfaceShading

说完了surfaceShading,下面就来说Subsurface的部分。

首先是最普通的SubsurfaceShading:

NormalText Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
float3  SubsurfaceShadingSubsurf ace(  FGBufferData  GBuffer,  float3  L,  float3  V,  half3  )
{
        float3  SubsurfaceColor  ExtractSubsurfaceColor(GBuffer);
        float  Opacity  GBuffer.CustomData.a;

        float3  normalize(V  L);

        //  to  get  an  effect  when  you  see  through  the  material
        //  hard  coded  pow  constant
        float  InScatter  pow(saturate(dot(L,  -V)),  12)  lerp(3,  .1f,  Opacity);
        //  wrap  around  lighting,  /(PI*2)  to  be  energy  consistent  (hack  do  get  some  view  dependnt  and  light  dependent  effect)
        //  Opacity  of  gives  no  normal  dependent  lighting,  Opacity  of  gives  strong  normal  contribution
        float  NormalContribution  saturate(dot(N,  H)  Opacity  Opacity);
        float  BackScatter  GBuffer.GBufferAO  NormalContribution  (PI  2);

        //  lerp  to  never  exceed  (energy  conserving)
        return  SubsurfaceColor  lerp(BackScatter,  1,  InScatter);
}

观察NormalContribution项,显然Opacity=1时,NormalContribution=dot(N,H)。也就是说次表面的深度受到法线和半角的点积影响。

然后是皮肤的次表面

NormalText Code 
1
2
3
4
5
6
7
8
float3  SubsurfaceShadingPreinte gratedSkin(  FGBufferData  GBuffer,  float3  L,  float3  V,  half3  )
{
        float3  SubsurfaceColor  ExtractSubsurfaceColor(GBuffer);
        float  Opacity  GBuffer.CustomData.a;

        float3  PreintegratedBRDF  Texture2DSampleLevel(PreIntegratedBRDF,  PreIntegratedBRDFSampler float2(saturate(dot(N,  L)  .5  .5),  Opacity),  0).rgb;
        return  PreintegratedBRDF  SubsurfaceColor;
}

意外的简单,其实就是查找一张LUT的值。
这张图很容易找到,打开unreal4,在content browser的View Options里选择Show Engine Content


这样就可以在Engine Content/EngineMaterials/中找到这张LUT了


然后是twosided的次表面:

NormalText Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
float3  SubsurfaceShadingTwoSide d(  float3  SubsurfaceColor,  float3  L,  float3  V,  half3  )
{
        //  http://blog.stevemcauley.com/2011/12/03/energy-conserving-wrapped-diffuse/
        float  Wrap  0.5;
        float  NoL  saturate(  dot(-N,  L)  Wrap  Square(  Wrap  );

        //  GGX  scatter  distribution
        float  VoL  saturate(  dot(V,  -L)  );
        float  0.6;
        float  a2  a;
        float  VoL  a2  VoL  VoL  1;  //  mad
        float  GGX  (a2  PI)  (d  d);                //  mul,  rcp
        return  NoL  GGX  SubsurfaceColor;
}

NoL经过了Wrap来保证能量守恒,乘以GGX的NormalDistribution得到最终结果
这里注意GGX没有使用roughness,而是将a=roughness*roughness设成0.6。相当于roughness=0.77

hair的次表面特别复杂,分R、TT、TRT等很多部分,以后单独拿出来讲。eye也一样



Reference

[1] https://de45xmedrsdbp.Resources/files/2013SiggraphPresentation sNotes-26915738.pdf

这篇关于[PBS真的可以为所欲为] Unreal材质模型源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/227577

相关文章

Android 缓存日志Logcat导出与分析最佳实践

《Android缓存日志Logcat导出与分析最佳实践》本文全面介绍AndroidLogcat缓存日志的导出与分析方法,涵盖按进程、缓冲区类型及日志级别过滤,自动化工具使用,常见问题解决方案和最佳实... 目录android 缓存日志(Logcat)导出与分析全攻略为什么要导出缓存日志?按需过滤导出1. 按

Linux中的HTTPS协议原理分析

《Linux中的HTTPS协议原理分析》文章解释了HTTPS的必要性:HTTP明文传输易被篡改和劫持,HTTPS通过非对称加密协商对称密钥、CA证书认证和混合加密机制,有效防范中间人攻击,保障通信安全... 目录一、什么是加密和解密?二、为什么需要加密?三、常见的加密方式3.1 对称加密3.2非对称加密四、

MySQL中读写分离方案对比分析与选型建议

《MySQL中读写分离方案对比分析与选型建议》MySQL读写分离是提升数据库可用性和性能的常见手段,本文将围绕现实生产环境中常见的几种读写分离模式进行系统对比,希望对大家有所帮助... 目录一、问题背景介绍二、多种解决方案对比2.1 原生mysql主从复制2.2 Proxy层中间件:ProxySQL2.3

python使用Akshare与Streamlit实现股票估值分析教程(图文代码)

《python使用Akshare与Streamlit实现股票估值分析教程(图文代码)》入职测试中的一道题,要求:从Akshare下载某一个股票近十年的财务报表包括,资产负债表,利润表,现金流量表,保存... 目录一、前言二、核心知识点梳理1、Akshare数据获取2、Pandas数据处理3、Matplotl

python panda库从基础到高级操作分析

《pythonpanda库从基础到高级操作分析》本文介绍了Pandas库的核心功能,包括处理结构化数据的Series和DataFrame数据结构,数据读取、清洗、分组聚合、合并、时间序列分析及大数据... 目录1. Pandas 概述2. 基本操作:数据读取与查看3. 索引操作:精准定位数据4. Group

MySQL中EXISTS与IN用法使用与对比分析

《MySQL中EXISTS与IN用法使用与对比分析》在MySQL中,EXISTS和IN都用于子查询中根据另一个查询的结果来过滤主查询的记录,本文将基于工作原理、效率和应用场景进行全面对比... 目录一、基本用法详解1. IN 运算符2. EXISTS 运算符二、EXISTS 与 IN 的选择策略三、性能对比

MySQL 内存使用率常用分析语句

《MySQL内存使用率常用分析语句》用户整理了MySQL内存占用过高的分析方法,涵盖操作系统层确认及数据库层bufferpool、内存模块差值、线程状态、performance_schema性能数据... 目录一、 OS层二、 DB层1. 全局情况2. 内存占js用详情最近连续遇到mysql内存占用过高导致

深度解析Nginx日志分析与499状态码问题解决

《深度解析Nginx日志分析与499状态码问题解决》在Web服务器运维和性能优化过程中,Nginx日志是排查问题的重要依据,本文将围绕Nginx日志分析、499状态码的成因、排查方法及解决方案展开讨论... 目录前言1. Nginx日志基础1.1 Nginx日志存放位置1.2 Nginx日志格式2. 499

Olingo分析和实践之EDM 辅助序列化器详解(最佳实践)

《Olingo分析和实践之EDM辅助序列化器详解(最佳实践)》EDM辅助序列化器是ApacheOlingoOData框架中无需完整EDM模型的智能序列化工具,通过运行时类型推断实现灵活数据转换,适用... 目录概念与定义什么是 EDM 辅助序列化器?核心概念设计目标核心特点1. EDM 信息可选2. 智能类

Olingo分析和实践之OData框架核心组件初始化(关键步骤)

《Olingo分析和实践之OData框架核心组件初始化(关键步骤)》ODataSpringBootService通过初始化OData实例和服务元数据,构建框架核心能力与数据模型结构,实现序列化、URI... 目录概述第一步:OData实例创建1.1 OData.newInstance() 详细分析1.1.1