Tony Lukasavage

Caffeine. Whiskey. Code. Mostly the last one.

Web-based STL Viewing: Three.js, WebGL, and Javascript Typed Arrays

| Comments

Get the full demo: jsstl on Github

Recently Github announced that they were integrating a web-based STL viewer into their interface. The STL file format has become very well known as of late do to the growing popularity of 3D printing among makers. STL is the format of choice for most 3D printing devices and is as such the format used by almost all accompanying software. So whether you want to print, manage STL files, or convert them to some other format, you need to get to know them well.

Seeing as how I’m not a maker but I am intrigued by the 3D printing process, about a year ago I implemented a pure Javascript STL parser (both ascii and binary format) and web-based renderer. It’s far from polished, but more than usable. Go ahead and check it out on Github. It makes use of a few cool technologies, including Javascript typed arrays, WebGL, and three.js.

Binary Parsing

Parsing the ascii format of STL files was pretty straight forward based on the specification. Verbose, but easy. The binary format on the other hand was a bit trickier. Javascript isn’t exactly known for its robust binary data handling. Despite this shortcoming, I really wanted to see if I could handle this in pure Javascript. Enter Javascript typed arrays.

Javascript typed arrays are a relatively new addition to some major browsers (see also, IE). Check compatibility here: caniuse.com/typedarrays

I won’t go into it all too deeply here, other than to say that they make binary parsing possible in Javascript. ArrayBuffers represent a generic, fixed-length data buffer, in this case used to store the data from a binary formatted STL file. The DataView in turn exposes a low-level interface for reading, manipulating, and writing ArrayBuffers. Both are used in conjunction to read and pull apart the binary STL into a format that can be used by the web-based rendering engine.

This small snippet below shows how a binary STL file can be read using the DataView. Be sure to check out the APIs for DataView and ArrayBuffer to get the full scope of what they can do.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// "stl" represents a raw STL binary read from HTTP response data
var parseStlBinary = function(stl) {
  // create three.js geometry object, discussed later
  var geo = new THREE.Geometry();

  // The stl binary is read into a DataView for processing
    var dv = new DataView(stl, 80); // 80 == unused header
    var isLittleEndian = true;

    // Read a 32 bit unsigned integer
    var triangles = dv.getUint32(0, isLittleEndian);

    var offset = 4;
    for (var i = 0; i < triangles; i++) {
        // Get the normal for this triangle by reading 3 32 but floats
        var normal = new THREE.Vector3(
            dv.getFloat32(offset, isLittleEndian),
            dv.getFloat32(offset+4, isLittleEndian),
            dv.getFloat32(offset+8, isLittleEndian)
        );
        offset += 12;

        // Get all 3 vertices for this triangle, each represented
        // by 3 32 bit floats.
        for (var j = 0; j < 3; j++) {
            geo.vertices.push(
                new THREE.Vector3(
                    dv.getFloat32(offset, isLittleEndian),
                    dv.getFloat32(offset+4, isLittleEndian),
                    dv.getFloat32(offset+8, isLittleEndian)
                )
            );
            offset += 12
        }

        // there's also a Uint16 "attribute byte count" that we
        // don't need, it should always be zero.
        offset += 2;

        // Create a new face for from the vertices and the normal
        geo.faces.push(new THREE.Face3(i*3, i*3+1, i*3+2, normal));
    }

    // continue parsing STL faces for rendering...
};

Rendering

Since STLs represent real objects, they obviously need to be rendered in 3 dimensions. On the web we have a few choices for that, but I’m going to let my framework of choice do the selection for me. In this case I used three.js. Three.js has exposed a 3D rendering API in Javascript that is compatible with both WebGL and the HTML5 canvas element. In this way you can gracefully fail back to canvas when operating in a browser that does not support the higher performing WebGL.

It’s not dumb luck that I chose to use this terrific library, I’ve used it before. Over a year ago I used three.js and Titanium to create an experimental 3D demonstration across multiple mobile devices using socket communication in realtime. Instead of trying to explain it all, you can check out the screencast I did regarding it below. This is part 3 of a 3 part series. Click here to check out the previous parts.

So needless to say I was already primed to use it again. In the STL viewer I would be using it to render the 3D triangle information from the STL files into faces of a mesh. This turned out to be pretty easy with three.js. The snippet below shows how I took the data I read from the STL in the Binary Parsing section above and then used it to render a series of triangles that would compose a mesh of the STL object.

1
2
3
4
5
6
7
8
9
10
11
mesh = new THREE.Mesh(
    // the "geo" object we filled with normals and vertices above
    geo,

    // create a material for the mesh
    new THREE.MeshLambertMaterial({
        overdraw:true,
        color: 0xaa0000,
        shading: THREE.FlatShading
    }
));

And that’s it. The hard part was done creating that geo object. We now have the mesh object to which we can add to a prepared three.js scene. For the full code, check the repo.

The Result

To keep things interesting, naturally I chose a weird, frankenstein of an STL in octocat that I found on thingiverse.com for my testing. I did this for 3 reasons.

  1. It was too unusual and cool to pass up.
  2. It had both the ascii and binary format available.
  3. It’s composed of almost 38,000 triangles. I wanted to see how well a web-based 3D renderer could handle a complex model.

So without further ado, here’s the end result, provided your browser supports it. Feel free to use, bend, mold, and/or steal this code as you like. A digital high five would be nice, but is not required.

Comments