Equal Width Outline Cover

在Unity中绘制等宽的描边

对于描边的思考 描边可以说是一个特别关键的效果,不仅仅是二次元卡通渲染需要用到描边,在用户交互的方面,描边也是一个增强用户交互的关键效果。 一般的描边的做法是绘制一个沿物体空间顶点法线(或是记录在顶点色中的描边方向)外扩的模型背面,这种做法在绝大部分情况都看上去不错,但是描边的深度测试会有一些小瑕疵,同时在物体距离摄像机较近的时候,描边会显得较粗,此外这种描边没有抗锯齿的效果,绘制模型的背面也让造成了性能的浪费。另外一种方法是使用Multiple Render Targets,渲染出一个模型的剪影,然后使用类似高斯模糊的办法,对采样进行偏移,这样可以渲染出一个较好的可以有抗锯齿效果的描边,但是仅限于模型向外的描边,缺少模型内部的描边效果。 最好的描边应该是能够支持模型外描边、内描边、材质描边的描边效果,pencil +实现了这些效果,但是效率不是很高,这里有相关的演示(我也是看了这个之后才决定用安吉拉的模型的)。我看到的较好的方案应该还是L-灵刃的使用退化四边形生成描边的办法,github上也分享了源码。 这篇博客中介绍的描边,是基于我上一篇博客中讲的世界空间中绘制等宽线条的方法,使用DrawProcedural绘制的等宽的描边。我认为只有等宽的描边,才是最能表现二次元画面特征的描边。这里的“等宽”,并不是说线条的宽度处处相等,线条当然可以控制每一部分的粗细,但是这个控制的粗细是基于一个固定值的相对粗细(也就是存在顶点色中的描边粗细值),当粗细值相同时,不管是画面的哪个部分的描边的粗细(不管是内描边还是外描边),都应该是相同的。 实现描边时需要注意的点 首先参考退化四边形的案例,需要先对模型文件进行预处理。这里我做了简化,只去寻找两个三角面共用的边,忽略了只属于一个三角面的边的情况(事实上我觉得这样看上去的视觉效果也蛮不错的)。一条共用边对应了这条边的两个顶点,两侧的两个三角形和这两个三角形对应的额外的两个顶点。这里都用序号来表示,需要6个int值(事实上可以忽略两个三角形的编号,就能存在一个int4里了)。判断一条边共同属于两个三角形,就相当于判断两个三角形中的某两个顶点的序号是相同的(实际上顺序是相反的)。但是实际操作中,即使是相同的顶点,在两个三角形中顶点的序号也不一定是相同的,因此需要先把两个相同的顶点(使用距离来判断)合并成一个顶点,这也就是我使用vertRemapping这个数组的目的。剩下的就是循环所有三角形,获取共用边的算法部分了,尽可能的优化一下,不然三角面一多运算的时间要很久。 有了共用边的数据,通过SkinnedMeshRenderer.BakeMesh()可以获取到当前帧每个顶点的物体空间的坐标,就能进行描边的计算了。使用DrawProcedural时顶点的数量可以是公用边数量的两倍,这样需要在Geometry Shader中把顶点数目从2扩充到6,或者是在绘制时将顶点数量设置成共用边数量的六倍,可能后者效率会高一点,不过思考的时候会有点乱,这里就使用Geometry Shader的方法了。 如果是像之前的博客介绍的,以共用边两个顶点为中心,同时向左右两侧外扩的话,会因为深度测试的原因,导致描边部分被模型遮挡,这个问题比较严重,他直接导致了外描边和内描边的粗细不一样,也导致了在一条描边中会露出一部分模型的问题。这里采用的方法是仅向外侧描边,在计算是不是轮廓边的时候同时计算需要描边的方向,使用这个方向向外扩展描边,最后效果还蛮不错的。 要实现风格化描边的话,除了使用顶点色来控制描边的粗细之外,还能使用一张贴图作为描边的笔刷,在绘制描边的时候采样这张贴图,本篇博客就暂不使用这种方法了。 具体的实现描边的操作 对当前模型获取到所有的共用边对应的四个顶点序号,严格保持顺序! 每一帧使用SkinnedMeshRenderer.BakeMesh,获取所有顶点当前的物体空间的坐标。 传入顶点坐标,顶点重映射数组,共用边信息,如果需要的话还要传顶点色到描边的Shader中。 使用DrawProcedural绘制描边,顶点数量为共用边的数量的两倍。 在顶点着色器中,计算共用边四个顶点的裁剪空间(实际上用的是屏幕空间)的坐标,判断这条边是不是轮廓边,同时记录描边外扩的方向,相当于对于每一条边(每两个点)存两个bool变量。 在几何体着色器中,计算共用边两个顶点的屏幕空间的坐标,计算出两个点之间的向量,计算与之相垂直的外扩的方向,根据两个向量,计算出描边的四个顶点的裁剪空间的坐标,并赋予uv的值。 在片元着色器中,根据uv计算出描边的颜色,可以采样贴图,也可以直接返回计算的颜色。 由于整个描边的操作较为复杂,我尽可能多的写了注释。 OutlineObject.cs 定义共用边的结构体,也定义用于保存共用边数据的ScriptableObject。 using UnityEngine; namespace ZZNEWCLEAR13.Outline { [System.Serializable] public class OutlineObject : ScriptableObject { [System.Serializable] public class MeshOutlineInfo { public string meshName; //尽量不要都显示出来,不然很卡。。 //顶点、法线、切线和顶点色 [HideInInspector] public Vector3[] vertices; [HideInInspector] public Vector3[] normals; [HideInInspector] public Vector4[] tangents; [HideInInspector] public Color[] colors; //vertRemapping把相同位置的顶点编号映射到第一个该位置顶点的编号 [HideInInspector] public int[] vertRemapping; //三角形对应的顶点编号 [HideInInspector] public Vector3Int[] triangles; public Line[] commonLines; } public MeshOutlineInfo outlineInfo; } [System....

December 12, 2021 · zznewclear13
Equal Width Line Cover

在Unity中绘制等宽线条

动机 直接动机是想要在unity中制作一个描边效果。对于卡通渲染的描边效果,已经有很多很多的案例了,但是我觉得这些案例不一定能完全满足我的需求,于是想要从画直线开始研究。 从普通绘画的角度来看,很重要的一点就是描边的宽度基本上是一致的:考虑一下远景的物体,画家在绘画的时候使用和近景相同粗细的笔(用一样或者稍弱的力),绘制一个较不精细的物体,而不是使用很细的笔去绘制一个精细的物体,结论就是远处的描边可能稍细一些,但最好是相同粗细,其颜色可能会变浅。 从另一个角度来看,描边往往需要能够控制其宽度,对于较细的线,则会有较明显的锯齿(事实上只要角度不太好的描边,就会有很明显的边缘锯齿),那么控制粗细和进行一定程度的抗锯齿也是一个研究的方向。 从第三个角度,碰巧看到了Freya Holmér制作的Shapes插件,能够绘制高质量的线条画,看上去渲染的效果很好。但是价格过于高昂,于是想要研究一个能够做到差不多效果的工具。 抗锯齿和宽度的思考 在Unity中画直线有蛮多办法,Debug.DrawLine或者Gizmos.DrawLine都能绘制等宽的直线,不过只能固定一个像素宽,而且因为只有一个像素宽,所以会有明显的锯齿。使用LineRenderer可以绘制任意宽度的直线,写特定的shader通过透明度混合能够防止锯齿的出现(不过线段两端不太好做抗锯齿),但是不能保证在不同角度下直线的宽度相等。那么答案就很明显了,通过线段的顶点的数据,生成两个三角面(一个Quad),在GPU中计算出三角面每个顶点的屏幕空间的位置,确保线段的宽度一致。也由于有宽度的存在,给后续的透明度混合抗锯齿留下了操作的空间。 具体的绘制线条的操作 首先看一张图(完了三角形顶点顺序画反了,已经积重难返了,代码表现对就当做对的吧) 我们将使用Graphics.DrawProcedural这个方法来绘制我们的直线,每一段直线由两个三角形组成(这里要注意在unity中三角形顶点顺序是顺时针的),也就是说要绘制N条直线的话,要传入N+1个节点位置数据,而实际绘制时会绘制6N个顶点。这样传入Shader的是一个表示线段节点位置的长度是N+1的Vector3数据_VerticesBuffer,和一个表示顶点序号的从0到6N-1的uint数据,也就是在绘制时图形API自动传输的SV_VERTEXID。 这也就导致了我们的顶点着色器输入数据和普通的Shader有所区别,只有一个uint数据: struct Attributes { uint vertexID : SV_VERTEXID; }; 在顶点着色器获取到SV_VERTEXID之后,我们可以以此来计算出线段的序号lineID和每个顶点在绘制线段时的序号vertexID。在绘制时我们又要知道当前线段的两个节点的位置,在传入_VerticesBuffer之后,我们需要对每个顶点确定其对应的线段节点的序号,即lineID + 0或者lineID + 1,我们可以将这个可以通过vertexID确定的0或者1储存到一个数组vertexIndexes中,方便使用vertexID读取。 为了让线段取得较好的抗锯齿效果,我们把线段整体外扩了_OutlineWidth个像素宽(实际线段宽度是这个的两倍)。此时对于每一个顶点,我们需要知道外扩之后的屏幕空间的顶点位置.由于我们需要使用从LineStart到LineEnd的方向offset(对于1, 2, 4这几个顶点来说,offset是从LineEnd到LineStart的方向)和与其相垂直的方向来做外扩,我们需要把每个顶点的两个方向值储存到数组vertexOffsets中。可以看到顶点永远是往远离较远节点的方向移动的,因此vertexOffset的x值都是-1。而我们选取offset逆时针旋转90°的方向作为相垂直的方向,因此vertexOffset的y值会有正负之分。 最后是uv的数值,我们还需要给每个顶点传入uv的数值,就比较简单了,储存到数组vertexUVs中。 此外还要注意一点,在片元着色器中,我们需要知道LineStart和LineEnd对应的uv位置来做线段的圆形端点,因此我们还需要把这个长方体的高宽比ratio传给片元着色器。 这样,我们片元着色器的输入数据就是positionCS,uv和ratio了: struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float ratio : TEXCOORD1; }; 绘制线条的流程 C#脚本获取要绘制线条的节点数据,传入_VerticesBuffer,调用Graphics.DrawProcedural方法进行绘制。 Shader的顶点着色器根据SV_VERTEXID计算出每个顶点对应的线段序号lineID和顶点序号vertexID。 通过线段序号、顶点序号和vertexIndexes从_VerticesBuffer中找到较近的线段节点的世界坐标,和较远的线段节点的世界坐标。 将两个世界坐标转换到屏幕空间,得到屏幕空间两点对应的向量和与之相垂直的向量,进行归一化。 将每个顶点根据vertexOffsets和描边宽度进行外扩。 将外扩后的裁剪空间的坐标,从vertexUVs中获得的uv和高宽比ratio传给片元着色器。 片元着色器根据ratio和uv,绘制出一个两头圆形的线段。 DrawEqualWidthLine.cs C#脚本比较简单了,没什么特别需要注意的地方。 using UnityEngine; public class DrawEqualWidthLine : MonoBehaviour { public Vector3[] vertices; private int vertexCount; public Material equalWidthMaterial; ComputeBuffer verticesBuffer; private void EnsureBuffer(ref ComputeBuffer buffer, int count, int stride) { if (buffer == null) { buffer = new ComputeBuffer(count, stride, ComputeBufferType....

December 2, 2021 · zznewclear13
Color Inspector Example

GPU颜色查看器

制作这个颜色查看器的目的 一直以来我都觉得调试GPU代码是一件很困难的事情,在屏幕上显示的颜色会被限制在0和1之间,所以我很希望能够有那么一个工具,能够让我切切实实的看到输出的数据。如果是写的CPU代码的话,又会对伽马空间的矫正之类的有一些担心,而且可复用程度不够高。因此我就希望能够在GPU传回的图像上直接显示颜色和其对应的数值。 碰巧之前在Shader Toy上看到了一些在GPU上输出文字的案例,研究了一下之后,就制作了这么一个颜色查看器。 GPU显示文字的原理 一种是将文字储存到一张贴图上,通过采样这张贴图来显示这些文字,可以通过SDF的方法,自由的调节文字的粗细,也不会有锯齿;另一种是将文字直接用01数据来储存,对应到屏幕上,0的区域显示黑色,1的区域显示白色,本文就采用了这种方式;当然Shader Toy上还有一种是将文字用曲线来储存的,应该是涉及到傅里叶变换。 本文的标准文字是8*12个像素大,每三行进行合并的话,就是一个24 * 4的01矩阵,这就能将一个文字使用uint4来储存,每个通道还多余8bit。具体可以看Shader Toy案例里的描述,我觉得写的十分直观了。 但是这个案例里面有一点我觉得不太好的地方,他对于每一个像素,都要计算每一个文字的颜色,也就是说文字越多计算的复杂度越大。我想的是最好能够使用StructuredBuffer的方式,先用一个pass计算要显示的文字,然后对于每一个像素,计算出要显示的文字的编号,再在Structured Buffer中找到对应的文字,最终显示出来。这样每个像素应该之多只需要做一次文字到颜色的计算,能够提高效率。 颜色查看器具体的操作 首先要先把需要显示的文字对应的uint4值计算出来,这一步之前的案例里已经有现成的数据了。最好是将这些数据组成一个数列,这样我们之后能够用其编号来访问文字对应的数据。 执行一个(1, 1, 1)的Compute Shader,获取要采样的像素点的颜色。对像素点的每一位,都将其转换成要输出的文字对应的编号,储存到_Append_CharacterIndexBuffer中。像本文中需要显示7这个数字的话,需要添加23这个编号到我们的输出数据中。同时,由于每一个像素点有rgba四个通道,但我们输出的是一个RWStructuredBuffer<int>的数据,我们还需要一个列表_RW_LengthBuffer(相当于指针),记录每一个通道对应的数据数列的区间。 对整个画面执行Compute Shader,根据每一个像素点的位置,判断是显示放大的图像、每个通道对应的文字或是原始的画面。如果要显示文字的话,根据需要显示的RGBA通道,找到对应的_RW_LengthBuffer获得编号区间,再找到_Struct_CharacterIndexBuffer中获得对应的编号,通过编号找到对应的文字。最后再根据像素的位置,求出在该文字中应当显示的颜色。 在Unity中我选择了用Volume和Renderer Feature的方法,对后处理之前的画面进行颜色查看的操作。 整体想法还是尽量的减少采样,减少文字到颜色的计算,以及减少分支判断。 Font.hlsl 就是案例中的8*12的文字对应的uint4表,这里将其用数列来保存。 #define character_spc characterSet[0] #define character_exc characterSet[1] #define character_quo characterSet[2] #define character_hsh characterSet[3] #define character_dol characterSet[4] #define character_pct characterSet[5] #define character_amp characterSet[6] #define character_apo characterSet[7] #define character_lbr characterSet[8] #define character_rbr characterSet[9] #define character_ast characterSet[10] #define character_crs characterSet[11] #define character_com characterSet[12] #define character_dsh characterSet[13] #define character_per characterSet[14] #define character_lsl characterSet[15] #define character_0 characterSet[16] #define character_1 characterSet[17] #define character_2 characterSet[18] #define character_3 characterSet[19] #define character_4 characterSet[20] #define character_5 characterSet[21] #define character_6 characterSet[22] #define character_7 characterSet[23] #define character_8 characterSet[24] #define character_9 characterSet[25] #define character_col characterSet[26] #define character_scl characterSet[27] #define character_les characterSet[28] #define character_equ characterSet[29] #define character_grt characterSet[30] #define character_que characterSet[31] #define character_ats characterSet[32] #define character_A characterSet[33] #define character_B characterSet[34] #define character_C characterSet[35] #define character_D characterSet[36] #define character_E characterSet[37] #define character_F characterSet[38] #define character_G characterSet[39] #define character_H characterSet[40] #define character_I characterSet[41] #define character_J characterSet[42] #define character_K characterSet[43] #define character_L characterSet[44] #define character_M characterSet[45] #define character_N characterSet[46] #define character_O characterSet[47] #define character_P characterSet[48] #define character_Q characterSet[49] #define character_R characterSet[50] #define character_S characterSet[51] #define character_T characterSet[52] #define character_U characterSet[53] #define character_V characterSet[54] #define character_W characterSet[55] #define character_X characterSet[56] #define character_Y characterSet[57] #define character_Z characterSet[58] #define character_lsb characterSet[59] #define character_rsl characterSet[60] #define character_rsb characterSet[61] #define character_pow characterSet[62] #define character_usc characterSet[63] #define character_a characterSet[64] #define character_b characterSet[65] #define character_c characterSet[66] #define character_d characterSet[67] #define character_e characterSet[68] #define character_f characterSet[69] #define character_g characterSet[70] #define character_h characterSet[71] #define character_i characterSet[72] #define character_j characterSet[73] #define character_k characterSet[74] #define character_l characterSet[75] #define character_m characterSet[76] #define character_n characterSet[77] #define character_o characterSet[78] #define character_p characterSet[79] #define character_q characterSet[80] #define character_r characterSet[81] #define character_s characterSet[82] #define character_t characterSet[83] #define character_u characterSet[84] #define character_v characterSet[85] #define character_w characterSet[86] #define character_x characterSet[87] #define character_y characterSet[88] #define character_z characterSet[89] #define character_lpa characterSet[90] #define character_bar characterSet[91] #define character_rpa characterSet[92] #define character_tid characterSet[93] #define character_lar characterSet[94] static int4 characterSet[] = { int4(0x000000,0x000000,0x000000,0x000000), int4(0x003078,0x787830,0x300030,0x300000), int4(0x006666,0x662400,0x000000,0x000000), int4(0x006C6C,0xFE6C6C,0x6CFE6C,0x6C0000), int4(0x30307C,0xC0C078,0x0C0CF8,0x303000), int4(0x000000,0xC4CC18,0x3060CC,0x8C0000), int4(0x0070D8,0xD870FA,0xDECCDC,0x760000), int4(0x003030,0x306000,0x000000,0x000000), int4(0x000C18,0x306060,0x603018,0x0C0000), int4(0x006030,0x180C0C,0x0C1830,0x600000), int4(0x000000,0x663CFF,0x3C6600,0x000000), int4(0x000000,0x18187E,0x181800,0x000000), int4(0x000000,0x000000,0x000038,0x386000), int4(0x000000,0x0000FE,0x000000,0x000000), int4(0x000000,0x000000,0x000038,0x380000), int4(0x000002,0x060C18,0x3060C0,0x800000), int4(0x007CC6,0xD6D6D6,0xD6D6C6,0x7C0000), int4(0x001030,0xF03030,0x303030,0xFC0000), int4(0x0078CC,0xCC0C18,0x3060CC,0xFC0000), int4(0x0078CC,0x0C0C38,0x0C0CCC,0x780000), int4(0x000C1C,0x3C6CCC,0xFE0C0C,0x1E0000), int4(0x00FCC0,0xC0C0F8,0x0C0CCC,0x780000), int4(0x003860,0xC0C0F8,0xCCCCCC,0x780000), int4(0x00FEC6,0xC6060C,0x183030,0x300000), int4(0x0078CC,0xCCEC78,0xDCCCCC,0x780000), int4(0x0078CC,0xCCCC7C,0x181830,0x700000), int4(0x000000,0x383800,0x003838,0x000000), int4(0x000000,0x383800,0x003838,0x183000), int4(0x000C18,0x3060C0,0x603018,0x0C0000), int4(0x000000,0x007E00,0x7E0000,0x000000), int4(0x006030,0x180C06,0x0C1830,0x600000), int4(0x0078CC,0x0C1830,0x300030,0x300000), int4(0x007CC6,0xC6DEDE,0xDEC0C0,0x7C0000), int4(0x003078,0xCCCCCC,0xFCCCCC,0xCC0000), int4(0x00FC66,0x66667C,0x666666,0xFC0000), int4(0x003C66,0xC6C0C0,0xC0C666,0x3C0000), int4(0x00F86C,0x666666,0x66666C,0xF80000), int4(0x00FE62,0x60647C,0x646062,0xFE0000), int4(0x00FE66,0x62647C,0x646060,0xF00000), int4(0x003C66,0xC6C0C0,0xCEC666,0x3E0000), int4(0x00CCCC,0xCCCCFC,0xCCCCCC,0xCC0000), int4(0x007830,0x303030,0x303030,0x780000), int4(0x001E0C,0x0C0C0C,0xCCCCCC,0x780000), int4(0x00E666,0x6C6C78,0x6C6C66,0xE60000), int4(0x00F060,0x606060,0x626666,0xFE0000), int4(0x00C6EE,0xFEFED6,0xC6C6C6,0xC60000), int4(0x00C6C6,0xE6F6FE,0xDECEC6,0xC60000), int4(0x00386C,0xC6C6C6,0xC6C66C,0x380000), int4(0x00FC66,0x66667C,0x606060,0xF00000), int4(0x00386C,0xC6C6C6,0xCEDE7C,0x0C1E00), int4(0x00FC66,0x66667C,0x6C6666,0xE60000), int4(0x0078CC,0xCCC070,0x18CCCC,0x780000), int4(0x00FCB4,0x303030,0x303030,0x780000), int4(0x00CCCC,0xCCCCCC,0xCCCCCC,0x780000), int4(0x00CCCC,0xCCCCCC,0xCCCC78,0x300000), int4(0x00C6C6,0xC6C6D6,0xD66C6C,0x6C0000), int4(0x00CCCC,0xCC7830,0x78CCCC,0xCC0000), int4(0x00CCCC,0xCCCC78,0x303030,0x780000), int4(0x00FECE,0x981830,0x6062C6,0xFE0000), int4(0x003C30,0x303030,0x303030,0x3C0000), int4(0x000080,0xC06030,0x180C06,0x020000), int4(0x003C0C,0x0C0C0C,0x0C0C0C,0x3C0000), int4(0x10386C,0xC60000,0x000000,0x000000), int4(0x000000,0x000000,0x000000,0x00FF00), int4(0x000000,0x00780C,0x7CCCCC,0x760000), int4(0x00E060,0x607C66,0x666666,0xDC0000), int4(0x000000,0x0078CC,0xC0C0CC,0x780000), int4(0x001C0C,0x0C7CCC,0xCCCCCC,0x760000), int4(0x000000,0x0078CC,0xFCC0CC,0x780000), int4(0x00386C,0x6060F8,0x606060,0xF00000), int4(0x000000,0x0076CC,0xCCCC7C,0x0CCC78), int4(0x00E060,0x606C76,0x666666,0xE60000), int4(0x001818,0x007818,0x181818,0x7E0000), int4(0x000C0C,0x003C0C,0x0C0C0C,0xCCCC78), int4(0x00E060,0x60666C,0x786C66,0xE60000), int4(0x007818,0x181818,0x181818,0x7E0000), int4(0x000000,0x00FCD6,0xD6D6D6,0xC60000), int4(0x000000,0x00F8CC,0xCCCCCC,0xCC0000), int4(0x000000,0x0078CC,0xCCCCCC,0x780000), int4(0x000000,0x00DC66,0x666666,0x7C60F0), int4(0x000000,0x0076CC,0xCCCCCC,0x7C0C1E), int4(0x000000,0x00EC6E,0x766060,0xF00000), int4(0x000000,0x0078CC,0x6018CC,0x780000), int4(0x000020,0x60FC60,0x60606C,0x380000), int4(0x000000,0x00CCCC,0xCCCCCC,0x760000), int4(0x000000,0x00CCCC,0xCCCC78,0x300000), int4(0x000000,0x00C6C6,0xD6D66C,0x6C0000), int4(0x000000,0x00C66C,0x38386C,0xC60000), int4(0x000000,0x006666,0x66663C,0x0C18F0), int4(0x000000,0x00FC8C,0x1860C4,0xFC0000), int4(0x001C30,0x3060C0,0x603030,0x1C0000), int4(0x001818,0x181800,0x181818,0x180000), int4(0x00E030,0x30180C,0x183030,0xE00000), int4(0x0073DA,0xCE0000,0x000000,0x000000), int4(0x000000,0x10386C,0xC6C6FE,0x000000), }; ColorInspector....

October 20, 2021 · zznewclear13
Gaussian Blur Cover

使用Group Shared Memory加速高斯模糊

2023年4月5日更新 再议高斯模糊,更实用的高斯模糊。 为什么要用Compute Shader来做高斯模糊 在之前的博客中我说Compute Shader的优势就是快,因为GPU的并行运算比CPU强大很多。但是相比于同样运行在GPU上的Fragment Shader之类,Compute Shader是不是就毫无优势了呢?答案是否定的,在DX11的文档中我们可以看到Thread Group Shared Memory这么一个概念,Compute Shader的Thread Group中的每一个Thread,都可以极快速的访问到对应的Group Shared Memory中的数据,这个效率比采样一张贴图来的高。因此在进行高斯模糊这样的需要大量贴图采样的计算时,先将贴图数据缓存到Group Shared Memory中,再多次访问Group Shared Memory,这样运行的效率会高得多。 相关的一些参考可以在英伟达的PPT里找到。 Compute Shader进行高斯模糊的具体操作 这里暂时不使用半分辨率的优化方法,目的是把SrcIden对应的RenderTexture经过高斯模糊储存到DestIden中,这时我们需要一张临时的相同大小的RenderTextureBlurIden。 确定需要的最大的高斯模糊的像素宽度MAX_RADIUS,越大的高斯模糊宽度,需要越大的Group Shared Memory,而Group Shared Memory的大小是有上限的(cs_5.0是32768 bytes)。当然也没有必要在全分辨率的情况下做特别大的高斯模糊就是了。这里我设置最大的高斯模糊像素数是32,即高斯模糊当前像素点和最远采样点之间的距离不能超过32(双线性采样的话还要缩小一个像素)。 高斯模糊往往使用水平和竖直两个高斯核心进行模糊,对应的需要两个Compute Shader Kernel(也可以写成一个,不过思考起来有点绕),我们这里设置两个Kernel,对应水平和竖直两个pass。水平Kernel使用[numthreads(64, 1, 1)],最高可以是1024,竖直Kernel则使用[numthreads(1, 64, 1)]。 由于高斯模糊中会采样像素点的左右(上下)两侧的像素,Group Shared Memory需要在GroupThreads的基础上向两侧扩大最大高斯模糊像素数MAX_RADIUS,用于保存额外的像素数据。这时我们需要的Group Shared Memory的大小是numthreads + 2 * MAX_RADIUS个,在本文章中是64 + 2 * 32个float3的数据。 GaussianBlur.cs 这里略去Unity SRP的设置pass的操作,仅展示高斯模糊相关的操作。当blurRadius过大时,就能看到明显的多重采样的痕迹了。 private void DoGaussianBlurHorizontal(CommandBuffer cmd, RenderTargetIdentifier srcid, RenderTargetIdentifier dstid, ComputeShader computeShader, float blurRadius) { int gaussianBlurKernel = computeShader.FindKernel("GaussianBlurHorizontalMain"); computeShader.GetKernelThreadGroupSizes(gaussianBlurKernel, out uint x, out uint y, out uint z); cmd....

August 19, 2021 · zznewclear13
Temporal Anti-Aliasing Cover

在Unity SRP中实现TAA效果

TAA的原理 首先是要了解画面上的锯齿是如何产生的。锯齿发生在光栅化的阶段,光栅化的时候会丢失掉小于一个像素宽的细节,也就导致了锯齿的产生。 从字面上来看,TAA (Temporal Anti-Aliasing)的抗锯齿效果来源于Temporal一词,是一种时间上的抗锯齿。TAA会结合当前渲染的画面和之前渲染的画面,通过这两个画面之间的融合,达成抗锯齿的效果。基本思想是在光栅化的时候对画面进行抖动,让亚像素的细节在不同帧渲染到不同的像素上,最后再对这些像素按时间的权重来混合,就能达到抗锯齿的效果。 Temporal Reprojection Anti-Aliasing Temporal Reprojection Anti-Aliasing是由PlayDead在他们的游戏Inside中使用的一种TAA的方法,他们在GDC2016的演示中分享了这个方法。相较于普通的TAA来说,Temporal Reprojection Anti-Aliasing中使用了Velocity Buffer中的屏幕像素的速度信息和Depth Buffer中对应的屏幕像素的世界坐标信息,这样当物体移动或者相机移动的时候,在做到抗锯齿的同时也减少了TAA带来的拖影效果,同时也把TAA和运动模糊相结合达到更理想的抗锯齿的效果。 PlayDead提供了对应的源代码。本博客中TAA在SRP中的实现也参考了sienaiwun的TAA代码。 在Unity SRP中实现TAA的操作 我们通过RendererFeature的方式在渲染管线中加入TAA。在ForwardRendererData中加入RendererFeature后,往Global Volume中添加Temporal Anti-Aliasing以在场景中启用TAA效果。启用TAA效果后,会现在渲染不透明物体之前调用一个Jitter Pass对相机的栅格化阶段进行抖动;在渲染TAA Pass时(在Bloom等跟物体渲染相关的后处理效果之后,在Chromatic Aberration等跟屏幕空间位置相关的后处理效果之前)根据抖动值还原出正常的不抖动的画面,并和AccumTexture进行混合,获得最终的渲染画面。因此我们需要TAARendererFeature、TAAJitterPass、TAARenderPass这三个脚本来处理渲染管线,TemporalAntiAliasing这个脚本来处理Volume,TAAShader这个Shader文件来进行TAA的混合操作。 对栅格化阶段进行抖动,也就相当于是修改了相机的透视变换矩阵的第一第二行的第三位的值,抖动值最好和TexelSize相结合,这样在TAA反向抖动还原正常值的时候,在shader中会比较好写。抖动值和TAA的反向抖动是正比关系,因此可以不需要特别纠结于计算,在shader中传入一个debug值再和抖动值相乘用作反向抖动,观察最后的画面是否存在抖动,就能很好的判断出这两个值的比例了。抖动的方式有很多,纯随机的抖动也可以选择,不过稍不如使用均匀分布的随机抖动的效果好,这里使用Inside中的方式即利用Halton数列进行抖动。 为了让相机移动时也能有较好的抗锯齿效果且削弱拖影现象,Temporal Reprojection Anti-Aliasing需要采样当前的深度贴图,还原出物体的世界空间的坐标,再计算出这个世界空间在AccumTexture中的UV值(Reprojection),使用这个UV值采样AccumTexture再和当前渲染画面进行融合。 因为Velocity Buffer比较麻烦,这里暂且忽略掉物体移动对TAA带来的影响。 在ScriptableRenderPass中使用cmd.GetTemporaryRT()获得的Render Texture,在当帧过后就会被回收,因此AccumTexture需要使用RenderTexture.GetTemporary()来获取。这里我把AccumTexture放在TemporalAntiAliasing.cs中,方便使用。 TemporalAntiAliasing.cs 除了普通的Volume的设置之外,还需要提供Render Texture的接口。lastFrame的x值和y值分别对应最后渲染画面中对AccumTexture进行线性插值的最小和最大系数。 using System; namespace UnityEngine.Rendering.Universal { [Serializable, VolumeComponentMenu("Post-processing/Temporal Anti-Aliasing")] public class TemporalAntiAliasing : VolumeComponent, IPostProcessComponent { public BoolParameter isEnabled = new BoolParameter(false); public NoInterpFloatRangeParameter lastFrame = new NoInterpFloatRangeParameter(new Vector2(0.2f, 0.8f), 0f, 1f); public Vector2Parameter jitterIntensity = new Vector2Parameter(Vector2....

July 15, 2021 · zznewclear13
zznewclear13 技术美术 图形学 个人博客 technical art computer graphics