您的位置:首页 > 其它

PBRT阅读:第二章 几何和变换 第2.1 - 2.6节

2012-08-18 10:19 357 查看
http://69.163.227.177/forum.php?mod=viewthread&tid=4291

二章
几何和变换

几乎所有图形软件都以几何类(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 向量

<Geometry Delcarations> =

class COREDLL Vector {

public:

<Vector Public Methods >

<Vector Public Data >

};

一个向量表达了三维空间内的一个方向, 它由三个浮点数定义:

<Vector Public Data>=

float x, y, z;

x,y,z被定义为公共成员, 不太符合C++的封装原则, 但我们这样做是为了代码的清晰和效率.

缺省情况下, (x,y,z)被设成0. 用户可以选择给定任意值:

<Vector Public Methods>=

Vector(float _x = 0, float _y = 0, float _z = 0)

: x(_x), y(_y), z(_z) {

}

2.2.1 向量运算

向量加法运算:

<Vector Public Methods> +=

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 Public Methods> +=

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;

}

<Geometry Inline Functions> =

inline Vector operator*(float f, const Vector &v) {

return v*f;

}

类似地,我们可以定义纯量除法 "operator/" 和 "operator /=", 此略.

Vector类还有一个"取负值"的单操作符定义, 用来返回一个方向相反的向量:

<Vector Public Methods> +=

Vector operator-() const {

return Vector(-x, -y, -z);

}

下面两个函数可以用索引值0,1,2方便地使用向量的各个分量: v[0]­得到x值, v[1]­得到y值, v[2]­得到z值.

<Vector Public Methods> +=

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.

<Geometry Inline Functions> =

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)

我们常常要计算点积的绝对值, 故有如下函数:

<Geometry Inline Functions> =

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). 注意这只是一个记忆工具, 而不是严格的数学表达, 因为矩阵把纯量和向量混合在一起用了.

<Geometry Inline Functions> +=

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 向量正规化

把一个向量变换成具有相同方向的单位向量就是向量的正规化, 方法是将向量的各个分量除以向量的长度:

<Vector Public Methods> +=

float LengthSquared() const { return x * x + y * y + z * z;}

float Length() const { return sqrtf(LengthSquared());}

<Geometry Inline Function> +=

inline Vector Normalize (const Vector &v) {

Return v / v.Length();

}

2.2.5 由一个向量建立的坐标系

我们会经常用一个向量构造一个坐标系. 由于叉积跟两个向量垂直, 我们可以通过两次叉积(该向量跟任意一个向量叉积得到第二个向量, 第一和第二向量叉积得到第三个向量) 来得到三个相互垂直的向量,因而得到一个坐标系.

Pbrt所用的方法是: 给定一个正规化的向量v1, 把该向量其中一个分量置零并互换另外两个分量的值, 然后对之正规化,就得到第二个向量 v2 (可以验证v1和v2相互垂直), 再由v1和v2的叉积得到第三个向量:

<Geometry Inline Functions> +=

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 点

<Geometry Declarations> +=

Class COREDLL Point {

Public:

<Point Methods>

<Point Public Data>

};

点是三维空间的位置.虽然它跟向量一样也是用(x,y,z)三个坐标值表示, 但由于它们本质上的不同, 处理它们的方式也是不同的.

<Point Public Data> =

Float x, y, z;

跟Vector的构造器一样, Point构造器也是用可选的参数设置x,y,z的坐标值:

<Point Mehods> =

Point(float _x = 0, float _y = 0, float _z = 0)

: x(_x), y(_y), z(_z) {

}

有一些Point的函数返回一个Vector,或者用一个Vector作为参数. 比如, 把一个向量加到一个点上, 就是相当于将它在给定的方向上偏移而得到一个新的向量. 同样地, 两个点相减,得到它们之间的向量:

<Point Methods> +=

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;

}

<Point Methods> +=

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;

}

下面是求两点之间距离的函数:

<Geometry Inline Functions> +=

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 法向量

<Geometry Declarations> +=

class COREDLL Normal {

public:

<Normal Methods>

<Normal Public Data>

};

法向量是在给定点上垂直于表面的向量。它可以被定义成两个互相不平行的表面切向量的叉积。虽然法向量跟向量很相似,但是应知它们的不同:因为法向量是根据它跟特定的曲面来定义的,在某些情况下跟向量是不同的,特别是使用变换的时候。(见第2.8节)。

Normal和Vector的实现很相似,都是用三个浮点数x,y,z表示,并定义了法向量之间的加,减,纯量乘,正规化等运算。但是,法向量不能跟一个点相加,也不能取两个法向量的叉积。还有,法向量不一定是正规化的。

