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
zznewclear13 技术美术 图形学 个人博客 technical art computer graphics