Shader Graph自定义节点获取光照踩坑实录

总之终于有阴影了,也很优雅(大概),测试的模型我是从Asset Store下的。

零、最开始的坑

相信不少人看过这份教程:

Custom Lighting in Shader Graph: Expanding your graphs in 2019​blogs.unity3d.com/2019/07/31/custom-lighting-in-shader-graph-expanding-your-graphs-in-2019/#comment-

用Custom Function自定义扩展节点获取灯光。

本文亦为基于此教程的一个扩展与补充,也默认各位看官已经看过操作过这份教程。

但是我实际上经过操作会发现,在Unity 2019.3.0f3(我使用的)和 URP 7.1.8环境下,只能拿到灯光向量,颜色等参数,但是阴影贴图无法获取。

在原帖的底部也有指出这个问题,是因为丢失了_MAIN_LIGHT_SHADOWS,不过如果直接给加上去,那就会报错了。

如图所示:

在原帖下面的回复里面。也有朋友回复表示,直接用PBR节点整,然后连到Emission上面就完事了。

但是这样的做法微妙过于的不优雅。

所以还是从头理顺一下,并且找到一个尽可能优雅的解决方案。

壹、分析修改一波推

首先从报错信息开始

Shader error in Unlit Master: invalid subscript shadowCoord at /TShoot/TShoot_T/Library/PackageCache/[email protected]/Editor/ShaderGraph/Includes/Varyings.hlsl(118) (on d3d11)

先找到URP包的源码,打开Varyings.hlsl代码【请注意,往后的操作我们都在URP的包里面进行,请确定好打开的包的版本没错】

可以看到是Varyings.hlsl(118)调用的shadowCoord字段丢失。

此处的output,是一个Varyings的结构体。

也就是说Varyings结构体里面shadowCoord这个字段并没有被定义。

而Shader Graph的代码生成过程,可以理解成是一个个节点,各自带着一段代码,最后组装而成。也就是说,Unlit Master同样不例外。

由此所以可以推测,要不就是Master有节点模板,类似于Amplify Shader Editor插件一样的模式。要不就是Unlit Master的C#代码里面带上了什么操作。

那么先直接简单粗暴的搜索Varyings看看能得到些什么。

一般来说,因为是结构体的定义,所以以关键字struct Varyings直接搜就可以搜到定义名称为Varyings的结构体定义的地方。但是实际上我们这样做并不能得到什么有意义的东西。

在URP的源码里面,Varyings的出现率就跟Build-in管线里面的appdata和v2f一样普遍。

要是干脆的以Varyings为关键字搜索的话,倒是运气比较好了

我们搜到了看起来就很有意思的东西,而且出现在UniversalPBRSubShader.cs里面

这一段是出现在new Shader Pass里面的操作。给一个名叫requiredVaryings的字段创建了一条List,里面的内容就包含了我们定义所需Shader的Varyings结构体需要的内容。而最后一行也就是我们想要的shaowCoord。

而事实上,回忆一下刚才所说,在原帖的回复里面有人指出在PBR节点下进行灯光获取。

也就是说,这个很可能就是我们想要的东西了。

同样,顺着名字找到UniversalUnlitSubShader.cs文件

然后我们惊恐的发现,他压根就没给new这个List!

总之暴力的从UniversalPBRSubShader把这段复制一下,丢到UniversalUnlitSubShader里面

修改完,保存一下回到Unity

重新点一下Save,我们可以看到,错没报了,然后影子也出来了。

恭喜恭喜!虽然小小的改了一下URP源码,但是结果达成了嘛对不对,虽然还是欠缺一点优雅,但是

又不是不能用.jpg

然后我们愉快的关掉Unity,直到下次再打开...

他又红了!

回去再看看,发现我们改过的地方没了!

这里是我踩到的第一个坑,因为一个意外的操作,我打开的并不是

C:\Users[UserName]\AppData\Local\Unity\cache\packages\http://packages.unity.com\[email protected]

的目录进行修改,而是工程下的Packages目录下的文件进行修改。所以重启之后就被真正的包覆盖掉了。

要想再次启动不出问题,就必须去到这个真正的包目录下修改代码。

但是问题又来了,一次可以这样改,但是要是多人合作呢?要是官方更新了一下URP包,那么岂不是每次/每个人的包都要修改,随着越来越多需求引起的修改,我们甚至会得到一个和官方版本完全不同的分支。

这样做实在缺乏优雅。

贰、没事还是不动URP管线的源码了

前情提要:

所以,要是基于URP操作,还是尽量不去修改URP管线的源码就得到想要的效果就是最好的。

既然这样,我们还是回头继续去分析好了。

因为keyword _MAIN_LIGHT_SHADOWS 的出现,使得产生了对应的编译分支出现。因此Varying.hlsl第118行:

output.shadowCoord = GetShadowCoord(vertexInput);

出现在激活了_MAIN_LIGHT_SHADOWS的分支里面。

然而Varyings里面并没定义shadowCoord字段

而不能动源码的情况下,也就是我们Varying.hlsl也不能动。

那么我们就得避开_MAIN_LIGHT_SHADOWS的出现。那么要是这个keyword不出现,自然这行代码就不会被编译出来。但是同样的,也不会出现阴影。

