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
Gaussian Blur Cover

再议高斯模糊

将近两年之后再回过头来制作高斯模糊 虽然两年前已经写过了使用Group Shared Memory加速高斯模糊这篇文章了。但当时写的时候仍有一些遗憾的地方,由于使用的是长度为17的静态的高斯模糊的数组(实际上只有9个权重),虽然在一定程度上能够达到任意调节高斯模糊的程度的效果,但在较低程度的高斯模糊时,是通过手动线性插值找到合适的采样颜色,且一定会有17次的颜色和权重的运算;而在较高程度的高斯模糊时,由于仅有十七个有效的颜色点,会有明显的采样次数不足的瑕疵。 而这两年之间我也曾考虑使用不同的方法来制作一个既能满足很高程度的高斯模糊,又能兼顾很小程度的高斯模糊,性能上也相对高效,且使用同一套通用的代码,的高斯模糊效果。下面便是我之前在Shadertoy上写的通过随机采样和历史混合的高斯模糊效果。 Stochastic Gaussian Blur 但随机带来的噪声和历史混合带来的限制,决定了这种方法终究不能真正地使用在项目中,于是我又开始回到了使用Compute Shader和Group Shared Memory来计算高斯模糊效果的老路子上。不同的是,这次我使用了Compute Buffer把高斯模糊的参数传给Shader,这样就能确保范围内的每一个采样点都能够对最后的颜色产生应有的贡献。 正态分布(Normal Distribution) 和之前不同的是,这次我们要先从正态分布入手,从正态分布的特性来考虑我们的计算方式。正态分布的概率密度函数(probability density function)如下所示: $$ f(x) = \frac 1 {\sigma \sqrt{2 \pi}} e^{- \frac 1 2 (\frac {x-\mu} \sigma)^2} $$ 使用正态分布对信号进行过滤,被称作高斯滤波器(Gaussian Filter)。我们在使用的时候会把\(\mu\)设成0,这样永远是最中心的信号带来最大的贡献。但是这个概率密度函数的\(x\)的范围是\((-\infin, \infin)\),我们不可能对所有的信号都进行采样,于是我们一般对\(3\sigma\)范围内的信号进行采样,对1D的正态分布来说,\((-3\sigma, 3\sigma)\)占据了约99.7%的面积。因此我们往往使用三倍的\(sigma\)作为采样的半径,事实上在2D的时候,可能需要更大的采样半径才能消除明显的采样半径过小的瑕疵。 有一点值得一提的是,虽然我并不会具体的微积分的计算,但据我所知先后执行两个\(\sigma\)值分别为\(x\)和\(y\)高斯模糊,等价于执行一次\(\sigma\)值为\(\sqrt {x^2+y^2}\)的高斯模糊。 另一个有趣的点是,在普通的模糊操作是我们往往会用降采样再升采样的方式来减少采样的次数。对于半分辨率的线性1D降采样和升采样,中心像素保留了\(\frac 3 8\)的之前像素的信息,我们可以找到那么一个\(\sigma\)的值使得其在\((-0.5, 0.5)\)之间的面积约等于\(\frac 3 8\),这样我们就能说我们通过线性降采样和升采样做到了近似对应\(\sigma\)的高斯模糊的效果。可惜这个\(\sigma\)不太好算,有Group Shared Memory也没有必要去做额外的降采样和升采样了。 在本文中,会通过横竖两个1D高斯滤波器来等效一个2D的高斯滤波器,使用Group Shared Memory的话,倒是一个2D的高斯滤波器效率更高一些,不过为了后续的扩展性,本文拆成了两个滤波器。 具体的实现方法 剩下的就和之前大同小异了,为了确保每个像素只会进行至多两次采样,需要限制高斯模糊的最大半径GAUSSIAN_BLUR_MAX_RADIUS为THREAD_GROUP_SIZE的一半。而为了2D的高斯模糊在比较极端的情况下也能有比较好的效果,我的高斯模糊的半径会是\(\sigma\)的3.8倍向上取整。 GaussianBlurComputeShader.compute 这是一个横竖两次高斯模糊的Compute Shader,通过Group Shared Memory优化了原本高斯模糊的每个像素的采样操作(至多两次)。最大模糊半径为128个像素。 #pragma kernel GaussianH #pragma kernel GaussianV Texture2D<float4> _SourceTex; RWTexture2D<float4> _RW_TargetTex; StructuredBuffer<float> _GaussianWeights; float4 _TextureSize; #define GAUSSIAN_BLUR_MAX_RADIUS 128 #define THREAD_GROUP_SIZE 256 const static int CACHED_COLOR_SIZE = THREAD_GROUP_SIZE +GAUSSIAN_BLUR_MAX_RADIUS*2; groupshared half3 cachedColor[CACHED_COLOR_SIZE]; void SetCachedColor(half3 color, int index) { cachedColor[index] = color; } half3 GetCachedColor(int threadPos) { return cachedColor[threadPos + GAUSSIAN_BLUR_MAX_RADIUS]; } void CacheColor(int2 groupCacheStartPos, int cacheIndex, int isHorizontal) { int2 texturePos = groupCacheStartPos + cacheIndex * int2(isHorizontal, 1 - isHorizontal); texturePos = clamp(texturePos, 0, _TextureSize....

