STL Object Loader for 3D rendering.

Hi all.

Thought I’d share some code for loading *.STL object definition files into a vertex array for use in rendering them as a 3D object. I developed this for an image generation program that created previews from different perspectives of the object. I wrote everything in VB, but I am sure it can be converted to C# (for you poor souls).

The first thing that was needed was a custom Vertex definition:

' Copyright 2017 by James Plotts.
' Licensed under Gnu GPL 3.0.

Imports Microsoft.Xna.Framework
Imports Microsoft.Xna.Framework.Graphics

Namespace OpenForge.Development


    ''' <summary>
    ''' Custom Vertex that allows the use of Position, Color and a Normal.
    ''' </summary>
    ''' <remarks>  Thanks to Riemer's XNA Tutorials for this format.  
    ''' http://www.riemers.net/eng/Tutorials/XNA/Csharp/Series1/Lighting_basics.php 
    ''' 
    ''' </remarks>
    Public Structure VertexPositionColorNormal

        ''' <summary>
        ''' Vector3 describing the Vertex Location in 3D space.
        ''' </summary>
        ''' <remarks></remarks>
        Public Position As Vector3

        ''' <summary>
        ''' Color at the Vertex positon.
        ''' </summary>
        ''' <remarks></remarks>
        Public Color As Color

        ''' <summary>
        ''' Normal Coordinates for the Vertex
        ''' </summary>
        ''' <remarks></remarks>
        Public Normal As Vector3

        Public Sub New(ByVal vPos As Vector3, ByVal vCol As Color, ByVal vNor As Vector3)
            Position = vPos
            Color = vCol
            Normal = vNor
        End Sub


        ''' <summary>
        ''' Necessary VertexDeclaration information to use this Vertex with GraphicsDevice.DrawUserPrimitives.
        ''' </summary>
        ''' <value></value>
        ''' <returns>Valid VertexDeclaration defining the elements of this structure.</returns>
        ''' <remarks>
        ''' Example use:
        ''' GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, NumTriangles, VertexPositionColorNormal.VertexDeclaration)
        '''                                                                                                                    ^^^^^^^^^^^^^^^^^
        ''' </remarks>
        Public Shared ReadOnly Property VertexDeclaration() As VertexDeclaration
            Get
                Return New VertexDeclaration({ _
                    New VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), _
                    New VertexElement(12, VertexElementFormat.Color, VertexElementUsage.Color, 0), _
                    New VertexElement(16, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0)})
            End Get
        End Property

    End Structure

End Namespace

The next was a structure to load the STL object data from a stream and convert it to a vertex array:

' Copyright 2017 by James Plotts.
' Licensed under Gnu GPL 3.0.
' Thanks to thjerman for the Stereolithography File Formats post at:
'   http://forums.codeguru.com/showthread.php?148668-loading-a-stl-3d-model-file


Imports System
Imports System.IO

Namespace OpenForge.Development

    Public Class STLDefinition

        Public Class STLObject
            Public STLHeader As New STLHeader
            Public Vertices() As VertexPositionColorNormal
            Public Facets() As Facet
        End Class

        Public Class STLHeader
            'id' is a null-terminated string of the form "filename.stl", where filename is the name of the converted ".bin" file.
            Public Id(22) As Char
            'date' is the date stamp in UNIX ctime() format.
            Public DateCreated(26) As Char
            'xmin' - 'zmax' are the geometric bounds on the data 
            Public xmin As Single
            Public xmax As Single
            Public ymin As Single
            Public ymax As Single
            Public zmin As Single
            Public zmax As Single
            Public xpixelsize As Single  ' Dimensions of grid for this model
            Public ypixelsize As Single  ' in user units.
            Public nfacets As UInt32

            Public ReadOnly Property XCenter As Single
                Get
                    Return (xmax - xmin) / 2 + xmin
                End Get
            End Property

            Public ReadOnly Property YCenter As Single
                Get
                    Return (ymax - ymin) / 2 + ymin
                End Get
            End Property

            Public ReadOnly Property ZCenter As Single
                Get
                    Return (zmax - zmin) / 2 + zmin
                End Get
            End Property
        End Class

        Public Class Vertex
            Public x As Single
            Public y As Single
            Public z As Single
        End Class

        Public Class Facet
            Public normal As New Vertex '  facet surface normal
            Public v1 As New Vertex     '  vertex 1
            Public v2 As New Vertex     '  vertex 2
            Public v3 As New Vertex     '  vertex 3
        End Class

        ''' <summary>
        ''' LoadSTL reads a stream and converts the data to an STLObject.  
        ''' Stream must contain an STL formatted file.
        ''' </summary>
        ''' <param name="stream"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Shared Function LoadSTL(ByVal stream As Stream) As STLDefinition.STLObject
            Dim vStl As New STLDefinition.STLObject
            Try
                Dim index As Int32 = 0
                Dim tb(4) As Byte
                Dim tb2(30) As Byte
                Dim sr As Stream = stream
                Dim x1 As Single, x2 As Single
                Dim y1 As Single, y2 As Single
                Dim z1 As Single, z2 As Single
                stream.Read(tb2, 0, 22)
                stream.Read(tb2, 0, 26)
                With vStl
                    With .STLHeader
                        .xmin = rc(sr)
                        .xmax = rc(sr)

                        .ymin = rc(sr)
                        .ymax = rc(sr)

                        .xmin = rc(sr)
                        .xmax = rc(sr)

                        .xpixelsize = rc(sr)
                        .ypixelsize = rc(sr)
                        sr.Read(tb, 0, 4)
                        .nfacets = BitConverter.ToUInt32(tb, 0)
                    End With
                    Dim retval As Facet
                    ReDim .Facets(CInt(.STLHeader.nfacets))
                    For i As Int32 = 0 To CInt(.STLHeader.nfacets) - 1
                        retval = New Facet
                        With retval
                            With .normal
                                .x = rc(sr)
                                .y = rc(sr)
                                .z = -rc(sr) ' Negative Z value Flips to our coordinate system
                            End With
                            With .v1
                                .x = rc(sr)
                                If .x < x1 Then x1 = .x
                                If .x > x2 Then x2 = .x
                                .y = rc(sr)
                                If .y < y1 Then y1 = .y
                                If .y > y2 Then y2 = .y
                                .z = -rc(sr)
                                If .z < z1 Then z1 = .z
                                If .z > z2 Then z2 = .z
                            End With
                            With .v2
                                .x = rc(sr)
                                If .x < x1 Then x1 = .x
                                If .x > x2 Then x2 = .x
                                .y = rc(sr)
                                If .y < y1 Then y1 = .y
                                If .y > y2 Then y2 = .y
                                .z = -rc(sr)
                                If .z < z1 Then z1 = .z
                                If .z > z2 Then z2 = .z
                            End With
                            With .v3
                                .x = rc(sr)
                                If .x < x1 Then x1 = .x
                                If .x > x2 Then x2 = .x
                                .y = rc(sr)
                                If .y < y1 Then y1 = .y
                                If .y > y2 Then y2 = .y
                                .z = -rc(sr)
                                If .z < z1 Then z1 = .z
                                If .z > z2 Then z2 = .z
                            End With
                        End With
                        sr.Read(tb, 0, 2) ' just padding bytes not used.
                        .Facets(i) = retval
                    Next
                End With
                With vStl.STLHeader
                    If x1 > x2 Then
                        .xmin = x2
                        .xmax = x1
                    Else
                        .xmin = x1
                        .xmax = x2
                    End If
                    If y1 > y2 Then
                        .ymin = y2
                        .ymax = y1
                    Else
                        .ymin = y1
                        .ymax = y2
                    End If
                    If z1 > z2 Then
                        .zmin = z2
                        .zmax = z1
                    Else
                        .zmin = z1
                        .zmax = z2
                    End If
                End With
            Catch
                vStl = Nothing
            End Try
            Return vStl
        End Function

        ''' <summary>
        ''' Reads 4 bytes from the supplied Stream,
        ''' converts them to a Single and returns the result.
        ''' </summary>
        ''' <param name="sr">A valid Stream object</param>
        ''' <returns>A Single value read from the stream.</returns>
        ''' <remarks></remarks>
        Private Shared Function rc(ByVal sr As Stream) As Single
            Dim tb(4) As Byte
            sr.Read(tb, 0, 4)
            Return BitConverter.ToSingle(tb, 0)
        End Function

    End Class

End Namespace

Then, the function that loads the file:

        Private pvtUS1 As Single, pvtUS2 As Single
        ''' <summary>
        ''' Displays an open file dialog and then loads the chosen STL object.
        ''' </summary>
        ''' <remarks></remarks>
        <STAThreadAttribute> _
        Sub BackgroundLoader()
            Dim fd As New OpenFileDialog
            Dim dlgres As DialogResult
            fd.ShowReadOnly = True
            fd.Title = "Open *.STL File"
            fd.Multiselect = False
            fd.Filter = "STL Files (*.stl)|*.stl"

            fd.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
            dlgres = fd.ShowDialog()
            If dlgres = DialogResult.OK Then
                Dim stl As STLDefinition.STLObject
                SavePath = fd.FileName
                stl = STLDefinition.LoadSTL(fd.OpenFile())
                NumFacets = CInt(stl.STLHeader.nfacets)
                pvtUS1 = stl.STLHeader.xpixelsize
                pvtUS2 = stl.STLHeader.ypixelsize
                Dim vn As Vector3
                Dim lVertices(NumFacets * 3) As VertexPositionColorNormal
                With stl
                    For i As Int32 = 0 To NumFacets - 1
                        With .Facets(i)
                            With .normal
                                vn = New Vector3(.x, .y, .z)
                            End With
                            With .v1
                                lVertices(i * 3) = New VertexPositionColorNormal(New Vector3(.x, .y, .z), ObjectColor, vn)
                            End With
                        End With
                        With .Facets(i)
                            With .normal
                                vn = New Vector3(.x, .y, .z)
                            End With
                            With .v2
                                lVertices(i * 3 + 1) = New VertexPositionColorNormal(New Vector3(.x, .y, .z), ObjectColor, vn)
                            End With
                        End With
                        With .Facets(i)
                            With .normal
                                vn = New Vector3(.x, .y, .z)
                            End With
                            With .v3
                                lVertices(i * 3 + 2) = New VertexPositionColorNormal(New Vector3(.x, .y, .z), ObjectColor, vn)
                            End With
                        End With
                    Next
                    With .STLHeader
                        ObjectCenter = Matrix.CreateTranslation(-.XCenter, -.YCenter, .ZCenter)
                        xMin = .xmin
                        xMax = .xmax
                        yMin = .ymin
                        yMax = .ymax
                        zMin = .zmin
                        zMax = .zmax
                    End With
                End With
                verticesloaded = False
                'ReDim vertices(NumFacets * 3)
                vertices = lVertices
                verticesloaded = True
                For i As Int32 = 0 To 4
                    OutputGenerated(i) = False
                Next
                bolRotateToggle = True
                CurDir = eDir.North
                RotateY = Matrix.Identity
                FocusPoint.Z = 0
                FocusPoint.X = 0
                CameraOffset.X = 1000
                CameraOffset.Z = 1000
                
            End If
            loadthreadrunning = False

        End Sub

And lastly, a function that illustrates how to render the verticies and also save the screenshot:

        Private Function GrabScreenshot() As Bitmap
            Dim ss As RenderTarget2D
            Dim b As System.Drawing.Bitmap = Nothing
            Static bolGeneratingCurrently As Boolean
            If Not bolGeneratingCurrently Then
                bolGeneratingCurrently = True
                Scales = New Vector3(ScaleValue, ScaleValue, ScaleValue)
                worldMatrix = ObjectCenter * Matrix.CreateScale(Scales) * Matrix.CreateRotationX(MathHelper.ToRadians(90.0F)) * RotateY * RotateTop
                BasicEffect.Projection = projectionMatrix
                BasicEffect.View = ViewMatrix
                BasicEffect.World = worldMatrix

                ' Turn off culling so we see both sides of our rendered triangle
                Dim RasterizerState As New RasterizerState()
                RasterizerState.CullMode = CullMode.None

                ' Prepare GraphicsDevice
                'GraphicsDevice.Clear(Microsoft.Xna.Framework.Color.CornflowerBlue)
                GraphicsDevice.RasterizerState = RasterizerState

                ss = New RenderTarget2D(GraphicsDevice, width, height, False, SurfaceFormat.Color, DepthFormat.Depth24)
                GraphicsDevice.SetRenderTarget(ss)
                Dim bolRunOnce As Boolean = True
                Do While bolRunOnce
                    GraphicsDevice.Clear(Microsoft.Xna.Framework.Color.CornflowerBlue)
                    For Each pass As EffectPass In BasicEffect.CurrentTechnique.Passes
                        pass.Apply()
                        If verticesloaded = True Then
                            GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, NumFacets, VertexPositionColorNormal.VertexDeclaration)
                        End If
                    Next
                    bolRunOnce = False
                Loop
                GraphicsDevice.SetRenderTarget(Nothing) ' finished with render target

                Dim fs As New MemoryStream
                Try
                    ' save intermediate PNG image to stream
                    ss.SaveAsPng(fs, width, height)
                    ' read image from stream to a bitmap object
                    b = New Bitmap(fs)
                Catch
                End Try
                fs.Close()
                ss = Nothing
                fs = Nothing
                bolGeneratingCurrently = False
            End If
            Return b
        End Function

There are a bajillion STL 3D object files available on the internet, which makes this appealing. What might be interesting to game developers is a website called Thingiverse, which has 3D STL files for miniature tabletop gaming (for 3D printing). Since the STL files are freely usable there, then, hey, you have a big asset library.

2 Likes

Here is an example of the output:

And here is a perspective view of an object:

These are just examples of tabletop gaming tile STL files that are rendered. There are lots of 3D landscape object as well as miniature gaming figurines on Thingiverse.