// gPlane.h #ifndef gPlaneh #define gPlaneh #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #include "g3.h" /* gPlane is the first step to posing uniformly scaled 3D Objects. I'll start by describing the problems this solves: A Right-Handed World: Put a piece of paper on a table and think of its bottom left corner at the Global Origin - the point (0,0,0). The bottom edge is the X-axis pointing to the right, and the Left edge is the Y-Axis pointing forwards just like a map; the Z-Axis is 'up'. The paper is occupying the global XY plane: the place where z is always zero and only X and Y have useful values. A pen resting on the paper would exist in the space where X and Y and Z are all positive numbers. A pen to the left of the page would have a negative X value. This global coordinate system is using the 'Right-Hand Rule' (thumb is x, fore-finger is y, middle-finger is z), which is useful to remember when playing with 'Vector Cross Products'. Put the back of your hand on the bottom left corner of your paper, with your thumb pointing along the bottom edge and your fore-finger pointing along the left edge: they are orthogonal (at 90 degrees to each other). Now point your middle finger 'up' and all three fingers are each orthogonal to each other. If you did the same with your left hand, your middle finger would end up pointing down, so choosing a hand to use is choosing how the third dimension is going to relate to the first two. Back to your Right Hand: without moving your (now orthogonal) fingers, move your hand so your thumb touches your nose, and your hands new position describes some random new plane relative to the global plane (the paper). Your new plane has a new origin and probably new axes that no longer line up with the paper, but all planes in the world must have x,y and z axes in the same direction relative to each other (your fingers are not allowed to move although you can move your hand anywhere). When given a choice, I opt for the most natural option: I use the Right Hand Rule because everyone (especially left-handed people) agrees that we live in a right-handed person's world, so it is easy to remember "Its a Right-Handed World". OpenGL uses a Right Hand world with the Y-Axis being up (it's thinking of the world through a vertical screen, not as a map on a desk). DirectX uses a Left Hand world with the Y-Axis being up. Feeding your worlds global coordinate system to another system is not difficult, so (as no standard exists) use the most natural: which, for me, is my right hand and a map (or XY graph that I was taught to draw at school). Another way to see the world as a coordinate system is to think of a tiled floor: each tile is a coordinate in the XY plane. Pick an origin and define the location of your chair relative to the origin. Planes are for drawing on: To fully imagine a new plane that is more useful, we want a plane we can draw on, but rather than hold up a sheet of paper for ages, look for a rectangular flat window with a spot of dirt on. The bottom left-hand corner is the window's origin, its X-Axis is along its bottom, its Y-Axis is up its left-hand edge and its Z-Axis will be towards you. The spot of dirt on the window is at a particular point on the window's plane which you can measure in two dimensions (X,Y) where X is how far from the left edge the spot is (distance from its origin in the X direction), and Y is how far from the bottom edge the spot is (distance from its origin in the Y direction). From the Plane's drawing to the World and back: gPlane holds the window's details and allows you to do things like convert the spot's coordinates from being relative to the Window Plane, to relative to the piece of paper on your desk. Transforming coordinates from Local to Global and back allows us to draw on a two dimensional plane and place the two dimensional objects in the three dimensional world. It also allows us to render the three dimensional world onto a two dimensional screen. If the spot of dirt on the window lines up with something outside (I'm assuming you're currently inside), you can imagine that the thing outside has global coordinates that can be mapped onto the window's two dimensional coordinate system. gPlane can do that for you very quickly and efficiently. To know everything about the window, gPlane needs to hold a the Window's Origin as a point relative to the Global Origin (the corner of your piece of paper). gPlane also needs to store the coordinate system of the window as three orthogonal unit vectors relative to the Global unit vectors (the edges of your piece of paper). It it not enough to just store the Plane's Normal (the Z-Axis) because the spot of dirt can not be located without specifying an axis on the plane. Advanced: There are conventions for creating the other axes from a Normal axis that are used in Computer Aided Machining: a system may specify that the X or Y axis should be horizontal. That leaves two potential directions for the third axis to lie. That decision is made using another boolean: 'Clockwise Twist' which means: looking down the horizontal axis to the origin, the third axis is clockwise from the normal (or not). Those principles are handled within g3Vector::GetAxes which uses HorizontalAxisType to create new axes from a normal vector. So it is necessary to store at least one other axis as well as the Normal... If all three axes are stored, they can be held in such a way that they form a transformation matrix which can directly transform points! This speeds up many processes including 'camera placement' as used by gPose which is derived from gPlane. To make this work, the axes need to be stored as 'unit vectors' which means that the vector length is exactly 1, g3Vector::Normalise() will set the length to 1. The Magic Begins... If 'Origin' is the Local Origin (in World Coordinates) and xAxis, yAxis and zAxis are the normalised local axes, they can be stored as a transformation matrix as follows: M[12]={xAxis.x, yAxis.x, zAxis.x, Origin.x, xAxis.y, yAxis.y, zAxis.y, Origin.y, xAxis.z, yAxis.z, zAxis.z, Origin.z}; Since gPlane owns this transformation matrix, it is natural to think of that matrix as the plane's way to convert from Local to Global coordinates. The method: g3Point TransformToPlane(const g3Point& p) const; simply multiplies the point, p by the above matrix and the result is the point in global coordinates. To translate from global to local coordinates, the global point must be multiplied by the inverse of the matrix. Calculating the inverse of a matrix is verbose but, fortunately, our matrix is a little special in that the 3x3 part holding the normalised vectors is entirely orthogonal and the inverse of an orthogonal matrix is its transpose, so all that is needed for that part is to mirror the positions of the numbers in the matrix around the leading diagonal! The inverse of the point section is a projection that reduces to a dot product leaving: I[12]={xAxis.x, xAxis.y, xAxis.z, -xAxis.Dot(Origin), yAxis.x, yAxis.y, yAxis.z, -yAxis.Dot(Origin), zAxis.x, zAxis.y, zAxis.z, -zAxis.Dot(Origin)}; Its almost interesting that the z projection I[11] is the distance from the global origin of the closest part of the plane to the global origin... Rather than re-calculate these values, gPlane stores both the matrix and its inverse for fast transformations. When you come to serialize a gPlane (save and load) you only need to save xAxis,zAxis and Origin. zAxis is generally the most important one to be accurate, so store that and when you load the gPlane, yAxis=zAxis.Cross(xAxis); Graphics libraries like OpenGL work using 4x4 matrices but that holds no advantages here and makes finding the inverse far more time consuming and verbose. gglCamera.h shows how to implement an OpenGL interface from a gPlane. Flat Land: Imagine a tree with rectangular leaves. Most of the leaves will need their own plane onto which is drawn a rectangle. An optimisation is to notice that all horizontal leaves could use the global XY plane as a drawing surface and the z coordinate says how high off the floor that leaf is. gPlane can recognise that you asked for a horizontal plane and simply use Flat-land (no transormation required). Unless you call NoFlatLand(), optimisations are used if the plane is horizontal and its X-Axis is in the same direction as the global X-Axis (FlatLand==true) and Local coordinates are the same as Global coordinates. Advanced: If you are creating many planes (as you would for a tree with rectangular leaves), it is valuable to minimise the number of planes you are creating: the ideal is to have no parallel planes. Create a g3PV with NormalisePlaneOrigin=true in the constructor and it will take the origin and normal you provide and create a plane with a new origin (on the same plane) which is the closest point on that plane to the Global Origin. When you create the next plane, compare the origins and normals to see if the objects could use the same plane but be on different z-levels. To use this, when you ask your application for a new plane to be created, check if that plane already exists and return a pointer to the existing one if it does. */ class gPlane { // No need to call it g3Plane because there is no g2Plane! friend struct gPlaneTester; protected: bool FlatLand; public: double Inverse[12]; double Matrix [12]; gPlane(); virtual ~gPlane(); gPlane(const double* Matrix, bool IsInverse=false); // Axis-defined Plane gPlane(const g3Vector& zAxis, const g3Point& Origin=g3Point(0,0,0), const g3Vector& dx=g3Vector(0,0,0)); // Axis-defined Plane gPlane(const g3PV& PV, const g3Vector& xAxis=g3Vector(0,0,0)); void Set(const g3PV& PV, const g3Vector& xAxis=g3Vector(0,0,0)); // Set from Origin, Normal(zAxis), and xAxis. The vectors don't need to be normalised. void Set(const g3Vector& zAxis, const g3Point& Origin=g3Point(0,0,0), const g3Vector& xAxis=g3Vector(0,0,0)); // Set from Origin, Normal(zAxis), and xAxis. The vectors don't need to be normalised. void SetToIdentity(); bool SetFromMatrix (const double* M=0); // M=0 sets to identity and returns false bool SetFromInverse(const double* I=0); // I=0 sets to identity and returns false void GetArray(double* dst, bool WantInverse=false) const; void MakeFlatLand(bool flatLand=true); // FlatLand: no transformations required bool SetFlatLand (); // FlatLand: no transformations required void NoFlatLand (); bool IsFlatLand () const; bool IsHorizontal(const g3Vector& v) const; // Dot Product of v with z axis = 0 if perp to z axis. bool IsUp (const g3Vector& v) const; bool IsDown (const g3Vector& v) const; bool IsVertical (const g3Vector& v) const; g3PV GetPV(bool NormalisePlaneOrigin=false) const; g3Vector GetXAxis() const; // Assumes the matrix holds a unit Vector (which it should). g3Vector GetYAxis() const; // Assumes the matrix holds a unit Vector (which it should). g3Vector GetZAxis() const; // Assumes the matrix holds a unit Vector (which it should). g3Point GetOrigin() const; gCoord GetDisplacement() const; // from Origin (negative if in lower global hemisphere) gCoord GetDistanceFromPlane(const g3Point& P) const; // Optimised from: (P-GetOrigin()).Dot(GetZAxis());} g3Point GetClosestPointTo (const g3Point& P) const; // Optimised from: P-dz*(GetDisplacement()+dz.Dot(P))); g3Vector TransformToLocal (const g3Vector& v)const; g3Vector TransformToGlobal (const g3Vector& v)const; g3Point TransformToLocal (const g3Point& p) const; g3Point TransformToGlobal (const g3Point& p) const; #ifdef g2h // Some applications may not want to bother with the 2D classes g2Vector TransformToPlane (const g3Vector& v)const; g3Vector TransformToGlobal (const g2Vector& v)const; g2Point TransformToPlane (const g3Point& p) const; g3Point TransformToGlobal (const g2Point& p) const; g3D Multiply (const g2D& d, const double m[12]) const; g2D Multiply2(const g3D& d, const double m[12]) const; #endif g3D Multiply(const g3D& d, const double m[12]) const; // Note that the system decides to stop FlatLand optimisation as soon as you Set using any of the following methods: // Call SetFlatLand(); to get the system to determine if FlatLand can be set for the plane after the Sets are complete. void SetMatrices(const g3Point& P, const g3Vector& dx, const g3Vector& dy, const g3Vector& dz); void SetOrigin(const g3Point& P); // Must have valid Axes to set Inverse! void SetXAxis(const g3Vector& dx); // Must have a valid Origin! void SetYAxis(const g3Vector& dy); // Must have a valid Origin! void SetZAxis(const g3Vector& dz); // Must have a valid Origin! }; #ifdef Assert static struct gPlaneTester : Tester { gPlaneTester() { Assert(Transform(g3Vector(0, 0, 1), g3Point(1, 0, 0))==g3Point(1,0, 0)); Assert(Transform(g3Vector(0, 0, 1), g3Point(0, 1, 0))==g3Point(0,1, 0)); Assert(Transform(g3Vector(0, 0, 1), g3Point(0, 0, 1))==g3Point(0,0, 1)); Assert(Transform(g3Vector(0, 0, 1), g3Point(1, 1, 1))==g3Point(1,1, 1)); // Assert(Transform(g3Vector(3, 0, 4), g3Point(5, 0, 0))==g3Point(4,0, 3)); // 3,4,5 Triangle (2D) Assert(Transform(g3Vector(9,12,20), g3Point(9,12,20))==g3Point(0,0,25)); // 9,12,20,25 Triangle (3D) gCoord Tmat[12]={0.84661562570854743,0.16531198380769438,0.50587936340168049,0,0 ,0.95053508308550994,-0.31061721752604554,0,-0.532204831156318 , 0.26297338997166103,0.80473785412436516,0}; gCoord Imat[12]={0.84661562570854743,0 ,-0.532204831156318 ,0,0.16531198380769438,0.95053508308550994, 0.26297338997166103,0, 0.50587936340168049,-0.31061721752604554,0.80473785412436516,0}; gPlane M(reinterpret_cast(Tmat)); gPlane I(reinterpret_cast(Imat),true); Assert(M.GetOrigin()==I.GetOrigin()); Assert(M.GetZAxis ()==I.GetZAxis ()); for(int i=0; i<12; ++i) Assert(Tmat[i]==M.Matrix[i]); for(int i=0; i<12; ++i) Assert(Imat[i]==M.Inverse[i]); for(int i=0; i<12; ++i) Assert(Tmat[i]==I.Matrix[i]); for(int i=0; i<12; ++i) Assert(Imat[i]==I.Inverse[i]); } g3Point Transform(g3Vector& zAxis, g3Point& Point) { zAxis.Normalise(); gPlane WP(zAxis); g3Point p1(WP.TransformToPlane(Point)); Assert(Point==WP.TransformToGlobal(p1)); return p1; } } PlaneTester; #endif // def Assert #endif // ndef gPlaneh