有时候
我会想简单的全屏输出点什么来看,但是又懒得专门写个Feature去调用底层DrawMesh或者DrawProcedual方法,那么有没有什么懒人包可用么?也许可以张罗一块Quad,然后调整到既能覆盖住摄像机视窗又与某个裁剪面平行。这自然是一种可行的方案,但是弊端也是极大的,比如说我们必须保证摄像机和Quad保持相对静止,再者调整Quad的手动操作也是个吃力的麻烦活。那么到底有没有轻巧快捷的全屏Mesh绘制方案呢?答案是有!下面我分享的就是自己常用的一种方法,虽然猜测有人早已给这种方法取了名字,但是胡Cares?知道怎么用就行。
原料有二,首先是需要准备一块Unity的Quad作为基础Mesh
除此之外,就只需要再配合一份特制的shader就可以愉快地向全屏输出你的渲染了(确切的说只有VS是特制的shader):
v2f vert (appdata v)
{
v2f o = (v2f)0;
//o.vertex = TransformObjectToHClip(v.vertex); //默认方法,可行,但是需要提前布置正确Quad位置
o.vertex = float4(v.vertex.xy * 2 * float2(1, -1), 0.1, 1); //偷懒方法,直接设置到NDC空间去
o.uv = v.uv;
return o;
}
在上述VS中,我们舍弃了默认的TransformObjectToHClip
操作,该操作将处于Object sapce(对象空间)将Quad转换到了HClip space(齐次裁剪空间)。取而代之的是,我们充分利用了Quad网格顶点的特殊属性,直接构造符合HClip space特征的VS Out!
话说什么是Unity默认Quad的顶点特征?请参考下图:
矩形Mesh由2个长边重合的三角形黏合而成,Mesh的中心(root)处于4个顶点连线交叉的位置(重心),左下角的模型坐标正好是[-0.5, -0.5]
,右上角则是[0.5, 0.5]
。至于z
轴,是一个足以忽略的超级小数,目前将其视为0
无妨。
首先回顾一下,一块可以用来进行全屏Blit操作的Mesh,其在经历了MVP矩阵变换后,应该出现在什么位置,是个什么形状呢?以近裁剪面来说,它中心点应该位于原点[0,0,0]
点,总体来说是一个正方形,边长为2*NearPlaneDist
,左下角的坐标值均为近平面距离的负数[x, y, z] = -NearPlaneDist
,因为只有这样,Unity才能在后续的齐次除法里(除以同样值为NearPlaneDist
的w
分量)顺利得将处于HClip space中的矩形顶点变换到NDC space中的4个边界点上去,还是以左下角为例,变换后其具体数值正好是[-1, -1,-1]
。
上述是走正统变换逻辑,鉴于我们的目的是快速构建能覆盖全屏的Mesh,那么思路自然要跳脱一些,不妨跳过模型+相机+投影的变换,直接参考最终成型的NDC坐标点,将特制的齐次裁剪坐标布置到VS Out(顶点输出)上去!所谓齐次裁剪空间与NDC空间,笼统的说不过是多了一步齐次除法:将vector4
的每个分量各自除以vector4.w
的过程。于是我们不妨耍个技巧,直接定义w
分量为1
,这样一来NDC与HClip之间隔着的那层齐次除法就约等于不存在了!我们可以大方的宣布下式是成立的:
NDC Coordination== HClip Coordination
一言以蔽之,就是拿w
分量为1
的NDC坐标来替代原本要在VS Out阶段输出的HClip坐标!
最后谈起如何构建NDC坐标,请直接参考上图Unity Quad中的顶点,将数值放大一倍* 2
即可!
上文解释了为何VS shader中会有v.vertex.xy * 2
这样的操作
-> 那是为了得到NDC/HClip空间下的边界点,它们的绝对值都为1
。
但是为何后面会有~ * float2(1, -1)
这项呢?
-> 是因为我们需要将y
轴上下对调,从而修正面片三角形的遍历顺序,让其正面朝向摄像机!不妨做个实验,去除~ * float2(1, -1)
这部分代码,但是调整Pass的渲染状态:添加上Cull Front
或者Cull Off
这样的指令(既让模型的背面也显示出来),如此一来,即便不翻转y
轴,我们也能得到覆盖全屏的Mesh,只不过顶点顺序不对,会导致uv采样后出现y
轴方向颠倒的现象:)
提问!三角形的遍历顺序不是写死在Mesh.triangles中的么,3个一组记录有顶点位置的下标。
答:虽然索引的顺序是固定的,但是顶点坐标的上下对调的话,肯定会影响到相邻两向量Cross Product的输出方向,从而会影响到硬件判断三角形的正反面特性。
最后你可能还会注意到,在关键代码段o.vertex = float4(v.vertex.xy * 2 * float2(1, -1), 0.1, 1)
中,第三项z
轴是有赋值的,而且这个数值建议> 0
。这么做不为别的,只是考虑到不同的底层图像API对NDC的定义略有不同(有的中心点在[0, 0]
点,有的中心点在[0.5, 0.5]
处),为了确保我们的全屏观察屏幕不被几何阶段最后埋伏着的Clipper无情修剪掉,这里设置个大于0
的常数(比方说0.1
)会比较百搭。
以下就是最终的VS Out截帧,三角形顶点以CW顺序排列,完美覆盖了NDC的[-1,1]
空间,至于uv,使用Quad自带的即可,实践证明在PS里采样一切正常。
参考我在延迟渲染的几何阶段,用此方法创建了个Quad,然后直接就能全屏输出一些测试纹理到GBuffer中去了~
备注
本文使用的Mesh是Quad,不是Triangle,因此这里有必要提醒一点,如果你打算进行的Blit操作有性能上的要求,那么强烈建议使用三角形网格(DrawTriangle)来实现,因为Quad是由2块三角形拼装起来的,会在屏幕对角线处留下交界线,而基于PixelQuad的影响,这条线在光栅化阶段会带来额外的性能开销。
以上