[译] URP shader coding教程_1 - Unlit Soft Shadow

大家好,我是shader artist Madumpa。我猜大家点开这篇文章,大概是因为大伙想更好地使用shader graph,但在实际操作过程中,却遇到了一些奇怪的问题和阻碍,所以我写了这篇文章。

其实我刚开始对shader graph的期待非常高...(相比Amplify,我还在用shader graph...)

为了整理诸如Header, Space等的shader属性,因不能用代码处理导致的操作不便;不能在Alpha blend模式下使用Enum导致的不方便;在制作轮廓线时,得用multi-material或render feature override导致的不便;(Override有个致命的缺点,即无法单独设置)在ShadowCaster pass, DepthOnly pass中,会生成很多多余的代码等等

使用起来麻烦的地方还挺多的,比较难用。

但我本着这是官方推出的资源,以后的发展会更好的期待,期间用生成代码 & 修改等繁琐的办法,也都忍过来了,在操作过程中,我发现,随着遍历本地空间(local space)中各个空间,计算矩阵得到的值跟写代码得出的值不一样。(其实,在本地空间里,除开本地->clipspace矩阵外,计算复杂的矩阵没那么多)

当遇到这种问题时,我应该要用节点方式生成代码来解决...但因为自动生成的代码的可读性非常差,我读这些代码的时候,脑袋都会想这到底是什么鬼。最后我还是放弃了shader graph,决定用HLSL写shader。

虽然我不太清楚以后shader graph会怎么发展,但我觉得这篇文章的内容能帮到目前急需使用URP的同学。如果大家有用代码写过shader的话...从管理维护层面来说,我们能受益的地方还是很多的。

在现有的延迟管线中,大家用过片元着色器(fragmentshader)的话,会非常不满意;大家用surface方式写过shader的话,会发现有些概念理解起来有点难度。如果从最基础的概念开始解释,那我们要讨论的就太多了,所以下文我会假设大家之前都写过片元着色器。

下文参考了Sangyun的blog。

URP Default Unlit Based to Custom Lighting​illu.tistory.com/1407?fbclid=IwAR01E3o_ChG35IOS7FADDbKRKi8P6T1viUaSgV4AVdksTR0UvFP0Fdq0aHc

那正切主题,先用Unity URP创建一个项目。

如上图所示,我们在Packages中添加Core RP Library和Universal RP文件夹。如果没有添加这两个文件夹的话,希望大家能在packagemanage中添加Universal RP。现在,我们来做一个Unlit shader。

通过这种方法,大家熟悉的片元着色器代码就出来了。红框内的代码包含了UnityCG.cginc文件,URP中依旧支持CG方式(Unity延迟方式)。其实CG代码写的Unlit shader在URP中能运行,但这种方式实现的shader无法正常发挥URP的优势。我会把基本Unlit shader变成URP HLSL shader。

TAG

首先,我在Tags中添加了“RenderPipeline” =“UniversalPipeline“。排序(Queue)默认应该是2000(Geometry),排序代码是否添加随你便。当用法线来计算光照时,我们要换成ForwardBase,但现在可以先不管。

INCLUDE & multi compile

大家要记得把CGPROGRAM这块更改为HLSLPROGRAM,ENDCG也要换成ENDHLSL。然后,删除包含UnityCG的内容,并添加位于UniversalRP的ShaderLibrary下面的 Lighting.hlsl(因为我想试下简单的光照)。因为Shadows.hlsl包含在Lighting.hlsl中,所以我们也无需额外添加。

为了实现接受阴影(receive shadow)功能,大家要记得要添加_MAIN_LIGHT_SHADOW,这点我们稍后再说。

Lighting.hlsl的位置如上图所示。在Lighting.hlsl文件中,包含了自定义光照的重要内容。

因为这样一来,我们便能得到主光的方向、颜色、实时阴影和按距离衰减等信息。大家仔细看代码的话,会发现两个版本overloading,根据有无shadowCoord,大家可以确定是否需要添加实时阴影。我运行了下MainLightRealtimeShadow函数,这个函数在Shadows.hlsl文件中。

这部分内容,取决于你是用屏幕空间阴影还是主光阴影,计算方式会不同。我们之后再详说阴影这块,现在先看下Unlit.shader。

VARIABLE (CBUFFER & SAMPLER)

为了在代码中也能使用属性声明的内容(上图中的声明),我们第一次看到了CBUFFER。CBUFFER是常量缓冲(Constant Buffer),当进行批处理时,进入CBuffer的数据从buffer中读取数据,通过调控变量产生变化,同时使用相同的RenderState来实现一次性绘制。大家理解这个概念就行,写代码的时候,这部分声明内容放在START和END里。CBUFFER和SRP Batcher关联紧密,我也会在下篇文章中讲SRP Batcher。

