A 3D Printer in Space

Building a 3d printer for building animations in indie games

Easy in-progress building animation

Gravity Matters Fabrication Station

A couple months ago, I released a game where you’re piloting a spaceship on a galactic quest based on real physics (strongly inspired by KSP), and I wanted you to be able to upgrade your ship while you’re on your journey. For those who dealt with adding construction to video games, you know it means adding a lot of graphics to animate the various in-progress construction phases, or you can put all the materials in a pot, throw the magic video game flash, and out pops your finished part.

The game, Gravity Matters®, started as a solo pandemic project, and I learned lowpoly modeling to up my game jam skills. Trying to add construction meant a ton more art, and for circumstances beyond my control, time was already tight. I was about to head to the local makerspace when it hit me.

Why not add a futuristic 3d printer to the spaceship?

You can read more about my rationale below, but the quick version was my game was supposed to incorporate hard scifi, so I didn’t want a replicator fade in or a magic flash when you built things. Envisioning a future 3dprinter seemed much more realistic.

The proposed pipeline

  1. Make the part in freecad (model to an STL).
  2. Make the same part in blender (model to a GLB).
  3. Slice the STL for 3d printing (STL –> g-code).
  4. Process the g-code to a format the game could use (g-code –> gmcode).
  5. Use it:
    1. When the player starts building the part, render the gmcode with a virtual 3d printer, creating geometry to mimic melted material (lots of polygons since it has all of the interior infill).
    2. When the part was done, swap it out with an identical surface mesh / regular 3d asset.

I ran into some trouble along the way, but this article is about how I did it in the launch version.

All you need to know about 3d printing

3d printing is a huge topic, but for our purposes, we only need a little. If you already know about FFF (FDM™) printing, slicing, and extruding, just skip to the next section.

The Printer

To 3d print something, we need a way to feed material, heat (and cool) it, push it through a nozzle, and make sure the nozzle is at the right place when we do it. As the material cools, it solidifies, and then we can build up on top of it until we finish fabricating our piece.

While you can do this by hand, we usually think of 3d printing with a computer driving things. To simplify placing the material extruder / nozzle, we can build up in flat layers, which guarantees the nozzle won’t bump into anything we set down before (although there are a lot of other less commonly used ways to do this). For the most common printers (FFF Cartesian or Core XY), the flat plane is usually X-axis and Y-axis, or the X-Y plane, and moving up (or down) layers is usually the Z-axis. Godot assigns these differently, but we’ll get to that later.

The printer itself doesn’t have much logic. There’s usually one motor for each access (X, Y, and Z), a motor to feed the material into the hot end (or extruder), and a way to heat up / cool down the hot end (and sometimes to turn on a fan that helps cool down the extruder or the piece). More advanced printers may add more motors, but that’s for a printing blog. The code you feed it just tells it how far to move each motor, heat the material, feed the material, or toggle the fan.

Despite their coolness, regular (FFF) 3d printers are generally simple devices. While there’s many ways to make one, I’ll assume a simple 3-axis PLA type printer. Take 3 motors, one each for X and Y movement to fill in a thin flat piece, then one for the Z axis to pull up to the next flat piece. There’s more complex ways to move the motors, but this was a common way to do it. The printer pretty much only responds to movement commands, the complexity is all hidden in the slicer.

The Slicer

g-code is really just a list of printer commands. Most of it is just telling the motors how far to move. You could write it by hand (it was originally designed as a text format for this purpose), but these days, that’s the slicer’s job. A slicer takes a 3d model (preferably an STL file made in something like freecad or the solid modeler of your choice) and handles the complexity of splitting it up into flat layers a 3d printer can actually print. This adheres to rules like, “you can’t just place material hanging anywhere in the air, it has to rest on something” (although you can overhang gaps to some extent). Slicers typically know the dialect of g-code your printer knows how to follow, properties of the material you’re using (such as how hot it needs to be to melt), where your structure might need to have support structures added, etc.

g-code looks like this:

# Don't run this on a real printer!
# preamble, cleaning, skirt, homing, feed information removed

# move to (10.0, 20.0, 2.0), no extrusion:
G1 X10.0 Y20.0 Z2.0

# move to (15.0, 20.0, 2.0), extrude 5.7mm of material:
G1 X15.0 Y20.0 E5.7

# and so on...

In real g-code, you’d want to set more parameters, such as clearing the nozzle, setting the heat of the extruder (or print bed if it’s heated), setting the feedrate, etc, but we’re only approximating the print for game purposes here. You can read more in the Marlin docs.

gmcode, not g-code

