Tutorials

Drawing thick outlines in OpenGL

Drawing outlines around 3D shapes is a common feature request, be it for selected items, quest markers, or what have you. In OpenGL there’s a few ways to do it and today I’d like to share with you the two methods I’ve tried – the OpenGL2 offset normals method (left) and the OpenGL3 per-pixel shader (right). Both use a variation on stencils. Stencils are like green screens. By isolating the thing you want to keep from the rest, it’s easy to do all kinds of neat tricks.

For those of you in the know, I’m using a brute force method. Skip to final thoughts for my reasoning.


OpenGL stencils

The first method I tried went something like this:

  • Draw every mesh
  • Draw selected stuff a second time, but this time to the stencil buffers
  • Lock the stencil buffer but mask everything – only draw to the non empty parts of the stencil.
  • Draw selected stuff a third time in the original buffer, but only edge lines in the selected color.

So let’s assume, for brevity, that there’s a drawAllMeshes method that works as advertised.
Somewhere in the pipeline there’s something like

drawAllMeshes(everything);
outlineSelectedMeshes(probablyLessThanEverything);

then outlineSelectedMeshes does all the stencil magic.

gl3.glEnable(GL3.GL_STENCIL_TEST);  // enable stencil testing  
gl3.glClear(GL3.GL_STENCIL_BUFFER_BIT | GL3.GL_DEPTH_BUFFER_BIT);  // clear the buffer  
gl3.glColorMask(false,false,false,false);  // disable writing to the color buffer  
gl3.glDepthMask(true);  // enable writing to the depth buffer  
gl3.glStencilMask(0xFF);  // enable writing to the stencil buffer  
gl3.glStencilFunc(GL3.GL_ALWAYS,1,0xFF);  // always pass the stencil test and set the value to 1  
gl3.glStencilOp(GL3.GL_KEEP,GL3.GL_KEEP,GL3.GL_REPLACE);  // set the stencil value to 1 when the depth test passes  

drawAllMeshes(gl3, selectedMeshes, camera, originShift);  // draw the selected meshes.  Only now they'll go to the stencil buffer.

gl3.glStencilFunc(GL3.GL_NOTEQUAL,1,0xFF);  // pass the stencil test if the value is not 1  
gl3.glStencilMask(0x00);  // disable writing to the stencil buffer  
gl3.glDepthMask(false);  // disable writing to the depth buffer  
gl3.glColorMask(true,true,true,true);  // enable writing to the color buffer 
gl3.glStencilOp(GL3.GL_KEEP,GL3.GL_KEEP,GL3.GL_KEEP);  // keep the stencil value  
gl3.glDisable(GL3.GL_CULL_FACE);  // draw both sides of the outline

gl3.glLineWidth(outlineThickness);  // set the line thickness
gl3.glPolygonMode(GL3.GL_FRONT_AND_BACK,GL3.GL_LINE);  // draw only lines

// outlineShader is an instance of ShaderProgram which gives me all kinds of convenience methods.
outlineShader.use(gl3);  
outlineShader.setMatrix4d(gl3, "viewMatrix", camera.getViewMatrix());  
outlineShader.setMatrix4d(gl3, "projectionMatrix", camera.getChosenProjectionMatrix(canvasWidth, canvasHeight));  
outlineShader.setColor(gl3, "outlineColor", Color.GREEN);  

// render the selected set with thick lines.
// drawAllMeshes would use the default shader, so I do it manually here
// to control the shader choice.
for(var mesh : selectedMeshes) {  
    outlineShader.setMatrix4d(gl3,"modelMatrix",mesh.getMatrix());  
    mesh.render(gl3);
}

gl3.glPolygonMode(GL3.GL_FRONT_AND_BACK,GL3.GL_FILL);  // reset shapes
gl3.glEnable(GL3.GL_CULL_FACE);  // restore settings
gl3.glDepthMask(true);  // restore settings
gl3.glLineWidth(1);  // restore settings
gl3.glStencilFunc(GL3.GL_ALWAYS,1,0xFF);  // turn off stencil testing  
gl3.glStencilOp(GL3.GL_KEEP, GL3.GL_KEEP, GL3.GL_REPLACE);  
gl3.glDisable(GL3.GL_STENCIL_TEST);

