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.