Skip to main content

OpenSCAD Rendering Tricks, Part 3: Web viewer

This is my sixth post in a series about the open source split-flap display I’ve been designing in my free time. Check out a video of the prototype.

Posts in the series:
Scripting KiCad Pcbnew exports
Automated KiCad, OpenSCAD rendering using Travis CI
Using UI automation to export KiCad schematics
OpenSCAD Rendering Tricks, Part 2: Laser Cutting
OpenSCAD Rendering Tricks, Part 3: Web viewer

One of my goals when building the split-flap display was to make sure it was easy to visualize the end product and look at the design in detail without having to download the full source or install any programs. It’s hard to get excited about a project you find online if you need to invest time and effort before you even know how it works or what it looks like. I’ve previously blogged about automatically exporting the schematics, PCB layout, and even an animated gif of the 3D model to make it easier to understand the project at a glance, but I wanted to take things a step further, with a full interactive 3D viewer right in the browser. Go ahead, click around:



(You can also find this on the split-flap project page)

The hard part about this, surprisingly, is not actually doing the 3d rendering in the browser with WebGL (I used three.js for that), but actually the process of converting an OpenSCAD model to something that three.js can render.

OpenSCAD can export an STL file of a model, which encodes the geometry of 3d shapes, but STL files don’t support any color or material properties. If we took the raw STL that OpenSCAD generates and gave it to three.js to render, we’d end up with something like this:


That cool 3d model we built doesn’t look so great when it’s monochromatic.

Are there other options for color? OpenSCAD can also export Additive Manufacturing File Format (AMF) files, which in theory can contain material/color information, but for a number of perfectly sensible reasons (discussed here, here, and here), OpenSCAD doesn’t actually include color when exporting AMF files either.

With a bit of scripting though, we can hack around this OpenSCAD limitation. Instead of exporting a single multi-color 3d model, we can export multiple STL files - one for each color in the model - and then tell three.js to render each in its appropriate color.

Color in OpenSCAD

Let’s first take a step back and look at an OpenSCAD model. Color can be applied to a component using RGB values like so:

color([0.8, 0.2, 0.1]) cube([2, 4, 8]);


Let’s suppose we have a more complicated model, with 3 distinct shapes in 2 colors:

color([0.8, 0.2, 0.1]) cube([2, 4, 8]);
color([0, 1, 0]) translate([5, 0, 0]) cube([2, 2, 2]);
color([0.8, 0.2, 0.1]) translate([0, 0, -5]) sphere(r=3, $fn=30);



In the more complicated case we’d want to export two STL files: one for the 2x4x8 box and sphere which are both the same red color, and one for the 2x2x2 cube which should be rendered in green.

Extracting colors from a .scad file

In order to automate per-color STL exports, we can start by reading the original .scad model and finding all unique colors. In the example above you could try simply searching for the color keyword and extracting the RGB value inside the parenthesis, but that approach falls apart for more sophisticated OpenSCAD models like this one:

really_cool_red = [0.8, 0.2, 0.1];
color(really_cool_red) cube([2, 4, 8]);


If we did a naive search for color(<value>) we’d end up extracting the string “really_cool_red” from inside the parenthesis, which is just a variable name and doesn’t actually tell us the RGB values!

It’s clear that just reading the raw source code won’t work; we need to actually run the code to evaluate expressions and variables. To do this, we can define an extremely straightforward “color extractor” module to help:

module color_extractor(c) {
    echo(extracted_color=c);
    children();
}


