快速 SSE 射线 - 4 个三角形相交

Clo*_*oyz 4 c++ raytracing

我目前正在研究路径追踪器,我正在寻找优化我的光线三角形交叉点的方法。我目前使用以下 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;
}
Run Code Online (Sandbox Code Playgroud)

(如果有人看到优化它的方法,请告诉我)。然后我读到可以使用 SIMD 指令同时对 4 个三角形执行相交测试。但是怎么做呢?我不知道怎么可能以比我的顺序方式更有效的方式来实现它。

这里是我的渲染器相关的小代码。

rob*_*oke 6

使用 AVX512 最多可以制作 16 个三角形,使用 AVX2 制作 8 个三角形,使用 SSE 制作 4 个三角形。不过,诀窍是确保数据采用 SOA 格式。另一个技巧是在任何时候都不要“返回 false”(只需在最后过滤结果)。所以你的三角形输入看起来像:

  struct Tri {
    __m256 e1[3];
    __m256 e2[3];
    __m256 v0[3];
  };
Run Code Online (Sandbox Code Playgroud)

你的射线看起来像:

  struct Ray {
    __m256 dir[3];
    __m256 pos[3];
  };
Run Code Online (Sandbox Code Playgroud)

然后数学代码看起来好多了(请注意 _mm_dp_ps 不是有史以来最快的函数 - 还要注意访问 __m128/__m256/__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])));
  }
Run Code Online (Sandbox Code Playgroud)

该方法中基本上有 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)
Run Code Online (Sandbox Code Playgroud)

如果这些条件中的任何一个为真,则不存在交集。这基本上需要一点重构(在伪代码中)

__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));
Run Code Online (Sandbox Code Playgroud)

所以最后,如果发生交集,结果将是 t。如果没有发生交集,那么我们需要将结果设置为错误(在这种情况下,负数可能是一个不错的选择!)

// if(failed) return -1;
// else return t;
return _mm256_blendv_ps(t, _mm256_set1_ps(-1.0f), failed);
Run Code Online (Sandbox Code Playgroud)

虽然最终代码可能看起来有点讨厌,但它最终会比您的方法快得多。然而,魔鬼在细节中......

这种方法的一个主要问题是您可以选择是针对 8 个三角形测试 1 条光线,还是针对 1 个三角形测试 8 条光线。对于主要光线来说,这可能不是什么大问题。对于习惯于向不同方向散射的二次光线),事情可能会开始变得有点烦人。大多数光线追踪代码很有可能会遵循以下模式:测试 -> 排序 -> 批处理 -> 测试 -> 排序 -> 批处理

如果您不遵循该模式,您几乎永远无法充分利用矢量单位。(幸好 AVX512 中的压缩/扩展指令对此有很大帮助!)