快速 SSE 射线 - 4 个三角形相交
Fast SSE ray - 4 triangle intersection
我目前正在研究 Path Tracer,我正在寻找优化光线-三角形相交的方法。我目前使用 Moller-Trumbore 算法的以下 sse4 实现:
bool Ray::intersectTriangle(const Triangle tri, float& result) const
{
__m128 q = _mm_cross_ps(m_directionM128, tri.e2);
float a = _mm_dp_ps(tri.e1, q, dotProductMask).m128_f32[0];
if (a > negativeEpsilon && a < positiveEpsilon)
return false;
float f = 1.0f / a;
__m128 s = _mm_sub_ps(m_originM128, tri.v0);
float u = f * _mm_dp_ps(s, q, dotProductMask).m128_f32[0];
if (u < 0.0f)
return false;
__m128 r = _mm_cross_ps(s, tri.e1);
float v = f * _mm_dp_ps(m_directionM128, r, dotProductMask).m128_f32[0];
if (v < 0.0f || (u + v > 1.0f))
return false;
float t = f * _mm_dp_ps(tri.e2, r, dotProductMask).m128_f32[0];
if (t < 0.0f || t > m_length)
return false;
result = t;
return true;
}
(如果有人看到优化方法请告诉我)。
然后我读到可以使用 SIMD 指令同时对 4 个三角形执行相交测试。但是怎么做呢?我看不出如何以比我的顺序方式更有效的方式来实现它。
Here我的渲染器相关的小代码
AVX512 最多可以做 16 个三角形,AVX2 最多可以做 8 个,SSE 最多可以做 4 个。不过,诀窍在于确保数据采用 SOA 格式。另一个技巧是在任何时候都不要 'return false' (只需在最后过滤结果)。所以你的三角形输入看起来像:
struct Tri {
__m256 e1[3];
__m256 e2[3];
__m256 v0[3];
};
你的光线看起来像:
struct Ray {
__m256 dir[3];
__m256 pos[3];
};
然后数学代码开始看起来更好(请注意 _mm_dp_ps 不是有史以来最快的函数 - 还要注意访问 __m128/[= 的内部实现40=]/__m512 类型不可移植)。
#define or8f _mm256_or_ps
#define mul _mm256_mul_ps
#define fmsub _mm256_fmsub_ps
#define fmadd _mm256_fmadd_ps
void cross(__m256 result[3], const __m256 a[3], const __m256 b[3])
{
result[0] = fmsub(a[1], b[2], mul(b[1], a[2]));
result[1] = fmsub(a[2], b[0], mul(b[2], a[0]));
result[2] = fmsub(a[0], b[1], mul(b[0], a[1]));
}
__m256 dot(const __m256 a[3], const __m256 b[3])
{
return fmadd(a[2], b[2], fmadd(a[1], b[1], mul(a[0], b[0])));
}
你的方法基本上有4个条件:
if (a > negativeEpsilon && a < positiveEpsilon)
if (u < 0.0f)
if (v < 0.0f || (u + v > 1.0f))
if (t < 0.0f || t > m_length)
如果这些条件中的任何一个为真,则不存在交集。这基本上需要一点重构(伪代码)
__m256 condition0 = (a > negativeEpsilon && a < positiveEpsilon);
__m256 condition1 = (u < 0.0f)
__m256 condition2 = (v < 0.0f || (u + v > 1.0f))
__m256 condition3 = (t < 0.0f || t > m_length)
// combine all conditions that can cause failure.
__m256 failed = or8f(or8f(condition0, condition1), or8f(condition2, condition3));
所以最后,如果发生交集,结果将是t。如果交集 DID NOT 发生,那么我们需要将结果设置为错误(在这种情况下负数可能是一个不错的选择!)
// if(failed) return -1;
// else return t;
return _mm256_blendv_ps(t, _mm256_set1_ps(-1.0f), failed);
虽然最终代码可能看起来有点糟糕,但它最终会比您的方法快得多。细节决定成败......
此方法的一个主要问题是您可以选择针对 8 个三角形测试 1 条射线,或针对 1 个三角形测试 8 条射线。对于主要射线,这可能不是什么大问题。对于习惯于向不同方向散射的二次射线),事情会开始变得有点烦人。大多数光线追踪代码很有可能最终会遵循以下模式:测试 -> 排序 -> 批处理 -> 测试 -> 排序 -> 批处理
如果您不遵循该模式,您几乎永远无法充分利用向量单元。 (谢天谢地,AVX512 中的 compress/expand 指令对此有很大帮助!)
我以以下工作代码结束
struct PackedTriangles
{
__m256 e1[3];
__m256 e2[3];
__m256 v0[3];
__m256 inactiveMask; // Required. We cant always have 8 triangles per packet.
};
struct PackedIntersectionResult
{
float t = Math::infinity<float>();
int idx;
};
struct PackedRay
{
__m256 m_origin[3];
__m256 m_direction[3];
__m256 m_length;
bool intersect(const PackedTriangles& packedTris, PackedIntersectionResult& result) const;
};
#define or8f _mm256_or_ps
#define mul _mm256_mul_ps
#define fmsub _mm256_fmsub_ps
#define fmadd _mm256_fmadd_ps
#define cmp _mm256_cmp_ps
#define div _mm256_div_ps
void avx_multi_cross(__m256 result[3], const __m256 a[3], const __m256 b[3])
{
result[0] = fmsub(a[1], b[2], mul(b[1], a[2]));
result[1] = fmsub(a[2], b[0], mul(b[2], a[0]));
result[2] = fmsub(a[0], b[1], mul(b[0], a[1]));
}
__m256 avx_multi_dot(const __m256 a[3], const __m256 b[3])
{
return fmadd(a[2], b[2], fmadd(a[1], b[1], mul(a[0], b[0])));
}
void avx_multi_sub(__m256 result[3], const __m256 a[3], const __m256 b[3])
{
result[0] = _mm256_sub_ps(a[0], b[0]);
result[1] = _mm256_sub_ps(a[1], b[1]);
result[2] = _mm256_sub_ps(a[2], b[2]);
}
const __m256 oneM256 = _mm256_set1_ps(1.0f);
const __m256 minusOneM256 = _mm256_set1_ps(-1.0f);
const __m256 positiveEpsilonM256 = _mm256_set1_ps(1e-6f);
const __m256 negativeEpsilonM256 = _mm256_set1_ps(-1e-6f);
const __m256 zeroM256 = _mm256_set1_ps(0.0f);
bool PackedRay::intersect(const PackedTriangles& packedTris, PackedIntersectionResult& result) const
{
__m256 q[3];
avx_multi_cross(q, m_direction, packedTris.e2);
__m256 a = avx_multi_dot(packedTris.e1, q);
__m256 f = div(oneM256, a);
__m256 s[3];
avx_multi_sub(s, m_origin, packedTris.v0);
__m256 u = mul(f, avx_multi_dot(s, q));
__m256 r[3];
avx_multi_cross(r, s, packedTris.e1);
__m256 v = mul(f, avx_multi_dot(m_direction, r));
__m256 t = mul(f, avx_multi_dot(packedTris.e2, r));
// Failure conditions
__m256 failed = _mm256_and_ps(
cmp(a, negativeEpsilonM256, _CMP_GT_OQ),
cmp(a, positiveEpsilonM256, _CMP_LT_OQ)
);
failed = or8f(failed, cmp(u, zeroM256, _CMP_LT_OQ));
failed = or8f(failed, cmp(v, zeroM256, _CMP_LT_OQ));
failed = or8f(failed, cmp(_mm256_add_ps(u, v), oneM256, _CMP_GT_OQ));
failed = or8f(failed, cmp(t, zeroM256, _CMP_LT_OQ));
failed = or8f(failed, cmp(t, m_length, _CMP_GT_OQ));
failed = or8f(failed, packedTris.inactiveMask);
__m256 tResults = _mm256_blendv_ps(t, minusOneM256, failed);
int mask = _mm256_movemask_ps(tResults);
if (mask != 0xFF)
{
// There is at least one intersection
result.idx = -1;
float* ptr = (float*)&tResults;
for (int i = 0; i < 8; ++i)
{
if (ptr[i] >= 0.0f && ptr[i] < result.t)
{
result.t = ptr[i];
result.idx = i;
}
}
return result.idx != -1;
}
return false;
}
结果
结果很棒。对于 100k 三角形场景 我有 84% 的加速 !!。对于一个非常小的场景(20 个三角形),我的性能损失为 13%。但没关系,因为这些不常见。
我目前正在研究 Path Tracer,我正在寻找优化光线-三角形相交的方法。我目前使用 Moller-Trumbore 算法的以下 sse4 实现:
bool Ray::intersectTriangle(const Triangle tri, float& result) const
{
__m128 q = _mm_cross_ps(m_directionM128, tri.e2);
float a = _mm_dp_ps(tri.e1, q, dotProductMask).m128_f32[0];
if (a > negativeEpsilon && a < positiveEpsilon)
return false;
float f = 1.0f / a;
__m128 s = _mm_sub_ps(m_originM128, tri.v0);
float u = f * _mm_dp_ps(s, q, dotProductMask).m128_f32[0];
if (u < 0.0f)
return false;
__m128 r = _mm_cross_ps(s, tri.e1);
float v = f * _mm_dp_ps(m_directionM128, r, dotProductMask).m128_f32[0];
if (v < 0.0f || (u + v > 1.0f))
return false;
float t = f * _mm_dp_ps(tri.e2, r, dotProductMask).m128_f32[0];
if (t < 0.0f || t > m_length)
return false;
result = t;
return true;
}
(如果有人看到优化方法请告诉我)。 然后我读到可以使用 SIMD 指令同时对 4 个三角形执行相交测试。但是怎么做呢?我看不出如何以比我的顺序方式更有效的方式来实现它。
Here我的渲染器相关的小代码
AVX512 最多可以做 16 个三角形,AVX2 最多可以做 8 个,SSE 最多可以做 4 个。不过,诀窍在于确保数据采用 SOA 格式。另一个技巧是在任何时候都不要 'return false' (只需在最后过滤结果)。所以你的三角形输入看起来像:
struct Tri {
__m256 e1[3];
__m256 e2[3];
__m256 v0[3];
};
你的光线看起来像:
struct Ray {
__m256 dir[3];
__m256 pos[3];
};
然后数学代码开始看起来更好(请注意 _mm_dp_ps 不是有史以来最快的函数 - 还要注意访问 __m128/[= 的内部实现40=]/__m512 类型不可移植)。
#define or8f _mm256_or_ps
#define mul _mm256_mul_ps
#define fmsub _mm256_fmsub_ps
#define fmadd _mm256_fmadd_ps
void cross(__m256 result[3], const __m256 a[3], const __m256 b[3])
{
result[0] = fmsub(a[1], b[2], mul(b[1], a[2]));
result[1] = fmsub(a[2], b[0], mul(b[2], a[0]));
result[2] = fmsub(a[0], b[1], mul(b[0], a[1]));
}
__m256 dot(const __m256 a[3], const __m256 b[3])
{
return fmadd(a[2], b[2], fmadd(a[1], b[1], mul(a[0], b[0])));
}
你的方法基本上有4个条件:
if (a > negativeEpsilon && a < positiveEpsilon)
if (u < 0.0f)
if (v < 0.0f || (u + v > 1.0f))
if (t < 0.0f || t > m_length)
如果这些条件中的任何一个为真,则不存在交集。这基本上需要一点重构(伪代码)
__m256 condition0 = (a > negativeEpsilon && a < positiveEpsilon);
__m256 condition1 = (u < 0.0f)
__m256 condition2 = (v < 0.0f || (u + v > 1.0f))
__m256 condition3 = (t < 0.0f || t > m_length)
// combine all conditions that can cause failure.
__m256 failed = or8f(or8f(condition0, condition1), or8f(condition2, condition3));
所以最后,如果发生交集,结果将是t。如果交集 DID NOT 发生,那么我们需要将结果设置为错误(在这种情况下负数可能是一个不错的选择!)
// if(failed) return -1;
// else return t;
return _mm256_blendv_ps(t, _mm256_set1_ps(-1.0f), failed);
虽然最终代码可能看起来有点糟糕,但它最终会比您的方法快得多。细节决定成败......
此方法的一个主要问题是您可以选择针对 8 个三角形测试 1 条射线,或针对 1 个三角形测试 8 条射线。对于主要射线,这可能不是什么大问题。对于习惯于向不同方向散射的二次射线),事情会开始变得有点烦人。大多数光线追踪代码很有可能最终会遵循以下模式:测试 -> 排序 -> 批处理 -> 测试 -> 排序 -> 批处理
如果您不遵循该模式,您几乎永远无法充分利用向量单元。 (谢天谢地,AVX512 中的 compress/expand 指令对此有很大帮助!)
我以以下工作代码结束
struct PackedTriangles
{
__m256 e1[3];
__m256 e2[3];
__m256 v0[3];
__m256 inactiveMask; // Required. We cant always have 8 triangles per packet.
};
struct PackedIntersectionResult
{
float t = Math::infinity<float>();
int idx;
};
struct PackedRay
{
__m256 m_origin[3];
__m256 m_direction[3];
__m256 m_length;
bool intersect(const PackedTriangles& packedTris, PackedIntersectionResult& result) const;
};
#define or8f _mm256_or_ps
#define mul _mm256_mul_ps
#define fmsub _mm256_fmsub_ps
#define fmadd _mm256_fmadd_ps
#define cmp _mm256_cmp_ps
#define div _mm256_div_ps
void avx_multi_cross(__m256 result[3], const __m256 a[3], const __m256 b[3])
{
result[0] = fmsub(a[1], b[2], mul(b[1], a[2]));
result[1] = fmsub(a[2], b[0], mul(b[2], a[0]));
result[2] = fmsub(a[0], b[1], mul(b[0], a[1]));
}
__m256 avx_multi_dot(const __m256 a[3], const __m256 b[3])
{
return fmadd(a[2], b[2], fmadd(a[1], b[1], mul(a[0], b[0])));
}
void avx_multi_sub(__m256 result[3], const __m256 a[3], const __m256 b[3])
{
result[0] = _mm256_sub_ps(a[0], b[0]);
result[1] = _mm256_sub_ps(a[1], b[1]);
result[2] = _mm256_sub_ps(a[2], b[2]);
}
const __m256 oneM256 = _mm256_set1_ps(1.0f);
const __m256 minusOneM256 = _mm256_set1_ps(-1.0f);
const __m256 positiveEpsilonM256 = _mm256_set1_ps(1e-6f);
const __m256 negativeEpsilonM256 = _mm256_set1_ps(-1e-6f);
const __m256 zeroM256 = _mm256_set1_ps(0.0f);
bool PackedRay::intersect(const PackedTriangles& packedTris, PackedIntersectionResult& result) const
{
__m256 q[3];
avx_multi_cross(q, m_direction, packedTris.e2);
__m256 a = avx_multi_dot(packedTris.e1, q);
__m256 f = div(oneM256, a);
__m256 s[3];
avx_multi_sub(s, m_origin, packedTris.v0);
__m256 u = mul(f, avx_multi_dot(s, q));
__m256 r[3];
avx_multi_cross(r, s, packedTris.e1);
__m256 v = mul(f, avx_multi_dot(m_direction, r));
__m256 t = mul(f, avx_multi_dot(packedTris.e2, r));
// Failure conditions
__m256 failed = _mm256_and_ps(
cmp(a, negativeEpsilonM256, _CMP_GT_OQ),
cmp(a, positiveEpsilonM256, _CMP_LT_OQ)
);
failed = or8f(failed, cmp(u, zeroM256, _CMP_LT_OQ));
failed = or8f(failed, cmp(v, zeroM256, _CMP_LT_OQ));
failed = or8f(failed, cmp(_mm256_add_ps(u, v), oneM256, _CMP_GT_OQ));
failed = or8f(failed, cmp(t, zeroM256, _CMP_LT_OQ));
failed = or8f(failed, cmp(t, m_length, _CMP_GT_OQ));
failed = or8f(failed, packedTris.inactiveMask);
__m256 tResults = _mm256_blendv_ps(t, minusOneM256, failed);
int mask = _mm256_movemask_ps(tResults);
if (mask != 0xFF)
{
// There is at least one intersection
result.idx = -1;
float* ptr = (float*)&tResults;
for (int i = 0; i < 8; ++i)
{
if (ptr[i] >= 0.0f && ptr[i] < result.t)
{
result.t = ptr[i];
result.idx = i;
}
}
return result.idx != -1;
}
return false;
}
结果
结果很棒。对于 100k 三角形场景 我有 84% 的加速 !!。对于一个非常小的场景(20 个三角形),我的性能损失为 13%。但没关系,因为这些不常见。