Shader Graph踩坑实录

  欢迎参与讨论,转载请注明出处。

前言

  Demo也到了做渲染的时候了,经过一番鏖战后,算是大体完成了。但随着后续需求的到来,发现这套纯代码的Shader方案对于扩展、复用等方面有着诸多不便。于是便打起了Shader Graph的主意……经过一番纠缠,于是有了本篇踩坑实录。另附源码地址,但本篇并不会对其做讲解。基于Unity2020.1.0a20,渲染管线为URP7.18。

优劣

  首先要明确的是:Shader Graph不支持Builtin渲染管线,且尚未处于彻底成熟的阶段,哪怕是最新版本尚有不少缺陷。但瑕不掩瑜,且说其优劣:

  • 优点
    • 由于节点化与Sub Graph的存在,Shader的组装将变得相当容易,极大提升了模块化水平。
    • 调整Property与Keyword变得相当方便,纯代码下将需要多些工序。
    • 内置各种节点,降低了美术参与创作的门槛。
    • 能够实时预览每个节点造成的变化预览,虽然对我而言没啥用。
  • 缺点
    • 编辑器尚不够稳定,经常会出现整个程序崩溃的情况。
    • 生成的代码不够优化,比较暴力,存在各种分支判断、重复函数(也许最终会优化?)
    • 由于Slot机制的原因,可能会出现很多运算集中在片元着色器中。
    • 对于创建Pass并不友好,需要修改源码。

来龙

  其实一般的浅度Shader Graph使用者并不会如我这般踩这么多坑:直接使用内置的Graph模板进行创作即可。不幸的是,如前文所言:对于创建Pass并不友好,需要修改源码。于是便开始了踩坑之旅……
  在默认情况下,Unity的Package将保存在工程的Library/PackageCache目录下,这样子是不能直接修改源码的(Library目录下的东西属于可再生物,随时会被覆盖),需将之搬迁至工程的Packages目录下。
  对于考虑到日后Shader Graph的版本升级情况,所以尽可能的不要修改原工程的内容,而是尽量新建文件。但考虑到Shader Graph源码下存在不少inner元素,直接在外面写自己的内容也并非彻底可行。只能直接在Shader Graph包下进行添加文件的方式了,这也是要将之移至Packages目录的原因。
  我要做的事情相当明确:新建一个自定义的Graph类型。在URP下已经自带Unlit与PBR两种类型了,于是本人便基于Unlit Graph并结合先前实现的Shader的特性进行新类型的创作。

去脉

  我们能接触到Unlit Graph创建的起点便是Project区下右键菜单的Create->Shader->Unlit Graph了,直接在Shader Graph源码包下全局搜索Create/Shader/Unlit Graph即可找到:
0
  照葫芦画瓢在同目录下弄个新文件实现相同功能即可,这下我们便知道Unlit Graph的正主了:UnlitMasterNode。经过研究发现,它决定了在编辑器下Unlit Master Node的样式:
1
2
  但这只是个壳子罢了,根据代码中的IUnlitSubShader为引,找到了其核心:
3
  这个UniversalUnlitSubShader的作用相当简单:根据编辑器的设置生成Shader代码。如上图便可看出定义Pass数据结构的行为,这也是诱使我来改源码的直接原因。在里面你将见到形如这般的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var unlitMasterNode = masterNode as UnlitMasterNode;
var subShader = new ShaderGenerator();
subShader.AddShaderChunk("SubShader", true);
subShader.AddShaderChunk("{", true);
subShader.Indent();
{
var surfaceTags = ShaderGenerator.BuildMaterialTags(unlitMasterNode.surfaceType);
var tagsBuilder = new ShaderStringBuilder(0);
surfaceTags.GetTags(tagsBuilder, "UniversalPipeline");
subShader.AddShaderChunk(tagsBuilder.ToString());
GenerateShaderPass(unlitMasterNode, m_UnlitPass, mode, subShader, sourceAssetDependencyPaths);
GenerateShaderPass(unlitMasterNode, m_ShadowCasterPass, mode, subShader, sourceAssetDependencyPaths);
GenerateShaderPass(unlitMasterNode, m_DepthOnlyPass, mode, subShader, sourceAssetDependencyPaths);
}
subShader.Deindent();
subShader.AddShaderChunk("}", true);

  如此情况便变得相当清晰了,只要清楚你想生成怎样的Shader代码,在摸熟了生成代码的API,便可自由地进行创作了。通过右键节点可以随时查看生成的代码情况:
4

注意事项

  也许看似还算简单,但其中坑点还是有不少的:

  • 不要尝试采用继承的形式去新建新类型,其本身代码就没打算让你这么做,必然会碰壁,除非改源码(如此便违反原则了)
  • 做出了修改后,要到对应的Graph文件进行Save操作触发检测。
  • 也许是检测的原因,有时候HLSL代码做出了修改后不会被识别到,需要换下行。
  • Shader Graph生成的着色器参数有着自己的一套处理方式,务必参考自带的代码。
  • Unlit Graph的主Pass并没有LightMode,想做背面Pass的时候要注意下。
  • Unlit Graph将渲染模式、混合模式、剔除做成节点设置并不是一个好选择(无法让材质修改),推荐按照URP的方式做成材质属性。5
  • Shader Graph对于生成的Shader代码存在分支数限制,需要到Preference->Shader Graph进行修改上限。值得一提的是,全局Keyword与局部Keyword似乎是分别对待的。
  • Shader Graph并不存在完整的环境,它是无法识别到一些渲染管线里的函数的。所以在编写Custom Shader的时候需要加上#if SHADERGRAPH_PREVIEW分支判定以处理在编辑模式下的情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void MainLight_float(float3 WorldPos, out float3 Direction, out float3 Color, out float DistanceAtten, out float ShadowAtten)
{
#if SHADERGRAPH_PREVIEW
Direction = float3(0.5, 0.5, 0);
Color = 1;
DistanceAtten = 1;
ShadowAtten = 1;
#else
#if SHADOWS_SCREEN
float4 clipPos = TransformWorldToHClip(WorldPos);
float4 shadowCoord = ComputeScreenPos(clipPos);
#else
float4 shadowCoord = TransformWorldToShadowCoord(WorldPos);
#endif
Light mainLight = GetMainLight(shadowCoord);
Direction = mainLight.direction;
Color = mainLight.color;
DistanceAtten = mainLight.distanceAttenuation;
ShadowAtten = mainLight.shadowAttenuation;
#endif
}

后记

  现在感觉游戏开发的未来方向就是连连看了,从这点来说UE4的确算是时代前沿。在编辑器里加入逻辑控制元素,让更多人能加入创作,尽可能地解放生产力,的确是游戏开发所需要的。