.
Multiple Axis Interpolation 19.march.1998 GFX

by Hin Jang

The classic digital line drawing algorithm developed by Jack Bresenham in 1965 has been subject to several optimisations and extensions. The multiple axis interpolator (MAI) algorithm is one such extension. Its derivation was necessary to find a method the interpolate efficiently four axes (red, green, blue and z) across of polygon, in addition to the x and y values [2]. The algorithm uses integer math for speed and its hardware implementation can yield one pixel per clock cycle. In the following discussion, assume x is the major axis and incremented for each loop interation; xs and ys are the starting values for x and y; xe and ye are the ending values for x and y.

A digital differential analyser (DDA) evaluates the value of y without having to calculate

   y  =  mx + b

for each value of x. The linear differential of the line is m and is a constant. Successive values of the (x, y) pair is the result of incrementing x by one and adding m to y. Adding the initial value of y by 0.5 and truncating each successive result will yield the correct integer values for y. As pseudocode, the algorithm is

   DDA(int xs, ys, xe, ye, value)
   {
      int    n, x
      float  m, y

      x = xs
      y = ys + 0.5
      m = (ye - ys) / (xe - xe)
      n = xe - xs

      PutPixel(x, int(y), value)
      while (n is not zero) {
         x++
         y += m
         n--
         PutPixel(x, int(y), value)
      }
   }

The fractional and integer parts of y can be treated independently as long as the integer values generated by adding m to the y-fractional part are transfered to the y-integer part. This is accomplished by subtracting one from the y-fractional part and adding one to the y-integer part when the y-fractional part is greater than one [2]. To avoid the "greater-than-or-equal-to-one" comparison the initial y-fractional value is reduced by one so that a "greater-than-zero" comparison can be used instead. If yi represents the y-integer part and yf represents the y-fractional part, the algorithm becomes

   DDA_2(int xs, ys, xe, ye, value)
   {
      int    n, x, yi
      float  m, y, yf

      x  = xs
      yi = ys
      yf = -0.5
      m  = (ye - ys) / (xe - xe)
      n  = xe - xs

      PutPixel(x, yi, value)
      while (n is not zero) {
         x++
         yf += m
         if (yf is not negative) {
            yf--
            yi++
         }
         n--
         PutPixel(x, yi, value)
      }
   }

If the initial fractional value is scaled by the constant 2(xe - xs), the initial divide is eliminated and the fractional variables become integers. The result is the algorithm that Bresenham presented [1]. The restriction, however, is that m must be within the range 0 and 1. Other slopes are handled by swapping x, y and/or mirroring across the x and/or y axis. Modifying the algorithm DDA( ) above, however, does not have this restriction. Assuming that x still represents the major axis and that z is any other axis, the algorithm becomes

   DDA_3(int xs, zs, xe, ze, value)
   {
      int    mi, zi, n, x
      float  mf, zf

      x  = xs
      zi = zs
      zf = -0.5
      mi = (ze - zs) DIV (xe - xs)
      mf = FRACT((ze - zs) / (xe - xs))
      n  = xe - xs

      PutPixel(x, zi, value)
      while (n is not zero) {
         x++
         zi += mi
         zf += mf
         if (zf is not negative) {
            zf--
            zi++
         }
         n--
         PutPixel(x, zi, value)
      }
   }

Converting all fractional parts to integers by scaling with the constant 2(xe - xs) yields the multiple axis interpolator (MAI) algorithm where
   b MOD a  =  a * FRACT(b / a)
            =  remainder of (b DIV a)

   MAI(int xs, zs, xe, ze, value)
   {
      int    mi, zi, n, x
      float  mf, zf

      x  = xs
      zi = zs
      zf = -(xe - xs)
      mi = (ze - zs) DIV (xe - xs)
      mf = 2 * ((ze - zs) MOD (xe - xs))
      n  = xe - xs

      PutPixel(x, zi, value)
      while (n is not zero) {
         x++
         zi += mi
         zf += mf
         if (zf is not negative) {
            zf -= 2*(xe - xs)
            zi++
         }
         n--
         PutPixel(x, zi, value)
      }
   }

The multiplication within the inner loop can be avoided by precomputing the constant mf2. As such, the final implementation of the MAI algorithm is as follows [2], again written as pseudocode.

   MAI_2(int xs, zs, xe, ze, value)
   {
      int    mi, zi, n, x
      float  mf, mf2, zf

      x   = xs
      zi  = zs
      n   = xe - xs
      mi  = (ze - zs) DIV n
      mf  = 2 * ((ze - zs) MOD n)
      mf2 = mf - 2 * n
      zf  = mf - n

      PutPixel(x, zi, value)
      while (n is not zero) {
         x++
         zi += mi
         if (zf is not negative) {
            zf += mf2
            zi++
         } else {
            zf += mf
         }
         n--
         PutPixel(x, zi, value)
      }
   }



