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
Relaxed Cone Step Mapping Cover

在Unity里实现松散圆锥步进Relaxed Cone Step Mapping

阅读前提示 由于本文使用的贴图均为LearnOpenGL网站上的贴图,其法线贴图和一般Unity或Unreal引擎中的法线贴图的Y分量是相反的,因此在计算世界坐标的bitangent的时候会额外再乘上一个sign,在正常情况下是不需要的。 视差效果 在三角形面数比较受限的情况下,往往会考虑使用一张高度图,通过视差的计算去渲染出一种3D的效果(虽然现在直接用曲面细分Tessellation似乎是一种更普遍的且更有效的方法)。有两种计算视差的方法,一种叫做Parallax Occlusion Mapping,先假定高度的层数,然后对每一层计算出合适的位置和颜色,从而达到3D效果;另一种叫做Cone Step Mapping,是根据高度图预先计算出每个点对于其他所有像素的最大的圆锥张角(有点像AO),根据圆锥张角快速步进,最后使用二分法计算出最终的交点的颜色。第一种方法有一个比较大的缺点,就是在视角比较接近平面的时候,如果采样次数不是很高,就会看到一层一层的效果,可以通过对最后一次计算深度进行线性插值在一定程度上减轻一层一层的问题;第二种方法的缺点是,当采样次数较小时,产生的图像会有一定程度的扭曲,但不会有一层一层的感觉,此外相较于第一种会有一个优点,较细物体不会被跳过。在GPU Gems 3中提到了一种Cone Step Mapping的优化,叫做Relaxed Cone Step Mapping,相较于之前计算最大张角的方式,这种优化通过确保通过圆锥的射线与圆锥内部的高度图至多只有一个交点,减少了一开始圆锥步进的次数。本文就主要使用这种方法进行计算,也许将圆锥的顶部放在比当前高度图更深的位置能够更加减少步进的次数,不过我稍微尝试了一下好像效果并不是特别理想。 Parallax Occlusion Mapping可以在Learn OpenGL里找到介绍和优化方案,Shadertoy上也有开源的代码可以参考。UE5中有一个叫Get Relief!的插件,可以用来快速生成Relaxed Cone Step Mapping的预计算的贴图,也提供了渲染的Shader。这个插件的作者Daniel Elliott也在GDC2023上分享了制作的思路,如果链接打不开的话这里还有一个GDC Vault的链接。 本文使用的贴图可以在Learn OpenGL中给出的下载链接中找到。为了看上去舒服一些,这里对displacement贴图的颜色进行了反向。 下图是两种视差做法的比较,左边是Parallax Occlusion Mapping,右边是Relaxed Cone Step Mapping,两者的采样次数是相同的,可以看到POM在较极限的情况下会有分层感而RCSM会有扭曲。RCSM使用的贴图也放在下面了,R通道是高度图,G通道是圆锥的张角。本文使用的是Unity 2021.3.19f1c1。 生成预计算的贴图 和Parallax Occlusion Mapping直接使用深度图不同的是,Cone Step Mapping需要预先计算出一张圆锥张角的图,圆锥的张角可以使用圆锥底的半径除以圆锥的高来表示,记为coneRatio。本文中使用的是高度图,但实际计算中会使用1减去高度值,对应的是从模型表面到实际高度的深度值。由于深度值只会在01之间,uv也只会在01之间,因此对于最深的点,其最大的圆锥张角不会大于1。 “确保通过圆锥的射线与圆锥内部的高度图至多只有一个交点”,对于圆锥顶部的currentPos和圆锥底部的rayStartPos(这个圆锥是一个倒立的圆锥,其底部和模型表面相平),可以采样一个目标点cachedPos,当cachedPos的深度小于currentPos的深度时,沿着cachedPos - rayStartPos的方向移动cachedPos的位置并一直采样所有像素samplePos,直到samplePos的深度值小于cachedPos(即射线穿过高度图并穿出),根据samplePos和currentPos就能计算出一个圆锥的张角coneRatio。循环所有的像素就能得到最小的圆锥张角了。 为了减少单次计算的消耗,本文会先将整张图片分成NxN大小的区域,在一次循环中会计算所有像素对于这NxN大小的区域的圆锥张角,循环所有的区域就能得到最后的圆锥张角了。同时只需要让N等于THREAD_GROUP_SIZE,就能使用group shared memory仅通过一次采样缓存这些区域的深度值。再有就是Early Exit的优化,当cachedPos在贴图外部,当cachedPos的深度大于currentPos的深度,当cachedPos的圆锥张角大于当前最小的圆锥张角,在这些情况下可以直接结束向外步进的循环。更多的优化方法也都能在Get Relief!的分享中找到。 具体的代码 RCSMComputeShader.compute 用于生成Relaxed Cone Step Mapping的贴图。PreProcessMain用于处理最一开始的深度图,预先设置最大的coneRatio为1。Early Exit是减少运算时间的关键。 #pragma kernel PreProcessMain #pragma kernel RCSMMain #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" Texture2D<float4> _SourceTex; RWTexture2D<float4> _RW_TargetTex; SamplerState sampler_LinearClamp; float4 _TextureSize; float2 _CacheOffset; #define THREAD_GROUP_SIZE 16u [numthreads(8, 8, 1)] void PreProcessMain(uint3 id : SV_DispatchThreadID) { uint2 tempID = uint2(id....

