[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

相关文章

Spring Boot Interceptor的原理、配置、顺序控制及与Filter的关键区别对比分析

《SpringBootInterceptor的原理、配置、顺序控制及与Filter的关键区别对比分析》本文主要介绍了SpringBoot中的拦截器(Interceptor)及其与过滤器(Filt... 目录前言一、核心功能二、拦截器的实现2.1 定义自定义拦截器2.2 注册拦截器三、多拦截器的执行顺序四、过

C++ scoped_ptr 和 unique_ptr对比分析

《C++scoped_ptr和unique_ptr对比分析》本文介绍了C++中的`scoped_ptr`和`unique_ptr`,详细比较了它们的特性、使用场景以及现代C++推荐的使用`uni... 目录1. scoped_ptr基本特性主要特点2. unique_ptr基本用法3. 主要区别对比4. u

Nginx内置变量应用场景分析

《Nginx内置变量应用场景分析》Nginx内置变量速查表,涵盖请求URI、客户端信息、服务器信息、文件路径、响应与性能等类别,这篇文章给大家介绍Nginx内置变量应用场景分析,感兴趣的朋友跟随小编一... 目录1. Nginx 内置变量速查表2. 核心变量详解与应用场景3. 实际应用举例4. 注意事项Ng

Java多种文件复制方式以及效率对比分析

《Java多种文件复制方式以及效率对比分析》本文总结了Java复制文件的多种方式,包括传统的字节流、字符流、NIO系列、第三方包中的FileUtils等,并提供了不同方式的效率比较,同时,还介绍了遍历... 目录1 背景2 概述3 遍历3.1listFiles()3.2list()3.3org.codeha

Java领域模型示例详解

《Java领域模型示例详解》本文介绍了Java领域模型(POJO/Entity/VO/DTO/BO)的定义、用途和区别,强调了它们在不同场景下的角色和使用场景,文章还通过一个流程示例展示了各模型如何协... 目录Java领域模型(POJO / Entity / VO/ DTO / BO)一、为什么需要领域模

深入理解Redis线程模型的原理及使用

《深入理解Redis线程模型的原理及使用》Redis的线程模型整体还是多线程的,只是后台执行指令的核心线程是单线程的,整个线程模型可以理解为还是以单线程为主,基于这种单线程为主的线程模型,不同客户端的... 目录1 Redis是单线程www.chinasem.cn还是多线程2 Redis如何保证指令原子性2.

Nginx分布式部署流程分析

《Nginx分布式部署流程分析》文章介绍Nginx在分布式部署中的反向代理和负载均衡作用,用于分发请求、减轻服务器压力及解决session共享问题,涵盖配置方法、策略及Java项目应用,并提及分布式事... 目录分布式部署NginxJava中的代理代理分为正向代理和反向代理正向代理反向代理Nginx应用场景

Redis中的有序集合zset从使用到原理分析

《Redis中的有序集合zset从使用到原理分析》Redis有序集合(zset)是字符串与分值的有序映射,通过跳跃表和哈希表结合实现高效有序性管理,适用于排行榜、延迟队列等场景,其时间复杂度低,内存占... 目录开篇:排行榜背后的秘密一、zset的基本使用1.1 常用命令1.2 Java客户端示例二、zse

Redis中的AOF原理及分析

《Redis中的AOF原理及分析》Redis的AOF通过记录所有写操作命令实现持久化,支持always/everysec/no三种同步策略,重写机制优化文件体积,与RDB结合可平衡数据安全与恢复效率... 目录开篇:从日记本到AOF一、AOF的基本执行流程1. 命令执行与记录2. AOF重写机制二、AOF的

MyBatis Plus大数据量查询慢原因分析及解决

《MyBatisPlus大数据量查询慢原因分析及解决》大数据量查询慢常因全表扫描、分页不当、索引缺失、内存占用高及ORM开销,优化措施包括分页查询、流式读取、SQL优化、批处理、多数据源、结果集二次... 目录大数据量查询慢的常见原因优化方案高级方案配置调优监控与诊断总结大数据量查询慢的常见原因MyBAT