// Graphics Math Library // // Copyright (C) 1982, 1985, 1992, 1995-1998 Earl F. Glynn, Overland Park, KS. // All Rights Reserved. UNIT GraphicsMathLibrary; // Matrix/Vector Operations for 2D/3D Graphics INTERFACE USES SysUtils; {Exception} CONST sizeUndefined = 1; size2D = 3; // 'size' of 2D homogeneous vector or transform matrix size3D = 4; // 'size' of 3D homogeneous vector or transform matrix TYPE EVectorError = CLASS(Exception); EMatrixError = CLASS(Exception); TAxis = (axisX, axisY, axisZ); TCoordinate = (coordCartesian, coordSpherical, coordCylindrical); TDimension = (dimen2D, dimen3D); // two- or three-dimensional TYPE TIndex = 1..4; // index of 'TMatrix' and 'TVector' TYPEs TMatrix = // transformation 'matrix' RECORD size: TIndex; matrix: ARRAY[TIndex,TIndex] OF DOUBLE END; Trotation = (rotateClockwise, rotateCounterClockwise); // Normally the TVector TYPE is used to define 2D/3D homogenous // cartesian coordinates for graphics, i.e., (x,y,1) for 2D and // (x,y,z,1) for 3D. // // Cartesian coordinates can be converted to spherical (r, theta, phi), // or cylindrical coordinates (r,theta, z). Spherical or cylindrical // coordinates can be converted back to cartesian coordinates. TVector = RECORD size: TIndex; CASE INTEGER OF 0: (vector: ARRAY[TIndex] OF DOUBLE); 1: (x: DOUBLE; y: DOUBLE; z: DOUBLE; // contains 'h' for 2D cartesian vector h: DOUBLE) END; // Vector Operations FUNCTION Vector2D (CONST xValue, yValue: DOUBLE): TVector; FUNCTION Vector3D (CONST xValue, yValue, zValue: DOUBLE): TVector; FUNCTION AddVectors (CONST u,v: TVector): TVector; FUNCTION Transform (CONST u: TVector; CONST a: TMatrix): TVector; FUNCTION DotProduct (CONST u,v: TVector): DOUBLE; FUNCTION CrossProduct(CONST u,v: TVector): TVector; // Basic Matrix Operations FUNCTION Matrix2D (CONST m11,m12,m13, // 2D "graphics" matrix m21,m22,m23, m31,m32,m33: DOUBLE): TMatrix; FUNCTION Matrix3D (CONST m11,m12,m13,m14, // 3D "graphics" matrix m21,m22,m23,m24, m31,m32,m33,m34, m41,m42,m43,m44: DOUBLE): TMatrix; FUNCTION MultiplyMatrices (CONST a,b: TMatrix): TMatrix; FUNCTION InvertMatrix (CONST a,b: TMatrix; VAR determinant: DOUBLE): TMatrix; // Transformation Matrices FUNCTION RotateMatrix (CONST dimension: TDimension; CONST xyz : TAxis; CONST angle : DOUBLE; CONST rotation : Trotation): TMatrix; FUNCTION ScaleMatrix (CONST s: TVector): TMatrix; FUNCTION TranslateMatrix (CONST t: TVector): TMatrix; FUNCTION ViewTransformMatrix (CONST coordinate: TCoordinate; CONST azimuth {or x}, elevation {or y}, distance {or z}: DOUBLE; CONST ScreenX, ScreenY, ScreenDistance: DOUBLE): TMatrix; // conversions FUNCTION FromCartesian (CONST ToCoordinate: TCoordinate; CONST u: TVector): TVector; FUNCTION ToCartesian (CONST FromCoordinate: TCoordinate; CONST u: TVector): TVector; FUNCTION ToDegrees(CONST angle {radians}: DOUBLE): DOUBLE {degrees}; FUNCTION ToRadians(CONST angle {degrees}: DOUBLE): DOUBLE {radians}; // miscellaneous FUNCTION Defuzz(CONST x: DOUBLE): DOUBLE; FUNCTION GetFuzz: DOUBLE; PROCEDURE SetFuzz(CONST x: DOUBLE); IMPLEMENTATION VAR fuzz : DOUBLE; // ************************* Vector Operations ************************* // This procedure defines two-dimensional homogeneous coordinates (x,y,1) // as a single 'vector' data element 'u'. The 'size' of a two-dimensional // homogenous vector is 3. FUNCTION Vector2D (CONST xValue, yValue: DOUBLE): TVector; BEGIN WITH RESULT DO BEGIN x := xValue; y := yValue; z := 1.0; // should be 'h' but let's use the 'z' slot to keep size := size2D // subscripting possible END END {Vector2D}; // This procedure defines three-dimensional homogeneous coordinates // (x,y,z,1) as a single 'vector' data element 'u'. The 'size' of a // three-dimensional homogenous vector is 4. FUNCTION Vector3D (CONST xValue, yValue, zValue: DOUBLE): TVector; BEGIN WITH RESULT DO BEGIN x := xValue; y := yValue; z := zValue; h := 1.0; // homogeneous coordinate size := size3D END END {Vector3D}; // AddVectors adds two vectors defined with homogeneous coordinates. FUNCTION AddVectors (CONST u,v: TVector): TVector; VAR i: TIndex; BEGIN IF (u.size IN [size2D..size3D]) AND (v.size IN [size2D..size3D]) AND (u.size = v.size) THEN BEGIN RESULT.size := u.size; FOR i := 1 TO u.size-1 DO {2D + 2D = 2D or 3D + 3D = 3D} BEGIN RESULT.vector[i] := u.vector[i] + v.vector[i] END; RESULT.vector[u.size] := 1.0 {homogeneous coordinate} END ELSE raise EVectorError.Create('Vector Addition Mismatch') END {AddVectors}; // 'Transform' multiplies a row 'vector' by a transformation 'matrix' // resulting in a new row 'vector'. The 'size' of the 'vector' and 'matrix' // must agree. To save execution time, the vectors are assumed to contain // a homogeneous coordinate. FUNCTION Transform (CONST u: TVector; CONST a: TMatrix): TVector; VAR i,k : TIndex; temp: DOUBLE; BEGIN RESULT.size := a.size; IF a.size = u.size THEN BEGIN FOR i := 1 TO a.size-1 DO BEGIN temp := 0.0; FOR k := 1 TO a.size DO BEGIN temp := temp + u.vector[k]*a.matrix[k,i]; END; RESULT.vector[i] := Defuzz(temp) END; RESULT.vector[a.size] := 1.0 {assume homogeneous coordinate} END ELSE raise EMatrixError.Create('Transform multiply error') END {Transform}; // Assume vector contains 'extra' homogeneous coordinate -- ignore it. FUNCTION DotProduct (CONST u,v: TVector): DOUBLE; VAR i: INTEGER; BEGIN IF (u.size = v.size) THEN BEGIN RESULT := 0.0; FOR i := 1 TO u.size-1 DO BEGIN RESULT := RESULT + u.vector[i] * v.vector[i]; END; END ELSE RAISE EMatrixError.Create('Vector dot product error') END {DotProduct}; // Assume vector contains 'extra' homogeneous coordinate -- ignore it. FUNCTION CrossProduct(CONST u,v: TVector): TVector; BEGIN IF (u.size = v.size) AND (u.size = size3D) THEN BEGIN RESULT := Vector3D( u.y*v.z - v.y*u.z, -u.x*v.z + v.x*u.z, u.x*v.y - v.x*u.y) END ELSE RAISE EMatrixError.Create('Vector cross product error') END {CrossProduct}; // *********************** Basic Matrix Operations ********************** FUNCTION Matrix2D (CONST m11,m12,m13, m21,m22,m23, m31,m32,m33: DOUBLE): TMatrix; BEGIN WITH RESULT DO BEGIN matrix[1,1] := m11; matrix[1,2] := m12; matrix[1,3] := m13; matrix[2,1] := m21; matrix[2,2] := m22; matrix[2,3] := m23; matrix[3,1] := m31; matrix[3,2] := m32; matrix[3,3] := m33; size := size2D END END {Matrix2D}; FUNCTION Matrix3D (CONST m11,m12,m13,m14, m21,m22,m23,m24, m31,m32,m33,m34, m41,m42,m43,m44: DOUBLE): TMatrix; BEGIN WITH RESULT DO BEGIN matrix[1,1] := m11; matrix[1,2] := m12; matrix[1,3] := m13; matrix[1,4] := m14; matrix[2,1] := m21; matrix[2,2] := m22; matrix[2,3] := m23; matrix[2,4] := m24; matrix[3,1] := m31; matrix[3,2] := m32; matrix[3,3] := m33; matrix[3,4] := m34; matrix[4,1] := m41; matrix[4,2] := m42; matrix[4,3] := m43; matrix[4,4] := m44; size := size3D END END {Matrix3D}; // Compound geometric transformation matrices can be formed by multiplying // simple transformation matrices. This procedure only multiplies together // matrices for two- or three-dimensional transformations, i.e., 3x3 or 4x4 // matrices. The multiplier and multiplicand must be of the same dimension. FUNCTION MultiplyMatrices (CONST a,b: TMatrix): TMatrix; VAR i,j,k: TIndex; temp : DOUBLE; BEGIN RESULT.size := a.size; IF a.size = b.size THEN FOR i := 1 TO a.size DO BEGIN FOR j := 1 TO a.size DO BEGIN temp := 0.0; FOR k := 1 TO a.size DO BEGIN temp := temp + a.matrix[i,k]*b.matrix[k,j]; END; RESULT.matrix[i,j] := Defuzz(temp) END END ELSE EMatrixError.Create('MultiplyMatrices error') END {MultiplyMatrices}; // This procedure inverts a general transformation matrix. The user need // not form an inverse geometric transformation by keeping a product of // the inverses of simple geometric transformations: translations, rotations // and scaling. A determinant of zero indicates no inverse is possible for // a singular matrix. FUNCTION InvertMatrix (CONST a,b: TMatrix; VAR determinant: DOUBLE): TMatrix; VAR c : TMatrix; i,i_pivot: TIndex; i_flag : ARRAY[TIndex] OF BOOLEAN; j,j_pivot: TIndex; j_flag : ARRAY[TIndex] OF BOOLEAN; modulus : DOUBLE; n : TIndex; pivot : DOUBLE; pivot_col: ARRAY[TIndex] OF TIndex; pivot_row: ARRAY[TIndex] OF TIndex; temporary: DOUBLE; BEGIN c := a; // The matrix inversion algorithm used here WITH c DO // is similar to the "maximum pivot strategy" BEGIN // described in "Applied Numerical Methods" FOR i := 1 TO size DO // by Carnahan, Luther and Wilkes, BEGIN // pp. 282-284. i_flag[i] := TRUE; j_flag[i] := TRUE END; modulus := 1.0; i_pivot := 1; // avoid initialization warning j_pivot := 1; // avoid initialization warning FOR n := 1 TO size DO BEGIN pivot := 0.0; IF ABS(modulus) > 0.0 THEN BEGIN FOR i := 1 TO size DO IF i_flag[i] THEN FOR j := 1 TO size DO IF j_flag[j] THEN IF ABS(matrix[i,j]) > ABS(pivot) THEN BEGIN pivot := matrix[i,j]; // largest value on which to pivot i_pivot := i; // indices of pivot element j_pivot := j END; IF Defuzz(pivot) = 0 // If pivot is too small, consider THEN modulus := 0 // the matrix to be singular ELSE BEGIN pivot_row[n] := i_pivot; pivot_col[n] := j_pivot; i_flag[i_pivot] := FALSE; j_flag[j_pivot] := FALSE; FOR i := 1 TO size DO IF i <> i_pivot THEN FOR j := 1 TO size DO // pivot column unchanged for elements IF j <> j_pivot // not in pivot row or column ... THEN matrix[i,j] := (matrix[i,j]*matrix[i_pivot,j_pivot] - matrix[i_pivot,j]*matrix[i,j_pivot]) / modulus; // 2x2 minor / modulus FOR j := 1 TO size DO IF j <> j_pivot // change signs of elements in pivot row THEN matrix[i_pivot,j] := -matrix[i_pivot,j]; temporary := modulus; // exchange pivot element and modulus modulus := matrix[i_pivot,j_pivot]; matrix[i_pivot,j_pivot] := temporary END END END {FOR n} END {WITH}; determinant := Defuzz(modulus); IF determinant <> 0 THEN BEGIN RESULT.size := c.size; // The matrix inverse must be unscrambled FOR i := 1 TO c.size DO // if pivoting was not along main diagonal. FOR j := 1 TO c.size DO RESULT.matrix[pivot_row[i],pivot_col[j]] := Defuzz(c.matrix[i,j]/determinant) END ELSE EMatrixError.Create('InvertMatrix error') END {InvertMatrix}; // *********************** Transformation Matrices ******************** // This procedure defines a matrix for a two- or three-dimensional rotation. // To avoid possible confusion in the sense of the rotation, 'rotateClockwise' // or 'roCounterlcockwise' must always be specified along with the axis // of rotation. Two-dimensional rotations are assumed to be about the z-axis // in the x-y plane. // // A rotation about an arbitrary axis can be performed with the following // steps: // (1) Translate the object into a new coordinate system where (x,y,z) // maps into the origin (0,0,0). // (2) Perform appropriate rotations about the x and y axes of the // coordinate system so that the unit vector (a,b,c) is mapped into // the unit vector along the z axis. // (3) Perform the desired rotation about the z-axis of the new // coordinate system. // (4) Apply the inverse of step (2). // (5) Apply the inverse of step (1). FUNCTION RotateMatrix (CONST dimension: TDimension; CONST xyz : TAxis; CONST angle : DOUBLE; CONST rotation : Trotation): TMatrix; VAR cosx : DOUBLE; sinx : DOUBLE; TempAngle: DOUBLE; BEGIN TempAngle := angle; // Use TempAngle since "angle" is CONST parameter IF rotation = rotateCounterClockwise THEN TempAngle := -TempAngle; cosx := Defuzz( COS(TempAngle) ); sinx := Defuzz( SIN(TempAngle) ); CASE dimension OF dimen2D: CASE xyz OF axisX,axisY: EMatrixError.Create('Invalid 2D rotation matrix. Specify axisZ'); axisZ: RESULT := Matrix2D ( cosx, -sinx, 0, sinx, cosx, 0, 0, 0, 1) END; dimen3D: CASE xyz OF axisX: RESULT := Matrix3D ( 1, 0, 0, 0, 0, cosx, -sinx, 0, 0, sinx, cosx, 0, 0, 0, 0, 1); axisY: RESULT := Matrix3D ( cosx, 0, sinx, 0, 0, 1, 0, 0, -sinx, 0, cosx, 0, 0, 0, 0, 1); axisZ: RESULT := Matrix3D ( cosx, -sinx, 0, 0, sinx, cosx, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); END END END {RotateMatrix}; // 'ScaleMatrix' accepts a 'vector' containing the scaling factors for // each of the dimensions and creates a scaling matrix. The size // of the vector dictates the size of the resulting matrix. FUNCTION ScaleMatrix (CONST s: TVector): TMatrix; BEGIN CASE s.size OF size2D: RESULT := Matrix2D (s.x, 0, 0, 0, s.y, 0, 0, 0, 1); size3D: RESULT := Matrix3D (s.x, 0, 0, 0, 0, s.y, 0, 0, 0, 0, s.z, 0, 0, 0, 0, 1) END END {ScaleMatrix}; // 'TranslateMatrix' defines a translation transformation matrix. The // components of the vector 't' determine the translation components. // (Note: 'Translate' here is from kinematics in physics.) FUNCTION TranslateMatrix (CONST t: TVector): TMatrix; BEGIN CASE t.size OF size2D: RESULT := Matrix2D ( 1, 0, 0, 0, 1, 0, t.x, t.y, 1); size3D: RESULT := Matrix3D ( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, t.x, t.y, t.z, 1) END END {TranslateMatrix}; // 'ViewTransformMatrix' creates a transformation matrix for changing // from world coordinates to eye coordinates. The location of the 'eye' // from the 'object' is given in spherical (azimuth,elevation,distance) // coordinates or Cartesian (x,y,z) coordinates. The size of the screen // is 'ScreenX' units horizontally and 'ScreenY' units vertically. The // eye is 'ScreenDistance' units from the viewing screen. A large ratio // 'ScreenDistance/ScreenX (or ScreenY)' specifies a narrow aperature // -- a telephoto view. Conversely, a small ratio specifies a large // aperature -- a wide-angle view. This view transform matrix is very // useful as the default three-dimensional transformation matrix. Once // set, all points are automatically transformed. FUNCTION ViewTransformMatrix (CONST coordinate: TCoordinate; CONST azimuth {or x}, elevation {or y}, distance {or z}: DOUBLE; CONST ScreenX, ScreenY, ScreenDistance: DOUBLE): TMatrix; CONST HalfPI = PI / 2.0; VAR a : TMatrix; b : TMatrix; cosm : DOUBLE; // COS(-angle) hypotenuse: DOUBLE; sinm : DOUBLE; // SIN(-angle) temporary : DOUBLE; u : TVector; x : DOUBLE ABSOLUTE azimuth; // x and azimuth are synonyms y : DOUBLE ABSOLUTE elevation; // synonyms z : DOUBLE ABSOLUTE distance; // synonyms BEGIN CASE coordinate OF coordCartesian: u := Vector3D (-x, -y, -z); coordSpherical: BEGIN temporary := -distance * COS(elevation); u := Vector3D (temporary * COS(azimuth - HalfPI), temporary * SIN(azimuth - HalfPI), -distance * SIN(elevation)); END END; a := TranslateMatrix(u); // translate origin to 'eye' b := RotateMatrix (dimen3D, axisX, HalfPI, rotateClockwise); a := MultiplyMatrices(a,b); CASE coordinate OF coordCartesian: BEGIN temporary := SQR(x) + SQR(y); hypotenuse := SQRT(temporary); cosm := -y/hypotenuse; sinm := x/hypotenuse; b := Matrix3D ( cosm, 0, sinm, 0, 0, 1, 0, 0, -sinm, 0, cosm, 0, 0, 0, 0, 1); a := MultiplyMatrices (a,b); cosm := hypotenuse; hypotenuse := SQRT(temporary + SQR(z)); cosm := cosm/hypotenuse; sinm := -z/hypotenuse; b := Matrix3D ( 1, 0, 0, 0, 0, cosm, -sinm, 0, 0, sinm, cosm, 0, 0, 0, 0, 1) END; coordSpherical: BEGIN b := RotateMatrix (dimen3D,axisY,-azimuth,rotateCounterClockwise); a := MultiplyMatrices(a,b); b := RotateMatrix (dimen3D,axisX,elevation,rotateCounterClockwise); END END {CASE}; a := MultiplyMatrices (a,b); u := Vector3D (ScreenDistance/(0.5*ScreenX), ScreenDistance/(0.5*ScreenY),-1.0); b := ScaleMatrix (u); // reverse sense of z-axis; screen transformation RESULT := MultiplyMatrices (a,b) END {ViewTransformMatrix}; // *************************** Conversions ************************** // This function converts the vector parameter from Cartesian // coordinates to the specified type of coordinates. FUNCTION FromCartesian (CONST ToCoordinate: TCoordinate; CONST u: TVector): TVector; VAR phi : DOUBLE; r : DOUBLE; temp : DOUBLE; theta: DOUBLE; BEGIN IF ToCoordinate = coordCartesian THEN RESULT := u ELSE BEGIN RESULT.size := u.size; IF (u.size = size3D) AND (ToCoordinate = coordSpherical) THEN BEGIN // spherical 3D temp := SQR(u.x)+SQR(u.y); // (x,y,z) -> (r,theta,phi) r := SQRT(temp+SQR(u.z)); IF Defuzz(u.x) = 0.0 THEN theta := PI/4 ELSE theta := ARCTAN(u.y/u.x); IF Defuzz(u.z) = 0.0 THEN phi := PI/4 ELSE phi := ARCTAN(SQRT(temp)/u.z); RESULT.x := r; RESULT.y := theta; RESULT.z := phi END ELSE BEGIN // cylindrical 2D/3D or spherical 2D // (x,y) -> (r,theta) or (x,y,z) -> (r,theta,z) r := SQRT( SQR(u.x) + SQR(u.y) ); IF Defuzz(u.x) = 0.0 THEN theta := PI/4 ELSE theta := ARCTAN(u.y/u.x); RESULT.x := r; RESULT.y := theta END END END {FromCartesian}; // This function converts the vector parameter from specified coordinates // into Cartesian coordinates. FUNCTION ToCartesian (CONST FromCoordinate: TCoordinate; CONST u: TVector): TVector; VAR phi : DOUBLE; r : DOUBLE; sinphi: DOUBLE; theta : DOUBLE; BEGIN RESULT := u; IF FromCoordinate = coordCartesian THEN RESULT := u ELSE BEGIN RESULT.size := u.size; IF (u.size = size3D) AND (FromCoordinate = coordSpherical) THEN BEGIN // spherical 3D r := u.x; // (r,theta,phi) -> (x,y,z) theta := u.y; phi := u.z; sinphi := SIN(phi); RESULT.x := r * COS(theta) * sinphi; RESULT.y := r * SIN(theta) * sinphi; RESULT.z := r * COS(phi) END ELSE BEGIN // cylindrical 2D/3D or spherical 2D r := u.x; // (r,theta) -> (x,y) or (r,theta,z) -> (x,y,z) theta := u.y; RESULT.x := r * COS(theta); RESULT.y := r * SIN(theta) END END END {ToCartesian}; // Convert angle in radians to degrees. FUNCTION ToDegrees(CONST angle {radians}: DOUBLE): DOUBLE {degrees}; BEGIN RESULT := 180.0/PI * angle END {ToRadians}; // Convert angle in degrees to radians. FUNCTION ToRadians (CONST angle: DOUBLE): DOUBLE; BEGIN RESULT := PI/180.0 * angle END {ToRadians}; // *************************** Miscellaneous ************************** // 'Defuzz' is used for comparisons and to avoid propagation of 'fuzzy', // nearly-zero values. DOUBLE calculations often result in 'fuzzy' values. // The term 'fuzz' was adapted from the APL language. FUNCTION Defuzz(CONST x: DOUBLE): DOUBLE; BEGIN IF ABS(x) < fuzz THEN RESULT := 0.0 ELSE RESULT := x END {Defuzz}; FUNCTION GetFuzz: DOUBLE; BEGIN RESULT := fuzz END {GetFuzz}; PROCEDURE SetFuzz(CONST x: DOUBLE); BEGIN fuzz := x END {SetFuzz}; INITIALIZATION SetFuzz(1.0E-6) END {GraphicsMath UNIT}.