Normal提供了由一个Vector初始化一个Normal的构造器。由于Normal和Vector有细微的差别,我们不希望它们之间有隐性的转换。为此,C++的explicit关键词可以保证它们之间显性的转换。

<Normal Methods> =

explict Normal(const Vector &v)

: x(v.x), y(v.y), z(v.z) {}

<Vector Public Methods> +=

explict Vector(const Normal &n);

<Geometry Inline Functions> +=

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 光线

<Geometry Declarations> +=

class COREDLL Ray {

public:

<Ray Public Methods>

<Ray Public Data>

};

光线是一条由其原点和方向定义的射线。pbrt用Ray类来表达光线,其中用一个Point成员变量表示其原点,用一个Vector表示其方向:

<Ray Public Data> =

Point o;

Vector d;

光线的参数化形式是一个关于纯量t的方程:

r(t) = o + t d 0 ≤t ≤ ∞

Ray类还包含两个值mint和maxt,把光线限定在[r(mint), r(maxt)]区间之间。 它们声明为mutable, 这意味着即使它们所在的Ray是const, 也是可以被改变的。其目的就是方便光线/物体的求交, 因为在这过程中,总是要记录最近的交点所对应的t值。

<Ray Public Data > +=

mutable float mint, maxt;

为了模拟运动模糊效果, 每条光线还需要一个时间值:

<Ray Public Data> +=

float time;

Ray的构造器很简单明了:

<Ray Public Methods> =

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) {}

<Global Constants> =

#define RAY_EPSILON 1e-3f

注意我们用一个极小的数(RAY_EPSILON)来初始化mint, 而不是用0, 原因是避免因浮点计算精度而引起的自相交的错误, 这是一个在光线追踪中的很典型的手法。

我们还重载函数操作符“()”, 来求和参数t对应的点:

<Ray Public Methods> +=

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就可以估算出用于反走样的平均值。

<Geometry Declarations> +=

class COREDLL RayDifferential : public Ray {

public:

<RayDifferential Methods>

<RayDifferential Public Data>

};

<RayDifferential Methods> =

RayDifferential() { hasDifferentials = false;}

RayDifferential(const Point &org, const Vector &dir) : Ray(org, dir) {

hasDifferentials = false;

}

注意我们用到关键字explicit,防止不经意的Ray到RayDifferential的转换。 变量hasDifferentials被初始化为false, 表示相邻的两条光线还是未知的。

<RayDifferential Methods> +=

explicit RayDifferential(const Ray &ray) : Ray(ray) {

hasDifferentials = false;

}

<RayDifferential Public Data> =

bool hasDifferentials;

Ray rx, ry;

2.6 三维包围盒

<Geometry Declarations> +=

class COREDLL BBOX {

public:

<BBOX Public Methods>

<BBOX Public Data>

};

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 Public Methods> =

BBox() {

pMin = Point (INFINITY, INFINITY, INFINITY);

pMax= Point (-INFINITY, -INFINITY, -INFINITY);

};

<BBOX Public Data> =

Point pMin, pMax;

有时我们用到包含一个点的包围盒:

<BBOX Public Methods> +=

BBox(const Point &p) : pMin(p), pMax(p) {}

我们还可以用两个点p1, p2来构造BBOX, p1和p2不必满足p1.x <= p2.x等条件, 构造器可以计算出最大、最小值:

<BBOX Public Methods> +=

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;

}

同样地,我们可以构造一个包含两个包围盒的包围盒:

<BBox Public Methods> +=

friend COREDLL BBox Union(const BBox &b, const BBox &b2);

很容易判定两个包围盒是否重叠:

<BBox Public Methods> +=

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);

}

下面函数判定一个点是否在包围盒内:

<BBox Public Methods> +=

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()用来计算包围盒的体积:

<BBox Public Methods> +=

void Expand(float delta) {

pMin -= Vector(delta, delta, delta);

pMax += Vector(delta, delta, delta);

}

<BBox Public Methods> +=

float Volume() const{

Vector d = pMax - pMin;

return d.x * d.y * d.z;

}

BBox::MaximumExtent()返回最长的那个轴。在建造kd树时,我们用它决定沿那个轴划分。

<BBox Public Methods> +=

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章, 我们用它得到包含整个场景的包围球,用以生成可能跟场景相交的随机光线。

<BBox Public Methods> +=

int BBox::BoundingSphere(Point *c, float *rad) const {

*c = 0.5f * pMin + 0.5*pMax;

*rad = Distance(*c, pMax);

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