[1] Bresenham, J.E., "Algorithm for Computer Control of a Digital Plotter," IBM Systems Journal, 4(1):25-30, 1965

[2] Swanson, R.W., and L.J. Thayer, "A Fast Shaded-Polygon Renderer," Computer Graphics, SIGGRAPH 1986 Proceedings, 20(4):95-101


.
Level of Detail 28.april.1998 GFX

by Hin Jang

Real-time rendering of dynamic scenes of high complexity require novel algorithms to generate surface models at varying levels of detail. Such techniques must be employed in accomodate the limitations of graphics hardware or to allow such hardware to maintain interactive frame rates. The text herein will discuss briefly a method of reducing the geometric complexity of three-dimensional models. Some of the principles can also be applied to real-time animation of rigid body dynamics [1]. Techniques that are applicable to triangular irregular networks [2] and wavelet-based representations [3] will not be covered.

An ideal level of detail (LOD) algorithm consists of the following features [5]

Deformations of the mesh are assumed to be dynamic and, thus, not governed by pre-recorded events. A LOD algorithm must therefore be able to handle such changes efficiently. The efficiency can be aided by a spatial organisation of mesh components to allow fast indexing of polygons and vertices. Another performance factor is the discreteness of the levels of detail. The transistion between two levels of detail may result in a large net change of polygons. This impacts the rendering capacity of the system and will result in undesirable fluctuations in frame rates. To ensure local changes do not affect the global complexity of the model, polygon densities should be handled separately in variable resolution sub-spaces. Level of detail selection should also be bound to some error metric with respect to the projected image [5]. Factors such as view direction, distance and angle are used to determine the appropriate level of detail to ensure smooth animation of polygonal models.

The premise of one LOD algorithm, developed by Xia et al, involves the edge collapse transformation and its dual, the vertex split transformation. Successive edge collapses result in simplier meshes. For a sequence of k collapses, the original mesh M becomes progressively coarse

   Mk  -->  Mk-1  -->  Mk-2  ...  M0   

Higher detail meshes can be retrieved by applying a sequence of k vertex splits.
   M0  -->  M1  -->  Mk-1  ...  Mk   

The edge collapses are stored in a hierarchy resembling a merge tree. Given an edge pc, for example, the vertex c is merged with vertex p as a result of collapsing the edge. During a vertex split, the child vertex c is created from the parent vertex p. The tree, where such child-parent relations are stored, is created as a preprocess and upwards from the high-detail mesh M to a low-detail mesh M0 over the surface of the object. At level h of the tree, those vertices determined to be children remain at h. Parent vertices are promoted to level h + 1. The construction of this tree continues recursively until either there remains a user-specified minimum of vertices or there are no parent-child relationships among the vertices at the given level [4] [6]. A balanced merge tree is produced by considering the region of influence of an edge e. This region is the the union of triangles adjacent to either endpoint of e. The optimal case for a balanced merge tree occurs when there are no common triangles in their respective regions of influence.

The data structure for a merge tree node is


   struct Node {
      struct Vertex   *vert, **adjacent_vert, **dependent_vert;
      struct Node     *parent, *child[2];
      struct Cone     *cone;
      double           upswitch, downswitch;
      long             adjacent_num, dependent_num;
   };

The collapse of an edge e is permitted when all the vertices defining the boundary of the region of influence of the edge exist and are adjacent to the edge [6]. The edge collapse dependencies, restricting the level difference between adjacent vertices are
  1. child vertex c can collapse to parent vertex p when n0, n1, ..., nk are neighbours of c and p
  2. n0, n1, ..., nk cannot merge with other vertices unless c first merges with p.
The vertex split dependencies that allow a safe split from p to p and c are
  1. parent vertex p can split to c and p when n0, n1, ..., nk are neighbours of p
  2. n0, n1, ..., nk cannot split unless p first splits to p and c.
The variations of the normal vectors of a vertex and its decendants fall within a cone. During the rendering phase of the object, if the normal vector cone of a vertex lies entirely in a direction away from the viewer, the vertex is marked as incative for display. For node v, the upswitch is the distance between v's child and v. The downswitch is the distance between v and its parent. These object-space distances are used to determine when the merging takes place and are built up during the creation of the merge tree. If the projection of the downswitch distance at v in object-space is greater the some preset threshold, refinement at v proceeds. If the projection of the upswitch distance at v in the object-space is less than the threshold, the region occupies very little screen space and can be simplified (i.e., marked as inactive for display)

