Saving a binary STL file in Java

2.6k views Asked by At

I am trying to save some data as an STL file for use on a 3D printer. The STL file has two forms: ASCII and Binary. The ASCII format is relatively easy to understand and create but most 3D printing services require it to be in binary format.

The information about STL Binary is explained on the Wikipedia page here: http://en.wikipedia.org/wiki/STL_(file_format)

I know that I will require the data to be in a byte array but I have no idea how to go about interpreting the information from Wikipedia and creating the byte array. This is what I would like help with.

The code I have so far simply saves an empty byte array:

byte[] bytes = null;
FileOutputStream stream = new FileOutputStream("test.stl");
    try {
        stream.write(bytes);
    } finally {
        stream.close();
}
4

There are 4 answers

6
Holger On BEST ANSWER

If you start a new project on an up-to-date Java version, you should not hassle with OutputStreams. Use Channels and ByteBuffers instead.

try(FileChannel ch=new RandomAccessFile("test.stl", "rw").getChannel())
{
  ByteBuffer bb=ByteBuffer.allocate(10000).order(ByteOrder.LITTLE_ENDIAN);
  // ...
  // e.g. store a vertex:
  bb.putFloat(0.0f).putFloat(1.0f).putFloat(42);
  bb.flip();
  ch.write(bb);
  bb.clear();
  // ...
}

This is the only API providing you with the little-endian support as required. Then match the datatypes: UINT8 means unsigned byte, UINT32 means unsigned int, REAL32 means float, UINT16 means unsigned short, REAL32[3] means three floats (i.e. an array)

You don’t have to worry about the unsigned nature of the data types as long as you don’t exceed the max values of the corresponding signed Java types.

1
sigi On

You should generate this file in ASCII and use an ASCII to Binary STL Converter.

If you are unable to answer this question yourself, it is probably way easier to do it in ascii first.

http://www.thingiverse.com/thing:39655

0
Mike On

This is shouldn't be so ambiguous. The spec says:

UINT8[80] – Header
UINT32 – Number of triangles

foreach triangle
  REAL32[3] – Normal vector
  REAL32[3] – Vertex 1
  REAL32[3] – Vertex 2
  REAL32[3] – Vertex 3
  UINT16 – Attribute byte count
end     

That means a total file size of: 80 + 4 + Number of Triangles * ( 4 * 3 * 4 + 2 ).

So for example, 100 triangles ( 84+100*50 ), produces a 5084 byte file.

You can optimize the following functional code. Open the file and write the header:

        RandomAccessFile raf = new RandomAccessFile( fileName, "rw" );
        raf.setLength( 0L );
        FileChannel ch = raf.getChannel();

        ByteBuffer bb = ByteBuffer.allocate( 1024 ).order( ByteOrder.LITTLE_ENDIAN );

        byte titleByte[] = new byte[ 80 ];
        System.arraycopy( title.getBytes(), 0, titleByte, 0, title.length() );
        bb.put( titleByte );

        bb.putInt( nofTriangles );              // Number of triangles

        bb.flip();                              // prep for writing
        ch.write( bb );

In this code, the point vertices and triangle indices are in an arrays like this:

Vector3 vertices[ index ]
int indices[ index ][ triangle point number ]

Write the point data:

        for ( int i = 0; i < nofIndices; i++ )  // triangles
        {
            bb.clear();
            Vector3 normal = getNormal( indices[ i ][ 0 ], indices[ i ][ 1 ], indices[ i ][ 2 ] );
            bb.putFloat( normal[ k ].x );
            bb.putFloat( normal[ k ].y );
            bb.putFloat( normal[ k ].z );
                
            for ( int j = 0; j < 3; j++ )           // triangle indices
            {
                bb.putFloat( vertices[ indices[ i ][ j ] ].x );
                bb.putFloat( vertices[ indices[ i ][ j ] ].y );
                bb.putFloat( vertices[ indices[ i ][ j ] ].z );
            }
            bb.putShort( ( short ) 0 );             // number of attributes
            bb.flip();
            ch.write( bb );
        }

close the file:

        ch.close();

Get the normals:

Vector3 getNormal( int ind1, int ind2, int ind3 )
{
    Vector3 p1 = vertices[ ind1 ];
    Vector3 p2 = vertices[ ind2 ];
    Vector3 p3 = vertices[ ind3 ];
    return p1.cpy().sub( p2 ).crs( p2.x - p3.x, p2.y - p3.y, p2.z - p3.z ) ).nor();
}

See also:

Vector3

0
Mike On

Since your question was based on writing a file to send to a 3D printer, I suggest you ditch the STL format file and use an OBJ format file instead. It is much simpler to compose, and it produces a much smaller file. There isn't a binary flavor of OBJ, but it's still a pretty compact file as you will see.

The (abbreviated) spec says:

List all the geometric vertex coordinates as a "v", followed by x, y, z values, like:
    v 123.45 234.56 345.67

then List all the triangle as "f", followed by indices in a CCW order, like:
    f 1 2 3

Indices start with 1.
Use a # character to start a comment line. Don't append comments anywhere else in a line.
Blank lines are ok.

There's a whole bunch of other things it supports, like normals, and textures. But if all you want to do is write your geometry to file to import into a 3D printer then OBJ is actually preferred, and this simple content is valid and adequate.

Here is an example of a perfectly valid file composing a 1 unit cube, as imported successfully in Microsoft 3D Viewer (included in Win/10), AutoDesk MeshMixer (free download), and PrusaSlicers (free download)

# vertices
v 0 0 0
v 0 1 0
v 1 1 0
v 1 0 0
v 0 0 1
v 0 1 1
v 1 1 1
v 1 0 1
# triangle indices
f 1 3 4
f 1 2 3
f 1 6 2
f 1 5 6
f 1 8 5
f 1 4 8
f 3 7 8
f 3 8 4
f 3 6 7
f 2 6 3
f 5 8 7
f 5 7 6

If you've got data in multiple meshes, you ought to coalesce the vertices to eliminate duplicate points. But because the file is plain text you can use PrintWriter() object and println() methods to write the whole thing.