Given the licensing around most slicers, I didn’t want to include any slicer software (or a service to perform that) in Gravity Matters. As I wasn’t sure about the confusing licensing for g-code either, I rolled another standard called gmcode, which just accepts an array of a few movement commands, an extruder on/off state in a Resource format that’s easy to work with in Godot’s built in loaders.

g-code has a long history in computer controlled manufacturing. Like most de facto standards, it’s basically a non-standard as nearly every implementation of it seems to do it differently (think character sets before the Unicode bound them all). I didn’t want to go down that rabbit hole, so for my use, I set the slicer to output commands for an extremely basic printer and removed everything but a very small subset before converting it to resource format.

In particular, I removed arc movements in favor of straight linear movements, removed some slight offsets in the z-axis to clamp to fixed integral layers, and tracked the extrusion state. I removed the feed information and the material accumulator used in many g-code formats.

The Fabrication Station

In game, behind the cockpit, there’s a small area called the Fabrication Station, which has a material processing hopper and a scifi futuristic 3d printer. As you play, you have the option to mine material in space, process it in the hopper, and you get building materials. With the right materials, you can choose a recipe, and the 3d printer to starts interpreting the gmcode for it. Due to the rush at launch, I only loaded a single part - it was an arm from a swinging arm robot I made for a robotics class I taught at the makerspace, but more parts will come in the patches ahead).

So what does it do? It processes gmcode, that looks something like this:

# 3 movements on layer height 0.35:
[
  ...
  # [X, Z, LAYER_HEIGHT, EXTRUDER_ON_OFF]
  #   (EXTRUDER_ON_OFF: 1 == extrude material or 0 == don't)
  [59.0, 88.375, 0.35, 0],
  [141.0, 88.375, 0.35, 1],
  [141.829, 88.416, 0.35, 1]
  ...
]

Using the same idea as the 3d printer, we don’t need to build a complex machine to do the processing, just enough of a gmcode interpreter to move between the specified positions, either building geometry between then or not. The numbers need to be rescaled depending on the size of world space, but assuming we’re following the example above and we’re working on the 0.35 layer, this becomes a 2d problem:

  1. Move to (59, 88.375) (extruder was off)
  2. Draw geometry from there to (141, 88.375)
  3. Draw geometry from there to (141.829, 88.416).
  4. and so on

To run the gmcode, I needed to track some state about the machine, namely whether it was extruding material or not, where the extruder last was, what rescaling was applied, and any hints about the geometry, but that was it. In our space 3d printer, we don’t need to worry about running out of material, only if we’ve drawn too many triangles for the GPU!

For the geometry, I’m just using rectangular solids as an approximation. A real 3d printer puts out lines that look more circular (melted to settle a little), but I’m not aiming for that level of realism here nor do I want to spend the geometry on it. The geometry is created using an ArrayMesh with Mesh.PRIMITIVE_TRIANGLE_STRIP, using the nozzle width as a guide to the cross-section size.

Several optimizations are available here already. I realized afterwards that there’s no need to parse the gmcode in the game, or dynamically create the model geometry, that could be done ahead of time by creating a model of each layer with hinting information about the extruder movement path, but given that I was already crunched for time, I opted to go with the naive solution first and get fancier later. Some quantization could also remove the floating point if needed, but before heading that way, it probably makes more sense to build a custom slicer without license restrictions for the STL to gmcode transform and just solve the problem directly.

Too Slow (the Frame Limited version)

As I was in full hack mode, I didn’t think too much about speed initially. While The prototype set up the initial printer state, then used the _process function. If the appropriate amount of time had passed from the last command, it read the next element from the gmcode array, calculate a tween for the extruder head to the new position, then add a new solid rectangle between the last point and the current one if it was currently extruding.

It was a mixture of happiness and surprise when I saw the result. After some debugging, the 3dprinter looked more or less like a real 3d printer, providing some validation to the idea, as I watched it sketch in the infill in virtual part build.

Space 3d Printer, frame limited

But it was much too slow.

Printing the full part (and this was a small part) took minutes. Giving the Nixiesoft discord an early look confirmed my suspicion - no one wanted to wait that long for a part to show up.

I removed the tween to have the extruder warp between successive positions. Saved some time, but still too slow. I then removed the delays and had it process a command on every frame. Faster, but at around 400 seconds, it was still too slow.

With _process set to the frame rate, I was now frame rate limited, processing one command per frame. The simulation was now inconsistent based on different hardware, which isn’t great either. I tried looping and looking ahead several commands, but now when I got the geometry right, the extruder wasn’t close, leading to a snapping effect and destroying the smooth print, and it would still be inconsistent.

