biVector.net   /doc

Clean up your Mesh.

plane and simplex
Steven De Keninck, Martin Roelfs, Leo Dorst, David Eelbode

In "Clean up your Mesh" we propose the use of PGA as a geometric representation for $k$-simplices and $k$-complexes, and demonstrate how this leads to elegant formulas for $k$-magnitude, center of mass and moments of inertia. On this page, we provide a brief overview and example implementations.

In the above interactive example, the volume under the plane is calculated without the explicit construction of caps, using only a single scalar for runtime storage. The source code is available here

$k$-simplex and $k$-complex

After embedding our vertices as dual vectors, joining $k+1$ points produces a $k$-simplex carrier $S_k$. $$ v_i = (\mathbf e_0 + \vec v)^*,\qquad S_k = v_0 \vee \dots \vee v_k,\qquad C_k = \sum S_k $$ While the $S_k$ does not contain the vertex positions, it represents the carrier line, plane, etc as well as the length, area, volume, etc of the segment, triangle, tetra, ... respectively. Similarly, while summing the individual $S_k$ into a complex carrier $C_k$ retains no information of the original simplices, the resulting $C_k$ does still encode the magnitude of the complex.

Boundary and Gap

A $k$-complex (e.g. a 3D mesh) $M$ can be represented by the $k$-simplices in its interior (a set of tetrahedrons), or by the $k-1$-simplices on its boundary $\partial M$ (a set of triangles).
Such a boundary $\partial M$ is said to be closed when each $k-2$-simplex it contains is connected to exactly two $k-1$ simplices. (each point on the boundary of a polygon belongs to two edges, or each edge of a 3D mesh belongs to two triangles, etc). When the boundary is not closed, the missing $k-1$ simplices are called the gap and written $\xcancel \partial M$.

$k$-magnitude

The $k$-magnitude (amount, length, area, volume, ...) of any $k$-simplex is now given by $$ {1 \over k!} \lVert S_k \rVert = {1 \over k!} \lVert v_0 \vee \dots \vee v_k \rVert $$ The $k$-magnitude of a $k$-simplex can also be calculated from its boundary $k-1$ complex $$ {1 \over k!} \lVert C_{\partial k-1} \rVert_\infty $$

The $k-1$-magnitude of the sum of the missing $k-1$ simplices from a boundary complex is given by $${1 \over (k-1)!} \lVert C_{\xcancel \partial k-1} \rVert = {1 \over (k-1)!} \lVert C_{\partial k-1} \rVert$$
Cap-free $k$-magnitudes

These formulas allow us to calculate the area of a polygon or the volume of a mesh that is being sliced without the need to reconstruct, or 'cap' the $k$-complex, or without the need to triangulate or subdivide. In a first 2D example we compare this approach to a classic ear-clipping approach. The code for the below example is available here.

Second moments for arbitrary meshes

After calculating the 0th (volume) and 1st (com) moments, it is natural to wonder if the calculation of the 2nd moments of inertia also can be peformed within the PGA framework. The example below (available here) demonstrates the method from the paper for the known case of an arbitrary box.




				
Dimension Agnostic

While the generalisation of our formulae to higher dimensional spaces is fun, it is also trivial - enumerating the boundary boxes of a tesseract will produce a sum of joins of 4 points. Each of which encoding the signed hypervolume with the origin.

The moniker dimension agnostic however applies also in a different and perhaps more unexpected way. Consider the formula for the center of mass, and how it applies to finding the center of mass of an arbitrary planar polygon. We express this c.o.m. as a sum over the boundary edges :

$$ \tfrac 1 6 {\sum_\partial\underbrace{(v_0 + v_1 + o)}_{\stackrel{\tiny \text{2D: point}}{\tiny \text{3D: point}}}\underbrace{(v_0 \vee v_1 \vee o)}_{\stackrel{\tiny \text{2D: scalar}}{\tiny \text{3D: vector}}}}$$ In 2D PGA, the right hand factor $(v_0 \vee v_1 \vee o)$ is a scalar quantity. It is (twice) the signed area of the triangle formed by $v_0,v_1,o$. This signed area is then used as weight for the center of mass ${v_0 + v_1 + o}$ of the same triangle. Hence the final center of mass is a sum of scalar-weighted-points, which is again a point.

In 3D PGA however, there is no well defined signed area as ${(v_0 \vee v_1 \vee o)}$ is no longer a scalar quantity, but a vector. Yet, applying those weights to the triangle com's still produces a sensible outcome. These vector-weighted-points now are a set of lines, orthogonal to their corresponding triangles and through the corresponding com's. The sum of those lines, which are generally skew lines, is always another simple line, orthogonal to our initial polygon, and through it's center of mass.

The situation is illustrated for an identical setup below. Be sure to move the camera on the right and manipulate the points. The weights that are applied to the individual com's of the triangles are displayed in the top left. For the 2D case they are scalar, for the 3D case they are vector.


Expressive, not expensive

