找回密码
 立即注册
查看: 455|回复: 0

Unity Shader资源(一)基础机制

[复制链接]
发表于 2023-4-1 16:31 | 显示全部楼层 |阅读模式
前段时间在处理项目资源相关工作,发现Unity对Shader的处理与常规资源差异较大。之前对这部分内容了解较少,记录一下学习内容。(不关注Shader具体写法)
一、概述

Shader是运行在GPU上的代码,不同的图形API能够处理的Shader类型不同,下表给出了常见的平台使用的API以及Shader类型。Unity在构建应用时,需要将Shader编译为目标平台能够识别的类型。因此Unity Shader需要有自己的一套逻辑框架(ShaderLab),以便针对特定图形API生成相应的Shader。
图形API适用平台Shader
DirectXWindowHLSL
OpenGLWindows,macOS和Linux等GLSL
VulkanWindows,macOS和Linux等SPIR-V
OpenCLWindows,macOS和Linux等OpenCL C
MetalmacOS和iOSMetal Shading Language
CUDANVIDIA GPUCUDA C/C++



NGFX方案

Unity中常见的Shader作用类型:

  • 常用渲染Shader:计算像素颜色
  • Computer Shader:利用GPU强大的并行计算能力
  • Ray tracing:特定硬件支持光追
二、ShaderLab工作流

2.1 ShaderLab In Editor




ShaderLab Import in Editor

Unity中编写的Shader文件,当Unity对这部分资源导入时,会进预编译将数据缓存到Library\ShaderCache目录下。预处理的结果Shader Compilation Info是一种中间状态的数据,


预处理做了以下几步操作:

  • 语法、语义分析
  • 切割特定类型Shader代码,例如上面CGPROGRAM ... ENDCG之间的代码。
  • 写入本地缓存
2.2 Build



中间资源时无法直接使用的,当我们点击Play或者打包时,存在明确的目标平台,此时Shader Compiler会根据缓存数据,生成真正的Shader资源(变体)。打包时Unity可以对Shader做Strip处理(此处需要小心动态加载的Shader)


2.3 ShaderLab In RunTime

运行时Unity会将Shader资源加载到游戏中,并在运行时动态编译和处理。



ShaderLab In RunTime

在真机环境下,代码调用Warm up或者相关引擎API,进行获取Shader时,会通过Persistent Manager生成标准的Shader Class实例。并且加载Shader资源,反序列化赋值给生成的Shader Class实例。在获取变体时,Unity会对所有Shader变体进行匹配,选择最适合的变体(若目标变体不存在,则使用接近的变体替代)。


Unity完成Shader的Warm Up后,会将CPU端的Shader(变体)拷贝到GPU,并移除CPU端的内存。若Profiler中发现ShaderLab占用过大,大概率是打包了很多变体数据,但并没有被GPU使用。通常项目会使用WormUp将Shader提前加载到GPU。
三、编译与运行

在说明Shade Branching和Shade  Variants两个概念前,我们需要先了解一下CPU和GPU逻辑执行层面的差异。GPU采用了数量众多的计算单元和超长的流水线,但只有非常简单的控制逻辑并省去了Cache。而CPU不仅被Cache占据了大量空间,而且还有有复杂的控制逻辑和诸多优化电路,计算能力只是CPU很小的一部分。


当Shader程序中出现条件分支语句时,这会破坏SIMD并行执行的方式,从而降低执行效率。GPU会对这种情况进行优化:将条件分支中的代码简化为只包含一种情况,从而使得不同的线程或片元可以同时执行相同的指令,提高执行效率。这个优化过程被称为“flatten branch”。
3.1 Shade Branching

在介绍逻辑分支前,先了解Shader的一些基础概念。uniform变量可以从CPU传递到GPU的全局变量,它的值在渲染每个物体时都是相同的,比如Unity Shader内置变量UNITY_MATRIX_MVP就是一个uniform。Shader宏可以在编译时定义的预处理指令,以适应不同的平台、渲染质量、特性需求。
// 声明宏:UNITY_EDITOR => 编译产生Shader变体 => 运行时切换开关
#pragma multi_compile UNITY_EDITOR
// 定义全局变量_MyUniform:所有Shader共享
uniform float _MyUniform;

void vert (inout appdata_full v) {
    ...
#if UNITY_EDITOR
    _MyUniform = 0.5;
#else
    _MyUniform = 1.0;
#end
}ShaderLab的条件分支分为Static BranchingDynamic Branching
Static Branching是指通过宏定义直接对代码进行隔离,编译时会直接将相应的代码排除。通过#if\#ifdef\ #ifndef...#elif...#endif来处理。Static Branching是控制全局的代码分割,避免了逻辑分支的缺点。
以下方示例来说,若开启了Shader宏UNITY_PASS_META则,在渲染时会选择包含其中逻辑的变体进行处理。
void surf (Input IN, inout SurfaceOutput o) {
    fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
    fixed4 c = tex * _Color;
    o.Albedo = c.rgb;
    o.Emission = c.rgb * tex2D(_Illum, IN.uv_Illum).a;
#if defined (UNITY_PASS_META)
    o.Emission *= _Emission.rrr;
#endif
    o.Alpha = c.a;
    o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
}
Dynamic Branching是指运行时处理的逻辑分支,这种分支也分为两种类型:基于uniform变量(Shader层面的宏)、非uniform变量。当Shader程序需要访问uniform变量时,只需要在内存中寻址一次,而使用其他变量或常量时,则需要在每个线程或片元的内存中进行寻址,这会带来额外的内存访问和延迟。通常优先使用uniform做逻辑分支。

  • 对于非uniform做逻辑分支,GPU需要对两个逻辑分支都进行执行,对结果进行裁剪。uniform做逻辑分支GPU必须flatten the branch
  • 无论何种动态分支,GPU必须为其最坏的情况分配寄存空间。如果其中一个分支比另一个有很多的性能消耗,这可能会有性能浪费。
