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
Ray Marched Volumetric Fog Cover

使用Ray Marching来渲染体积雾

为什么要用Ray Marching 要不还是别用Ray Marching了(除非是SDF Ray Marching),采样次数又多又不好debug,不过写起来比较快(如果要写二分法的话就又复杂了)。如前文所说,使用Ray Marching的体积雾只能在后处理阶段使用了,在处理不写深度的透明物体的时候,会有一些瑕疵。 体积雾相关的就参考前文就好了,这里只是作为一个方法的补充。 相关代码和说明 为了和使用3D纹理的体积雾作区分,这边所有代码的名字前加上了RM(Ray Marching)。 RMVolumetricFog.cs 这个脚本和3D纹理的体积雾的参数几乎完全一致,只是多了用于控制Ray Marching次数的step。 using System; namespace UnityEngine.Rendering.Universal { [Serializable, VolumeComponentMenu("Post-processing/RM Volumetric Fog")] public class RMVolumetricFog : VolumeComponent, IPostProcessComponent { [Tooltip("是否启用体积雾")] public BoolParameter enabled = new BoolParameter(false); [Tooltip("整体控制体积雾强度")] public ClampedFloatParameter intensity = new ClampedFloatParameter(1.0f, 0f, 1.0f); [Tooltip("体积雾最大的透明程度(用于和天空混合)")] public ClampedFloatParameter maxTransmittance = new ClampedFloatParameter(1.0f, 0f, 1.0f); [Tooltip("体积雾的颜色倾向,目前强度为0.03")] public ColorParameter fogTint = new ColorParameter(Color.white); [Tooltip("体积雾距离相机最近的距离")] public ClampedFloatParameter fogNear = new ClampedFloatParameter(0.1f, 0.01f, 10f); [Tooltip("体积雾距离相机最远的距离")] public ClampedFloatParameter fogFar = new ClampedFloatParameter(100f, 1....

August 24, 2022 · zznewclear13
Volumetric Fog Cover

使用和视锥体对齐的3D纹理来渲染体积雾

为什么要渲染体积雾 因为它就在那里。 当然了,更重要的是因为体积雾能迅速的营造出场景的真实感与氛围感,谁不喜欢光源边上还有一小圈光晕呢,如果什么高亮的物体都能影响体积雾的话,是不是就不太需要bloom效果了呢。我实际地在生活中观察了一下,发现人眼所看到的光晕的效果,是光线进入眼睛之后产生的,也就是说bloom和体积雾确确实实是两种不同的效果。 体积雾的渲染方法 体积雾一般有两种渲染方法,一种是单纯的从相机出发对场景进行Ray Marching,每次进行采样和混合。这种方法主要的缺点是Ray Marching的次数会比较高才能有较好的渲染效果。在我的测试中,开启TAA的时候,20次Ray Marching就能得到很好的体积雾效果了;但是不开启TAA的话,可能会需要60次甚至更高的Ray Marching才能得到和TAA类似的效果。同时,Ray Marching体积雾只能在后处理阶段使用,在处理不写深度的透明物体的时候,会有一些瑕疵。 另一种方法就是使用一张3D纹理,将整个场景的体积雾储存在这张3D纹理中,当绘制物体的时候使用物体的世界空间坐标采样这张3D纹理,直接在片元着色器中计算雾效之后的颜色。这种方法使用的3D纹理会占用更多的内存,但是一定程度上能够正确的渲染所有物体,和60次Ray Marching相比,性能上也说不定会有一些优势。 本文的体积雾实现,参考了EA的寒霜引擎在Siggraph 2015年时的演讲和diharaw的OpenGL的体积雾效果。值得一看的还有Bart Wronski在Siggraph 2014年的演讲,以及之后的荒野大镖客在Siggraph 2019年的课程。使用的是Unity2019.4.29的URP工程。 具体的实现方法 将场景中的需要渲染的雾的信息和阴影信息储存到一张和相机的视锥体对齐的3D纹理中。按照寒霜引擎的做法,纹理大小为(分辨率宽/8)x(分辨率高/8)x64,这样就和屏幕大小的2D纹理占用的内存大小一致了,但我看Unity官方的体积雾工程中,3D纹理的深度为128,就也把自己的设置成128了,纹理深度越深,体积雾的细节就能越高。3D纹理的宽高和视锥体对齐,这很好理解,而这张贴图的纵向深度和实际的深度要怎么对齐呢?最简单的就是和视空间的深度线性对应,但是这会导致近处体积雾的分辨率不够;另一种是和裁剪空间的深度线性对应,经过一些分析可以知道这比之前的方法更糟糕;目前我看下来最好的应该是和视空间的深度指数型对应,这样离相机越近3D纹理的像素会越多,越远则越少。本文只使用了均一的雾,但是可以使用世界空间的坐标、噪波和一系列的运算,计算出某一点的体积雾的浓度。 使用上面的雾的信息和阴影信息计算出散射的值Lscat,从下面的图可以看到Lscat是对所有的光源(本文只有主光源)计算\(f(v, l)Vis(x, l)Li(x, l)\)的和,\(Vis(x, l)\)即为在x点l光的可见性,可以通过采样阴影贴图来获得,\(Li(x, l)\)即为在x点l光的光强,可以简单的计算获得,\(f(v, l)\)用来表述在v的方向观察雾时得到l的散射量,一般被叫做Phase Function,我们使用的是Henyey-Greenstein Phase Function,其中参数g是雾的各向异性的程度,越靠近1表示光线穿过雾时越保持之前的方向,越靠近0表示光线穿过雾时均匀的散射,越靠近-1表示光线穿过雾时越会进行反射(在实际的光照中,我们会去掉\(\pi\)这一项,这样能和Unity的光照模型保持一致)。时空混合也在这一步可以完成。 $$ \tag{Henyey-Greenstein} p(\theta) = \frac 1 {4\pi} \frac {1 - g^2} {(1 + g^2 - 2g \cos \theta)^{\frac 3 2}} $$ 对3D纹理从相机近点到远点进行混合,这其实是一种Ray Marching,不过是在3D纹理的纹理空间进行Ray Marching,一次前进一个像素。当混合当前像素和上一个像素时,需要考虑符合物理的透光率(transmittance), \(\varepsilon\)是一个用于归一化的常量,l是两点之间的距离,c是介质的吸收率(一定程度上可以用雾的密度来表示)。具体的混合的计算和说明可以看EA寒霜引擎的PPT第28、29页。 $$ \tag{Beer-Lambert} transmittance = e^{-\varepsilon l c} $$ 最终在绘制物体时,使用物体的世界空间的坐标,转换到3D纹理的坐标,采样3D纹理,使用透光率乘上物体本身的颜色,再加上雾的颜色,就得到了最终的体积雾的效果了。 相关代码和说明 VolumetricFog.cs 用于Global Volume中方便添加体积雾和控制各种参数。值得考虑的是maxTransmittance的值,因为相机远裁剪面会比较远,即使雾并不是很大,在最远处也总是能变成单一的颜色,这个值用来防止这种情况,人为地限制了最大不透光率(但是还是叫maxTransmittance)。fogNear这个参数实际是影响了3D纹理和相机之间的距离,最好还是设置成0,不然时空混合时会有一些瑕疵。 using System; namespace UnityEngine....

August 23, 2022 · zznewclear13
zznewclear13 技术美术 图形学 个人博客 technical art computer graphics