Published on

glm:我踩过的坑

Authors
  • Name

使用 glm 时经常出现「de 了很久 bug 才发现其实是个 feature」的情况。用这篇记录一下我用 glm 踩过的坑。


左乘 v.s. 右乘

根据基本的线性代数常识我们知道,矩阵 Am×nA_{m \times n} 能够左乘矩阵 Bs×tB_{s \times t} 的充分必要条件是 n=sn = s,此时有

Am×nBs×t=Cm×t A_{m \times n} \cdot B_{s \times t} = C_{m \times t}

在另一个被广泛使用的的线性代数库 Eigen 中,想求这样两个矩阵的乘积时可以方便地写为:

auto C = A * B;

然而在 glm 中不是这样(没想到吧!)。在 glm 中,要求矩阵 AA 左乘矩阵 BB 的结果,应该写成:

auto C = B * A;

结论

也就是左乘写在右边,右乘写在左边。

维度 v.s. 长度 v.s. 模长

维度,长度,模长,这三个概念对于一个 n×1n \times 1 的向量来讲,我的第一感觉是后两者 (长度和模长) 表示向量在空间中的长度,而向量的维度指的是向量分量个数。于是为了判断一个颜色是否是黑色,我用了向量的成员函数 .length()

if ( sign(this->material->emission.length()) == 0 ) { ... }

结论

这样写不仅是错的,而且是可以编译通过的:向量的 .length() 方法返回的是向量的维度。正确的写法是用全局函数 glm::length(vec const&) 求模长。

四元数的构造函数与 operator[]

结论

目前开发版本(未发布)的 glm 中四元数的构造函数的默认行为与 operator[] 的默认行为已经一致。一些相关 pull request: #1069 / #1074 / #1076 / #1084 (其中 #1069 是我提的)。

glm 的四元数实现中有一个宏定义 GLM_FORCE_QUAT_DATA_WXYZ,顾名思义,有这个宏定义时,glm 将把四元数的数据用 wxyz 的布局来存,glm 默认是不定义这个宏的,此时四元数的默认数据布局是 xyzw

那么重点来了:

#include <glm/glm.hpp>
#include <glm/gtx/quaternion.hpp>

#include <cstdio>

using quat = glm::qua<double>;

int main() {
    quat q1(0, 1, 2, 3);

    quat q2;
    q2[0] = 0;
    q2[1] = 1;
    q2[2] = 2;
    q2[3] = 3;

    printf("q1: (%f %f %f %f)\n", q1.x, q1.y, q1.z, q1.w);
    // Outputs:
    // q1: (1.000000 2.000000 3.000000 0.000000)

    printf("q2: (%f %f %f %f)\n", q2.x, q2.y, q2.z, q2.w);
    // Outputs (without `g++ -DGLM_FORCE_QUAT_DATA_WXYZ`):
    // q2: (0.000000 1.000000 2.000000 3.000000)
    // Outputs (with `g++ -DGLM_FORCE_QUAT_DATA_WXYZ`):
    // q2: (1.000000 2.000000 3.000000 0.000000)

    return 0;
}

注意分别在编译时是否定义宏 GLM_FORCE_QUAT_DATA_WXYZ 的情况下,四元数 q2 的不同行为。这段代码说明在默认情况下(编译时未定义宏 GLM_FORCE_QUAT_DATA_WXYZ)你以为的相同的初始化,其实是不同的(如果不能叫做相反的话)。从 glm 的源码里可以看到,无论前面提到的宏 GLM_FORCE_QUAT_DATA_WXYZ 是否被定义,四元数的构造函数接受的参数都是 (T _w, T _x, T _y, T _z), 并且内部赋值也总是把第一个参数赋值给四元数的 w, 以此类推:

  template <typename T, qualifier Q>
  GLM_FUNC_QUALIFIER GLM_CONSTEXPR qua<T, Q>::qua(T _w, T _x, T _y, T _z)
#   ifdef GLM_FORCE_QUAT_DATA_WXYZ
      : w(_w), x(_x), y(_y), z(_z)
#   else
      : x(_x), y(_y), z(_z), w(_w)
#   endif
  {}

而四元数的 [] 运算符的行为根据 GLM_FORCE_QUAT_DATA_WXYZ 宏是否被定义有不同的实现

  template<typename T, qualifier Q>
  GLM_FUNC_QUALIFIER GLM_CONSTEXPR T & qua<T, Q>::operator[](typename qua<T, Q>::length_type i)
  {
    assert(i >= 0 && i < this->length());
#   ifdef GLM_FORCE_QUAT_DATA_WXYZ
      return (&w)[i];
#   else
      return (&x)[i];
#   endif

这就造成了上面的代码中生成了两个不同的四元数的情况。