The outline shader v1

outline.vert was doing almost nothing.

#version 330 core  

layout(location = 0) in vec3 aPosition;  
layout(location = 1) in vec3 aNormal;  
layout(location = 2) in vec4 aColor;  
layout(location = 3) in vec2 aTexture;  

uniform mat4 projectionMatrix;  
uniform mat4 viewMatrix;  
uniform mat4 modelMatrix;

void main() {  
    vec3 offsetPosition = aPosition + aNormal * outlineSize;  
    vec4 worldPose = modelMatrix * vec4(offsetPosition, 1.0);  
    gl_Position = projectionMatrix * viewMatrix * worldPose;  
}

and outline.frag was doing truly nothing.

#version 330 core
out vec4 fragColor;  
uniform vec4 outlineColor;  // the outline color

void main() {  
    fragColor = outlineColor;
}

The effect on a basic cube looks like this:

Note that OpenGL also doesn’t give a way to adjust line end caps.

OpenGL 3 per pixel shaders

The OpenGL3 version required more setup but less work at run time. The process is:

  • Draw every mesh
  • Draw a second time to a stencil buffer FBO
  • Use the FBO as a reference texture (not the technical name)
  • Draw a rectangle over the whole screen.
  • The shader color every pixel in the rectangle near (but not on) the stencil.

Full screen rectangle

I’m sorely tempted to say “eh, I have a Mesh wrapper class, you don’t need these details”… but in the interest of being thorough, here’s the whole rectangle setup.

private final int [] rectangleVAO[1];
private final int [] rectangleVBO[1];

private void generateFullscreenQuad(GL3 gl3) {  
    float v = 1.0f;  // make smaller to check position on screen
    float[] quadVertices = {  
        // positions  // texcoords  
        -v, -v, 0f,   0f, 0f, // bottom-left  
         v, -v, 0f,   1f, 0f, // bottom-right  
        -v,  v, 0f,   0f, 1f, // top-left  
         v,  v, 0f,   1f, 1f  // top-right  
     };

    gl3.glGenVertexArrays(1, rectangleVAO, 0);  // make vao
    gl3.glBindVertexArray(VAO[0]);  // use vao
    gl3.glGenBuffers(2, rectangleVBO, 0);  // make vbos
    gl3.glBindBuffer(GL3.GL_ARRAY_BUFFER, rectangleVBO[0]);    // use vbos
    gl3.glBufferData(GL3.GL_ARRAY_BUFFER, quadVertices.length * Float.BYTES, FloatBuffer.wrap(quadVertices), GL3.GL_STATIC_DRAW);  // fill vbos
    gl3.glBindVertexArray(0);  // stop using vao
}

Frame buffer object

Normally the video card draws into an offscreen back buffer and at the right time swaps the back buffer and the front buffer. This way you don’t see all triangles being drawn one by one. We can create our own Frame buffer object (FBO) as canvas-sized textures. (The canvas is the whole drawing area, which might not be the entire window.) The video card can draw into the FBO of our choosing, and then we can do stuff with it. I couldn’t find an easy way to access the built-in stencil buffer so I did it this way.

// class parameters  
private int outlineThickness = 5;  
private final int [] stencilFBO = new int[1];
private final int [] stencilTexture = new int [1];

