1

Prologue

This is my attempt to re-ask the closed Generating supporters for 3D printing as it is interesting question but lacking important details ... This is intended as Q&A and currently I am working on the code for the answer but feel free to answer (I accept the best answer).

Problem description

OK here some basic info about the problem:

As this is a huge problem I will focus on the generic mesh/support-pattern merging geometry problem.

In a nutshell If we want to print any mesh we can do it only if it is connected to the starting plane up to angle of ~45 degrees (+/- for different printing technologies). So if we got parts that are not connected to this plane we need to create a bridge that will hold/connect it to it. Something like this (image taken from the page linked above):

overview

Of coarse we need to add as small amount of material possible and still has it strong enough to hold our mesh in place without bending. On top of all this we need to weaken the support near the mesh so it can be break off after printing easily.

Do not forget that shape and placement is dependent on many things like material and technology used, heat flow.

Question:

To narrow this huge topic to answerable question let us focus solely on this problem:

How to merge 3D triangulated mesh (boundary representation like STL) with predefined support pattern (like 3 side prism) connecting it from defined plane perpendicularly ?

Using simple C++.

Spektre
  • 49,595
  • 11
  • 110
  • 380
  • just throwing basic ideas out there (hence the comment and not answer): if the support pattern is predefined can it not be merged with the existing to be printed mesh using constructive solid geometry ? if the mesh to be printed is not predefined, but the triangles are known maybe the triangles can be traversed, normals calculated and based on that, only the faces pointing downward at the ground, with an area large enough will be considered for needing support structures. In terms of generating the structures themselves, a naive projection of the face to the ground plane and splitting... – George Profenza Mar 29 '18 at 07:11
  • ...into a grid (with enough spacing to save material) would generate the points at which to generate each support column: the bottom point is the ground plane, the top being is the intersection with the face that needs splitting, the points are connected via an extrusion with a variable width and the top of is a face at the same angle as the face that needs splitting. In terms of figuring out the parameters for the support structure (thickness of material, density on faces that need support, etc.), perhaps voxelising the mesh and running a physics simulation (adding mass/etc) might help... – George Profenza Mar 29 '18 at 07:23
  • ...doing a low-res simulation capturing some of the potential issues (but not all). if these support columns are generated parametrically hopefully they can be fed to an optimisation algorithm (e.g. genetic algorithm, where the fitness rule is based on minimising time/cost of the simulation and maximising stability/fidelity with the original input mesh) – George Profenza Mar 29 '18 at 07:28
  • another thought is is analysing the triangles as mentioned above, but if the mesh has a top and bottom side (this may not apply for highly irregular meshes), evaluate which side has the most "stable" triangle and flip the model accordingly. perhaps the model can be split by a horizontal plane into two or more smaller models that can flipped with the slice plan on the bottom then reassembled physically ? (adhesive/magnets/etc.) ? – George Profenza Mar 29 '18 at 07:35
  • 1
    @GeorgeProfenza this is intended as an Q&A (I have ideas how to do this) I will answer when finished coding ... but feel free to answer if it will be better than mine I will accept. There is a problem to consider with STL however as you need not to break the STL triangulation rules. Yes traversing the triangles is the way. The main problem is the final stage of the support near mesh and triangle subdivision ... some density and rules has to be in place to make this work correctly and efficiently. – Spektre Mar 29 '18 at 07:49
  • @GeorgeProfenza I posted first part of the answer. Check it out in case you're interested – Spektre Mar 29 '18 at 12:08
  • I am not sure what to think of this question. On the one hand, the 3D printing context makes it much too broad (because placing supports is a big research area on its own). On the other hand, the narrowed question is almost too specific to be useful for other people in the intended Q/A format. In the end, it is just CSG. Generating the prisms isn't really that hard, even if they have a narrower tip. – Nico Schertler Mar 29 '18 at 17:39
  • @Spektre well done (sorry I didn't focus on the core of what you were asking) – George Profenza Mar 29 '18 at 18:03
  • @NicoSchertler exactly mi taught but ahmed wanted to see how it is done anyway ... the only thing really hard to achieve correctly is the last bullet (the STL joints) as the STL specs are a bit fighting with this but I already know how to solve it (after consultation with friend from the field) and that is by defining separate solids for support and for mesh but still physically connected ... this way the mesh does not need to be retriangulated nor containing holes... – Spektre Mar 29 '18 at 18:15
  • I added the final part ... the grace period will end in ~20h so if nothing better pops up here and not get closed (votes stop at 2 yesterday) I will accept my answer ... – Spektre Mar 30 '18 at 10:39

1 Answers1

3

OK lets start with the absolute basics.

  1. support shape

    You can use any shape to meet the specifics of used printing technology. The easiest to generate within STL is 3 side prism like shape which contains 2 triangular bases (top and bottom) and 3 sides all of which have 2 triangles. So 8 triangles total.

    support pattern shape

    This shape will start at some base plane (Z=0) and will go up until it hits the mesh. However to make this work the support must have a small gap between mesh and itself where we will add our weakened joint structure with mesh latter on.

  2. support pattern

    there are a lot of options here so I chose the simplest (not foul proof however) and that is to place the supports in a uniform grid with constant distance grid between the supports.

    so simply cast a ray from each grid position on the base plane in up direction and check for intersection with mesh. If found place the support at that position with height just gap below the intersection point.

  3. Joints

    The idea is to join fan of very thin supports in cone like shape connecting & covering the supported surface above main support prism with less than 45 deg angle (so the gap should be big enough to cover grid distance in such manner).

    The main problem here is that we must subdivide the triangles we are connecting to so we meet the STL mesh properties. To solve the connection problem (avoid holes or breaking connection requirements of the STL) we can use different solid for supports and different for our mesh. That will also allow us to touch surfaces without re-triangulating them making this a lot easier task.

    For simplicity I chose tetrahedron shape which is simple to construct from triangles and also present weakness at the mesh/support joint.

So let us take some test STL mesh and place it above our base plane:

placement

and place our main supports:

main supports

and also the joints:

joints

joint base detail

Here VCL/C++ code for this STL3D.h:

//---------------------------------------------------------------------------
//--- simple STL 3D mesh ----------------------------------------------------
//---------------------------------------------------------------------------
#ifndef _STL3D_h
#define _STL3D_h
//---------------------------------------------------------------------------
#ifdef ComctrlsHPP
TProgressBar *progress=NULL;        // loading progress bar for realy big STL files
#endif
void _progress_init(int n);
void _progress     (int ix);
void _progress_done();
//---------------------------------------------------------------------------
class STL3D                         // STL 3D mesh
    {                                                                      
public:
    double center[3],size[3],rmax;  // bbox center,half sizes, max(size[])
    struct _fac
        {
        float p[3][3];              // triangle vertexes CCW order
        float n[3];                 // triangle unit normal pointing out
        WORD attr;
        _fac()  {}
        _fac(_fac& a)   { *this=a; }
        ~_fac() {}
        _fac* operator = (const _fac *a) { *this=*a; return this; }
        //_fac* operator = (const _fac &a) { ...copy... return this; }
        void compute()                                  // compute normal
            {
            float a[3],b[3];
            vectorf_sub(a,p[1],p[0]);
            vectorf_sub(b,p[2],p[1]);
            vectorf_mul(n,a,b);
            vectorf_one(n,n);
            }
        double intersect_ray(double *pos,double *dir)   // return -1 or distance to triangle and unit ray intersection
            {
            double p0[3],p1[3],p2[3];                   // input triangle vertexes
            double e1[3],e2[3],pp[3],qq[3],rr[3];       // dir must be unit vector !!!
            double t,u,v,det,idet;
            // get points
            vector_ld(p0,p[0][0],p[0][1],p[0][2]);
            vector_ld(p1,p[1][0],p[1][1],p[1][2]);
            vector_ld(p2,p[2][0],p[2][1],p[2][2]);
            //compute ray triangle intersection
            vector_sub(e1,p1,p0);
            vector_sub(e2,p2,p0);
            // Calculate planes normal vector
            vector_mul(pp,dir,e2);
            det=vector_mul(e1,pp);
            // Ray is parallel to plane
            if (fabs(det)<1e-8) return -1.0;
            idet=1.0/det;
            vector_sub(rr,pos,p0);
            u=vector_mul(rr,pp)*idet;
            if ((u<0.0)||(u>1.0)) return -1.0;
            vector_mul(qq,rr,e1);
            v=vector_mul(dir,qq)*idet;
            if ((v<0.0)||(u+v>1.0)) return -1.0;
            // distance
            t=vector_mul(e2,qq)*idet;
            if (t<0.0) t=-1.0;
            return t;
            }
        };
    List<_fac> fac;                         // faces

    STL3D() { reset(); }
    STL3D(STL3D& a) { *this=a; }
    ~STL3D() {}
    STL3D* operator = (const STL3D *a) { *this=*a; return this; }
    //STL3D* operator = (const STL3D &a) { ...copy... return this; }

    void reset(){ fac.num=0; compute(); }   // clear STL
    void draw();                            // render STL mesh (OpenGL)
    void draw_normals(float size);          // render STL normals (OpenGL)
    void compute();                         // compute bbox
    void compute_normals();                 // recompute normals from points
    void supports(reper &obj);              // compute supports with obj placement above base plane z=0
    void load(AnsiString name);
    void save(AnsiString name);
    };
//---------------------------------------------------------------------------
void STL3D::draw()
    {
    _fac *f; int i,j; BYTE r,g,b;
    glBegin(GL_TRIANGLES);
    for (f=fac.dat,i=0;i<fac.num;i++,f++)
        {
        glNormal3fv(f->n);
        if (f->attr<32768)
            {
            r= f->attr     &31; r<<=3;
            g=(f->attr>> 5)&31; g<<=3;
            b=(f->attr>>10)&31; b<<=3;
            glColor3ub(r,g,b);
            }
        for (j=0;j<3;j++) glVertex3fv(f->p[j]);
        }
    glEnd();
    }
//---------------------------------------------------------------------------
void STL3D::draw_normals(float size)
    {
    _fac *f;
    int i; float a[3],b[3];
    glBegin(GL_LINES);
    for (f=fac.dat,i=0;i<fac.num;i++,f++)
        {
        vectorf_add(a,f->p[0],f->p[1]);
        vectorf_add(a,a      ,f->p[2]);
        vectorf_mul(a,a,1.0/3.0);
        vectorf_mul(b,f->n,size); glVertex3fv(a);
        vectorf_add(b,b,a);       glVertex3fv(b);
        }
    glEnd();
    }
//---------------------------------------------------------------------------
void STL3D::compute()
    {
    _fac *f;
    int i,j,k;
    double p0[3],p1[3];
    vector_ld(center,0.0,0.0,0.0);
    vector_ld(size,0.0,0.0,0.0);
    rmax=0.0;
    if (fac.num==0) return;
    // bbox
    for (k=0;k<3;k++) p0[k]=fac.dat[0].p[0][k];
    for (k=0;k<3;k++) p1[k]=fac.dat[0].p[0][k];
    for (f=fac.dat,i=0;i<fac.num;i++,f++)
     for (j=0;j<3;j++)
      for (k=0;k<3;k++)
        {
        if (p0[k]>f->p[j][k]) p0[k]=f->p[j][k];
        if (p1[k]<f->p[j][k]) p1[k]=f->p[j][k];
        }
    vector_add(center,p0,p1); vector_mul(center,center,0.5);
    vector_sub(size  ,p1,p0); vector_mul(size  ,size  ,0.5);
                      rmax=size[0];
    if (rmax<size[1]) rmax=size[1];
    if (rmax<size[2]) rmax=size[2];
    // attr repair
    for (f=fac.dat,i=0;i<fac.num;i++,f++)
     if (f->attr==0) f->attr=32768;
    }
//---------------------------------------------------------------------------
void STL3D::compute_normals()
    {
    _fac *f; int i;
    for (f=fac.dat,i=0;i<fac.num;i++,f++) f->compute();
    }
//---------------------------------------------------------------------------
void STL3D::supports(reper &obj)
    {
    _fac *f,ff;
    int i,j,k;
    double p[3],dp[3],x0,y0,h0,x1,y1,x2,y2,h1,t;
    // some config values first
    const WORD   attr0=31<<10;              // support attr should be different than joint
    const WORD   attr1=31<<5;               // joint attr should be different than mesh,support
    const double grid0=8.0;                 // distance between supports
    const double grid1=2.0;                 // distance between joints
    const double gap=grid0/tan(45.0*deg);// distance between main support and mesh (joint size)
    const double ha=1.0;                    // main support side size
    // do not mess with these
    const double hx=    ha*cos(60.0*deg);   // half size of main support in x
    const double hy=0.5*ha*sin(60.0*deg);   // half size of main support in y
    const double grid2=0.4*hy;              // distance between joints bases
    const double ga=2.0*grid2*grid1/grid0;  // main support side size
    const double gx=hx*grid2/grid0;         // half size of joint support in x
    const double gy=hy*grid2/grid0;         // half size of joint support in y

    // apply placement obj (may lose some accuracy) not needed if matrices are not used
    for (f=fac.dat,i=0;i<fac.num;i++,f++)
        {
        for (j=0;j<3;j++)
            {
            for (k=0;k<3;k++) p[k]=f->p[j][k];  // float->double
            obj.l2g(p,p);
            for (k=0;k<3;k++) f->p[j][k]=p[k];  // double->float
            }
        for (k=0;k<3;k++) p[k]=f->n[k]; // float->double
        obj.l2g_dir(p,p);
        for (k=0;k<3;k++) f->n[k]=p[k]; // double->float
        } compute();

    // create supports
    for (x0=center[0]-size[0]+(0.5*grid0);x0<=center[0]+size[0]-(0.5*grid0);x0+=grid0)
     for (y0=center[1]-size[1]+(0.5*grid0);y0<=center[1]+size[1]-(0.5*grid0);y0+=grid0)
        {
        // cast ray x0,y0,0 in Z+ direction to check for mesh intersection to compute the support height h0
        h0=center[2]+size[2]+1e6;
        vector_ld(p,x0,y0,0.0);
        vector_ld(dp,0.0,0.0,+1.0);
        for (f=fac.dat,i=0;i<fac.num;i++,f++)
            {
            t=f->intersect_ray(p,dp);
            if ((t>=0.0)&&(t<h0)) h0=t;
            }
        if (h0>center[2]+size[2]+1e5) continue; // skip non intersected rays
        h0-=gap; if (h0<0.0) h0=0.0;
        // main suport prism
        ff.attr=attr0;
        // sides
        ff.attr=attr0;
        vectorf_ld(ff.p[0],x0-hx,y0-hy,0.0);
        vectorf_ld(ff.p[1],x0+hx,y0-hy,0.0);
        vectorf_ld(ff.p[2],x0-hx,y0-hy, h0); ff.compute(); fac.add(ff);
        vectorf_ld(ff.p[0],x0+hx,y0-hy,0.0);
        vectorf_ld(ff.p[1],x0+hx,y0-hy, h0);
        vectorf_ld(ff.p[2],x0-hx,y0-hy, h0); ff.compute(); fac.add(ff);

        vectorf_ld(ff.p[0],x0-hx,y0-hy, h0);
        vectorf_ld(ff.p[1],x0   ,y0+hy,0.0);
        vectorf_ld(ff.p[2],x0-hx,y0-hy,0.0); ff.compute(); fac.add(ff);
        vectorf_ld(ff.p[0],x0-hx,y0-hy, h0);
        vectorf_ld(ff.p[1],x0   ,y0+hy, h0);
        vectorf_ld(ff.p[2],x0   ,y0+hy,0.0); ff.compute(); fac.add(ff);

        vectorf_ld(ff.p[0],x0   ,y0+hy, h0);
        vectorf_ld(ff.p[1],x0+hx,y0-hy,0.0);
        vectorf_ld(ff.p[2],x0   ,y0+hy,0.0); ff.compute(); fac.add(ff);
        vectorf_ld(ff.p[0],x0   ,y0+hy, h0);
        vectorf_ld(ff.p[1],x0+hx,y0-hy, h0);
        vectorf_ld(ff.p[2],x0+hx,y0-hy,0.0); ff.compute(); fac.add(ff);
        // base triangles
        vectorf_ld(ff.p[0],x0   ,y0+hy,0.0);
        vectorf_ld(ff.p[1],x0+hx,y0-hy,0.0);
        vectorf_ld(ff.p[2],x0-hx,y0-hy,0.0); ff.compute(); fac.add(ff);
        vectorf_ld(ff.p[0],x0-hx,y0-hy, h0);
        vectorf_ld(ff.p[1],x0+hx,y0-hy, h0);
        vectorf_ld(ff.p[2],x0   ,y0+hy, h0); ff.compute(); fac.add(ff);

        // joints
        for (x1=x0-(0.5*grid0),x2=x0-(0.5*grid2);x1<=x0+(0.5*grid0);x1+=grid1,x2+=ga)
         for (y1=y0-(0.5*grid0),y2=y0-(1.9*grid2);y1<=y0+(0.5*grid0);y1+=grid1,y2+=ga)
            {
            // cast ray x1,y1,0 in Z+ direction to check for mesh intersection to compute the joint height h1
            h1=h0+gap+1e6;
            vector_ld(p,x1,y1,0.0);
            vector_ld(dp,0.0,0.0,+1.0);
            for (f=fac.dat,i=0;i<fac.num;i++,f++)
                {
                t=f->intersect_ray(p,dp);
                if ((t>=0.0)&&(t<h1)) h1=t;
                }
            if (h1>h0+gap+1e5) continue; // skip non intersected rays
            // tetrahedron joints
            ff.attr=attr1;
            // base triangle
            vectorf_ld(ff.p[0],x2   ,y2+gy,h0);
            vectorf_ld(ff.p[1],x2+gx,y2-gy,h0);
            vectorf_ld(ff.p[2],x2-gx,y2-gy,h0); ff.compute(); fac.add(ff);
            // sides
            vectorf_ld(ff.p[0],x2+gx,y2-gy,h0);
            vectorf_ld(ff.p[1],x2   ,y2+gy,h0);
            vectorf_ld(ff.p[2],x1   ,y1   ,h1); ff.compute(); fac.add(ff);
            vectorf_ld(ff.p[0],x2   ,y2+gy,h0);
            vectorf_ld(ff.p[1],x2-gx,y2-gy,h0);
            vectorf_ld(ff.p[2],x1   ,y1   ,h1); ff.compute(); fac.add(ff);
            vectorf_ld(ff.p[0],x2+gx,y2+gy,h0);
            vectorf_ld(ff.p[1],x2-gx,y2-gy,h0);
            vectorf_ld(ff.p[2],x1   ,y1   ,h1); ff.compute(); fac.add(ff);
            }
        }

    // reverse placement obj (may lose some accuracy) not needed if matrices are not used
    for (f=fac.dat,i=0;i<fac.num;i++,f++)
        {
        for (j=0;j<3;j++)
            {
            for (k=0;k<3;k++) p[k]=f->p[j][k];  // float->double
            obj.g2l(p,p);
            for (k=0;k<3;k++) f->p[j][k]=p[k];  // double->float
            }
        for (k=0;k<3;k++) p[k]=f->n[k]; // float->double
        obj.g2l_dir(p,p);
        for (k=0;k<3;k++) f->n[k]=p[k]; // double->float
        } compute();
    }
//---------------------------------------------------------------------------
void STL3D::load(AnsiString name)
    {
    int   adr,siz,hnd;
    BYTE *dat;
    AnsiString lin,s;
    int i,j,l,n;
    _fac f;

    reset(); f.attr=0;
    siz=0;
    hnd=FileOpen(name,fmOpenRead);
    if (hnd<0) return;
    siz=FileSeek(hnd,0,2);
        FileSeek(hnd,0,0);
    dat=new BYTE[siz];
    if (dat==NULL) { FileClose(hnd); return; }
    FileRead(hnd,dat,siz);
    FileClose(hnd);

    adr=0; s=txt_load_str(dat,siz,adr,true);
    // ASCII
    if (s=="solid")
        {
        _progress_init(siz); int progress_cnt=0;
        for (adr=0;adr<siz;)
            {
            progress_cnt++; if (progress_cnt>=128) { progress_cnt=0; _progress(adr); }
            lin=txt_load_lin(dat,siz,adr,true);
            for (i=1,l=lin.Length();i<=l;)
                {
                s=str_load_str(lin,i,true);
                if (s=="solid") { name=str_load_str(lin,i,true); break; }
                if (s=="endsolid") break;
                if (s=="facet")
                    {
                    j=0;
                    s=str_load_str(lin,i,true);
                    f.n[0]=str2num(str_load_str(lin,i,true));
                    f.n[1]=str2num(str_load_str(lin,i,true));
                    f.n[2]=str2num(str_load_str(lin,i,true));
                    }
                if (s=="vertex")
                 if (j<3)
                    {
                    f.p[j][0]=str2num(str_load_str(lin,i,true));
                    f.p[j][1]=str2num(str_load_str(lin,i,true));
                    f.p[j][2]=str2num(str_load_str(lin,i,true));
                    j++;
                    if (j==3) fac.add(f);
                    }
                break;
                }
            }
        }
    // binary
    else{
        adr=80;
        n=((DWORD*)(dat+adr))[0]; adr+=4;
        fac.allocate(n); fac.num=0;
        _progress_init(n); int progress_cnt=0;
        for (i=0;i<n;i++)
            {
            if (adr+50>siz) break;  // error
            progress_cnt++; if (progress_cnt>=128) { progress_cnt=0; _progress(i); }
            f.n[0]=((float*)(dat+adr))[0]; adr+=4;
            f.n[1]=((float*)(dat+adr))[0]; adr+=4;
            f.n[2]=((float*)(dat+adr))[0]; adr+=4;
            for (j=0;j<3;j++)
                {
                f.p[j][0]=((float*)(dat+adr))[0]; adr+=4;
                f.p[j][1]=((float*)(dat+adr))[0]; adr+=4;
                f.p[j][2]=((float*)(dat+adr))[0]; adr+=4;
                }
            f.attr=((WORD*)(dat+adr))[0]; adr+=2;   // attributes
            fac.add(f);
            }
        }
    _progress_done();
    delete[] dat;
    compute();
    }
//---------------------------------------------------------------------------
void STL3D::save(AnsiString name)
    {
    // ToDo
    }
//---------------------------------------------------------------------------
void _progress_init(int n)
    {
    #ifdef ComctrlsHPP
    if (progress==NULL) return;
    progress->Position=0;
    progress->Max=n;
    progress->Visible=true;
    #endif
    }
//---------------------------------------------------------------------------
void _progress     (int ix)
    {
    #ifdef ComctrlsHPP
    if (progress==NULL) return;
    progress->Position=ix;
    progress->Update();
    #endif
    }
//---------------------------------------------------------------------------
void _progress_done()
    {
    #ifdef ComctrlsHPP
    if (progress==NULL) return;
    progress->Visible=false;
    #endif
    }
//---------------------------------------------------------------------------
#endif
//---------------------------------------------------------------------------

Usage is simple:

#include "STL3D.h"                  // STL mesh (this is the important stuff)
STL3D mesh;                         // point cloud and tetrahedronal mesh

mesh.load("space_invader_magnet.stl");
mesh.supports(obj); //  obj is object holding 4x4 uniform matrix of placement if you STL is already placed than it is not needed

I used a lot of stuff from mine OpenGL engine like dynamic List<> template:


List<double> xxx; is the same as double xxx[];
xxx.add(5); adds 5 to end of the list
xxx[7] access array element (safe)
xxx.dat[7] access array element (unsafe but fast direct access)
xxx.num is the actual used size of the array
xxx.reset() clears the array and set xxx.num=0
xxx.allocate(100) preallocate space for 100 items

or vector and matrix math (vectorf_ works with float* and vector_ with double) which is not too important. If you need the math see:

If the STL is already placed (no matrix) than no placement conversions nor the obj is needed at all. The code reflects the bullets above. I wanted to keep it as simple as I could so no optimizations are there yet.

The gap and grid constants are hard-coded in the supports function and are not yet set to the valid values.

[Notes]

Now this barely cover only the very basic of the problem and there are a lot of edge cases left un-handled to keep this "short". The code itself does not check if triangles are above the 45 degree slope but that can be done with simple normal angle checking like:

if (acos(dot(normal,(0.0,0.0,1.0))<45.0*deg) continue;

There is the need to add also the supports between parts of mesh for example if your object has more layers than only the first layer will be supported from base plane. the rest must use the layer below itself ... and using weakened joint on both sides of support. That is similar to place the first layer of supports you just need to cast the ray in both directions ... or cast continuous ray going through the whole bbox and check for start/end surfaces by analyzing normal direction to ray (simple sign of dot product). For example this is mesh placement that would possibly need this (for some technologies):

star

While designing the supports take in mind you should meet the proper winding rule (CCW) and normal direction (out) for the printing process ...

Spektre
  • 49,595
  • 11
  • 110
  • 380