Skip to content

Commit

Permalink
STL Loader (#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
Floppy authored Nov 11, 2024
1 parent b0bf979 commit 30d0199
Show file tree
Hide file tree
Showing 7 changed files with 392 additions and 0 deletions.
68 changes: 68 additions & 0 deletions examples/10_stl_loader_example.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
require_relative './example_helper'

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
ASPECT = SCREEN_WIDTH.to_f / SCREEN_HEIGHT.to_f

scene = Mittsu::Scene.new
camera = Mittsu::PerspectiveCamera.new(75.0, ASPECT, 0.1, 1000.0)

renderer = Mittsu::OpenGLRenderer.new width: SCREEN_WIDTH, height: SCREEN_HEIGHT, title: '10 STL Loader Example'
renderer.shadow_map_enabled = true
renderer.shadow_map_type = Mittsu::PCFSoftShadowMap

loader = Mittsu::STLLoader.new

object = loader.load(File.expand_path('../male02.stl', __FILE__))

object.receive_shadow = true
object.cast_shadow = true

object.traverse do |child|
child.receive_shadow = true
child.cast_shadow = true
end

scene.add(object)

scene.print_tree

floor = Mittsu::Mesh.new(
Mittsu::BoxGeometry.new(1000.0, 1.0, 1000.0),
Mittsu::MeshPhongMaterial.new(color: 0xffffff)
)
floor.position.y = -1.0
floor.receive_shadow = true
scene.add(floor)

scene.add Mittsu::AmbientLight.new(0xffffff)

light = Mittsu::SpotLight.new(0xffffff, 1.0)
light.position.set(300.0, 200.0, 0.0)

light.cast_shadow = true
light.shadow_darkness = 0.5

light.shadow_map_width = 1024
light.shadow_map_height = 1024

light.shadow_camera_near = 1.0
light.shadow_camera_far = 2000.0
light.shadow_camera_fov = 60.0

light.shadow_camera_visible = false
scene.add(light)

camera.position.z = 200.0
camera.position.y = 100.0

renderer.window.on_resize do |width, height|
renderer.set_viewport(0, 0, width, height)
camera.aspect = width.to_f / height.to_f
camera.update_projection_matrix
end

renderer.window.run do
object.rotation.y += 0.1
renderer.render(scene, camera)
end
Binary file added examples/male02.stl
Binary file not shown.
1 change: 1 addition & 0 deletions lib/mittsu/loaders.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
require 'mittsu/loaders/obj_loader'
require 'mittsu/loaders/mtl_loader'
require 'mittsu/loaders/obj_mtl_loader'
require 'mittsu/loaders/stl_loader'
186 changes: 186 additions & 0 deletions lib/mittsu/loaders/stl_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
require 'stringio'

module Mittsu
class STLLoader
include EventDispatcher

FLOAT = /[\d|.|+|\-|e]+/
NORMAL_PATTERN = /normal\s+(#{FLOAT})\s+(#{FLOAT})\s+(#{FLOAT})/
VERTEX_PATTERN = /^\s*vertex\s+(#{FLOAT})\s+(#{FLOAT})\s+(#{FLOAT})/

def initialize(manager = DefaultLoadingManager)
@manager = manager
@_listeners = {}
end

def load(url)
loader = FileLoader.new(@manager)

data = loader.load(url)
parse(data)
end

def parse(data)
reset_loader_vars
stream = StringIO.new(data, "rb")
# Load STL header (first 80 bytes max)
header = stream.read(80)
if header.slice(0,5) === "solid"
stream.rewind
parse_ascii(stream)
else
parse_binary(stream)
end
@group
end

private

def reset_loader_vars
@vertex_hash = {}
@vertex_count = 0
@line_num = 0
@group = Group.new
end

def parse_ascii(stream)
while line = read_line(stream)
case line
when /^\s*solid/
parse_ascii_solid(stream)
else
raise_error
end
end
end

def parse_ascii_solid(stream)
vertices = []
faces = []
while line = read_line(stream)
case line
when /^\s*facet/
facet_vertices, face = parse_ascii_facet(line, stream)
vertices += facet_vertices
faces << face
when /^\s*endsolid/
break
else
raise_error
end
end
add_mesh vertices, faces
end

def parse_ascii_facet(line, stream)
vertices = []
normal = nil
if line.match NORMAL_PATTERN
normal = Vector3.new($1, $2, $3)
end
while line = read_line(stream)
case line
when /^\s*outer loop/
nil # Ignored
when /^\s*endloop/
nil # Ignored
when VERTEX_PATTERN
vertices << Vector3.new($1, $2, $3)
when /^\s*endfacet/
break
else
raise_error
end
end
return nil if vertices.length != 3
# Merge with existing vertices
face, new_vertices = face_with_merged_vertices(vertices)
face.normal = normal
return new_vertices, face
end

def parse_binary(stream)
vertices = []
faces = []
num_faces = stream.read(4).unpack('L<').first
num_faces.times do |i|
# Face normal
normal = read_binary_vector(stream)
# Vertices
face_vertices = []
face_vertices << read_binary_vector(stream)
face_vertices << read_binary_vector(stream)
face_vertices << read_binary_vector(stream)
# Throw away the attribute bytes
stream.read(2)
# Store data
face, new_vertices = face_with_merged_vertices(face_vertices)
face.normal = normal
faces << face
vertices += new_vertices
end
add_mesh vertices, faces
end

def face_with_merged_vertices(vertices)
new_vertices = []
indices = []
vertices.each do |v|
index, is_new = vertex_index(v)
indices << index
if is_new
new_vertices << v
@vertex_count += 1
end
end
# Return face and new vertex list
return Face3.new(
indices[0],
indices[1],
indices[2]
), new_vertices
end

def vertex_index(vertex)
key = vertex_key(vertex)
if i = @vertex_hash[key]
return i, false
else
return (@vertex_hash[key] = @vertex_count), true
end
end

def vertex_key(vertex)
vertex.elements.pack("D*")
end

def add_mesh(vertices, faces)
geometry = Geometry.new
geometry.vertices = vertices
geometry.faces = faces
geometry.compute_bounding_sphere
@group.add Mesh.new(geometry)
end

def read_binary_vector(stream)
Vector3.new(
read_le_float(stream),
read_le_float(stream),
read_le_float(stream)
)
end

def read_le_float(stream)
stream.read(4).unpack('e').first
end

def read_line(stream)
@line_num += 1
stream.gets
end

def raise_error
raise "Mittsu::STLLoader: Unhandled line #{@line_num}"
end
end
end
97 changes: 97 additions & 0 deletions test/loaders/test_stl_ascii_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
require 'minitest_helper'

class TestSTLASCIILoader < Minitest::Test
def test_parse
loader = Mittsu::STLLoader.new

object = loader.parse """solid
facet normal 0 0 1
outer loop
vertex -1 100.0e-2 0
vertex -1 -1e0 0
vertex 1 100.0e-2 0
endloop
endfacet
facet normal 0 0 1
outer loop
vertex -1 -1e0 0
vertex 1 -1e0 0
vertex 1 100.0e-2 0
endloop
endfacet
endsolid
"""

assert_kind_of Mittsu::Group, object
assert_equal 1, object.children.count

square_mesh = object.children.first
assert_kind_of Mittsu::Mesh, square_mesh
assert_kind_of Mittsu::Geometry, square_mesh.geometry
[
Mittsu::Vector3.new(-1.0, 1.0, 0),
Mittsu::Vector3.new(-1.0, -1.0, 0),
Mittsu::Vector3.new(1.0, 1.0, 0),
Mittsu::Vector3.new(1.0, -1.0, 0)
].each_with_index { |v, i|
assert_equal v, square_mesh.geometry.vertices[i]
}
[
[0, 1, 2],
[1, 3, 2]
].each_with_index { |f, i|
face = square_mesh.geometry.faces[i]
a = f[0]
b = f[1]
c = f[2]
assert_equal(a, face.a)
assert_equal(b, face.b)
assert_equal(c, face.c)
assert_equal(Mittsu::Vector3.new(0, 0, 1), face.normal)
}
end

def test_parse_with_error
loader = Mittsu::STLLoader.new

assert_raises('Mittsu::STLLoader: Unhandled line 3') { loader.parse """solid
facet normal 0 0 1
broken
outer loop
vertex -1 1 -1
vertex -1 -1 -1
vertex 1 1 -1
endloop
endfacet
endsolid
""" }
end


def test_parse_multiple_solids
loader = Mittsu::STLLoader.new

object = loader.parse """solid
facet normal 0 0 1
outer loop
vertex -1 100.0e-2 0
vertex -1 -1e0 0
vertex 1 100.0e-2 0
endloop
endfacet
endsolid
solid
facet normal 0 0 1
outer loop
vertex -1 -1e0 0
vertex 1 -1e0 0
vertex 1 100.0e-2 0
endloop
endfacet
endsolid
"""

assert_kind_of Mittsu::Group, object
assert_equal 2, object.children.count
end
end
40 changes: 40 additions & 0 deletions test/loaders/test_stl_binary_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require 'minitest_helper'

class TestSTLBinaryLoader < Minitest::Test
def test_parse_binary
loader = Mittsu::STLLoader.new

object = loader.load(
File.expand_path('../../support/samples/square.binary.stl', __FILE__)
)

assert_kind_of Mittsu::Group, object
assert_equal 1, object.children.count

square_mesh = object.children.first
assert_kind_of Mittsu::Mesh, square_mesh
assert_kind_of Mittsu::Geometry, square_mesh.geometry
[
Mittsu::Vector3.new(0.0, 2.0, 0.0),
Mittsu::Vector3.new(0.0, 0.0, 0.0),
Mittsu::Vector3.new(2.0, 2.0, 0.0),
Mittsu::Vector3.new(2.2037353515625, 0.0, 0.0) # 2.2 in order to test EOL conversion - hex includes 0D0A
].each_with_index { |v, i|
assert_equal v, square_mesh.geometry.vertices[i]
}
[
[0, 1, 2],
[1, 3, 2]
].each_with_index { |f, i|
face = square_mesh.geometry.faces[i]
a = f[0]
b = f[1]
c = f[2]
assert_equal(a, face.a)
assert_equal(b, face.b)
assert_equal(c, face.c)
assert_equal(Mittsu::Vector3.new(0, 0, 1), face.normal)
}
end

end
Binary file added test/support/samples/square.binary.stl
Binary file not shown.

0 comments on commit 30d0199

Please sign in to comment.