// width and height should be the canvas dimensions.
// every time the canvas changes size this should be called again.
private void setupStencilFramebuffer(GL3 gl3, int width, int height) {  
    deleteStencilBuffer(gl3);  
    // Create FBO if not already created  
    gl3.glGenFramebuffers(1, stencilFBO, 0);  
    // Create stencil texture if not created  
    gl3.glGenTextures(1, stencilTexture, 0);  

    gl3.glBindTexture(GL3.GL_TEXTURE_2D, stencilTexture[0]);  
    // create with a single 8-bit red channel
    gl3.glTexImage2D(GL3.GL_TEXTURE_2D, 0, GL3.GL_R8, width, height, 0, GL3.GL_RED, GL3.GL_UNSIGNED_BYTE, null);  
    gl3.glTexParameteri(GL3.GL_TEXTURE_2D, GL3.GL_TEXTURE_MIN_FILTER, GL3.GL_NEAREST);  
    gl3.glTexParameteri(GL3.GL_TEXTURE_2D, GL3.GL_TEXTURE_MAG_FILTER, GL3.GL_NEAREST);  
    gl3.glTexParameteri(GL3.GL_TEXTURE_2D, GL3.GL_TEXTURE_WRAP_S, GL3.GL_CLAMP_TO_EDGE);  
    gl3.glTexParameteri(GL3.GL_TEXTURE_2D, GL3.GL_TEXTURE_WRAP_T, GL3.GL_CLAMP_TO_EDGE);  

    // Bind the FBO and attach the stencil texture  
    gl3.glBindFramebuffer(GL3.GL_FRAMEBUFFER, stencilFBO[0]);  
    gl3.glFramebufferTexture2D(GL3.GL_FRAMEBUFFER, GL3.GL_COLOR_ATTACHMENT0, GL3.GL_TEXTURE_2D, stencilTexture[0], 0);  
    gl3.glDrawBuffer(GL3.GL_COLOR_ATTACHMENT0);  

    // Check FBO status  
    int status = gl3.glCheckFramebufferStatus(GL3.GL_FRAMEBUFFER);  
    if (status != GL3.GL_FRAMEBUFFER_COMPLETE) {  
        throw new RuntimeException("Failed to setup stencil framebuffer: " + status);  
    }  

    // Unbind FBO  
    gl3.glBindFramebuffer(GL3.GL_FRAMEBUFFER, 0);  
}

// called when the canvas changes size or it is disposed.
private void deleteStencilBuffer(GL3 gl3) {  
    if(stencilFBO[0]!=-1) {  
        gl3.glDeleteFramebuffers(1, stencilFBO,0);  
        stencilFBO[0] = -1;  
    }  
    if(stencilTexture[0]!=-1) {  
        gl3.glDeleteTextures(1, stencilTexture,0);  
        stencilTexture[0] = -1;  
    }  
}

Testing the stencil buffer

// Step 1: Render the stencil into an offscreen texture using the FBO  
gl3.glBindFramebuffer(GL3.GL_FRAMEBUFFER, stencilFBO[0]);  
gl3.glViewport(0, 0, canvasWidth, canvasHeight);  
// Clear stencil texture  
gl3.glClearColor(0,0,0,0);  
gl3.glClear(GL3.GL_COLOR_BUFFER_BIT | GL3.GL_DEPTH_BUFFER_BIT);

// Please forgive my custom mesh shader here.
// Draw everything without lighting in flat white.
meshShader.use(gl3);  
meshShader.setMatrix4d(gl3, "viewMatrix", camera.getViewMatrix(originShift));  
meshShader.setMatrix4d(gl3, "projectionMatrix", camera.getChosenProjectionMatrix(canvasWidth, canvasHeight));  
meshShader.set1i(gl3, "useVertexColor", 0);  
meshShader.set1i(gl3, "useLighting", 0);  
meshShader.setColor(gl3,"diffuseColor",Color.WHITE);

for(var mesh : selectedMeshes) {  
    meshShader.setMatrix4d(gl3,"modelMatrix",mesh.getMatrix());
    mesh.render(gl3);
}

// resume editing the color buffer, do not change the depth mask or the stencil buffer.  
gl3.glBindFramebuffer(GL3.GL_FRAMEBUFFER, 0);  

// Step 2: Render outlines using the stencil texture  
gl3.glActiveTexture(GL3.GL_TEXTURE0);  
gl3.glBindTexture(GL3.GL_TEXTURE_2D, stencilTexture[0]);  

captureTextureData(gl3,canvasWidth,canvasHeight);

