Part 9 of C++ tutorial – a 3D vector & transform library
“Homogeneous coordinates are ubiquitous in computer graphics because they allow common vector operations such as translation, rotation, scaling and perspective projection to be represented as a matrix by which the vector is multiplied.” wikipedia.org/Homogeneous_coordinates
Thus we introduce the HPoint
class as a 4-ordinate vector representation, i.e. x,y,z
with a w
divisor.
class XYZW_Const
{
public:
const double x,y,z,w;
XYZW_Const(double a,double b,double c,double d):x(a),y(b),z(c),w(d) { }
void Set(const XYZW_Const& xyzw)
{ const_cast<double&>(x)=xyzw.x;
const_cast<double&>(y)=xyzw.y;
const_cast<double&>(z)=xyzw.z;
const_cast<double&>(w)=xyzw.w;
}
void SetW(double ww)
{ const_cast<double&>(w)=ww;
};
};
Very similar to the XYZ
triples, this representational class is very similar to XYZ_Const
except it has a fourth double, and a dedicated method to set the fourth element, w
.
The introduction of a fourth element means the VectorBase
class needs tweaking to cater for it (whilst remaining compatible with triple element representations). We can do that by adding a static w
member in the XYZ
and XYZ_Const
classes. Thus XYZ
becomes:
class XYZ // For Vector & Point
{
public:
double x,y,z;
static const double w; // Dummy w
XYZ(double a,double b,double c,double d):x(a),y(b),z(c) { }
void Set(const XYZ& xyz) { *this=xyz; }
};
const double XYZ::w=0.0;
So, for triples, the 4 element constructor ignores the 4th element, and the w
member exists only as a static constant. It can still be referenced as if a member variable, but without increasing the size of the object. This is one of the few examples where it is indispensable to be able to use the dot operator to access a static member (instead of the :: class scoping operator).
The HPoint
class is similar to the Point
class, except that while they can still be scaled by a scalar, it makes less sense to permit them to be directly translated by vectors. Translation is best left to homegenous transforms.
There are some query functions appropriate to HPoint
, i.e. to query whether it can be converted to a Point
(w!=0
), whether it is a direction (point at infinity), whether it is the origin, and whether it is valid (invalid being null – origin at infinity).
The implicit conversion of an HPoint
to a Point
simply involves dividing x,y,z,w
by w
, i.e. such that w'=1
.
Here is the code for the HPoint
class:
class HPoint: protected VectorBase<XYZW_Const>
{
public:
protected:
explicit HPoint(const VectorBase<XYZW_Const>& xyzw):VectorBase<XYZW_Const>(xyzw) { }
public:
using VectorBase<XYZW_Const>::x; // READ ONLY
using VectorBase<XYZW_Const>::y; // For write access, use a Point.
using VectorBase<XYZW_Const>::z; //
using VectorBase<XYZW_Const>::w; //
static const HPoint origin; // [0,0,0,1] NB Origin is xyz=0 for any non-zero value of w
using VectorBase<XYZW_Const>::SetW;
// Constructors
HPoint(double x,double y,double z,double w=1):VectorBase<XYZW_Const>(x,y,z,w) { }
HPoint(const Point& p):VectorBase<XYZW_Const>(p.x,p.y,p.z,1) { }
// Copy & Assign
HPoint(const HPoint& p):VectorBase<XYZW_Const>(p) { }
HPoint& operator=(const HPoint& p) { assign(p); return *this; }
operator Point() const
{ if (IsPoint()) return Point(x/w,y/w,z/w);
throw std::runtime_error("Cannot convert HPoint at infinity to Point");
}
bool IsPoint() const { return w; } // Point not at infinity simply requires non=zero w
bool IsDirection() const { return !w&&!is_null(); } // Direction (point at infinity) requires w=0, with non-zero xyz
bool IsOrigin() const { return w&&is_null(); } // Origin point if non-zero w, and xyz=0
bool IsValid() const { return w||!is_null(); } // Valid if non-zero w, or w=0 && xyz!=0
explicit operator bool() const { return w||!is_null(); } // True if any element non-zero, i.e. valid
bool operator!() const { return !w&&is_null(); } // True if all elements zero, i.e. invalid
// HPoints may be transformed by a scalar
HPoint& operator*=(double m) { multiply_by(m); return *this; }
HPoint& operator/=(double d) { SetW(w*d); return *this; }
friend HPoint operator*(const HPoint& p,double m) { return HPoint(p.multiplication(m)); }
friend HPoint operator*(double m,const HPoint& p) { return HPoint(p.multiplication(m)); }
friend HPoint operator/(const HPoint& p,double d) { return HPoint(p)/=d; }
friend std::ostream& operator<<(std::ostream& os,const HPoint& p) { return os<<'['<<p.x<<','<<p.y<<','<<p.z<<','<<p.w<<']'; } // E.g. [1,2,3,1]
};
const HPoint HPoint::origin=HPoint(0,0,0);
And here is a little test program:
int main() // The program
{
const Point x(1,2,3);
const HPoint p(7,3,5,2),q(x);
cout<<"p="<<p<<"\n";
cout<<"q="<<q<<"\n";
HPoint r=p/4;
cout<<"r=p/4 -> r="<<r<<"\n";
if (r.IsPoint())
cout<<"r is point\n";
else
cout<<"r is not point\n";
r/=0;
cout<<"r/=0 -> r="<<r<<"\n";
if (r.IsDirection())
cout<<"r is direction\n";
else
cout<<"r is not direction\n";
r=p*0;
cout<<"r=p*0 -> r="<<r<<"\n";
if (r.IsOrigin())
cout<<"r is origin\n";
else
cout<<"r is not origin\n";
r/=0;
cout<<"r/=0 -> r="<<r<<"\n";
if (r.IsValid())
cout<<"r is valid\n";
else
cout<<"r is invalid\n";
Point a=static_cast<Point>(p)+Vector(1,2,3);
cout<<"p+[1,2,3]="<<a<<"\n";
return 0;
}
Which produces the following output:
p=[7,3,5,2]
q=[1,2,3,1]
r=p/4 -> r=[7,3,5,8]
r is point
r/=0 -> r=[7,3,5,0]
r is direction
r=p*0 -> r=[0,0,0,2]
r is origin
r/=0 -> r=[0,0,0,0]
r is invalid
p+[1,2,3]=[4.5,3.5,5.5]