那么要做的事情也理清了,那就是我们自己越过这个keyword和对应的代码,把产生阴影的真正有用的部分抽出来。

源码分析:

首先我们可以看到

GetMainLight是我们获取的主灯光信息的函数,返回的是一个Light结构体,那么事情就很好办了,直接在URP源码里面以关键字Light GetMainLight搜,于是我们非常完美的只得到了两个结果。

总之先点进来看看:

可以看到,实际上我们已经得到了我们想要的灯光参数,但是丢的是阴影。

而MainLight的阴影可以看到是由MainLightRealtimeShadow获得的。

首先确定Light.shadowAttenuation是half类型(具体可以见源码Lighting.hlsl第45行)

那么我们再以half MainLightRealtimeShadow为关键字搜这个函数,再次完美的只有一个结果:

于是再打开Shadows.hlsl看看

我们在找的keyword出现了,这里是一个宏定义的判断,要是没定义 _MAIN_LIGHT_SHADOWS 或者定义了_RECEIVE_SHADOWS_OFF就会直接返回1.0,不会再编译后面的代码。也就是我们没有阴影的原因。所以我们需要的就是后面的部分。

同理,既然手动获取阴影了,那么灯光数据我们也手动写一起就完事了,打开Lighting.hlsl,找到98行Light GetMainLight()

然后对着需求一路照抄

最后我们得到这个修改版的函数

void MainLight_half(float3 WorldPos, out half3 Direction, out half3 Color, out half DistanceAtten, out half ShadowAtten) { #if SHADERGRAPH_PREVIEW Direction = half3(0.5, 0.5, 0); Color = 1; DistanceAtten = 1; ShadowAtten = 1; #else #if SHADOWS_SCREEN half4 clipPos = TransformWorldToHClip(WorldPos); half4 shadowCoord = ComputeScreenPos(clipPos); ShadowAtten = SampleScreenSpaceShadowmap(shadowCoord); #else half4 shadowCoord = TransformWorldToShadowCoord(WorldPos); ShadowSamplingData shadowSamplingData = GetMainLightShadowSamplingData(); half4 shadowParams = GetMainLightShadowParams(); ShadowAtten = SampleShadowmap(TEXTURE2D_ARGS(_MainLightShadowmapTexture, sampler_MainLightShadowmapTexture), shadowCoord, shadowSamplingData, shadowParams, false); #endif Direction = _MainLightPosition.xyz; Color = _MainLightColor.rgb; DistanceAtten = _MainLightPosition.z; #endif }

保存一下回去Unity看看

记得把刚定义的_MAIN_LIGHT_SHADOWS 的keyword去掉

这次不报错了,阴影也来了。

剩下的该干嘛干嘛。

弎、总归还是不够优雅,但是还是做个简单的整理

比起修改URP的源码,现在的做法总体来说优雅多了。

为了让他用起来更舒服,我们先去看看Lit.shader的源码,看看官方的PBR材质到底对阴影定义了哪些keyword。

直接去找现成的实现,是比我们自己一层层方法展开检查宏定义舒服得多的方法。

打开位于Shader/Lit.shader文件

然后往下看,看到第一个Pass,其Name "ForwardLit"

由此可知,这个就是URP管线的标准PBR的PASS

再101行开始就是阴影相关的定义。

_MAIN_LIGHT_SHADOWS因为shadowcoord的报错原因,所以我们不能要。而现在我们处理的是MainLight的数据,所以可以无视ADDITIONAL_LIGHT相关的keyword

那么需要的就是

_MAIN_LIGHT_SHADOWS_CASCADE

_SHADOWS_SOFT

然后为了方便使用,所以我们直接把这个Custom Function节点打包成一个Sub Graph方便以后使用。

最后我们得到了这样的一个节点。

肆、终于真的可以用了,可喜可贺。稍微总结一下吧。

到这一步为止,我们终于得到了一个可以有效利用的MainLight节点,也无需单独定义一个cs脚本进行节点扩展。

最大的好处莫过于轻便快捷。不过因为没了_MAIN_LIGHT_SHADOWS 这个Keyword的存在,所以实际最后编译的时候是没了这部分的编译分支。仅限于当前的灯光阴影的获取需求下看起来并非什么大碍。

但是要是其他更多的复杂需求下,这种越过某些keyword进行代码编写,可能会引起失去编译变体,导致这部分带来的优化不存在的情况。比如在本案例的环境下,如果本身并不需要获取阴影_MAIN_LIGHT_SHADOWS的keyword被关闭的情况下,获取阴影是直接返回了一个1.0的确定值,而修改后这部分的优化便失去了,不管实际上有没使用阴影,都会执行全部的采样阴影纹理的操作。

其次是Unlit的Varying.hlsl的问题,既然这里面调用了shadowcoord这个字段,却没有在Unlit Master节点里面对requiredVaryings进行操作,添加Varyings的shadowcroord字段。这个应该可以认为是管线的BUG吧?

还是因为默认Unlit并不会接受投影,所以实际上custom function获取投影这个操作本身就是破坏了设计的行为?