captureTextureData is used to write stencilTexture to a BufferedImage, and then I can use a breakpoint to view the stencil buffer.

Of course this only works if something there is a selected mesh!
Also… it draws upside down because the screen Y axis is reverse from a BufferedImage y axis.

Drawing the quad

Once the stencil buffer looked good it was time to disable the capture and draw the quad on screen.

//captureTextureData(gl3,canvasWidth,canvasHeight);  

outlineShader.use(gl3);  
outlineShader.set1i(gl3, "stencilTexture", 0); // Texture unit 0  
outlineShader.set2f(gl3, "textureSize", canvasWidth, canvasHeight);  
outlineShader.setColor(gl3, "outlineColor", Color.GREEN);  
outlineShader.set1f(gl3, "outlineSize", outlineThickness);  

// Render the quad with the stencil texture to the screen  
gl3.glDisable(GL3.GL_CULL_FACE);  
fullScreenQuad.render(gl3);  
gl3.glEnable(GL3.GL_CULL_FACE);

All we need is…

Testing a new outline shader

outline.vert v2

#version 330 core  

layout(location = 0) in vec3 aPosition;  
layout(location = 1) in vec2 aTexture;  

void main() {  
    gl_Position = vec4(position, 1.0);  
}

outline.frag v2

#version 330 core  

uniform vec4 outlineColor = vec4(0.0, 1.0, 0.0, 1.0);  
uniform float outlineSize = 1.0;  
uniform vec2 canvasSize;  // Size of the texture/screen  

out vec4 finalColor;  // Output fragment color  

// Sampler for the stencil texture (or depth-stencil data)  
uniform sampler2D stencilTexture;  

// the screen and the stencilTexture have dimensions from (-1,-1) to (1,1)  
// the canvasSize, outlineSize, and gl_FragCoord are in pixels from (0,0) to (width,height)  
void main() {  
    // Size of a fragment in texture coordinates  
    vec2 texelSize = 1.0 / canvasSize;  
    // Current pixel position in the stencil texture  
    vec2 textureCoord = gl_FragCoord.xy / canvasSize;  
    vec4 stencilValue = texture(stencilTexture, textureCoord);  

    // If the stencil value is not zero we're inside the stencil area so skip.  
    if (stencilValue.r > 0.0) discard;  

    int outInt = int(ceil(outlineSize));  
    float o2 = outlineSize * outlineSize;  
    // loop over all pixels within +/-outline size  
    for (int y = -outInt; y <= outInt; y++) {  
        for (int x = -outInt; x <= outInt; x++) {  
            if(x*x + y*y > o2) continue; // Skip pixels outside the circle  
            // convert pixel offset to texture coordinate offset
            vec2 offset = vec2(x, y) * texelSize;  
            // Sample the stencil texture at the offset position  
            vec4 neighbor = texture(stencilTexture, textureCoord + offset);  
            if(neighbor.r > 0.0) {  
                // We're in range, set the outline color and exit  
                finalColor = outlineColor;  
                return;  
            }  
        }  
    }  
    // If no neighboring pixels are found with a stencil value do nothing.
    //finalColor = vec4(0,0,1,1);  // make the background blue for testing
    //finalColor = vec4(0,(textureCoord.x+1)/2,(textureCoord.y+1)/2,1);
    // gradient for testing
}

Tada!

Final thoughts

The approach I took is considered “Brute force”. Ben Golus’ Jump Flood Algorithm (JFA) is the final answer for most people. It’s the hardest to do and the most efficient at run time. Presently I am not doing huge outlines that would require the run time cost savings. Also I have not seen a GLSL step-by-step… so… Perfect is the enemy of good enough.

I write 3D stuff mainly to simulate robots I want to build. The app is called Robot Overlord and it’s totally open source, so feel free to click the link and join in the latest fun. Show the world how smart you are and make a PR with working JFA.

