一块Meshlet可以被一个圆锥简易表达,圆锥的表示为:顶点(Apex)、轴(Axis)和顶角(1/2锥角)。
圆锥的的范围包含了Meshlet所有法线方向,因此如果一个view向量和圆锥范围内所有向量的点积都是负数,就说明整个Meshlet都是背对着相机,可以整体剔除,体现在下图中,就是如果相机在蓝色区域内,就可以剔除这个Meshlet:
image.png
圆锥生成
1. 计算Axis
对所有normal生成最小包围球,球心的单位化向量既是圆锥的轴(Axis):
// Calculate the normal cone
// 1. Normalized center point of minimum bounding sphere of unit normals == conic axis
XMVECTOR normalBounds = MinimumBoundingSphere(normals, m.PrimCount);
// 2. Calculate dot product of all normals to conic axis, selecting minimum
XMVECTOR axis = XMVectorSetW(XMVector3Normalize(normalBounds), 0);
2. 去掉退化圆锥
如果圆锥的顶角是钝角,则没有剔除意义,所以对Axis和Meshlet每个面的法线做点积,如果有小于0的,则代表是退化圆锥:
XMVECTOR minDot = g_XMOne;
for (uint32_t i = 0; i < m.PrimCount; ++i)
{
XMVECTOR dot = XMVector3Dot(axis, XMLoadFloat3(&normals[i]));
minDot = XMVectorMin(minDot, dot);
}
if (XMVector4Less(minDot, XMVectorReplicate(0.1f)))
{
// Degenerate cone
c.NormalCone[0] = 127;
c.NormalCone[1] = 127;
c.NormalCone[2] = 127;
c.NormalCone[3] = 255;
continue;
}
在后续的Amplification Shader中,会拿取到当前数据,并判断假如w分享是0xff,则判断为退化圆椎:
bool IsConeDegenerate(CullData c)
{
//这里NormalCone4个分量被打包成uint,所以要>>24
return (c.NormalCone >> 24) == 0xff;
}
3. 偏移顶点
上图事例中,圆锥顶点都在Meshlet网格的后面,但实际上可能在网格的前面,这时候要将顶点按轴移动到所有Mesh的负半空间:
首先计算Meshlet的最小包围球(上面是normal,现在是mesh的),默认顶点是这个最小包围球的球心。
对于每个面,计算原顶点按轴移动,移动到负半轴的距离t,找到最大的t作为顶点偏移值。
如上图,深蓝色标记着3个面,红色点沿轴移动后,分别在三个粉色点达到平面,需要计算的是红色点和粉色点的距离t,并找到最大值后移动。
我们有normal、axis是单位向量,找三角形任意一点连接Center,将线段投影到normal,得到center到表面的距离:
上图标识的角度的cos是axis和normal的点积,通过点积和center到面距离,可以算出t
// Find the point on center-t*axis ray that lies in negative half-space of all triangles
float maxt = 0;
for (uint32_t i = 0; i < m.PrimCount; ++i)
{
auto primitive = primitiveIndices[m.PrimOffset + i];
uint32_t indices[3]
{
primitive.indices.i0,
primitive.indices.i1,
primitive.indices.i2,
};
XMVECTOR triangle[3]
{
XMLoadFloat3(&vertices[indices[0]]),
XMLoadFloat3(&vertices[indices[1]]),
XMLoadFloat3(&vertices[indices[2]]),
};
XMVECTOR c = positionBounds - triangle[0];
XMVECTOR n = XMLoadFloat3(&normals[i]);
float dc = XMVectorGetX(XMVector3Dot(c, n));
float dn = XMVectorGetX(XMVector3Dot(axis, n));
// dn should be larger than mindp cutoff above
assert(dn > 0.0f);
float t = dc / dn;
maxt = (t > maxt) ? t : maxt;
}
4. 预计算cutoff阈值
对于圆锥母线和轴的夹角cos值可以预计算,不过别忘了需要计算的是蓝色区域:
// cos(a) for normal cone is minDot; we need to add 90 degrees on both sides and invert the cone
// which gives us -cos(a+90) = -(-sin(a)) = sin(a) = sqrt(1 - cos^2(a))
XMVECTOR coneCutoff = XMVectorSqrt(g_XMOne - minDot * minDot);
在Amplification Shader中直接比较:
//bool IsVisible(CullData c, float4x4 world, float scale, float3 viewPos)
float4 center = mul(float4(c.BoundingSphere.xyz, 1), world);
// Transform axis to world space
float3 axis = normalize(mul(float4(normalCone.xyz, 0), world)).xyz;
// Offset the normal cone axis from the meshlet center-point - make sure to account for world scaling
float3 apex = center.xyz - axis * c.ApexOffset * scale;
float3 view = normalize(viewPos - apex);
// The normal cone w-component stores -cos(angle + 90 deg)
// This is the min dot product along the inverted axis from which all the meshlet's triangles are backface
if (dot(view, -axis) > normalCone.w)
{
return false;
}