Shadow Bias Comparison Cover

在URP的片元着色器中应用阴影偏移

Shadow Bias 有关Shadow Bias的介绍我只推荐看这篇知乎文章自适应Shadow Bias算法,里面介绍了有关Shadow Bias的几乎所有需要了解的信息。同时这篇文章也指出了Unity目前正在使用的Shadow Caster Vertex Based Bias方法的不足之处。 如果是平时使用URP的话,Unity使用的在绘制Shadow Caster Pass时将ShadowBias添加到顶点的偏移上的方法,虽然称不上尽善尽美,但是也完全够用了。但是在二次元角色渲染的时候,为了营造丝袜勒肉的效果,会在腿部的模型和丝袜的模型交接的区域做一个向内凹陷的效果。这个区域两侧的模型是正常闭合的,其法线是相对的,此时如果使用Unity默认的使用ShadowBias去调整顶点位置的方式,ShadowBias中的NormalBias就会导致这个区域两侧的模型朝着法线的反方向偏移,导致这个区域的阴影在某些角度时会出现漏洞,可以在文章封面图的左边看到明显的瑕疵,右边则是在顶点着色器中使用了ShadowBias,看上去的效果就比较正常了,而且角色的阴影和角色的模型的大小也基本保持一致。当然了,把NormalBias设置成0就不会有这个问题了,但是失去了NormalBias则会带来其他角度的阴影的瑕疵。 本文使用的是Unity2022.3.43f1c1,URP版本是14.0.11。 关于Shadows.hlsl的碎碎念 我讨厌Unity URP Package里的Shadows.hlsl,因为它使用了一个LerpWhiteTo的方法,这个方法定义在CoreRP的CommonMaterial.hlsl里,而不知道为什么Shadows.hlsl并没有包含这个CommonMaterial,这就导致了我只想单独使用Shadows.hlsl时,必须还得手动包含一遍CommonMaterial.hlsl,我觉得这很不合理。 更要命的是,如果在hlsl中想要使用URP的Light结构体,就起码得包含RealtimeLights.hlsl,这个hlsl又包含了Shadows.hlsl。索性我就直接从这些文件中摘取了一套自己的uber_lights.hlsl和uber_shadows.hlsl,这也方便后续做修改。 对于URP的修改 虽然我的想法是尽量做到即插即用,也就是说尽量不去修改URP默认的代码,但是很遗憾为了将ShadowBias移动到片元着色器里,还是得稍作修改。 URP设置ShadowBias是在绘制级联阴影的每一个slice的时候,将其作为参数传给顶点着色器的,也就是说虽然顶点着色器中的参数只有_ShadowBias一个,实际上Unity会使用级联阴影的层级数个ShadowBias,我们需要将其保存起来,在渲染LitForwardPass的时候,根据当前像素的世界坐标找到对应的级联阴影的层级,再查找对应的ShadowBias进行计算。 MainLightShadowCasterPass.cs 主要是在MainLightShadowCasterPass.cs的RenderMainLightCascadeShadowmap方法中,计算得到了当前层级的ShadowBias之后,将其保存在一个数组中留待后续使用。 for (int cascadeIndex = 0; cascadeIndex < m_ShadowCasterCascadesCount; ++cascadeIndex) { settings.splitData = m_CascadeSlices[cascadeIndex].splitData; Vector4 shadowBias = ShadowUtils.GetShadowBias(ref shadowLight, shadowLightIndex, ref renderingData.shadowData, m_CascadeSlices[cascadeIndex].projectionMatrix, m_CascadeSlices[cascadeIndex].resolution); m_MainLightShadowBiases[cascadeIndex] = shadowBias; // Shadow Biases for fragment shader. ShadowUtils.SetupShadowCasterConstantBuffer(cmd, ref shadowLight, shadowBias); CoreUtils.SetKeyword(cmd, ShaderKeywordStrings.CastingPunctualLightShadow, false); ShadowUtils.RenderShadowSlice(cmd, ref context, ref m_CascadeSlices[cascadeIndex], ref settings, m_CascadeSlices[cascadeIndex]....

October 5, 2024 · zznewclear13
Screen Space Contact Shadow Cover

使用Compute Shader计算屏幕空间接触阴影

屏幕空间接触阴影 屏幕空间接触阴影是用来解决普通的阴影贴图精度不够的问题而提出来的一种通过深度图在屏幕空间计算阴影的方法。索尼的Bend Studio的Graham Aldridge在Sigraph 2023的索尼创作者大会上,介绍了往日不再(Days Gone)中计算屏幕空间接触阴影的方式,这里可以找到演示文稿和参考代码。 本篇文章相当于是Radial Dispatch系列的第三篇文章了,与上一篇文章一样,这篇文章是基于径向分派Compute Shader中相关算法的实际应用,具体的缓存方式也可以参考上一篇文章使用Group Shared Memory加速径向模糊,这里就不再赘述了。实际上我发现了这样计算接触阴影的一个缺陷,就是不太好计算软阴影了,由于缓存的限制,随机采样只能在一个很小的范围内分布,基本上用不上了。由于使用的是屏幕空间的深度图的信息,加上厚度检测之后很容易出现漏面的问题,封面中的瑕疵也有一部分是来自于我的Relaxed Cone Step Mapping本身深度值的瑕疵,屏幕上半部分的阴影就好很多。这就当作是一个Proof of Concept吧,之后有机会的话再回来优化优化。 本文使用的是Unity 2022.3.21f1,URP版本是14.0.10。 具体的代码 ContactShadowComputeShader.compute 核心的代码来自于前一篇文章径向分派Compute Shader。前一篇文章在循环中是通过统一步长进行采样的,会采样到四个像素中间因此需要双线性插值,这次我们固定水平或者竖直方向的步长为一个像素,这样我们只需要在一个方向上进行线性插值了。由于深度和颜色信息是两种不同的信息,我们仅对距离很近的深度进行线性插值,对于距离较远的两个深度值,我们使用离采样点最近像素的深度值。至于如何判断深度远近,我使用了和屏幕空间反射中相同的_ThicknessParams,默认物体的厚度为linearSampleDepth * _Thickness.y + _Thickness.x。 #pragma kernel ContactShadowPoint #pragma kernel ClearMain // #pragma warning(disable: 3556) #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #define THREAD_COUNT 128 Texture2D<float4> _ColorTex; Texture2D<float> _CameraDepthTexture; RWTexture2D<float4> _RW_TargetTex; float3 _LightPosWS; float4 _LightPosCS; float2 _LightPosSS; float2 _ThicknessParams; float4 _TextureSize; float _LightRange; float _Debug; struct DispatchParams { int2 offset; int count; int stride; int xMajor; }; StructuredBuffer<DispatchParams> _DispatchData; int GetDispatchType(int index, out int dispatchIndex, out DispatchParams dispatchParams) { for (int i=0; i<8; ++i) { dispatchParams = _DispatchData[i]; dispatchIndex = dispatchParams....

April 20, 2024 · zznewclear13
Radial Blur Cover

使用Group Shared Memory加速径向模糊

径向模糊 在写了上一篇文章径向分派Compute Shader之后,很自然的就需要用径向模糊来检验我们的算法。 径向模糊的效果在网上可以说是一找一大堆了,百分之九十五的教程,会使用一个很大的循环重复采样纹理,百分之四点九的教程,会使用多pass的方法减少采样的次数(当然这些数字是我瞎编的,我还留了百分之零点一,以防真的有人也用Compute Shader做了径向模糊)。这篇文章就来探讨如何使用Compute Shader中的Group Shared Memory来加速径向模糊的计算。我们的目标是在一个Compute Shader中实现超大模糊距离(甚至能超过128像素)的径向模糊。 缓存颜色信息 照例我们要再看一遍索尼的Bend Studio的Graham Aldridge在Sigraph 2023的索尼创作者大会上,介绍的往日不再(Days Gone)中计算屏幕空间接触阴影的方式,这里可以找到演示文稿和参考代码。演示文稿的第24-25页介绍了如何在Group Shared Memory中缓存深度信息,当然在径向模糊中我们缓存的是颜色信息了。 下图是缓存颜色信息的示意图,绿色方块对应的像素是我们整个Thread Group的起点,红色方块是我们当前计算的像素,黑色和灰色的线代表了该像素到径向模糊中心的方向。我们的Thread Group Size是12,深蓝色(包括绿色和红色)的像素同属于一个Thread Group,我们需要计算这些像素径向模糊后的颜色。深蓝色、浅蓝色、灰色和浅灰色、镂空的灰色和浅灰色,代表了我们需要缓存的颜色,因为我们需要对缓存的颜色进行双线性插值,我们除了需要缓存射线经过的最近的像素(即蓝色区域)外,还需要缓存射线经过的第二近的像素(即灰色区域)和射线经过的第三近的像素(即镂空灰色区域)。也就是说当Thread Group Size为12时,我们需要缓存6 * 12个像素,亦即每个像素六次采样。 仅缓存第二近的像素在大部分情况下能够得到正确的双线性插值,但是注意看红色方块向右的第二次采样,仅对蓝色和实心灰色区域的颜色进行插值是不能得到正确的颜色的,因此我们需要额外缓存镂空灰色区域的颜色,亦即第三近的像素。 Group Shared Memory分配 如上所述,我们的Group Shared Memory的大小是THREAD_COUNT * 6(懒得把THREAD_COUNT统一改成THREAD_GROUP_SIZE了),其中包含了上述的深蓝色、浅蓝色、灰色、浅灰色、镂空灰色、镂空浅灰色区域对应像素的颜色信息。数组的长度是THREAD_COUNT * 6,在我们缓存时,我们会将其视为6行THREAD_COUNT列的表来储存颜色信息,而在我们读取时,我们会将其视为3行THREAD_COUNT*2列的表来读取数据。 groupshared float3 cachedColor[THREAD_COUNT * 6]; void SetCachedColor(float3 color, int2 threadPos) {cachedColor[threadPos.x+threadPos.y*THREAD_COUNT]=color;} float3 GetCachedColor(int2 threadPos) {return cachedColor[threadPos.x+threadPos.y*THREAD_COUNT*2];} 具体的代码 RadialDispatchComputeShader.compute 核心的代码来自于上一篇文章径向分派Compute Shader。循环读取颜色值的方式有很多,可以严格按照格点来读取,这样只需要对y方向做线性插值,也可以按照统一的步长来读取,这样需要对xy方向都做线性插值,我这里使用的是统一的步长。每一组缓存的起始像素是最接近于射线的像素的下面一个像素,读取缓存使用的偏移是用的当前采样点下第一个像素的坐标和当前缓存列最下面像素的坐标相减,这里面比较绕不太好描述。。。 #pragma kernel RadialBlurMain // #pragma warning(disable: 3556) #define THREAD_COUNT 128 Texture2D<float4> _ColorTex; RWTexture2D<float4> _RW_TargetTex; float2 _CenterPosSS; float4 _TextureSize; float _Intensity; struct DispatchParams { int2 offset; int count; int stride; int xMajor; }; StructuredBuffer<DispatchParams> _DispatchData; int GetDispatchType(int index, out int dispatchIndex, out DispatchParams dispatchParams) { for (int i=0; i<8; ++i) { dispatchParams = _DispatchData[i]; dispatchIndex = dispatchParams....

March 31, 2024 · zznewclear13
Radially Dispatched Cover

径向分派Compute Shader

动机 最直接的动机是我最近需要实现屏幕空间接触阴影了。索尼的Bend Studio的Graham Aldridge在Sigraph 2023的索尼创作者大会上,介绍了往日不再(Days Gone)中计算屏幕空间接触阴影的方式,这里可以找到演示文稿和参考代码。演示文稿的第24-27页,展示了一种新颖的分派Compute Shader的方法,传统的分派Compute Shader往往是将画面水平和竖直切分成像素数量为64倍数的小块,将分派的Compute Shader对应到这些小块上,而Days Gone中则是将分派的Compute Shader对应到呈放射状的像素小块上。大致的意思可以看下图,下图中相同颜色的相邻像素属于同一个thread group,左边是传统的分派方式,右边则是径向的分派方式。 当进行径向模糊或是计算接触阴影时,往往需要沿着某个方向连续采样纹理。对于多次采样,我们一般会想到使用Compute Shader中的Group Shared Memory进行缓存从而减少采样次数。但是对特定方向进行缓存的话,会要缓存O((N+C)^2)个颜色,如果分派的Thread Group Size或是步进的次数比较大,很容易就超出了Group Shared Memory的最大限制。如果我们使用径向分派的方式,将每一个Thread Group对应的像素沿着采样的方向排列,算上线性插值也只需要缓存(N+C)*2个颜色,这样就能很方便地进行较远的步进了。 相较于索尼的演示,本文解决了Thread Group对应的像素重叠的问题,也尽量地介绍了设置分派参数时的各种条件判断。本文使用的是Unity 2022.3.21f1,URP版本是14.0.10。 如何进行径向分派 分派方式和原因 首先我们注意到对于屏幕中所有指向中心的射线,可以将其分为左下、左上、右下、右上四种,这四种射线最明显的是符号相反,因此在我们分派的时候可以分成四组数据,每一组数据使用同样的方式找到对应的偏移值,再乘上符号和中心的坐标相加,就能得到对应的像素坐标。 因此我们只需要考虑一种情况,我们以右上角为例。下图是一个径向分派的示意图,绿色是我们的中心点,所有的Thread Group都会以绿点为中心放射状排布,黑框就是屏幕上中心点右上角对应的区域(为了简便这里选取了比较小的18x10像素),这里每四个相邻白色方框同属于一个Thread Group(更多的Thread Group我没有画出来),蓝色的区域是每一个Thread Group的起点,这里可以看到深蓝和浅蓝两种颜色,它们对应了两种分派的规律,一种是呈正方形的,另一种则是呈矩形的,灰色的区域是所有计算而得的每一个Thread对应的像素,为了让灰色的区域覆盖整个黑框的区域,我们需要做比当前像素更多的分派。 直接计算每一个Thread对应的像素似乎有点困难,我们可以将分派分成两个维度,用第一个维度计算Thread Group的起点,即上图的蓝色区域,用第二个维度和Thread Group的起点,计算对应的像素的位置。因此我们分派的数据也就变成了一个GroupID和GroupIndex了。注意到浅蓝色的区域的位置决定于黑框的长宽比,当黑框的高大于长时,浅蓝色的区域会在深蓝色的上方且横向排布。我们可以做一个xMajor的判断,如果不是xMajor,我们就调换xy分量,全部计算完毕之后再换回来。 根据图上的深蓝色和浅蓝色区域,我们会将两个区域分开来计算GroupID。比较简单的是浅蓝色的区域,从数学上我们需要传入每一列的列高,计算出GroupID的列序号和在一列中的序号,就能得到起点的坐标了。深蓝色的区域,如果单纯对每一圈求和的话,这是一个二次方程,虽然也能计算但效率肯定不会很高。我们可以考虑高斯求和的方法,将第一圈的竖向的像素和最后一圈的横向像素合并成一列(也就是图上深蓝色方框左上角图案相同的为同一列),这样得到的每一列的列高都是相同的,就能使用浅蓝色区域的方式计算序号了,之后我们再对比较序号的大小来决定是竖向的像素还是横向的像素。 得到了Thread Group的起点坐标之后,我们只需要使用起点坐标到中心的向量,对X方向或Y方向以1为单位步进,再对另一个方向取最近的整数,就能得到当前Thread对应的像素相对于整个Thread Group起点坐标的偏移,两者相加就能得到最终的像素坐标了。 事实上,我们的中心点有可能会在屏幕外部,这个时候上图就会变成这样,我们在计算列高的时候需要额外的考虑中心点的偏移,深蓝色的区域也不会考虑完全在屏幕外的圈。 径向分派的额外参数 为了在Compute Shader中计算每个Thread对应的像素,我们需要从CPU额外传递一些参数。在径向分派中,我们从SV_DispatchThreadID中获取到的其实是GroupID和GroupIndex两个参数。由上面的讨论,我们将所有情况分为4 * 2种,即左下、左上、右下、右上、深蓝、浅蓝的组合,对于每一种组合我们需要知道总的数量,才能计算在每一种组合中的GroupID。根据我们上述的计算方式,我们还需要知道每一种组合对应的列高和xMajor的信息。为了兼容中心点在屏幕外的情况,我们还需要知道中心点的偏移值。这样我们的参数就是8组5个int值,分别对应偏移值X,偏移值Y,当前总Thread Group数,列高和xMajor,其中xMajor其实是一个布尔值可以封装到列高的第一位,这样就刚好是四个int值了,我们这里为了方便演示就不做这样的优化了。 private struct DispatchParams { public int2 offset; public int count; public int stride; public int xMajor; public DispatchParams(int2 offset, int count, int stride, int xMajor) { this....

March 30, 2024 · zznewclear13
Hex Tiling Cover

在地形贴图混合时使用六边形平铺

动机 最近看到了三角洲行动介绍的在虚幻引擎4中实现的地形渲染方案,感觉受益匪浅。不过在Unity中要想实现一个即插即用的虚拟贴图的技术应该有些困难,于是我把目光放在了最一开始所介绍的对地形贴图做混合的方案上。 三角洲行动提出的方案是这样的,在地形计算的时候,从对材质ID图的四个像素采样退化成对三个像素采样,这样一来既能减少地形混合的时候的采样次数,二来相较于采样四个像素,三个像素多了一个斜向45度的效果,能够减轻一些地形的块面感。不过他们也有语焉不详的地方,虽然只采样三个像素能够提供斜向45度,但是对于斜向-45度,仅使用同一种方式采样三个像素是不能消除这个方向的块面感的,当然想必他们也有对应的解决方案就是了。此外他们声称材质ID图是一张R8的贴图,但这张贴图里面怎么会有5bit的下层ID,5bit的上层ID,再有3bit的权重值呢?我只能认为这张材质ID图实际上只包含了一个5bit的材质ID和3bit的权重了,这个3bit的权重值会在和另外几个像素混合时使用到。 不过三次采样倒是让我想起了Hex Tiling。在Practical Real-Time Hex-Tiling里介绍了一种通过六边形平铺来降低平铺时纹理重复感的算法,这种算法正巧需要对主贴图采样三次(不考虑随机采样的话)。Github中也能找到参考的代码。 这样一来我们就能在三角洲行动的方案上再提出一种新的地形混合的方法了。我们同样是采样三个像素,不过我们在地形中会将这三个像素用等边三角形的方式排布,而不是目前所用的直角三角形。所以我们的流程是,先将当前的世界空间或者uv做一次三角形格点的变换,使用变换后的格点采样材质ID图获得三个材质ID和权重,再使用获得的数据和本身的六边形平铺的权重进行混合,就能得到我们最终的混合后的地形材质了。如果把我们的材质ID图平铺到世界空间,看上去应该是这样的: 生成材质ID图 为了快速生成材质ID图(我可不想手画),我们考虑使用Compute Shader通过Perlin Noise生成材质ID,使用普通的hash生成权重。为了使我们的材质ID图也能正常的平铺,我们在计算Perlin Noise的时候,要注意使用取模的运算将计算的uv值限制在同一个范围内。 我们只需要一个8bit的数据,但是由于Unity保存贴图的种种限制,我们可以将R8_Uint的数据除以255转换成R8_UNorm类型的数据再储存到贴图中。 GenerateMatIDComputeShader.compute #pragma kernel MatIDGenMain RWTexture2D<float> _RW_MatIDTex; float4 _TextureSize; // https://www.shadertoy.com/view/4djSRW float hash12(float2 p) { float3 p3 = frac(float3(p.xyx) * .1031); p3 += dot(p3, p3.yzx + 33.33); return frac((p3.x + p3.y) * p3.z); } float2 repeat(float2 coord, float2 size, float2 offset) { return coord - floor(coord / size) * size + offset; } float encode(int weight, int index) { int encoded = (weight << 5) | index; return float(encoded) / 255....

March 19, 2024 · zznewclear13
zznewclear13 技术美术 图形学 个人博客 technical art computer graphics