The pseudocode to build the merge tree is


   Build_MergeTree(Mesh *m, Node **roots)
   {

      current = InitialiseHeap(m)
      next = InitialiseHeap(NULL)

      while (HeapSize(current) > MIN_RESOLUTION) {

         while (HeapSize(current) > 0) {
            edge = ExtractMinEdge(current)
            node = CreateNode(edge)
            SetDependices(node) 
            SetCone(node)
            SetSwitchDistances(node)
            InsertHeap(next, node)
         }

         FreeHeap(current)
         current = next
         next = InitialiseHeap(NULL)
      }

      FlattenHeap(roots, current)

   }

The pseudocode to traverse the merge tree is

   Traverse_MergeTree(Node **current, View view, Lights *lights)
   {
      for (each node in current) {
         switch = EvaluateSwitchDistance(node, view, lights)
         if (switch == REFINE)
            RefineNode(node)
         else if (switch == SIMPLIFY)
            MergeNode(node)
      }
   }

The primary display vertices are those which passed the occlusion test and either
  1. are leaf nodes and none of their parents have been marked as inactive, or
  2. have their immediate child marked as inactive
An examination of the merge dependencies of these primary vertices yields the secondary display vertices. If a vertex v was the result of a vertex split when the vertices vd0, vd1, ..., vdk were present, then these vertices are added to the list. The process continues recursively until no new vertices are added. The resulting set of vertices form the visible portion of the mesh at some level of detail k.



[1] Carlson, D.A., and J.K. Hodgins, Simulation Levels of Detail for Realtime Animation, TR-96-32, College of Computing and Graphics, Visualisation, and Usability Center, Georgia Institute of Technology, 1996

[2] Garland, M., and P.S. Heckbert, Fast Polygonal Approximation of Terrain and Height Fields, Technical Report CMU-CS-95-181, Computer Science Department, Carnegie Mellon University, 1995

[3] Gross, M.H., O.G. Staadt, and R. Gatti, "Efficient Triangular Surface Approximations Using Wavelets and Quadtree Data Structures," IEEE Transactions on Visualisation and Computer Graphics, 2(2):130-143, June 1996

[4] Hoppe, H., T. DeRose, T. Duchamp, J. McDonald, and W. Stuetzle, "Mesh Optimisation," Computer Graphics, SIGGRAPH 1993 Proceedings, 27(4):19-26

[5] Lindstrom, P., D. Koller, W. Ribarsky, L.F. Hodges, N. Faust, G.A. Turner, "Realtime, Continuous Level of Detail Rendering of Height Fields," Computer Graphics, SIGGRAPH 1996 Proceedings, 30(4):109-118

[6] Xia, J.C., J. El-Sana, and A. Varshney, "Adaptive Realtime Level of Detail-Based Rendering for Polygonal Models," IEEE Transactions on Visualisation and Computer Graphics, 3(2):171-183, April 1997


.
Delaunay Tetrahedralisation 28.april.1998 GFX

by Hin Jang

Tetrahedralisation is the process of filling a convex hull of points with tetrahedra. Delaunay tetrahedralisation has the property where the circumsphere of each tetrahedron does not contain any other point inside the sphere, and the vertices of the tetrahedra are those of the data points. Of the several algorithms [3, 4], the one that employs range searching and shelling has been shown to exhibit close to linear time complexity [2].

Given a set of points in , these points are placed into a data structure where a fourth point d can be determined efficently such that the Delaunay sphere <a, b, c, d> does not contain any point on the side of <a, b, c> opposite to the search direction. Herein, a three-dimensional cell structure is laid over the set of points. To compute this space, the first step is to calculate the min-max box of the data set. The space is offset by the point coincidence tolerance TOL so that any points that lie of a structure boundary will be handled appropriately.

   xmin  =  xmin - TOL      xmax  =  xmax + TOL
   ymin  =  ymin - TOL      ymax  =  ymax + TOL
   zmin  =  zmin - TOL      zmax  =  zmax + TOL

The size of the space is the cube root of
    (xmax - xmin)(ymax - ymin)(zmax - zmin)
   -----------------------------------------
                       n

where n is the number of points in the data set. The number of grid cells in the x, y and z directions is given by

   x_res  =  floor((xmax - xmin) / size) + 1
   y_res  =  floor((ymax - ymin) / size) + 1
   z_res  =  floor((zmax - zmin) / size) + 1

With the cell structure now established, to place each data point into one and only one cell the following steps are required:
  1. compute
       cell_x  =  (xi - xmin) / size
       cell_y  =  (yi - ymin) / size
       cell_z  =  (zi - zmin) / size
       i  =  floor(cell_x)
       j  =  floor(cell_y)
       k  =  floor(cell_z)
    
  2. if the cell at (i, j, k) is empty, place (xi, yi, zi) into the cell.
  3. if the cell is already occupied, check for point coincidence. If the current point coincides with any point in the cell, then reject this point. Otherwise, place it into the cell.