Seriously, if you know someone learning OpenGL… make them start with 2.0. If you can choose the language for them, pick Java. It’s just so much easier and more likely to succeed. Watching my nephew do a ten chapter tutorial on Vulkan for one triangle… that’s crazy pants.

Robot Arm Tutorials

Friday Facts 19: Marlin for Robot Arms

Today I’m going to show you how to set up Marlin firmware – the code in the robot brain – for your board so it thinks it is a robot arm and I will be using the Sixi 3 as my example. When we’re done we’ll have a 6 axis machine ready to receive gcode from apps like Robot Overlord.

Building a custom robot arm is easier if one uses common hardware. Thanks to the popularity of 3D printers it is now super easy to get stepper motors, limit switches, and MCUs that drive five, six, or even more motors. Marlin takes away all the headache and lets anyone talk to a robot with gcode, the standard language of CNC machines like 3D printers, mills, and lathes.

The major steps are:

  1. Fork Marlin
  2. Customize it
  3. Flash your board
  4. Test it

Fork Marlin

To “fork” is to make a special copy. it’s special because it includes a feature to update your copy with changes to the original. When the Marlin developers improve something, you can press a button and get all their updates.

The code for Marlin is available at https://github.com/MarlinFirmware/. I have a fork for robot arm Sixi 3 already started. You can get that fork as well right here: https://github.com/MarginallyClever/Marlin/

Make sure that (1) the branch is set to sixi3, then (2) click the code button and then (3) Open with Github Desktop or Download Zip. If it’s a ZIP you’ll have to unpack it somewhere like Documents/Github/Marlin.

Customize Marlin

Here’s a list of lines in Configuration.h that I’ve changed. The bold parts are unchanged so you can use that to search the file. The stepper motors in Marlin are named – internally only – as X, Y, Z, I, J, K.

#define STRING_CONFIG_H_AUTHOR “(Marginally Clever, Sixi3)”

#define MOTHERBOARD BOARD_RUMBA

#define CUSTOM_MACHINE_NAME “Sixi 3 robot arm”

#define EXTRUDERS 0

