Direct 3D Printer Control - Part 1

This is the first of a series of posts about how to directly generate GCode for your 3D printer. In the last article I demonstrated how to turn a mesh into GCode with about 12 lines of code. That's fine if you want to experiment with meshes. But what if you want to control the printer yourself? The Gradientspace Slicer is designed to make this easy at several different levels. 

To start, you'll need the geometry3Sharp, gsGCode, and gsSlicer libraries. If you check out the gsSlicerApps repository, this is all set up for you, and you'll find the sample code below in the GeneratedPathsDemo Visual Studio project.

Ok, here is the basic harness we will use for all our examples. This is largely boilerplate code, however you will need to use the right Settings object for your printer (assuming it's explicitly supported, otherwise you can try RepRapSettings, it will work with many printers). And you might need to modify some fields in the Settings. But otherwise you don't need to change this code, we'll just fill in the middle bit

// You'll need the Settings object suitable for your printer
RepRapSettings settings = new RepRapSettings();
// you can customize the settings below
settings.ExtruderTempC = 200;

// we want to accumulate the gcode commands to a GCodeFile
var gcode_accumulator = new GCodeFileAccumulator();
var builder = new GCodeBuilder(gcode_accumulator);

// the Compiler turns 2D/3D paths into GCode commands
SingleMaterialFFFCompiler compiler = new SingleMaterialFFFCompiler(
    builder, settings, RepRapAssembler.Factory);




// this bit writes out the GCodeFile to a .gcode file
GCodeFile gcode = gcode_accumulator.File;
using (StreamWriter w = new StreamWriter("c:\\demo\\generated.gcode")) {
    StandardGCodeWriter writer = new StandardGCodeWriter();
    writer.WriteFile(gcode, w);

Before we continue, a bit about the gsSlicer/gsGCode architecture and terminology. The basic idea here is that most of the time we want to be working at a higher level then GCode commands. We want to be able to define the geometric paths the print head will move along at the level of 2D/3D polygons and polylines. These are called Toolpaths.  We will build up ToolpathSet objects, which are just lists of IToolpath instances, and then pass these to a Compiler that will sort out how to turn them into GCode. 

The Compiler level is intended to support other outputs besides gcode, like laser paths for an SLS machine. The SingleMaterialFFFCompiler is suitable for use with 3-axis single-material FDM/FFF 3D printers. Internally, this Compiler creates an IDepositionAssembler instance, which currently is always a subclass of BaseDepositionAssembler. The Assembler provides lower-level commands like MoveTo, ExtrudeTo, and so on, which map more directly to actual GCode commands. The GCodeBuilder we created above is used by the Assembler to emit GCodeLine objects into a GCodeFile.

If that was confusing, here's a diagram. The basic idea is that, you can pass Toolpaths to the Compiler, and it will look at them and decide how to best turn them into lower-level commands that the Assembler will be able to translate into machine-specific GCode. This is what SingleMaterialFFFPrintGenerator does internally. That class takes the mesh slices and figures out how to fill them with Toolpaths, which it then compiles. But we can also generate Toolpaths directly with code, and send them to the Compiler. Or, we could skip over the Compiler entirely, and use the Assembler. You can even use the GCodeBuilder interface, if you really want to control every aspect of the printer (or program some other kind of machine). 


The Compiler/Assembler terminology here is borrowed from programming language compilers. I think about it the same way. GCode is very analogous to CPU assembly language - both are lists of very simple structured text commands, (mostly) evaluated in-order. If we push this analogy, we could think of Toolpaths as the "C" of 3D printers, and Meshes are perhaps a high-level scripting language. 

In this post we'll focus on the Toolpath level. To simplify the creation of Toolpaths we'll use a helper class called ToolpathSetBuilder. This class knows how to transform basic geometry3Sharp classes like Polygon2d into ToolpathSet objects, which are a bit more complicated. Here is a function that compiles a vertical stack of circles, to make a tube. You would call this function in the "// THIS IS WHERE WE ADD TOOLPATHS" space above.

static void generate_stacked_polygon(SingleMaterialFFFCompiler compiler,
    SingleMaterialFFFSettings settings)
    int NLayers = 10;
    for (int layer_i = 0; layer_i < NLayers; ++layer_i) {        
        // create data structures for organizing this layer
        ToolpathSetBuilder layer_builder = new ToolpathSetBuilder();
        SequentialScheduler2d scheduler = new SequentialScheduler2d(layer_builder, settings);
        if (layer_i == 0)  // go slower on first layer
            scheduler.SpeedHint = SchedulerSpeedHint.Careful;

        // initialize and layer-up
        layer_builder.AppendZChange(settings.LayerHeightMM, settings.ZTravelSpeed);

        // schedule a circle
        FillPolygon2d circle_poly = new FillPolygon2d(Polygon2d.MakeCircle(25.0f, 64));
        circle_poly.TypeFlags = FillTypeFlags.OuterPerimeter;

        // pass paths to compiler
        compiler.AppendPaths(layer_builder.Paths, settings);

As you can see, there really is not much to it. We create and initialize a ToolpathSetBuilder, add a Toolpath that moves up one layer in Z, and then add a second Toolpath that extrudes a circular polygon. We pass these to the Compiler, and repeat 10 times. The only extra bit is the SequentialScheduler2d. In many cases we would like to be able to add a set of Toolpaths and have the library figure out the best order to print them. This is what the Scheduler is for. Here we are using the dumbest possible Scheduler, that just passes on the paths in-order. SortingScheduler2d is an alternative that tries to be a bit smarter (but wouldn't matter here).

Ok, run this, get the .gcode file, and print it. You should get the result above right.

We made a tube - exciting! Now you know how to extrude any 2D outline that you can generate in code, without having to make a mesh. Here's another example, where I apply a simple deformation to the circle that varies with height. First we need a few parameters:

double height = 20.0;  // mm
int NLayers = (int)(height / settings.LayerHeightMM);  // 20mm
int NSteps = 128;
double radius = 15.0;
double frequency = 6;
double scale = 5.0;

Now replace the line that generates circle_poly above with this block:

// start with circle
FillPolygon2d circle_poly = new FillPolygon2d(Polygon2d.MakeCircle(radius, NSteps));

// apply a wave deformation to circle, with wave height increasing with Z
double layer_scale = MathUtil.Lerp(0, scale, (double)layer_i / (double)NLayers);
for ( int i = 0; i < NSteps; ++i ) {
    Vector2d v = circle_poly[i];
    double angle = Math.Atan2(v.y, v.x);
    double r = v.Length;
    r += layer_scale * Math.Sin(frequency * angle);
    circle_poly[i] = r * v.Normalized;

Run it, print it, and you should get the shape on the right. 

That's it for this tutorial. Next time we'll go further down the stack and generate individual print-head moves. But first, just a bit about how this ties into mesh toolpathing. What we've done above is exactly what SingleMaterialFFFPrintGenerator is doing. However instead of directly using the mesh slice polygons as toolpaths, the PrintGenerator uses various toolpathing strategies to fill in each polygon with nested perimeters, dense and sparse infill, support, and so on. These are all done by IFillPolygon implementations like ShellsFillPolygon. Each of these classes takes an input polygon and outputs a set of 2D polygon and polyline Toolpaths that are passed to the layer Scheduler, exactly like we did above.

You can also use these toolpathing classes yourself. So, if you wanted to do standard shells-and-infill for your procedurally-generated polygons, you could use a ShellsFillPolygon to create the perimeter toolpaths. This class also returns the "inner" polygons, which you could then pass to a SparseLinesFillPolygon to get the infill toolpaths. Easy! 






Mesh to GCode with gsSlicer

Most of the tutorials I've written so far have focused on geometry3Sharp, my geometry-processing library. But that's not my only commercial-friendly open-source library. I've also been working on another one, for 3D printing. In this tutorial, we'll use this library to convert a mesh to a GCode file that can be 3D printed. 

The gsSlicer library is in the gradientspace github, and we'll also need the gsGCode library. Combined, these three components - geometry3Sharp, gsSlicer, and gsGCode - have everything we need to build a command-line slicing tool. The gsSlicer library takes meshes as input, converts the meshes to a stack of planar slice polygons, and fills in those "2D solids" with paths ("toolpaths"). These are passed on down to gsGCode, which generates and manipulates GCode. If you aren't familiar, GCode is the command language used by most FDM/FFF 3D printers, and also by many other types of CNC machine. A GCode file is just a long list of of very simple commands - "move to x,y" and so on. In addition to generating GCode output, gsGCode can parse GCode files into various representations, including back into 2D/3D path geometry.

If you would like to avoid figuring out how to get all these libraries connected together, you can just check out the gsSlicerApps repository. This repo includes the others as submodules, and has various demo programs, including a 2D slice viewer which we will use below. Since everything is pure C#, you can open the solution file on windows or OSX using Visual Studio For Mac. The code I will explain below is taken directly from the MeshToGCodeDemo project in this solution.

A small caveat: gsSlicer is under active development, it's not (yet!) at the point where you should be throwing out your other printing tools. In terms of standard slicer capabilities, the one thing missing right now is bridging. Support structures are available but still a work-in-progress. And there are also lots of small tweaks that have yet to be done. But, if you want to experiment with printing from code, or try your own toolpathing techniques, I think you'll find gsSlicer is much cleaner and more straightforward than the other available open-source slicers, and you have the very strong toolset in geometry3Sharp to build on.

Ok, step 1 - we need a mesh. You could load one using StandardMeshReader, but I'll generate a cone procedurally instead:

CappedCylinderGenerator cylgen = new CappedCylinderGenerator() {
    BaseRadius = 10, TopRadius = 5, Height = 20, Slices = 32
DMesh3 mesh = cylgen.Generate().MakeDMesh();
MeshTransforms.ConvertYUpToZUp(mesh);       // g3 meshes are usually Y-up

// center mesh above origin
AxisAlignedBox3d bounds = mesh.CachedBounds;
Vector3d baseCenterPt = bounds.Center - bounds.Extents.z*Vector3d.AxisZ;
MeshTransforms.Translate(mesh, -baseCenterPt);

The only thing of note here is the call to ConvertYUpToZUp(). A problem you are likely to have already encountered in 3D printing is that many mesh modeling tools assume that "up" is the Y axis, but 3D printing tools always use the Z axis as up. If you load an STL file, this might not be necessary (but neither the STL or OBJ formats stores the up direction in the file).

Ok, next we create a PrintMeshAssembly, which in this case is just one mesh. However, gsSlicer has some advantages here over most existing slicers. For example, overlapping mesh components will be handled correctly in the vast majority of cases. You can also add open meshes to the assembly, and they will be printed as single-line-wide paths, like in the image on the right. And you can tag meshes as "support", in which case they are subtracted from the solids. 

// create print mesh set
PrintMeshAssembly meshes = new PrintMeshAssembly();
meshes.AddMesh(mesh, PrintMeshOptions.Default);

Next we need a Settings object. Currently gsSlicer does not support a very wide range of printers. However, the majority of printers out there use RepRap-style GCode, and so RepRapSettings will work fine with those machines. You will need to modify a few fields in the Settings though. In Settings.Machine, you will find the print bed dimensions, the nozzle and filament diameter, and the heated bed control. The Settings object has fields for the extruder and bed temperature, retraction, and the various print speeds. If you test a printer and it works, or it doesn't but you're willing to do some experimentation, let me know!

// create settings
MakerbotSettings settings = new MakerbotSettings(Makerbot.Models.Replicator2);
//PrintrbotSettings settings = new PrintrbotSettings(Printrbot.Models.Plus);
//RepRapSettings settings = new RepRapSettings(RepRap.Models.Unknown);

Ok, in the next few lines we will use MeshPlanarSlicer to convert the PrintMeshAssembly into a PlanarSliceStack, which is a list of per-layer 2D polygons and polyline paths. There's not really any options here, although if you are experimenting with open meshes you might need to tweak MeshPlanarSlicer.OpenPathDefaultWidthMM. 

// do slicing
MeshPlanarSlicer slicer = new MeshPlanarSlicer() {
    LayerHeightMM = settings.LayerHeightMM };
PlanarSliceStack slices = slicer.Compute();

And we're almost done, just one last block to do the toolpathing and write out the GCode. I will point out here that although the SingleMaterialFFFPrintGenerator does take the mesh assembly as input, the printing is entirely driven off the slices. So, you don't have to use MeshPlanarSlicer. In fact, you don't have to use a mesh at all! You can construct a 2D polygon stack in many other ways, and print it using this next block.

// run print generator
SingleMaterialFFFPrintGenerator printGen =
    new SingleMaterialFFFPrintGenerator(meshes, slices, settings);
if ( printGen.Generate() ) {
    // export gcode
    GCodeFile gcode = printGen.Result;
    using (StreamWriter w = new StreamWriter("c:\\demo\\cone.gcode")) {
        StandardGCodeWriter writer = new StandardGCodeWriter();
        writer.WriteFile(gcode, w);

Basically, printGen.Generate() does all the work here, and if you try a large model, this will take some time - possibly several minutes (having more cores helps!). But, load the GCode onto your printer and within a few minutes (7, on my ancient-but-so-reliable Replicator 2), you'll have a small plastic shape!

That's it! If we leave off the mesh setup, let's call it 12 lines of code, to convert a mesh to GCode. 


I mentioned a GCode viewer above. If you would like to see your GCode paths, the gsSlicerApps solution also includes a project called SliceViewer. This is a GTKSharp app that uses SkiaSharp for 2D vector graphics drawing. Both of these open-source libraries are also cross-platform, I use this app regularly on Windows and OSX. 

I also use this project for active development of gsSlicer, so by default SliceViewer will usually be loading and slicing one of the included sample files on startup. However, if you drag-and-drop a .gcode file onto the window, SliceViewer will use gsGCode to parse it and extract the layer path geometry, which you can scan through using the up/down arrow keys.