July 1, 2023 · zznewclear13
Circular Blur Cover

Unity两个Pass的圆形模糊

圆形模糊 圆形模糊,在Photoshop里又称镜头模糊(Lens Blur),和景深结合在一起的时候被称作散景(Bokeh),是指在摄影时失焦的区域产生的和光圈的形状一致的模糊效果,五边形八边形或是圆形都有可能。 在计算机图形学中实现景深效果基本上有两种方法:第一种也是最常用的,通过黄金率生成一系列的采样点,使得其形状接近想要模糊的形状,这种方法需要很多很多的采样点,基本上找到的都是60次以上的采样次数,由于采样点的分布不一定正好在像素点中心,也不能轻易地使用Group Shared Memory进行优化,事实上大的模糊半径很可能导致Group Shared Memory的大小不够;另一种是针对于特殊的模糊形状,比如正六边形,可以使用三次(MRT的话可以认为是两次)1D的模糊来组合而成,可以在Colin Barré-Brisebois的博客Hexagonal Bokeh Blur Revisited中看到详细的说明,值得一提的是他此前也在EA工作过(看来EA是真的很喜欢散景啊)。 EA的渲染工程师Kleber Garcia在2018年的GDC演讲Circular Separable Convolution Depth of Field中提到了通过复数的运算来实现圆形模糊的算法,其背后的数学我这里就不再赘述了,感兴趣的话可以参考Circularly symmetric convolution and lens blu这篇文章。圆形模糊的参数的生成的代码可以在Kleber Garcia的公开仓库里找到。Kleber Garcia本人也在Shadertoy上写了具体的圆形模糊的代码。 Circular Dof 由于景深效果相对来说比较复杂,这里就只考虑对整个屏幕施加相同程度的圆形模糊效果。 具体的实现方法 其实大部分和之前的高斯模糊没有什么差别。在分离卷积圆形模糊的算法中,圆形的效果是通过多个Filter叠加而成的,每个Filter对应实部和虚部两个参数。以本文为例,本文使用了两个Filter,对于一个Filter的一个颜色分量,需要储存实部虚部两个数据,总体就需要2x3x2=12个通道,使用三张R16G16B16A16_SFloat就能储存所有的数据。在Kleber Garcia的演讲中他还提到了,可以使用bracket的方法,将颜色的卷积数据储存到另一张图中,这样中间的Filter的结果就会落在[0, 1]的范围内,就能使用R8G8A8B8来储存了,可以节省一半的带宽(但是颜色的卷积数据不也要一张32位的图吗,这里我没太懂,感觉优化了但又没那么优化,索性就没那么做)。 整体的操作是:1. 采样源图片,对每个Filter和每个颜色分量计算实部和虚部的值,水平累加后储存到中间贴图中;2. 采样中间贴图,对每个Filter和每个颜色分量计算实部和虚部的值,竖直累加后乘上各自的权重就得到最终的颜色了。由于我没有使用bracket的方法,Filter中会有负值存在,在仅使用两个Filter且半径较大且像素颜色过亮的时候,由于banding的存在会使最终的颜色出现负值,解决方法是在读取颜色的时候做一次Clamp或者是ToneMapping到合理范围。 CircularBlurFilterGenerator.cs 改写自Kleber Garcia的公开仓库。 /* Copyright 2023 zznewclear13 (zznewclear@gmail.com) Copyright 2016 Kleber A Garcia (kecho_garcia@hotmail.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software....

April 9, 2023 · zznewclear13
zznewclear13 技术美术 图形学 个人博客 technical art computer graphics