接下来,我把sampler2D采样器和纹理对象一起声明的内容,分成了TEXTURE2D纹理对象和SAMPLER两部分(如上右图所示)。这部分是由于HLSL的特点,把便捷的CG方式换成HLSL,稍微有点麻烦。采样器(Sampler)用于设置纹理的wrapMode(Clamp/Refeat)或Filter设置(Linear/Point/Trilinear)等,但unity会代替采样器执行这两项操作,所以设置意义不是很大。

VERTEX INPUT & OUTPUT

本来在Unlit默认代码中没有normal和shadowCoord,但因为需要实现简单的光照,所以我另外添加了这两项以做对比。大家看代码就知道,顶点的输入 / 输出结构体和原有的差不多。因为我们不用CG了,所以之前用CG方式声明的函数,我们会用TEXCOORD1,TEXCOORD2替代shadow或fog,逻辑都是一样的。

然后我添加了放实例ID的代码。所谓实例ID是指,在使用实例(一次调用即可绘制多个对象的功能)时,输入索引号的部分。因为它是一个宏,我们不需要深入了解它的结构,直接使用宏就好。UNITY_VERTEX_OUTPUT_STEREO是只有在VR激活状态下才能使用的宏。在VR中有左眼和右眼,即,由于有两个相机,我们要给每个相机都插入正确的Clipspace值(主要是位置信息),所以需要这个宏。

VERTEX FUNCTION

接下来是关于顶点函数。使用实例时,我们要为实例ID添加一个函数,如果不用实例,这部分内容可以都删掉。VR部分的代码也一样。跳过这些,我们会发现一些不同点。

TRASNFORM MATRIX

首先,大家会发现空间变换的矩阵命名变了。如下,

UnityObjectToClipPos -> TransformObjectToHClip

UnityObjectToWorldNormal -> TransformObjectToWorldNormal

以Unity打头的矩阵名称变成了Transform。这部分

可在这里确认。

这是CoreRP中 ShaderLibrary的 SpaceTransform.hlsl。 记得不要和UniversalRP位置混淆哦。

在这里面可以确认。

我们可以看到UNITY_MATRIX_XX是这样warpping的,无需修改,沿用就行。 现在我来删除下以Unity开头的矩阵名称。

FOG

利用ComputeForFactor函数可求得Fog,

这部分的内容在Core.hlsl里面。

利用clipspace的near/far,按0到1对z位置进行排序,通过这个值和fog start, fog end值来计算雾的厚度。

unity_FogParams.z里有-1/(end-start)值,unity_ForParams.w里有end/(end-start)值。通过z这些,合适的fog值会被记录在fogCoord中。之后我会在片元中重新计算MixFog函数。

SHADOW COORD

Shadow和Fog差不多,取决于你在Shadow Coord中的数值设置,这点很重要。

要想求出ShadowCoord,我们要先加一个VertexInput结构体进来。VertexPositionInput是一个包含了WS(世界空间),VS(视口空间),CS(剔除空间),NDC(NDC空间)等位置信息的结构体(如下所示)。

我们可以用GetShadowCoord()函数求出shadowCoord,再来看下代码。这部分对应Shadow.hlsl。

Unity URP中的阴影有两种定义,一种是Screenspaceshadow,另一种是Mainlightshadow。我刚开始以为Screenspaceshadow是使用depth贴图的一般方法,但分析代码后,发现它是指Mainlightshadow,Screenspaceshadow是指contact shadow(接触阴影)。因为我不会用到Screenspaceshadow,所以这部分跳过,先看下TransformWorldToShadowCoord。

看代码大家可以知道,世界空间的位置(positionWS)是基于主光来变换空间的,针对阴影使用了Coord。对于Cascadeshadow,我添加了求cascade的代码。我会在片元着色器中进一步调整阴影。

Fragment Function

终于讲到片元函数这块了。计算在前文提到的在Lighting.hlsl中,GetMainLight的第二个版本 i.shadowCoord包含的函数,来求得 shadowAttenuation。这个值乘以NdotL的话,就能得到阴影效果。目前我们只有一个平行光(directional light ),所以distanceAttenuation不适用。(distanceAtten适用于点光和聚光灯)继续看代码的话,我们会发现纹理采样的方式变了,之前我们是通过 tex2D(_MainTex, i.uv)方式来实现采样的,就算没有采样器也能采样,但,

之后大家使用这个函数比较好,希望大家可以慢慢训练自己不会CG方式写代码呢。我们可以用Lighting.hlsl的SampleSH求得ambient。为了避免文章太长,球谐函数这块以后找机会再说。

到了这一步,按理说大家应该能得到下图的效果。