If we modify the .scad source by replacing instances of color( with color_extractor( [view code], then running OpenSCAD will print out the fully-evaluated color values for each usage of color_extractor:

ECHO: extracted_color = [0.8, 0.2, 0.1]
ECHO: extracted_color = [0, 1, 0]


It’s pretty easy to parse the RGB color values from that output [view code].

Rendering a single color at a time

The next step is to actually render STL files for each of those colors we identified.

Similar to our color_extractor approach above, we can define a “color_selector” module:

module color_selector(c) {
    if (c == <<<CURRENT COLOR>>>) {
        children();
    }
}


where <<<CURRENT COLOR>>> is some constant value.

Any child elements wrapped by a color_selector module will only be evaluated (and therefore rendered) if the color specified matches the constant <<<CURRENT COLOR>>>.

For each of the colors we identified earlier using the color_extractor we can do the following:
  • add the color_selector module definition to the .scad source, filling in the current color value for <<<CURRENT COLOR>>> [view code]
  • modify the .scad source to use color_selector( in place of color(. That is, color([1, 0, 0]) cube([2, 4, 8]) becomes color_selector([1, 0, 0]) cube([2, 4, 8]) [view code]
  • run OpenSCAD with the -o output.stl export option to generate an STL file containing only objects of the current color [view code]
A few of the single-color .scad models extracted from the splitflap design.

Putting it together

So far we can automatically detect all unique colors within a model and export separate STL files for each of those colors. The next step is to hook those up to three.js to render an interactive, full-color model.

Each of the the STL files only contains geometry and still no color info, so while we are exporting each color we can also generate a separate “manifest” json file that maps STL file names to the RGB color they represent:

    {
        "7c82990faec1bff4c63b2c808c53957a4a8846428de5f0f648a2550d7f22a6de.stl": [
            1.0,
            1.0,
            1.0
        ],
        "ef07a1fd1a788f2e516b8b90e6ce1b52186dc315adee7321b5cc79e6c0c2805f.stl": [
            0.0,
            0.0,
            0.0
        ],
        "809a14df161a03ec1105642994c3557f705c4d143339f6e12a87c6672b0dd420.stl": [
            1.0,
            0.843,
            0.0
        ],
        "26a4ace46ae39933e6a48c7216e05c492121851603e5b7af95fbc77a7f3d4698.stl": [
            0.882,
            0.694,
            0.486
        ],
        ...
    }


We finally have all the components we need to render the model!

Using three.js, we can start by loading that manifest file and for each entry call a helper function, passing the STL filename and RGB color for rendering:

var loader = new THREE.XHRLoader(THREE.DefaultLoadingManager);
loader.load('manifest.json', function(text) {
    var json = JSON.parse(text);
    for (var stl in Object.keys(json)) {
        var color = json[stl];
        loadStl(stl, new THREE.Color( color[0], color[1], color[2] ).getHex());
    }
});


The loadStl helper method needs to do a few things:
  • request and parse the STL file to get a THREE.Geometry for the shape (we use THREE.STLLoader() to do all the STL heavy lifting)
  • create a THREE.MeshPhongMaterial based on the RGB color from the manifest, to describe the object’s appearance
  • combine the Geometry and MeshPhongMaterial into a THREE.Mesh to represent the completed shape including color/texture
  • add the Mesh to the three.js Scene we’re building

var loadStl = function(url, color) {
    var loader = new THREE.STLLoader();
    loader.load(url, function(geometry) {
        var material = new THREE.MeshPhongMaterial({
            color: color,
            specular: 0x111111,
            shininess: 10
        });
        var mesh = new THREE.Mesh(geometry, material);
        mesh.castShadow = true;
        mesh.receiveShadow = true;
        scene.add(mesh);
    });
};


The rest of the viewer is a pretty standard three.js setup:
  • Set up a PerspectiveCamera [view code]
  • Configure a WebGLRenderer to render the scene to an html canvas element [view code]
  • Create a ground plane Mesh using a PlaneBufferGeometry and specify plane.receiveShadow = true so the model casts a shadow onto the ground [view code]
  • Add a HemisphereLight for general illumination [view code]
  • Add a few DirectionalLights to highlight the model and cast shadows [view code]
  • Create a Fog so that the ground plane fades away in the distance [view code]
  • Add OrbitControls so you can use the mouse/keyboard to move the camera around the model (see also Advanced Topics 3 below) [view code]

Conclusion

That’s about it! You can find a complete implementation of the OpenSCAD color exporter in the git repo, including the advanced topics discussed below. The three.js viewer javascript that powers https://scottbez1.github.io/splitflap and the interactive example above can be found in the /docs folder.

For the interactive example above, the STL and manifest are automatically generated by Travis and hosted on S3, based on the most recent code in the repo.

If you have any questions, reach out to me on Twitter or leave a comment below!

Advanced Topics 1: Walking a .scad dependency tree

The steps above described how to extract unique colors from a single .scad file, but more complex models often depend on components in multiple separate files (using a use<filename.scad> or include<filename.scad> statement to incorporate them).

In order to handle dependencies, we can do a basic breadth-first search (BFS) over .scad file nodes where the use and include statements define the edges to traverse [view code].

Advanced Topics 2: Keeping it clean

In the descriptions above, it was necessary to modify the .scad files in order to extract color information and export STL files, but it would be pretty bad etiquette to modify original source files from a script. Even if we made the script restore the files upon completion, you can still run into problems: what if you kill the script halfway through, before the restoration code runs?

The solution to this is to operate on a copy of the model rather than modifying the original. We can do this during the BFS by copying the contents of each .scad file we visit to an intermediate location for further processing [view code].

Of course, since each .scad file we visit might have dependencies, we have to remember to update any include<> statements to correctly point to the copied file paths rather than the originals [view code].

To make things simple, we name every copied intermediate file after its original file, but apply a hash to its absolute path. This gives an easy, deterministic filename that won’t contain any special characters:

def get_transformed_file_path(original_path):
    extension = os.path.splitext(original_path)[1]
    return hashlib.sha256(os.path.realpath(original_path)).hexdigest() + extension


[view code]

Using a hash of the full file path also effectively flattens the directory structure of files in the original model, which makes things easier to track and contain. For instance, some/referenced/file/in/some/subfolder/abc.scad becomes simply db8a65dd2f401b9bafc598c5693323f61c14ca5bdcec18a6d401524a99eaf6bf.scad. This works even if the original model used relative paths (include<../../foobar.scad>) or absolute paths (include</home/scott/model.scad>).

All of this .scad file copying and manipulation is wrapped up in a walk_and_mutate_scad_files helper, which takes a function that can mutate file content as it is copied to the intermediate output folder. [view code]

One additional advantage of copying files to another directory when modifying them is that it makes parallelization possible. Exporting each color requires slightly different .scad files each time; by copying the modified source files to distinct folders it’s possible to run multiple instances of OpenSCAD in parallel using a python Pool to export them faster [view code]

Advanced Topics 3: Bounded three.js OrbitControls

The standard three.js OrbitControls let you click to move the camera around the 3d scene. In our scene, however, there is a solid ground plane, so it doesn’t make sense to allow the camera to move below the ground.

OrbitControls offers a way to restrict the angle of the camera, with a minPolarAngle and maxPolarAngle option. Setting maxPolarAngle to PI/2 is close to the behavior we want - it only allows the camera to occupy the upper hemisphere of the scene - but isn’t quite right. In our scene, the camera is looking at a point roughly halfway up the model, so a maxPolarAngle of PI/2 would mean you could only look at the model straight on or from above; it would be impossible to look upward at the model like this:
This angle looking upward wouldn’t be possible if we restricted the maxPolarAngle to PI/2

Instead of restricting the camera angle, we want to restrict the camera’s location so it can never move below the ground plane. The standard OrbitControls doesn't support this, so we can add a customizable predicate function, validUpdate, to OrbitControls which determines whether or not the updated camera pose is allowed as the camera is moved. During the OrbitControls update, we can check if the new pose is valid, and if not, reset it to the previous pose [view code].

If we want to prevent the camera from moving below the ground plane, we can define a simple validUpdate predicate:

controls.validUpdate = function(position, quaternion, target) {
    // Don't allow camera to go below ground
    return position.y > 0;
};


Comments

  1. This is great! I've been settling for just exported images of my OpenSCAD projects when I blog about them, but I'm going to try this instead next time.

    ReplyDelete
  2. Thanks Scott, I got the python scripting going. But due to the solid nature of the OpenScad geometry, I was unable to work out a way to separate the regions in the below (experimantal) object by colour:

    difference() {
    color([1,1,1]) sphere(10,$fn=100,$fa=5);
    color([1,1,0]) translate([0,0,10]) sphere(3,$fn=100,$fa=5);
    color([0,0,1]) translate([2,2,sqrt(92)]) sphere(3,$fn=100,$fa=5);
    }

    I tried using intersection with difference to create a hollow white sphere and two thin scoopy pieces (one yellow, the other blue), but the python method - perhaps not surprisingly - generated three complete spheres in separate stl files.

    Any ideas?

    Cheers,

    Jim

    ReplyDelete
  3. That's a pretty interesting edge case. Multi-color volumes are tricky, as mentioned in one of the OpenSCAD threads on color exports:

    "So the question then is, what do the CSG ops mean where there is a
    material spec? Clearly for two objects A and B, if the material of A
    and B is the same, then result of all ops is the same material. If A
    and B are different, then their intersection creates a problem - what
    material is it?" http://forum.openscad.org/Exporting-colors-tp2651p2672.html

    Unfortunately to simplify things my python code assumes that each volume is a single color, but in your example different surfaces of the same volume end up with distinct colors.

    Exporting that to be rendered "correctly" in three.js would probably require a fairly different approach. The approach I took exports a separate STL mesh per volume/color and applies the color in three.js to each STL-based mesh as it's loaded. But to support multiple colors, you would probably want to use something like a MultiMaterial (https://threejs.org/docs/api/materials/MultiMaterial.html) to apply multiple colors to a single mesh, but I'm still not quite sure how you'd programmatically identify which surfaces should be assigned which color based on the original OpenSCAD model.

    So I don't have any great suggestions for doing that correctly. A less "correct," hacky idea would be to try to split the volume into separate, overlapping, volumes per color. So for a difference() operation like your example, you would first keep the original difference and assign it the primary volume's color (white). Then, for each of the subtracted child volumes in the difference you would replicate the entire difference operation, but with a slightly "inset" primary volume and with all child volumes outset a tiny amount, except for the current one which is instead inset a tiny amount.

    This has the effect of creating overlapping volumes in different colors where the colored portions are only exposed along the intersection surface, and looks pretty similar to the "correct" rendering with a small enough inset/offset epsilon.

    Unfortunately there isn't a 3d offset/inset operator in OpenSCAD, so I don't know how to do this for the general case (and there may in fact be some edge cases where this general inset/offset approach doesn't work at all, since I've only been thinking through your simple spheres example), but in this example I've applied the inset/offset by just adjusting the sphere radius.

    eps=0.01;
    color([1,1,1])
    difference() {
    sphere(10,$fn=100,$fa=5);
    translate([0,0,10]) sphere(3,$fn=100,$fa=5);
    translate([2,2,sqrt(92)]) sphere(3,$fn=100,$fa=5);
    }
    color([1,1,0])
    difference() {
    sphere(10 - eps,$fn=100,$fa=5);
    translate([0,0,10]) sphere(3 - eps,$fn=100,$fa=5);
    translate([2,2,sqrt(92)]) sphere(3 + eps,$fn=100,$fa=5);
    }
    color([0,0,1])
    difference() {
    sphere(10 - eps * 2,$fn=100,$fa=5);
    translate([0,0,10]) sphere(3 + eps,$fn=100,$fa=5);
    translate([2,2,sqrt(92)]) sphere(3 - eps,$fn=100,$fa=5);
    }

    You can try changing the eps value to a larger value like 0.2 to better visualize what's going on.

    Hopefully that makes sense, it's definitely a tricky problem.

    ReplyDelete
  4. Thanks Scott.

    What can be identified (mathematically) when objects A and B are intersected is the boundary of the intersection (e.g. the semispherical surface in

    color([1,0,0])
    intersection() {
    sphere(10,$fn=100,$fa=5);
    translate([0,0,10]) sphere(3,$fn=100,$fa=5);
    }

    ). In Maya for example, this part of the mesh is given its own node, making material assignation easy. Of course, the boundary is a surface and so is not intelligible (perhaps?) to a cad system that deals only with solids. (The problem with Maya from the point of view of my project is that it's booleans are infuriatingly error-prone for complex surface meshes developed using recursive differences.)

    I recognise now that I have chosen the wrong platform, but I am still attracted to the idea of using CGAL's boolean engine, with all of its mathematical precision, to perform my sphere-scooping operations.

    Update: I've managed to cajole CGAL into working on my Windows machine; so now for the experimentation.

    Jim

    ReplyDelete

Post a Comment

Popular posts from this blog

Using UI automation to export KiCad schematics

This is my third post in a series about the open source split-flap display I’ve been designing in my free time. I’ll hopefully write a bit more about the overall design process in the future, but for now wanted to start with some fairly technical posts about build automation on that project. Posts in the series: Scripting KiCad Pcbnew exports Automated KiCad, OpenSCAD rendering using Travis CI Using UI automation to export KiCad schematics OpenSCAD Rendering Tricks, Part 1: Animated GIF OpenSCAD Rendering Tricks, Part 2: Laser Cutting OpenSCAD Rendering Tricks, Part 3: Web viewer Since I’ve been designing the split-flap display as an open source project, I wanted to make sure that all of the different components were easily accessible and visible for someone new or just browsing the project. Today’s post continues the series on automatically rendering images to include in the project’s README, but this time we go beyond simple programmatic bindings to get what we want: the

Scripting KiCad Pcbnew exports

This is my first post in a series about the  open source split-flap display  I’ve been designing in my free time. Check out a  video of the prototype . Posts in the series: Scripting KiCad Pcbnew exports Automated KiCad, OpenSCAD rendering using Travis CI Using UI automation to export KiCad schematics OpenSCAD Rendering Tricks, Part 1: Animated GIF OpenSCAD Rendering Tricks, Part 2: Laser Cutting OpenSCAD Rendering Tricks, Part 3: Web viewer For the past few months I’ve been designing an open source split-flap display in my free time — the kind of retro electromechanical display that used to be in airports and train stations before LEDs and LCDs took over and makes that distinctive “tick tick tick tick” sound as the letters and numbers flip into place. I designed the electronics in KiCad, and one of the things I wanted to do was include a nice picture of the current state of the custom PCB design in the project’s README file. Of course, I could generate a snapshot of the