// condition == true
if (condition) {
    // branch 1
} else {
    // branch 2
}

// 优化代码
branch 1
3.2 Shader Variants & KeyWords

Variants
通常我们直接编写的Shader是Uber Shader,通常包含了多种不同的渲染功能,打包编译时才会产生运行时真正使用的Shader。Uber Shader通常会存在大量的逻辑分支,宏定义的逻辑分支会导致打包时产生Shader Variants (Shader Permutations)。
Shader变体可以理解为Shader多种可能性的坍缩态(Uber Shader有很多逻辑路线,但渲染对象时需要确定的渲染状态,来确定使用哪一个Shader变体),是渲染真正使用的Shader。

  • Shader变体可以在着色器程序中使用运行时条件,而不受逻辑分支影响。
  • 大量静态分支会导致Shader变体剧增,增加编译时间、Shader内存,严重影响游戏性能。
KeyWords
ShaderLab提供了两种方式来声明Shader宏(uniform):multi_compile和shader_feature。multi_compile声明一组宏,所有声明的宏在编译时都会被处理。下方Shader声明了两组宏,会生成2*3=6种变体。



multi_compile与变体数量关系

shader_feature是multi_compile的子集,在收集变体时会根据资源引用关系,只打包使用到的变体。以下图为例,A、B两个宏使用multi_compile会全部编译,Test材质引用了该Shader只标记了宏E,并且C作为该宏组的默认值也会被编译,所以会生成2*2=4种变体。



shader_feature与变体数量关系

运行时Unity没有宏组的概念,可以对任意key进行赋值,并且赋值时也不会影响其它宏。C#提供了Material和Shader两个层面的宏控制方式,Material只会影响单个材质资源,Shader则会影响所有使用Shader的材质。
public void EnableA()
{
    Shader.EnableKeyword("A");
    Shader.DisableKeyword("B");
}

public void EnableB()
{
    Shader.DisableKeyword("A");
    Shader.EnableKeyword("B");
}
还需要注意的是宏定义分为全局(上面的两种方式)和局部的声明(加上_local),全局的宏会影响整个项目,局部只是影响单个Shader。
#pragma multi_compile QUALITY_LOW QUALITY_MED QUALITY_HIGH
#pragma multi_compile_local _ HIT默认情况下,Unity会为Shader每个Stage生成keyword variants。 通常Shader包含顶点着色和片元着色两个阶段,Unity会自动识别并合并相同的变体。若只有特定Stage有宏的需求,这样不会增加构建包体大小,但仍然会影响Shader的编译时间、Shader加载时间以及运行时内存。
为了避免这一问题,可以针对特定Shader Stage进行宏编译,但使用这一功能存在限制:

  • OpenGL和Vulkan:在编译时,Unity自动将所有Stage的关键字指令转换为常规关键字指令。
  • Metal: 任何以vertex stages的关键字也会影响tessellation stages,反之亦然。
_vertex_fragment_hull_domain_geometry_raytracing
顶点着色阶段片元着色阶段~不知道怎么翻译~~不知道怎么翻译~几何阶段光追阶段
#pragma multi_compile_local_vertex _ HIT最后在定义宏时有以下限制:

  • 每组宏的组内和组间互斥
  • dynamic_branch与 shader_feature / multi_compile 同时使用,Unity会默认使用dynamic_branch
  • 2020及以下版本,最多可以声明384(2021以上版本几乎没有限制)个全局shader keywords,Unity内置Shader占用60个左右。
  • 每个Shader局部宏最多为64个
3.4 Shader运行时

通常Unity加载Shader执行以下步骤:

  • 当加载scene或者其它运行时资源, 会将与之相关的Shader变体加载到CPU。CPU解压Shader变体存储在一个独立的区域, 可以通过Other Settings > Shader Variant Loading配置。
  • 当GPU第一次需要使用Shader渲染对象时,通过图形API将Shader与其它渲染数据传到图形驱动。
  • 图形驱动创建GPU特定的Shader变体,并上传到GPU。
当不再有如何对象引用Shader时,Shader会从CPU和GPU内存中移除。选择变体时,会以相似度进行匹配。若希望严格执行变体的严格匹配,可以通过PlayerSettings.strictShaderVariantMatching设置。
上传GPU的过程会造成程序暂停,为了避免这一问题通常会通过Prewarming,提前将Shade变体加载到GPU:

  • 单个Shader预加载:ShaderWarmup.WarmupShader
  • 变体收集器预加载:ShaderVariantCollection.WarmUp
  • 所有变体预加载:Shader.WarmupAllShaders
对于DirectX 12、Metal和Vulkan的Prewarming,需要精准的GPU Shader变体数据才能完成:

  • 使用Experimental.Rendering.ShaderWarmup 需要提供顶点布局以及渲染状态。
  • 使用ShaderVariantCollection.Warmup或者Shader.WarmupAllShaders 会创建不精准的Prewarm GPU表现,因为无法提每个Shader的详细关联数据。
参考


  • Shaders core concepts
  • unwind:跨平台引擎Shader编译流程分析
  • [Unity 活动]-Unity 技术开放日 上海站录播_哔哩哔哩_bilibili

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-16 21:50 , Processed in 0.101018 second(s), 28 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表