Because it’s a RUMBA board I also had to redefine a few of the pin settings. Normally all supported boards are defined in Marlin/src/pins/*.

#define I_STEP_PIN                         23
#define I_DIR_PIN                          22
#define I_ENABLE_PIN                       24
#define J_STEP_PIN                         26
#define J_DIR_PIN                          25
#define J_ENABLE_PIN                       27
#define K_STEP_PIN                         29
#define K_DIR_PIN                          28
#define K_ENABLE_PIN                       39

#undef Y_MAX_PIN
#undef Z_MIN_PIN
#undef Z_MAX_PIN

#define I_MIN_PIN                           34
#define J_MIN_PIN                           33
#define K_MIN_PIN                           32

The type of driver and the external name of each motor is next.

#define X_DRIVER_TYPE  A4988
#define Y_DRIVER_TYPE  A4988
#define Z_DRIVER_TYPE  A4988
//#define X2_DRIVER_TYPE A4988
//#define Y2_DRIVER_TYPE A4988
//#define Z2_DRIVER_TYPE A4988
//#define Z3_DRIVER_TYPE A4988
//#define Z4_DRIVER_TYPE A4988
#define I_DRIVER_TYPE  A4988
#define J_DRIVER_TYPE  A4988
#define K_DRIVER_TYPE  A4988

...

#ifdef I_DRIVER_TYPE
  #define AXIS4_NAME 'U' // :['A', 'B', 'C', 'U', 'V', 'W']
  #define AXIS4_ROTATES
#endif
#ifdef J_DRIVER_TYPE
  #define AXIS5_NAME 'V' // :['B', 'C', 'U', 'V', 'W']
  #define AXIS5_ROTATES
#endif
#ifdef K_DRIVER_TYPE
  #define AXIS6_NAME 'W' // :['C', 'U', 'V', 'W']
  #define AXIS6_ROTATES
#endif

Limit switches are tricky because the original Sixi 3 still doesn’t have them. (The plan is a new PCB that has always-on sensors). For Sixi 3 only, I have to trick the sensor code. When the robot turns on it will believe it has already homed, no matter where it is. A better robot with switches would call G28 to find home, and then the invert would depend on the type of switch (normally open vs normally closed) and I don’t remember what plug does.

#define USE_XMIN_PLUG
#define USE_YMIN_PLUG
#define USE_ZMIN_PLUG
#define USE_IMIN_PLUG
#define USE_JMIN_PLUG
#define USE_KMIN_PLUG

...

#define X_MIN_ENDSTOP_INVERTING false 
#define Y_MIN_ENDSTOP_INVERTING false 
#define Z_MIN_ENDSTOP_INVERTING false 
#define I_MIN_ENDSTOP_INVERTING false 
#define J_MIN_ENDSTOP_INVERTING false 
#define K_MIN_ENDSTOP_INVERTING false 
#define X_MAX_ENDSTOP_INVERTING true 
#define Y_MAX_ENDSTOP_INVERTING true 
#define Z_MAX_ENDSTOP_INVERTING true 
#define I_MAX_ENDSTOP_INVERTING false 
#define J_MAX_ENDSTOP_INVERTING false 
#define K_MAX_ENDSTOP_INVERTING false 

Motors also need gearbox and speed settings. Sixi 3 has a 70:1 harmonic gearbox and then a further pulley reduction in each unit. Since each motor does 200 steps per turn, that makes 105 steps per degree!

#define DEFAULT_AXIS_STEPS_PER_UNIT   { 105, 105, 105, 105, 105, 105 }

105 steps per degree * 5 deg/s = 525 steps per second. Impressive for such tiny NEMA17 motors. It might not be fast but it works and it’s affordable. Cheap, fast, good… pick two.

#define DEFAULT_MAX_FEEDRATE          { 5,5,5,5,5,5 }

#define CLASSIC_JERK // uncommented this to turn it on

#define S_CURVE_ACCELERATION // uncommented this to turn it on

I make sure to leave motors on so the arm doesn’t suddenly “go limp” at the worst time.

#define DISABLE_X false
#define DISABLE_Y false
#define DISABLE_Z false
#define DISABLE_I false
#define DISABLE_J false
#define DISABLE_K false

Range of motion is important, Marlin won’t let you go outside the limits. Remember this code was written for square box 3D printers, so some of the terms are a little silly for our needs.

// The size of the printable area
#define X_BED_SIZE 360
#define Y_BED_SIZE 360

// Travel limits (linear=mm, rotational=°) after homing, corresponding to endstop positions.
#define X_MIN_POS 0
#define X_MAX_POS 360
#define Y_MIN_POS 0
#define Y_MAX_POS 360
#define Z_MIN_POS 0
#define Z_MAX_POS 360
#define I_MIN_POS 0
#define I_MAX_POS 360
#define J_MIN_POS 0
#define J_MAX_POS 360
#define K_MIN_POS 0
#define K_MAX_POS 360

#define MIN_SOFTWARE_ENDSTOPS  // but no sub-values like MIN_SOFTWARE_ENDSTOP_X
#define MAX_SOFTWARE_ENDSTOPS  // but no sub-values like MAX_SOFTWARE_ENDSTOP_X

#define EEPROM_SETTINGS // save important data to EEPROM

#define SDSUPPORT

#define REPRAP_DISCOUNT_SMART_CONTROLLER // or your favorite flavor here

#define NUM_SERVOS 1 // for the gripper

Flash your board

Press the Compile button (1) to check for errors. Press the Upload button (2) to send it to your connected device. Press the Connect button (3) to open a serial monitor and check that your device says it is now a Marlin device. If all goes well, you’re ready to rock!

Test your new device

Even before your board is installed in an arm you should be able to home it with G28 and move it with G0/G1. Remember: every bug is just a test you didn’t run!

Final thoughts

Now that you have a 3D printer board setup to run Marlin, it should be able to receive angle values as gcode. Each set is one forward kinematic pose of the arm. Moving between two poses will send the arm in curving arcs. Lots of poses close together will create the look of straight lines. Planning all those poses is easy for apps like Robot Overlord. That’s why I wrote it!

Got more questions? Something out of date in this post? Join us on Discord.

News

Friday Facts 12: How to use Marlin in a Robot Arm

Building a robot arm is one thing, but what about writing the code to make it run? Some people want to learn the fine points of precision stepper motor control, forward and inverse kinematics, and then debug all that stuff. For the rest, working together gets the job done faster. For those people the Marlin 3D printer firmware is a great option. Today I’m going to show how I tweaked it to run in the Sixi 3 robot arm. Please share your experience with us so we can improve this post.

Marlin?

Marlin 3D printer firmware is the code in the brain of a very large number of printers. It is very flexible with a few changes. Most people might think of printers as having four motors – one for each direction and one for the extruder. But recent changes mean that Marlin can run up to six motors. That’s great for us, because most robot arms are 6 or less.

With Marlin installed you’ll be able to control the angle of each motor by sending gcode commands and even drive them simultaneously. With Marlin’s homing routines you could locate position, and new options coming in the near future will give real time feed back (more on that later)

What needs to be tweaked

Pour yourself a drink and settle in. This list will touch at least two files and take some time… OR you can use the sixi3 branch I maintain and adjust it for your speeds and gear ratios.

I keep trying new ways to make this list less dry. What do you think?

/Marlin/Configuration.h

Old valueNew Value
#define STRING_CONFIG_H_AUTHOR “(none, default config)”#define STRING_CONFIG_H_AUTHOR “(Sixi3, Marginally Clever Robots)”
#define MOTHERBOARD BOARD_RAMPS_14_EFB#define MOTHERBOARD BOARD_RUMBA
//#define CUSTOM_MACHINE_NAME “3D Printer”#define CUSTOM_MACHINE_NAME “Robot Arm”
//#define LINEAR_AXES 3#define LINEAR_AXES 6
#define AXIS4_NAME ‘A’#define AXIS4_NAME ‘U’
#define AXIS5_NAME ‘B’#define AXIS5_NAME ‘V’
#define AXIS6_NAME ‘C’#define AXIS6_NAME ‘W’
#define EXTRUDERS 1define EXTRUDERS 0
#define USE_XMIN_PLUG
#define USE_YMIN_PLUG
#define USE_ZMIN_PLUG
//#define USE_XMIN_PLUG
//#define USE_YMIN_PLUG
//#define USE_ZMIN_PLUG
//#define I_DRIVER_TYPE A4988
//#define J_DRIVER_TYPE A4988
//#define K_DRIVER_TYPE A4988
#define E0_DRIVER_TYPE A4988
#define I_DRIVER_TYPE A4988
#define J_DRIVER_TYPE A4988
#define K_DRIVER_TYPE A4988
//#define E0_DRIVER_TYPE A4988
#define DEFAULT_AXIS_STEPS_PER_UNIT { 80, 80, 400, 500 }#define DEFAULT_AXIS_STEPS_PER_UNIT { 105, 105, 105, 105, 105, 105 }
#define DEFAULT_MAX_FEEDRATE { 300, 300, 5, 25 }#define DEFAULT_MAX_FEEDRATE { 5, 5, 5, 5, 5, 5 }
#define DEFAULT_MAX_ACCELERATION { 3000, 3000, 100, 10000 }#define DEFAULT_MAX_ACCELERATION { 10, 10, 10, 10, 10, 10 }
//#define CLASSIC_JERK#define CLASSIC_JERK
//#define S_CURVE_ACCELERATION#define S_CURVE_ACCELERATION
#define E_ENABLE_ON 0 // For all extruders
//#define I_ENABLE_ON 0
//#define J_ENABLE_ON 0
//#define K_ENABLE_ON 0
//#define E_ENABLE_ON 0 // For all extruders
#define I_ENABLE_ON 0
#define J_ENABLE_ON 0
#define K_ENABLE_ON 0
#define INVERT_Y_DIR true#define INVERT_Y_DIR false
//#define INVERT_I_DIR false
//#define INVERT_J_DIR false
//#define INVERT_K_DIR false
#define INVERT_I_DIR false
#define INVERT_J_DIR false
#define INVERT_K_DIR false
//#define I_HOME_DIR -1
//#define J_HOME_DIR -1
//#define K_HOME_DIR -1
#define I_HOME_DIR -1
#define J_HOME_DIR -1
#define K_HOME_DIR -1
define X_BED_SIZE 200
define Y_BED_SIZE 200
//#define X_BED_SIZE 200
//#define Y_BED_SIZE 200
#define X_MIN_POS 0
#define Y_MIN_POS 0
#define Z_MIN_POS 0
#define X_MAX_POS X_BED_SIZE
#define Y_MAX_POS Y_BED_SIZE
#define X_MIN_POS -360
#define Y_MIN_POS 360
#define Z_MIN_POS -360
#define X_MAX_POS 360
#define Y_MAX_POS -360
//#define I_MIN_POS 0
//#define I_MAX_POS 50
//#define J_MIN_POS 0
//#define J_MAX_POS 50
//#define K_MIN_POS 0
//#define K_MAX_POS 50
#define I_MIN_POS -360
#define I_MAX_POS 360
#define J_MIN_POS -360
#define J_MAX_POS 360
#define K_MIN_POS -360
#define K_MAX_POS 360
#define HOMING_FEEDRATE_MM_M { (50*60), (50*60), (4*60) }#define HOMING_FEEDRATE_MM_M { (4*60), (4*60), (4*60), (4*60), (4*60), (4*60) }
//#define EEPROM_SETTINGS#define EEPROM_SETTINGS
//#define SDSUPPORT#define SDSUPPORT
//#define REPRAP_DISCOUNT_SMART_CONTROLLER#define REPRAP_DISCOUNT_SMART_CONTROLLER

/Marlin/Configuration_adv.h

define AXIS_RELATIVE_MODES { false, false, false, false }#define AXIS_RELATIVE_MODES { false, false, false, false, false, false }
#define HOMING_BUMP_MM      { 5, 5, 2 }
#define HOMING_BUMP_DIVISOR { 2, 2, 4 }
#define HOMING_BUMP_MM      { 5, 5, 5, 5, 5, 5 }
#define HOMING_BUMP_DIVISOR { 2, 2, 2, 2, 2, 2 }

Notes

  • MOTHERBOARD is your choice of brain board. Anything Mariln supports AND has 6 axies will work.
  • DEFAULT_AXIS_STEPS_PER_UNIT is the gear ratio at the given joint. For all sixi3 gearboxes the ratio is 70:1 (harmonic) * 54:20 (timing belt) * 200/360 (for 1.8 degree stepper motors at full step) = 105.
  • Because the gear ratio is so high the motors are not physically able to exceed the DEFAULT_MAX_FEEDRATE. If you use faster motors or a faster brain board you may be able to improve on these numbers.
  • EEPROM_SETTINGS, SDSUPPORT, and REPRAP_DISCOUNT_SMART_CONTROLLER are not required. I use these to tweak settings for testing, run programs from the SD card, and to have an LCD panel on my robot.
  • Every other change is to adjust from 3 axies to 6.

Homing and Real time feedback

There are some exciting new features coming to Marlin that should make real time feedback possible. This means we’ll know the robot position without having to guess or to home. It also means we can tell when the actual position deviates from the expected position too much that a collision has occurred and that can save a lot of trouble! The new configuration options to explore are:

  • REALTIME_REPORTING_COMMANDS adds some “quick commands” that get processed before anything else in the gcode buffer of the robot. Great for emergency breaking and for requesting position information (Gcode “S000”)
  • M114_REALTIME adds “M114 R” which reports the real-time position of the robot instead of the projected position at the end of the planned moves.
  • I2C_POSITION_ENCODERS is a first pass at adding real time sensors. This will no doubt be expanded later to include other types and features.

Further Reading

The Marlin Configuration guide online