For each cell, there is a data structure that links all points that belong to the same cell.


   typedef struct cellnode Cell;

   struct cellnode
   {
      int      used, vertex_number;
      double   x, y, z;
      Cell     *next_point;
   };

where used is a flag to indicate whether a point is used to form a tetrahedron, vertex_number is a number between 0 and n. The normalised coordinates of the point are x, y and z. next_node points to the next node in the same cell.

The process to form the first tetrahedron involves three steps [2]

The next step is to apply range searching to form the first tetrahedron. The technique searches for a fourth point P4 so that the circumscribing sphere of the tetrahedron <P1, P2, P3, P4> does not contain any other point inside the sphere.

The range searching algorithm consists of six steps [2]

  1. compute the normal of the plane of the face.
  2. determine the direction in which the normal extends faster and define this direction as the search direction.
  3. initialise the buffer for each collection of cells in the search direction (i.e., the tunnel) by computing the lowest and highest cells intersecting the plane of the triangle. Between these cells, side tests are required to ensure that the candidate points lie to the right of the plane.
  4. begin the search for the fourth point in the tunnel occupied by the center of the triangle. For the closest point found for the circumsphere that is closest to the Delaunay sphere, compute the boxing box.
  5. search the neighbouring tunnels in the current box. If any new found point yields a circumscribing sphere closer to the last computed Delaunay sphere, update the box.
  6. stop the search when the current box does not contain any unsearch cells of the surrouding tunnels.
The pseudocode for Delaunay tetrahedralisation is

   Tetrahedralise()
   {
      p1 = FindFirstPoint(center cell)
      p2 = FindNearestPoint(p1)
      current_face = FindFirstTriangle(p1, p2)

      p4 = FindFourthPoint(current_face)
      tetrahedron = ConstructTetrahedron(current_face, p4)

      InitialiseFaceList(tetrahedron)
      InitialiseEdgeList(p1)
      InitialiseEdgeList(p2)
      InitialiseEdgeList(p3)
      InitialiseEdgeList(p4)

      while (face_list is not empty) {

         current_face = RemoveTopElement(face_list)

         if (fourth point does not exist)
            p4 = FindFourthPoint(current_face)
         else
            p4 = RestoreSearchResult(current_face)

         if (p4 is found) {

            if (p4 is used) {
               CheckTouchCase()
               UpdateFaceList by either 
                  a) moving rear pointer, or
                  b) adding new faces
            } else {
               UpdateEdgeList by adding new faces
               MarkAsUsed(p4)
            }

         } else
            boundary_face = RemoveTopElement(face_list)

      }
   }
             
The CheckTouchCase( ) routine ensures that no caves are created. The cases in which a cave may result are [2]
  1. one-face touch: given a current face <a, b, c> and a fourth point d is found, the tetrahedron <a, b, c, d> shares a face <a, c, d> with another tetrahedron in which case the appropriate action is to delete <a, b, c> and <a, c, d> from the face list and add two news faces <a, b, d> and <b, c, d>
  2. two-face touch: using the same notation as above, a newly created tetrahedron <a, b, c, d> shares two face with a previously created tetrahedron, <a, c, d> and <b, c, d>. in this case, faces <a, b, c>, <a, c, d> and <b, c, d> are deleted from the face list, and <a, b, d> is added.
  3. edge-and-face touch: if current face <a, d, f> and the fourth point found is b, the tetrahedron <a, d, f, b> shares the face <d, f, b> and the edge <a, b> with previously computed tetrahedra. if accepted the cave <a, b, c, d> is formed. to avoid creating the cave, one-face touch checks are performed for each face of this cave. when such a touch is detected, a check for an edge touch is also performed with the point not lying on the common face. for the example given herein, the current face <a, d, f> is skipped and the previous face in the list is used to form a new tetrahedron.



[1] Fang, T., and L. A. Piegl, "Delaunay Triangulation Using a Uniform Grid", IEEE Computer Graphics and Applications, 13(3):36-47, May 1993

[2] Fang, T.P., and L.A. Piegl, "Delaunay Triangulation in Three Dimensions," IEEE Computer Graphics and Applications, 15(5):62-69, September 1995

[3] Mitchell, S.A., and S.A. Vavasis, Quality Mesh Generation in Three Dimensions, TR-92-1267, Department of Computer Science, Cornell University, February 7, 1992

[4] Dey, T.K., "Delaunay Triangulations in Three Dimensions with Finite Precision Arithmetic," Computer Aided Geometric Design, 9:457-470, 1992