大家好,我是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 Lightingillu.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 Blogblogs.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; [#endifhttps://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 Shadowblog.naver.com/mnpshino/9| 作者 Madumpa
本文仅限于学习参考交流,请勿做商业用途和随意转载。
译 Qinfei
2020/5/7