Given this was a game, and ultimate precision wasn’t necessary, I tried selectively dropping commands from the print, but that led to a geometric mess that didn’t really match the piece I fed it.

While I could keep hacking along these avenues, I decided that having the command coupled to the frame rate was the wrong approach.

Too fast (the Layer at Once version)

Unlike a typical 3d printer bound to g-code, I have a lot of control over my production pipeline, in particular the ability to preprocess as needed. I figured that any preprocessing I did in game could be shifted to the production pipeline prior to import (and the formats changed), so I added a new set of initialization tasks:

  1. Create an array of layers
  2. Process each gmcode element:
    • For the first element, set the current layer to zero at this z-axis height.
    • For every other element:
      1. If the height is within a given tolerance of the current layer, add it to the current layer
      2. Otherwise if the height is beyond the tolerance, add a new layer and increment the current layer, and add this element to the new layer.

I switched to using a tolerance instead of exact values as the slicer I was using would occasionally drift a small amount on the z-axis (I’m not sure if that’s intentional or just floating point error, but that can be investigated later).

Now that the commands are all separated by layers, convenience values such as initial and final position, extents, and poly count can be precalculated to allow skipping parts of the fabrication process.

Given the launch date, I just set the extruder alternating sides of each layer and had it tween across the layer before it popped in the geometry. The effect isn’t as satisfying, but most importantly, I can now set all of these tweens based on dividing the total time the construction is supposed to take by the number of layers and update the clock in each _process invocation, giving a predictable fabrication time.

Space 3d Printer, Layer at Once

The Future

I didn’t plan on stopping with the layer-at-once version coded, but like many projects, the launch deadline approached (driven by statement of use filing date for the trademark), which led to a series of bad options. I opted to lower the price ($5.99 in the US) and ship in advance of the legal requirements, and go with early access, leaving room for updates. As of this article’s date, I’m planning on increasing the price as more content is added (provided free to the early players) until the original plans are met, something outlined in more detail in the discord (the link is currently on the store page).

With the layer at once data precalculated, in the next iteration with the position and geometry data decoupled from the frame rate, I’ll be able to skip frames or warp the extruder while keeping a smoothly animated 3d printer and work piece, at any speed required (within reason).

There’s been a lot of feedback from the community on the early access build! I put the updates to the fabrication station further down the list, since I’d like to have more levels and mining materials available before continuing, but models for the original upgrades will be available someday!

Happy orbiting!

Rationale

Let’s get the hot take out of the way.

It’s no secret that I love (well implemented[0]) crafting in video games.

From a world building perspective, it feels to me like the world is “done”[1] when the things in it can also be made. It usually helps to drive the game’s economy as well, giving it a more filled in feeling. Players have different motivations, but here’s why I usually like it:

  1. Figuring it out often allows for better gear earlier.
  2. Optimization, although there’s always the Soren Johnson concern “Given the opportunity, players will optimize the fun out of a game.
  3. In multiplayer games, crafting usually leads to bypassing built in resource deficits
    1. There’s nearly untapped access to in game currency when you can make things
    2. I like being able to help others. In FF14, for example, if you’re ok with a long grind to unlock the abilities, you can power up others at basically no cost, such as in cough my free classes available in my yt channel / twitch / blog / investing podcast cough
  4. There comes a point where the game opens up and you can relax and just enjoy the view, adding in whatever was limited / difficult earlier on.

A few months ago, I quietly released my first indie game, Gravity Matters, featuring realistic space flight (newtonian gravitation / n-body physics) where you need to complete a series of levels in space without enough fuel. Given my blog earlier this year, I hit some setbacks in development and the trademark deadline blindsided me. I pushed it out, but told the early audience I’d keep updating until we hit the game I wanted to release.

I wanted to keep the crafting system in Gravity Matters simple to most players at launch, and layer in more complexity for the interested later on. I stuck to a simple car style upgrade system - gain more responsiveness, more acceleration, or more spacey topics like better life support, or faster warps. The spirit animal in my mind was RC Pro AM, an old NES game with a few different upgrade options.

Despite my love of crafting, it was also important to me that players not interested could entirely skip or speedrun the content without ever touching it, if so desired. I’ve been careful to make sure it’s never a dependency in game progression, more of an additional thing to do if you choose.


Footnotes

[0] For clarity, I don’t think I’m there yet for Gravity Matters

[1] Assuming you have fishing. The game isn’t done until it has fishing.


Gravity Matters® is a registered trademark of Nixiesoft LLC. All uses of this name in this document are with Nixiesoft LLC’s permission.