PBRT 几何和变换
第二章 几何和变换
几乎所有图形软件都以几何类(geometric classes, 这里指c++类)为基础.这些类表示了诸如点,向量,光线等等的数学构件. 由于我们在系统中会到处用到这些类, 良好的抽象和有效的实现至关重要. 本章会讲解pbrt的几何基础的接口和实现.
几何类见文件 core/geometry.h 和core/geometry.cpp.
变换矩阵见文件 core/transform.h 和core/transform.cpp.
2.1 坐标系统
pbrt用三个浮点数坐标值x,y,z来表示三维点,向量和法向量. 当然,这些值只有在一个给定的坐标系下才有意义: 给定一个原点和三个定义x,y,z轴的向量,就定义了这个坐标系(frame).
在n维空间中, 坐标系的原点P0和其n个线性无关的基向量定义了n维仿射空间(affine space).所有空间中的向量V可以被表达成为基向量(V1,V2, …, Vn)的线性组合:
V = s1V1 + s2V2 + … + snVn (s1, s2, … sn是唯一存在的一组纯量, 被称为V关于基(V1,V2…Vn)的表达).
同样地, 对与点P而言, 它可用原点P0和基向量(V1,V2, …, Vn)表达:
P = P0 + s1V1 + s2V2 + … + snVn
以上讨论有点循环定义的味道: 要定义坐标系我们需要定义一个点和一组向量, 而点和向量只有在给定的一个坐标系下才有意义. 因此,我们需要一个标准坐标系, 其原点是(0,0,0), 基向量为(1,0,0), (0,1,0) 和(0,0,1).
2.1.1 左/右手坐标系
我们知道坐标系分左手坐标系和右手坐标系,pbrt用左手坐标系.
2.2 向量
class COREDLL Vector {
public:
};
一个向量表达了三维空间内的一个方向, 它由三个浮点数定义:
float x, y, z;
x,y,z被定义为公共成员, 不太符合C++的封装原则, 但我们这样做是为了代码的清晰和效率.
缺省情况下, (x,y,z)被设成0. 用户可以选择给定任意值:
Vector(float _x = 0, float _y = 0, float _z = 0)
: x(_x), y(_y), z(_z) {
}
2.2.1 向量运算
向量加法运算:
Vector operator+(const Vector &v) const {
return Vector(x+v.x, y + v.y, z + v.z);
}
Vector& operator+=(const Vector &v) const {
x += v.x; y += v.y; z += v.z;
return *this;
}
向量减法运算与上类似, 略.
2.2.2 比例运算
比例运算是纯量乘法, 即是将向量每个分量乘以一个纯量, 从而改变了它的长度.
Vector operator*(float f) const {
return Vector(f*x, f*y, f*z);
}
Vector& operator*=(const Vector &v) const {
x *=f; y *= f; z *= f;
return *this;
}
inline Vector operator*(float f, const Vector &v) {
return v*f;
}
类似地,我们可以定义纯量除法 “operator/” 和 “operator /=”, 此略.
Vector类还有一个”取负值”的单操作符定义, 用来返回一个方向相反的向量:
Vector operator-() const {
return Vector(-x, -y, -z);
}
下面两个函数可以用索引值0,1,2方便地使用向量的各个分量: v[0]得到x值, v[1]得到y值, v[2]得到z值.
float operator[](int i) const {
Assert(i >= 0 && i <= 2);
return (& x);
}
float &operator[](int i) {
Assert(i >= 0 && i <= 2);
return (&x);
}
2.2.3 点积和叉积
对于两个向量V和W, 它们的点积(V . W)定义为: Vx*Wx + Vy*Wy + Vy*Wy.
inline float Dot(const Vector &v1, const Vector &v2) {
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
}
点积跟两向量的夹角关系是:
(v.w) = |v| |w| cosθ
如果两个非退化(即非(0,0,0))的向量相互垂直, 则(v.w)为零; 反之,也成立. 两个或多个相互垂直的向量被称为是正交的(orthogonal), 一组正交的单位向量被称为规格化正交的(orthonormal).
假定u,v,w是向量, s是纯量, 则有下列性质:
(u . v) = (v . u)
(su . v) = s(v . u)
(u . (v + w)) = (u . v) + (u . w)
我们常常要计算点积的绝对值, 故有如下函数:
inline float AbsDot(const Vector &v1, const Vector &v2) {
return fabsf(Dot(v1,v2));
}
叉积是另一个很有用的向量操作. 给定三维空间的两个向量, 叉积v × w 是垂直于两者的向量. 注意这个新向量的朝向是由坐标系的左右手定则(handedness)决定的.给定两个正交的向量 v 和 w, 那么, (v, w, v × w) 就按照给定的左右手定则形成一个坐标系.
在左手系中, 叉积定义为:
(v × w)x = vy wz – vz wy
(v × w)y = vz wx – vx wz
(v × w)z = vx wy – vy wx
有一个帮助记忆的公式是计算下面矩阵的行列式值:
其中i, j, k分别代表轴(1, 0, 0), (0, 1, 0), (0, 0, 1). 注意这只是一个记忆工具, 而不是严格的数学表达, 因为矩阵把纯量和向量混合在一起用了.
inline Vector Cross(const Vector &v1, const Vector &v2) {
return Vector((v1.y * v2.z) – (v1.z * v2.y),
(v1.z * v2.x) – (v1.x * v2.z),
(v1.x* v2.y) – (v1.y * v2.z));
}
从叉积的定义中,我们得出:
| v × w | = | v | |w| sin θ (θ是v和w的夹角)
从上式可以看出, 两个相互垂直的单位向量的叉积也是一个单位向量. 如果两个向量平行, 则它们的叉积是个退化的向量. 另外, 可以看出, 以两个向量v1 , v2为边的平行四变形面积是| v1 × v2 |.
2.2.4 向量正规化
把一个向量变换成具有相同方向的单位向量就是向量的正规化, 方法是将向量的各个分量除以向量的长度:
float LengthSquared() const { return x * x + y * y + z * z;}
float Length() const { return sqrtf(LengthSquared());}
inline Vector Normalize (const Vector &v) {
Return v / v.Length();
}
2.2.5 由一个向量建立的坐标系
我们会经常用一个向量构造一个坐标系. 由于叉积跟两个向量垂直, 我们可以通过两次叉积(该向量跟任意一个向量叉积得到第二个向量, 第一和第二向量叉积得到第三个向量) 来得到三个相互垂直的向量,因而得到一个坐标系.
Pbrt所用的方法是: 给定一个正规化的向量v1, 把该向量其中一个分量置零并互换另外两个分量的值, 然后对之正规化,就得到第二个向量 v2 (可以验证v1和v2相互垂直), 再由v1和v2的叉积得到第三个向量:
inline void CoordinateSystem(const Vector &v1, Vector *v2, Vector *v3) {
if(fabsf(v1.x) > fabsf(v1.y)) {
Float invLen = 1.f/sqrtf(v1.x * v1.x + v1.z * v1.z);
*v2 = Vector(-v1.z * invLen, 0.f, v1.x * invLen);
}
else{
Float invLen = 1.f/sqrtf(v1.y * v1.y + v1.z * v1.z);
*v2 = Vector(0.f, v1.z * invLen, -v1.y* invLen);
}
*v3 = Cross(v1, *v2);
}
2.3 点
Class COREDLL Point {
Public:
};
点是三维空间的位置.虽然它跟向量一样也是用(x,y,z)三个坐标值表示, 但由于它们本质上的不同, 处理它们的方式也是不同的.
Float x, y, z;
跟Vector的构造器一样, Point构造器也是用可选的参数设置x,y,z的坐标值:
Point(float _x = 0, float _y = 0, float _z = 0)
: x(_x), y(_y), z(_z) {
}
有一些Point的函数返回一个Vector,或者用一个Vector作为参数. 比如, 把一个向量加到一个点上, 就是相当于将它在给定的方向上偏移而得到一个新的向量. 同样地, 两个点相减,得到它们之间的向量:
Point operator+ (const Vector &v) const {
return Point(x + v.x, y + v.y, z + v.z);
}
Point &operator += (const Vector &v) {
x += v.x; y += v.y; z += v.z;
return *this;
}
Vector operator- (const Point &p) const {
return Vector(x – p.x, y – p.y, z – p.z);
}
Point operator- (const Vector &v) const {
return Point(x – v.x, y – v.y, z – v.z);
}
Point &operator -= (const Vector &v) {
x -= v.x; y -= v.y; z -= v.z;
Return *this;
}
下面是求两点之间距离的函数:
inline float Distance(const Point &p1, const Point &p2) {
return (p1 – p2).Length();
}
inline float DistanceSquared(const Point &p1, const Point &p2) {
return (p1 – p2).LengthSquared();
}
虽然点乘以纯量不具数学意义, 但是Point类仍然支持纯量乘的定义, 用以求多个点的加权和. 其实现跟Vector中的实现类似, 从略.
2.4 法向量
class COREDLL Normal {
public:
};
法向量是在给定点上垂直于表面的向量。它可以被定义成两个互相不平行的表面切向量的叉积。虽然法向量跟向量很相似,但是应知它们的不同:因为法向量是根据它跟特定的曲面来定义的,在某些情况下跟向量是不同的,特别是使用变换的时候。(见第2.8节)。
Normal和Vector的实现很相似,都是用三个浮点数x,y,z表示,并定义了法向量之间的加,减,纯量乘,正规化等运算。但是,法向量不能跟一个点相加,也不能取两个法向量的叉积。还有,法向量不一定是正规化的。
Normal提供了由一个Vector初始化一个Normal的构造器。由于Normal和Vector有细微的差别,我们不希望它们之间有隐性的转换。为此,C++的explicit关键词可以保证它们之间显性的转换。
explict Normal(const Vector &v)
: x(v.x), y(v.y), z(v.z) {}
explict Vector(const Normal &n);
inline Vector::Vector(const Normal &n)
:x(n.x), y(n.y), z(n.z) { }
这样一来,如果声明了Vector v; Normal n; 那么 n = v 就是非法的,必须用显式的转换:n = Normal(v).
我们还重载了Dot()和AbsDot() 函数来覆盖求法向量和向量之间的求点积的各种组合情况, 另外,其它跟Vector类似的函数都不提及了。
2.5 光线
class COREDLL Ray {
public:
};
光线是一条由其原点和方向定义的射线。pbrt用Ray类来表达光线,其中用一个Point成员变量表示其原点,用一个Vector表示其方向:
Point o;
Vector d;
光线的参数化形式是一个关于纯量t的方程:
r(t) = o + t d 0 ≤t ≤ ∞
Ray类还包含两个值mint和maxt,把光线限定在[r(mint), r(maxt)]区间之间。 它们声明为mutable, 这意味着即使它们所在的Ray是const, 也是可以被改变的。其目的就是方便光线/物体的求交, 因为在这过程中,总是要记录最近的交点所对应的t值。
mutable float mint, maxt;
为了模拟运动模糊效果, 每条光线还需要一个时间值:
float time;
Ray的构造器很简单明了:
Ray() : mint(RAY_EPSILON), maxt(INFINITY), time(0.f) {}
Ray(const Point &origin, const Vector &direction,
float start = RAY_EPSION, float end = INFINITY, float t = 0.f)
: o(origin), d(direction), mint(start), maxt(end), time(t) {}
#define RAY_EPSILON 1e-3f
注意我们用一个极小的数(RAY_EPSILON)来初始化mint, 而不是用0, 原因是避免因浮点计算精度而引起的自相交的错误, 这是一个在光线追踪中的很典型的手法。
我们还重载函数操作符“()”, 来求和参数t对应的点:
Point operator() (float t) const { return o + d * t;}
这样,我们可以很方便地写类似下面的代码:
Ray r(Point(0,0,0), Vector(1,2,3));
Point p = r(1.7);
2.5.1 光线微分
为了更好地利用第11章定义的纹理函数进行反走样,pbrt对每条被追踪的光线都保持着一些附加的信息。 在第11.1节, 这些信息用在Texture类中估算一小部分的场景在图像平面上的投影面积。这样,Texture类就可以计算出纹理在这个面积上的平均值,从而得到更好的图像。
RayDifferential是Ray的子类, 并包含两条辅助光线的附加信息。 这两条光线表示从主光线向x和y方向分别偏置一个像素而得到的相机光线。确定了这三条光线投射到被着色物体上的区域,Texture就可以估算出用于反走样的平均值。
class COREDLL RayDifferential : public Ray {
public:
};
RayDifferential() { hasDifferentials = false;}
RayDifferential(const Point &org, const Vector &dir) : Ray(org, dir) {
hasDifferentials = false;
}
注意我们用到关键字explicit,防止不经意的Ray到RayDifferential的转换。 变量hasDifferentials被初始化为false, 表示相邻的两条光线还是未知的。
explicit RayDifferential(const Ray &ray) : Ray(ray) {
hasDifferentials = false;
}
bool hasDifferentials;
Ray rx, ry;
2.6 三维包围盒
class COREDLL BBOX {
public:
};
pbrt所要渲染的场景经常包含计算很费时的物体。 一个包含整个物体的三维包围体对很多操作而言都会非常有用。比如, 如果光线没有穿过包围盒, 就不必求光线和其中所包围的物体的交点了。
包围体的有效性跟两个因素有关:计算包围体的时间化费和包围盒包围物体的紧密程度。如果哦包围体太“宽松”了,就会浪费很多不必要的计算;反过来, 如果强求非常紧密的包围体,那么包围体很可能变得太复杂,时间耗费也会不菲。
包围体有很多种, pbrt用到沿轴的包围盒(axis-aligned bounding boxes, AABB). 其他的常见的选择包括沿方向的包围盒(oriented bounding boxes, OBB)和包围球。AABB可以由一个顶点和分别沿x,y,z轴方向的三个长度值来表示, 也可以由包围盒上两个相对的顶点来表示。pbrt就是用两点表示的,一个点的坐标是x,y,z的最小值,另一个是x,y,z的最大值。
BBOX缺省构造器把包围盒的范围定义成退化的 : pMin.x >pMax.x, 即是空包围盒。
BBox() {
pMin = Point (INFINITY, INFINITY, INFINITY);
pMax= Point (-INFINITY, -INFINITY, -INFINITY);
};
Point pMin, pMax;
有时我们用到包含一个点的包围盒:
BBox(const Point &p) : pMin(p), pMax(p) {}
我们还可以用两个点p1, p2来构造BBOX, p1和p2不必满足p1.x <= p2.x等条件, 构造器可以计算出最大、最小值:
BBox(const Point &p1,const Point &p2 ) {
pMin = Point(min(p1.x, p2.x), min(p1.y, p2.y), min(p1.z, p2.z));
pMax = Point(max(p1.x, p2.x), max(p1.y, p2.y), max(p1.z, p2.z));
}
给定一个包围盒和一个点,BBox::Union()计算并返回一个包含该点和原包围盒的新包围盒:
< BBox Method Definitions> =
COREDLL BBox Union(const BBOX &b, const Point &p) {
BBox ret = b;
ret.pMin.x = min(b.pMin.x, p.x);
ret.pMin.y = min(b.pMin.y, p.y);
ret.pMin.z = min(b.pMin.z, p.z);
ret.pMax.x = min(b.pMax.x, p.x);
ret.pMax.y = min(b.pMax.y, p.y);
ret.pMax.z = min(b.pMax.z, p.z);
return ret;
}
同样地,我们可以构造一个包含两个包围盒的包围盒:
friend COREDLL BBox Union(const BBox &b, const BBox &b2);
很容易判定两个包围盒是否重叠:
bool Overlaps(const BBox &b) {
bool x = (pMax.x >= b.pMin.x) && (pMin.x <= b.pMax.x);
bool y = (pMax.y >= b.pMin.y) && (pMin.y <= b.pMax.y);
bool z = (pMax.z >= b.pMin.z) && (pMin.z <= b.pMax.z);
return (x && y && z);
}
下面函数判定一个点是否在包围盒内:
bool Inside(const Point &pt) const {
return (pt.x >= pMin.x && pt.x <= pMax.x &&
pt.y >= pMin.y && pt.y<= pMax.y &&
pt.z >= pMin.z && pt.z<= pMax.z);
}
BBox::Expand()用来扩张包围盒, BBox::Volume()用来计算包围盒的体积:
void Expand(float delta) {
pMin -= Vector(delta, delta, delta);
pMax += Vector(delta, delta, delta);
}
float Volume() const{
Vector d = pMax – pMin;
return d.x * d.y * d.z;
}
BBox::MaximumExtent()返回最长的那个轴。在建造kd树时,我们用它决定沿那个轴划分。
int BBox::MaximumExtent() const {
Vector diag = pMax – pMin;
if (diag.x > diag.y && diag.x > diag.z)
return 0;
else if (diag.y > diag.z) return 1;
else return 2;
}
BBox::BoundingSphere()返回包含该包围盒的球的中心和半径。 虽然包围球比对应的包围盒要宽松得多, 但有时仍是很有用的。在第15章, 我们用它得到包含整个场景的包围球,用以生成可能跟场景相交的随机光线。
int BBox::BoundingSphere(Point *c, float *rad) const {
*c = 0.5f * pMin + 0.5*pMax;
*rad = Distance(*c, pMax);
}
Categories: Garfield's Diary