概述
在HDRP的后处理系统中,具体来说就是 PostProcessSystem
类中,在绘制全屏后效时调用了几次 HDUtils.DrawFullScreen
方法,
跳转到定义发现有五个重载的方法,但是最终都是通过调用
commandBuffer.DrawProcedural
方法来实现画三角形的,具体的调用是这样的:
commandBuffer.DrawProcedural(Matrix4x4.identity, material, shaderPassId, MeshTopology.Triangles, 3, 1, properties);
传了一个单位矩阵,指定好的材质球和pass,拓扑结构是三角形(说拓扑结构有点怪怪的,应该就是图元类型即primitive吧),使用3个顶点,画1个实例,以及MaterialPropertyBlock对象,可以携带一些材质球的参数,但是 PostProcessSystem
类中的这几处调用都传的是null
。
csharp部分看完了,接下来看下shader的部分。可以参考FinalPass.shader
,这个shader在后处理的最后一步会用到,可以通过FrameDebug找到:
这里只关注顶点着色方法就行,即Vert
方法:
Varyings Vert(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID);
output.texcoord = GetFullScreenTriangleTexCoord(input.vertexID);
return output;
}
方法里进行了两个操作:
- 把顶点位置转换到 clip space
- 计算纹理坐标
先说转换到 clip space 的操作,注意这里使用的不是常用的以_POSITION
语义修饰的vertexPos
,而是用 SV_VertexID
语义修饰的 vertexID
,即顶点ID。因为此处是画一个三角形所以 vertexID
的值分别是 0, 1, 2.
GetFullScreenTriangleVertexPosition
方法的定义在Common.hlsl
文件中,恰巧 GetFullScreenTriangleTexCoord
方法也在旁边,所以一块列出来:
// Generates a triangle in homogeneous clip space, s.t.
// v0 = (-1, -1, 1), v1 = (3, -1, 1), v2 = (-1, 3, 1).
float2 GetFullScreenTriangleTexCoord(uint vertexID)
{
#if UNITY_UV_STARTS_AT_TOP
return float2((vertexID << 1) & 2, 1.0 - (vertexID & 2));
#else
return float2((vertexID << 1) & 2, vertexID & 2);
#endif
}
float4 GetFullScreenTriangleVertexPosition(uint vertexID, float z = UNITY_NEAR_CLIP_VALUE)
{
float2 uv = float2((vertexID << 1) & 2, vertexID & 2);
return float4(uv * 2.0 - 1.0, z, 1.0);
}
positionCS
根据 vertexID
的值,用 GetFullScreenTriangleVertexPosition
方法可以分别求出三角形三个顶点的clip space 位置分别是:
pos0 = float4(-1, -1, UNITY_NEAR_CLIP_VALUE, 1);
pos1 = float4(3, -1, UNITY_NEAR_CLIP_VALUE, 1);
pos2 = float4(-1, 3, UNITY_NEAR_CLIP_VALUE, 1);
简化一下,不关注zw分量,只看xy分量就变成了:
pos0 = float2(-1, -1);
pos1 = float2(3, -1);
pos2 = float2(-1, 3);
texcoord
根据 vertexID
的值,用 GetFullScreenTriangleTexCoord
方法可以求出uv坐标,分别是:
(以下值是以没有定义UNITY_UV_STARTS_AT_TOP
宏的情况下算出,定义了该宏时的情况下可以自己计算下哈哈哈,道理是一样的)
uv0 = float2(0, 0);
uv1 = float2(2, 0);
uv2 = float2(0, 2);
确定屏幕范围
根据已经计算出的 positionCS
和 texcoord
现在可以知道这个三角形的具体情况以及屏幕范围和整个三角形的关系,我画了一张图可以方便理解下:
左下角是第一个顶点 v0, pos0 = float2(-1, -1); uv0 = float2(0, 0);
右下角是第二个顶点 v1, pos1 = float2(3, -1); uv1 = float2(2, 0);
右下角是第三个顶点 v2, pos2 = float2(-1, 3); uv2 = float2(0, 2);
中间虚线部分就是屏幕的范围。设屏幕左下角点为bl,右下角点为br,左上角点为tl,右上角点为tr,可以根据三角形三个顶点的坐标和uv,求出这四个点的坐标和uv,为了方便验证结果,把之前计算好的pos和uv数值并列摆出。
// 已知量, 三角形三个顶点的坐标和uv //
pos0 = float2(-1, -1);
pos1 = float2(3, -1);
pos2 = float2(-1, 3);
uv0 = float2(0, 0);
uv1 = float2(2, 0);
uv2 = float2(0, 2);
// 根据上面的已知量计算出屏幕四个点的坐标和uv //
pos_bl = pos0 = float2(-1,-1); uv_bl = uv0 = float2(0,0); // 屏幕左下角 //
pos_br = (pos0+pos1)*0.5 = float2(1,-1); uv_br = (uv0+uv1)*0.5 = float2(1,0); // 屏幕右下角 //
pos_tl = (pos0+pos2)*0.5 = float2(-1,1); uv_tl = (uv0+uv2)*0.5 = float2(0,1); // 屏幕左上角 //
pos_tr = (pos1+pos2)*0.5 = float2(1,1); uv_tr = (uv1+uv2)*0.5 = float2(1,1); // 屏幕右上角 //
可以看出屏幕范围读取的uv正好就是 (0,0), (1,0), (0,1), (1,1)。可以完整地读取一张贴图,完美。
以前版本的untiy好像是通过画一个quad来实现全屏效果(quad实际上也是画两个三角形来实现),后来改成了画一个三角形,可能是因为
- 画三角形比画四边形少了一个顶点,减少了计算(虽然没多少)
- 避免了quad方式在三角形拼接的地方,即对角线出可能出现的接缝问题
参考链接:
: https://rauwendaal.net/2014/06/14/rendering-a-screen-covering-triangle-in-opengl/
: https://gamedev.stackexchange.com/questions/155183/how-does-the-following-code-generate-a-full-screen-quad
: https://docs.unity3d.com/ScriptReference/Rendering.CommandBuffer.DrawProcedural.html
: https://docs.unity3d.com/Manual/SL-ShaderSemantics.html