硬阴影效果如上图

软阴影效果如上图

SHADER GRAPH CUSTOM NODE

在代码中,

如上图所示,只需要多重编译(Multi-compile),就能同时得到硬/软阴影效果,但Shader graph 自定义节点无法进行这样的多重编译。所以,如果你用的是Shader graph的话,要把预处理的内容全部删掉,再加入阴影。

关于Shader graph custom node这部分的内容,请查阅Unity的官方文档。

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

如果在Shader graph中,最终节点是PBR master的话,它应该能很好地接收shadowAttenuation,但unlit master不能接收shadowAttenuation(因为是Unlit不受光照影响,所以理所当然没有光照和阴影),但我们可以强制它接收阴影衰减,在MainLightRealtimeShadow中添加计算阴影的代码就行(如下图所示)。

嗯,现在有阴影了,但是即使我把阴影设成了软阴影,但出来的还是硬阴影。

我用Shader Graph的自定义节点试了下,也得到了一样的结果。大家可以看下SampleShadowmap函数,就能找到解决这个问题的头绪。

看代码我们会发现,单独运行SampleShadowmapFiltered函数时,就能得到软阴影。

这样做虽然能解决问题,但我们没办法在shader graph中这么操作,所以得用SampleShadowmapFiltered函数来实现。

这样做的话,

就可以强制?得到软阴影了。

我也试了下Shader graph。

我保留了shader graph自定义节点代码。完成多重编译后,通过 #if _SHADOW_SOFT进行分支设置,我先在这里添加了一个Soft项目。这部分稍有点可惜,之后我会再想办法来解决这个问题。因为有这些操作上的不便,所以我想用代码来试下。

#if](?blogId=mnpshino&encodedTagName=if) SHADERGRAPH_PREVIEW ​ •Direction = half3(0.7,0.5,0); ​ •Color = 1; ​ •DistanceAtten = 1; ​ •ShadowAtten = 1; ​ •ShadowAttenSoft = 1; ​ [#else](?blogId=mnpshino&encodedTagName=else) ​ [#if](?blogId=mnpshino&encodedTagName=if) SHADOWS_SCREEN ​ •half4 clipPos = TransformWorldToHClip(WorldPos); ​ •half4 shadowCoord = ComputeScreenPos(clipPos); ​ [#else](?blogId=mnpshino&encodedTagName=else) ​ •half4 shadowCoord = TransformWorldToShadowCoord(WorldPos); ​ [#endif](?blogId=mnpshino&encodedTagName=endif) ​ •Light mainLight = GetMainLight(shadowCoord); ​ •Direction = mainLight.direction; ​ •Color = mainLight.color; ​ •DistanceAtten = mainLight.distanceAttenuation; ​ •ShadowSamplingData shadowSamplingData = GetMainLightShadowSamplingData(); ​ •half4 shadowParams = GetMainLightShadowParams(); ​ •float shadowAttenSoft = SampleShadowmapFiltered(TEXTURE2D_SHADOW_ARGS(_MainLightShadowmapTexture, sampler_MainLightShadowmapTexture), shadowCoord, shadowSamplingData); ​ •float shadowAttenHard = SampleShadowmap(TEXTURE2D_ARGS(_MainLightShadowmapTexture, sampler_MainLightShadowmapTexture), shadowCoord, shadowSamplingData, shadowParams, false); ​ •ShadowAtten = shadowAttenHard; ​ •ShadowAttenSoft = shadowAttenSoft; ​ [#endif

https://blog.naver.com/PostListByTagName.nhn?blogId=mnpshino&encodedTagName=endif)

ShadowCaster Pass & DepthOnly Pass

片元处理的部分结束了,但还没完。因为还要处理ShadowCaster Pass和DepthOnly Pass。大家看base path这块就蛮累的吧TT,我这里会用个简单、快捷的方法处理。

打开Lit shader,

如上图所示,红框中include的ShadowCasterPass.hlsl直接整个拿来用就好。ShadowPassVertex, ShadowPassFragment等就都实现了。如果阴影上不用顶点动画的话,我们直接拿来用也挺好的。在属性中给Cull [_Cull] 这块添加Enum或者换成Cull Back。

看下depth pass这部分代码,我们可以删掉Smoothness纹理。因为我们只有在需要烘焙lightmap时才会用到Meta pass,所以这块先跳过。

谢谢大家花宝贵的时间来读我的文章,希望下次可以带来更好的分享。

[原文出处]

URP 셰이더 코딩 튜토리얼 : 제 1편 - Unlit Soft Shadow​blog.naver.com/mnpshino/9

| 作者 Madumpa

本文仅限于学习参考交流,请勿做商业用途和随意转载。

译 Qinfei

2020/5/7