PGA expressions are not more expensive than their vector or coordinate equivalents, in fact, they compile to them. A modern GA library indeed allows you to use geometrically meaningful, dimension agnostic and expressive formulas without imposing an extra computational cost. Let us examine how that works out for the basic construction of $k$-simplices. The results below were produced with GAmphetamine.js, but other libraries such as kingdon (py) provide the same functionality.

Vector algebramuls/addsPGA
Length$\small \lVert \vec v_1 - \vec v_0 \rVert$3/5$\small \lVert v_0 \vee v_1 \rVert$
Area $\small \tfrac 1 2 \lVert (\vec v_1 - \vec v_0) \times (\vec v_2 - \vec v_0) \rVert$9/11$\small \tfrac 1 2 \lVert v_0 \vee v_1 \vee v_2 \rVert$
Volume$\small \tfrac 1 6 \lvert \big((\vec v_1 - \vec v_0) \times (\vec v_2 - \vec v_0)\big)\cdot(\vec v_3 - \vec v_0) \rvert$9/14$\small \tfrac 1 6 \lvert v_0 \vee v_1 \vee v_2 \vee v_3 \rvert$

For brevity, let us ignore the square root and scalar prefactor that is equal on both sides and reconstruct the expressions for the squared norm. Does the PGA expression $(v_0 \vee v_1)\widetilde{(v_0 \vee v_1)}$ for the (square of) the length of a line segment, which involves a regressive and a geometric product, really come at just the cost of that single vector subtraction and dot we get with $(\vec v_1 - \vec v_0)\cdot(\vec v_1 - \vec v_0)$ in the Vector algebra case? One might be implied to think not, after all the regressive product alone is already far more expensive.

Let us begin by examining the code generated for the join of two points into a line.

GAmphetamine("3DPGA",()=>{
    var join = (a, b) => a & b;
    var func = Element.compile( join, [Element.point(), Element.point()]);
    console.log(func.toString());
});

// output:
// a & b
// 6 muls / 6 adds
function join_point_point (a,b,res=new classes.bivector()) {
  const a0=a[0],a1=a[1],a2=a[2],b0=b[0],b1=b[1],b2=b[2];
  res[0]=b0-a0;
  res[1]=b1-a1;
  res[2]=b2-a2;
  res[3]=a1*b2-a2*b1;
  res[4]=a2*b0-a0*b2;
  res[5]=a0*b1-a1*b0;
  return res;
}
With six multiplications and the same amount of additions it seems the battle is lost already. However, most of that work is the calculation of the ideal parts of the resulting bivector. These coefficients do not participate in the norm. Therefor, following the join operation with a norm does not increase, but in fact reduce the operation count at the coefficient level.

GAmphetamine("3DPGA",()=>{
    var squared_norm = (a, b) => (a & b) * ~(a & b);
    var func = Element.compile( squared_norm, [Element.point(), Element.point()]);
    console.log(func.toString());
});

// output:
// (a & b) * ~(a & b)
// 3 muls / 5 adds
function squared_norm_point_point (a,b) {
  const t0=a[0]-b[0],t1=a[1]-b[1],t2=a[2]-b[2];
  return t0*t0+t1*t1+t2*t2;
}
The resulting code produces the coordinate expressions we expect: three substractions and a dot product for a total of just 3 multiplies and 5 additions. Similarly, for the squared norm of the join of three points, the following code is produced.

// (a & b & c) * ~(a & b & c)
// 9 muls / 11 adds
function squared_norm_point_point_point (a,b,c) {
  const a0=a[0],a1=a[1],a2=a[2],
        _d0=a1-b[1],_d1=a0-c[0],_d2=a0-b[0],_d3=a1-c[1],_d4=a2-b[2],_d5=a2-c[2],
        u0=_d2*_d3-_d0*_d1,u1=_d2*_d5-_d4*_d1,u2=_d0*_d5-_d4*_d3;
  return u0*u0+u1*u1+u2*u2;
}
Finally, we consider the join of four points. Like its vector counterpart, the scalar tripple product, it produces a scalar result, removing the need to square. The resulting code footprint reduces to the same 9 multiplications and 14 additions.

// (a & b & c & d)
// 9 muls / 14 adds
function join_point_point_point_point (a,b,c,d) {
  const a0=a[0],a1=a[1],a2=a[2],d0=d[0],d1=d[1],d2=d[2],
        _e0=b[0]-a0,_e1=b[1]-a1,_e2=b[2]-a2,
        _d0=a2-c[2],_d1=a1-d1,_d2=a1-c[1],_d3=a2-d2,_d4=a0-c[0],_d5=a0-d0;
  return _e0*(_d2*_d3-_d0*_d1)+_e1*(_d5*_d0-_d3*_d4)+_e2*(_d4*_d1-_d2*_d5);
}
Given a sufficiently capable GA library for a language with sufficient compile time features, the expressive and elegant PGA syntax really is, a lunch paid by the compiler. Absent such tools, simple manual algebraic manipulations will produce identical results.