[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

相关文章

Go标准库常见错误分析和解决办法

《Go标准库常见错误分析和解决办法》Go语言的标准库为开发者提供了丰富且高效的工具,涵盖了从网络编程到文件操作等各个方面,然而,标准库虽好,使用不当却可能适得其反,正所谓工欲善其事,必先利其器,本文将... 目录1. 使用了错误的time.Duration2. time.After导致的内存泄漏3. jsO

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

Java的IO模型、Netty原理解析

《Java的IO模型、Netty原理解析》Java的I/O是以流的方式进行数据输入输出的,Java的类库涉及很多领域的IO内容:标准的输入输出,文件的操作、网络上的数据传输流、字符串流、对象流等,这篇... 目录1.什么是IO2.同步与异步、阻塞与非阻塞3.三种IO模型BIO(blocking I/O)NI

Spring事务中@Transactional注解不生效的原因分析与解决

《Spring事务中@Transactional注解不生效的原因分析与解决》在Spring框架中,@Transactional注解是管理数据库事务的核心方式,本文将深入分析事务自调用的底层原理,解释为... 目录1. 引言2. 事务自调用问题重现2.1 示例代码2.2 问题现象3. 为什么事务自调用会失效3

找不到Anaconda prompt终端的原因分析及解决方案

《找不到Anacondaprompt终端的原因分析及解决方案》因为anaconda还没有初始化,在安装anaconda的过程中,有一行是否要添加anaconda到菜单目录中,由于没有勾选,导致没有菜... 目录问题原因问http://www.chinasem.cn题解决安装了 Anaconda 却找不到 An

Spring定时任务只执行一次的原因分析与解决方案

《Spring定时任务只执行一次的原因分析与解决方案》在使用Spring的@Scheduled定时任务时,你是否遇到过任务只执行一次,后续不再触发的情况?这种情况可能由多种原因导致,如未启用调度、线程... 目录1. 问题背景2. Spring定时任务的基本用法3. 为什么定时任务只执行一次?3.1 未启用

基于Flask框架添加多个AI模型的API并进行交互

《基于Flask框架添加多个AI模型的API并进行交互》:本文主要介绍如何基于Flask框架开发AI模型API管理系统,允许用户添加、删除不同AI模型的API密钥,感兴趣的可以了解下... 目录1. 概述2. 后端代码说明2.1 依赖库导入2.2 应用初始化2.3 API 存储字典2.4 路由函数2.5 应

C++ 各种map特点对比分析

《C++各种map特点对比分析》文章比较了C++中不同类型的map(如std::map,std::unordered_map,std::multimap,std::unordered_multima... 目录特点比较C++ 示例代码 ​​​​​​代码解释特点比较1. std::map底层实现:基于红黑

Spring、Spring Boot、Spring Cloud 的区别与联系分析

《Spring、SpringBoot、SpringCloud的区别与联系分析》Spring、SpringBoot和SpringCloud是Java开发中常用的框架,分别针对企业级应用开发、快速开... 目录1. Spring 框架2. Spring Boot3. Spring Cloud总结1. Sprin

Spring 中 BeanFactoryPostProcessor 的作用和示例源码分析

《Spring中BeanFactoryPostProcessor的作用和示例源码分析》Spring的BeanFactoryPostProcessor是容器初始化的扩展接口,允许在Bean实例化前... 目录一、概览1. 核心定位2. 核心功能详解3. 关键特性二、Spring 内置的 BeanFactory