pbrt-v3中的局部与世界坐标系转换

424 阅读2分钟

引言

在学习 Nori 和 pbrt-v3 时发现他们在采样着色点时,都是先在局部坐标系中采样方向,再转到世界坐标系,Nori 设计了一个 Frame 类来构建和存储局部坐标系,而 pbrt-v3 则在 BSDF 类中实现了两种坐标系互相转换的方法。我选择学习 Nori 设计一个 Frame 类来存储该坐标系的信息。

构建局部坐标系

如果能在初始化时直接提供局部坐标系的三个基向量,自然可以直接构建局部坐标系。但当只提供单个向量 vv 时,就需要我们基于这个向量进行构建坐标系了。

首先要得到垂直于向量 vv 的单位向量,我们可以根据 vvx,yx,y 坐标值判断将哪个维度压扁。例如当 vvxx 坐标值大于 yy 时,将 yy 坐标值设为 00 就得到了压扁在 x,zx,z平面上的 v(x,0,z)v^\prime(x,0,z),将 xx 和 z 坐标值互换位置就得到了在该象限上对称的向量 v(z,0,x)v^{\prime\prime}(z,0,x),再将x坐标值取反得到了和 vv^\prime 垂直的 v(z,0,x)v^{\prime\prime\prime}(z,0,-x),即与 vv 垂直。


explicit Frame(const Vector3f& n) {
    CoordinateSystem(n, s, t);
}

static void CoordinateSystem(const Vector3f& v, Vector3f& other1, Vector3f& other2) {
    if (std::abs(v.x) > std::abs(v.y)) {
        float invLength = 1.0f / std::sqrt(v.x * v.x + v.z * v.z);
        other2 = Vector3f(v.z * invLength, 0.0f, -v.x * invLength);
    } else {
        float invLength = 1.0f / std::sqrt(v.y * v.y + v.z * v.z);
        other1 = Vector3f(0.0f, v.z * invLength, -v.y * invLength);
    }
}

在局部和世界之间转换

建立局部坐标系后,我们需要提供世界坐标系转换为局部坐标系的方法和局部坐标系转为世界坐标系的方法。

局部坐标系下的向量转到世界坐标系,等价于向量左乘变换矩阵。而让向量 vv 左乘由 s,t,ns,t,n 三个基向量所构成的初等变换矩阵,即视 vv 为一个世界坐标系下的向量经过相同的初等变换后来到了现在的位置。

Vector3f ToWorld(const Vector3f& v) const {
    return s * v.x + t * v.y + n * v.z;
}

而世界坐标系转到局部坐标系就相当于做一次以上的逆变换,而基向量 s,t,ns,t,n 所构成的变换矩阵显然是正交矩阵,可得其转置矩阵即为其逆变换矩阵。按照向量左乘矩阵的计算方法,我们发现新向量的各维度计算过程可以简化为点乘。

Vector3f ToLocal(const Vector3f& v) const {
    return {Dot(v, s), Dot(v, t), Dot(v, n)};
}

其他工具函数

在局部坐标系中,求向量 cos\cos 值的计算也变得简单了。这是因为由向量 v 和坐标系法线 n(0,0,1)n(0,0,1) 点乘即可得到,显然结果就是向量 vvzz 坐标。而有了 cos\cos 值后,我们就可以轻松求出其他三角函数。例如可以先计算出 sin\sin 的平方值,以简化 sin\sintan\tan 的代码逻辑。

static float CosTheta(const Vector3f& v) {
    return v.z;
}

static float SinTheta2(const Vector3f& v) {
    return 1 - v.z * v.z;
}

static float SinTheta(const Vector3f& v) {
    float temp = SinTheta2(v);
    return temp <= 0.0f ? 0.0f : std::sqrt(temp);
}

static float TanTheta(const Vector3f& v) {
    float temp = SinTheta2(v);
    return temp <= 0.0f ? 0.0f : std::sqrt(temp) / v.z;
}