April 5, 2023 · zznewclear13
Tile Rendering Cover

模仿缺氧的瓦片渲染方法

缺氧的瓦片渲染的特点 很可惜我没有在RenderDoc里截到缺氧的帧,不过我还是能从渲染表现上来分析一下缺氧的瓦片渲染的特点。经过一段时间的游玩和从下面这张图中可以看到,缺氧的游戏逻辑是把整个2D的地图分成一个一个格子,每个格子记录了气体、液体、固体和建筑物的信息。气体只是一个扭曲的Shader,液体渲染和计算比较复杂,这里暂时不考虑,建筑物中的墙和管线虽然也有程序化生成再渲染的效果,但和场景中资源类型的固体格子是硬相接的关系,这里也不考虑。本文的研究重点放在资源类型的固体格子的渲染上(不包括这些格子的程序化生成)。 资源类型的固体格子(这里就简称瓦片了)的特点如下: 有多种类型的瓦片 仅在不同类型的瓦片相接时会有黑色的描边 瓦片之间会有排序,优先级高的瓦片会更多地扩张 瓦片之间黑色的描边呈现周期性规律 模仿这种渲染的思路 最简单的思路肯定就是在CPU中计算每一个瓦片应当有的形态,然后找到对应的贴图,把瓦片在GPU中绘制出来了。但是这样子做的话就失去了本文的意义,也太过无趣了。我想的是尽量多地用GPU来计算每个瓦片的形态,同时使用Instancing的方式,绘制每一个瓦片。 第一个问题是,不规则的瓦片应当如何绘制。如果是正方形的瓦片,能够很轻易地使用一个Quad和纹理来绘制,但是不规则的瓦片,势必会使用透明度混合的方式来绘制,这时对应的模型就会超出瓦片的游戏逻辑上的位置。因此,我想的是绘制的Quad的数量是瓦片实际数量的两倍加一,如下图所示: 在这张图中,ABC代表了不同类型的瓦片,左边是游戏游玩的时候逻辑上的瓦片分布,ABC是相接的,右边是在渲染的时候的瓦片的分布,在原有瓦片中间插入新的瓦片,专门用来渲染接缝。对于2号瓦片,其左上角右上角右下角左下角(顺时针的顺序)分别是ABCC,决定了这是一块三块相接的瓦片;对于1号瓦片,对应的编号是AACC(通过一些对2取模取余的运算可以排除掉B),决定了这是一块两块相接的瓦片;而对于3和4号瓦片,其编号为CCCC,决定了这两块是没有接缝的瓦片。这时我们又考虑到了瓦片之间优先级的关系,假设C>B>A,则AACC和AABB的接缝应当是相同的,ABCC和BCCA是旋转了九十度的关系。考虑到必定会有一个瓦片处于最低优先级,我们只需要将最低优先级的瓦片固定到左上角,讨论剩下三个瓦片的优先级与顺序即可。循着这个思路,我们可以把所有可能的接缝画在一张图上,这张图的RGBA通道记录了瓦片的优先级(R优先级最低,A优先级最高,接缝我使用了一个统一的灰色以便后续渲染),图片如下所示,为了比较容易观察,我对A通道做了反向,且对应的在下方标注了优先级顺序。同时我们还对应的写好一个函数用于根据优先级顺序找到对应的接缝类型从而在渲染时找到接缝在图上的位置(见ONITileRender.hlsl中的GetMode(uint a, uint b, uint c))。 由于会有优先级的比较,不可避免地会在GPU中进行排序,使用MergeSort的话,4个元素会有5次比较,由于我们还需要获得每个瓦片在四个瓦片中排序的序号,这里就硬写了手动比较,6次比较和MergeSort的5次也差不太多。我们绘制的图上仅有最低优先级瓦片在左上角的情况,因此我们还需要找到最低优先级瓦片初始的序号,从而在渲染时旋转我们的接缝图(这里就体现了我们使用顺时针编号的优势,方便了旋转的操作,如果是左上角右上角左下角右下角的顺序,就不太好旋转了)。 知道了每一个接缝图的旋转,我们还需要为其每一个部分(通道)渲染不同的贴图。这里使用了DrawProceduralIndirect来进行Instancing的渲染,DrawCall数量会和瓦片类型的数量一样多。对于一种瓦片,需要渲染的总瓦片数相当于是这类瓦片的图形向外扩展一个瓦片的数量,我们可以通过判断左上右上右下左下的瓦片类型来轻易地判断当前瓦片是否应该和目标瓦片类型一起渲染。我们会使用一个数据数量为瓦片类型数量*(2*地图宽高+1)的StructuredBuffer来统计所有应当绘制的瓦片(实际使用的大小不会大于4*(2*地图宽+1)*(2*地图高+1))。同时我们会使用一个数据数量为瓦片类型数量*5的ByteAddressBuffer来统计每种瓦片类型Instancing时需要的参数。 本文中的岩石的2D无缝贴图来自OpenGameArt.org 具体的代码和相关的解释 由于会用到CommandBuffer进行瓦片的绘制,我就把相关的代码放到Universal RP的Package里了。CPU代码,ONITileRenderManager.cs放在Packages/com.unity.render-pipelines.universal/Runtime/Overrides/下,ONITileRendererFeature.cs放在Packages/com.unity.render-pipelines.universal/Runtime/RendererFeature/下,ONITileRenderPass.cs放在Packages/com.unity.render-pipelines.universal/Runtime/Passes/下;GPU代码,ONITileRender.hlsl,ONITileComputeShader.compute和ONITileRenderShader.shader放在Packages/com.unity.render-pipelines.universal/Shaders/ONITile/下。 ONITileRenderManager用于地图的设置、计算和Buffer的获取。ONITileRendererFeature和ONITileRenderPass用于在Unity URP中渲染瓦片,ONITileComputeShader用于瓦片相关的计算,ONITileRenderShader用于瓦片的渲染。 ONITileRenderManager.cs 这里尤其需要注意每个Buffer的大小。在这个脚本里使用Compute Shader做了三件事:1. 对地图每一个点生成一个随机数作为瓦片类型;2. 从地图中计算每一种瓦片类型需要绘制的数量、位置、解封类型、旋转和应当采样的通道;3. 把ByteAddressBuffer中的数据复制到IndirectArgumentBuffer里。事实上我感觉ComputeShader.Dispatch应该做成一个异步的方法,不过这个调用频率不高,就这样好了。 using UnityEngine; [ExecuteInEditMode] public class ONITileRenderManager : MonoBehaviour { [HideInInspector] public static ONITileRenderManager Instance { get; private set; } public ComputeShader oniTileComputeShader; public int tileTypeCount = 4; public Vector2Int tileCount = new Vector2Int(16, 16); public Vector2 tileSize = Vector2.one; public Vector3 tileStartPos; public Vector2 randomSeed; public Texture[] mainTextures = new Texture[] {}; public Vector4 mainTextureST = new Vector4(1....

February 20, 2023 · zznewclear13
Ground Truth Ambient Occlusion Cover

Unity使用ComputeShader计算GTAO

环境光遮蔽 环境光遮蔽,在很久很久以前玩刺客信条的时候就看到过这个词语,但是并不懂什么意思,本着画质拉满的原则总是会勾选这个选项。后来才知道环境光遮蔽翻译自Ambient Occlusion(还真是直白的翻译),用来表现角落里阴暗的效果。 环境光遮蔽作用在光线计算的间接光照的阶段,由于光栅化渲染的局限性,间接光照往往分为漫反射间接光照和高光间接光照,因此环境光遮蔽也分漫反射和高光两种,这里暂时只讨论作用于漫反射间接光照的漫反射环境光遮蔽。而又由于前向渲染的局限性,屏幕空间的环境光遮蔽不分差别地作用于直接光照和间接光照,因此其强度还需要特别地留意。 Ground Truth Ambient Occlusion是Jorge Jimenez在他的文章Practical Real-Time Strategies for Accurate Indirect Occlusion中介绍的一种在主机上能够符合事实环境光遮蔽效果的一种屏幕空间环境光遮蔽的算法。我认为这个算法相较于其他的环境光遮蔽的算法最大的优点是,暗部够暗,在很窄的缝隙中能够很黑很黑,这是别的算法做不到的。 本文极大地参考了英特尔的XeGTAO开源代码。 具体的操作 这篇文章着重要讲的是使用Compute Shader来加速计算的操作方式,因此不会具体涉及到GTAO算法本身,感兴趣的话可以去SIGGRAPH 2016 Course上阅读GTAO的ppt。 GTAO的计算需要视空间法线和深度两个数据,如果是延迟管线的话能轻易得拿到所有数据,但对于前向渲染来说,需要从深度数据还原出视空间法线。正好我之前的文章介绍了一些从深度图计算视空间法线的方法。但在原有文章的基础上,我们还能使用Group Shared Memory对采样数进行一系列的优化。 由于GTAO相对来说算是比较低频的信息,我们可以考虑使用下采样的方式只用半分辨率甚至是更低的分辨率来计算GTAO。这里使用的方法是对NxN大小的区域,每一帧只取一个采样点,最后通过TAA来进行混合。 GTAO本身的采样数也能使用时空噪声来生成较少的采样点,最后通过TAA来进行混合。但是实际使用中发现,如果使用较多的时间混合,当场景中的物体发生移动之后,会露出一部分白色的画面,和较深的AO有比较明显的对比,因此考虑尽量多地使用空间的混合。 得益于Group Shared Memory,可以在非常大的范围内进行空间的混合,这里使用高斯模糊的方式进行混合,能够尽量保持暗部较暗的颜色。如果直接对所有的采样进行平均的话,会导致暗部变得很亮,失去了GTAO最出众的优点。对水平和竖直方向做两次高斯模糊的话,由于本身还会根据深度和法线算出额外的几何上的权重,在下采样程度较大的时候会产生比较明显的瑕疵,可以用全分辨率的深度图和法线来解决,但这会带来额外的采样。 在高斯模糊的阶段,由于模糊是作用在低分辨率的图像上的,在我们的上采样的操作中,还需要根据上采样的位置进行双线性插值(实际上只要一个方向线性插值就好了)。 Render Texture的精度上,GTAO最后的值可以用8位通道来储存,如果不需要额外的视空间法线的话,可以把GTAO值和24位的深度一起存到RGBA32的RT中。这里就偷懒使用R16G16B16A16_SFloat来储存了。 如此一来整个路线图就比较清晰了 下采样深度图获取深度数据 使用深度图计算视空间的法线,或者从G Buffer直接获取法线数据 使用深度图和法线计算GTAO的值 横向上采样,计算水平高斯模糊后的GTAO的值 纵向上采样,计算垂直高斯模糊后的GTAO的值 相关代码和说明 GTAOComputeShader.compute 重中之重就是Compute Shader了。分了四个Kernel:第一个计算GTAO的值,同时还储存了深度图和法线(除了直接储存法线的两个分量,也可以Encode成八面体来储存);第二个和第三个分别是水平和竖直方向的模糊;最后一个用来可视化,实际项目中可以不用这个。 和XeGTAO不同的是,我增加了一个USE_AVERAGE_COS的宏,正常是在每一个Slice中选择最大的cos值,但是考虑到场景中有栅格这样的物体,在时空混合程度不是很大的时候,可以计算cos的平均值来降低栅格对GTAO的影响(也就是减弱了噪声),这个宏完全可以不用开启。 本文为了尽量多的使用空间混合(亦即不使用时间混合),在XeGTAO的时空平均噪波中限制了时间的参数为13,这样GTAO就不会随着时间而变化了,实际上可以传入_FrameIndex充分利用时空噪波的优势。 主要是用groupIndex来储存和读取Group Shared Memory,每个点至多采样两次。计算法线时会采样5x5的区域,因此NORMAL_FROM_DEPTH_PIXEL_RANGE的值是2;计算模糊时既有高斯模糊的采样,还有后续手动线性插值的采样,所以CACHED_AO_NORMAL_DEPTH_FOR_BLUR_SIZE会有两者之和。线性插值还需要注意subpixelBias对线性插值的权重产生的影响。 本文使用了宽度为29的高斯核,可以在demofox的网站上轻松的计算很大的高斯核。 可能会有报寄存器使用数量超过限制的问题,感觉是const array和循环导致的,不过reimport之后就不会报这个警告了。 #pragma kernel GTAOMain #pragma kernel BlurHorizontalMain #pragma kernel BlurVerticalMain #pragma kernel VisualizeMain #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" Texture2D<float4> _ColorTexture; Texture2D<float> _DepthTexture; Texture2D<float4> _GTAOTexture; RWTexture2D<float4> _RW_NormalTexture; RWTexture2D<float4> _RW_GTAOTexture; RWTexture2D<float4> _RW_BlurTexture; RWTexture2D<float4> _RW_VisualizeTexture; SamplerState sampler_LinearClamp; SamplerState sampler_PointClamp; //region Parameters uint _FrameIndex; uint _DownsamplingFactor; float _Intensity; float _SampleRadius; float _DistributionPower; float _FalloffRange; float2 _HeightFogFalloff; float4 _TextureSize; float4 _TAAOffsets; //endregion //region Pre-defined Marcos #define SQRT2_2 0....

December 2, 2022 · zznewclear13