├── .gitignore ├── LICENSE.md ├── README.md ├── binpack ├── pack.go ├── spatial.go └── vector.go ├── cmd ├── binpack │ └── main.go ├── bvh │ └── main.go ├── meshinfo │ └── main.go ├── pack3d │ └── main.go ├── render │ ├── main.go │ └── render.go ├── separate │ └── main.go └── sort │ └── main.go └── pack3d ├── anneal.go ├── axis.go ├── bvh.go └── model.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | *.obj 3 | *.stl 4 | 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2017 Michael Fogleman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pack3d 2 | 3 | Tightly pack 3D models. 4 | 5 | ### Installation 6 | 7 | First, install Go, set your GOPATH, and make sure $GOPATH/bin is on your PATH. 8 | 9 | ``` 10 | brew install go 11 | export GOPATH="$HOME/go" 12 | export PATH="$PATH:$GOPATH/bin" 13 | ``` 14 | 15 | Next, fetch and build the two binaries. 16 | 17 | ``` 18 | go get github.com/fogleman/pack3d/cmd/pack3d 19 | go get github.com/fogleman/pack3d/cmd/binpack 20 | ``` 21 | 22 | ### Usage Examples 23 | 24 | Note that pack3d runs until stopped, writing its output to disk whenever a new best is found. 25 | 26 | ``` 27 | pack3d 2 3DBenchy.stl # tightly pack 2 boats 28 | pack3d 4 3DBenchy.stl # tightly pack 4 boats 29 | pack3d 1 *.stl # tightly pack various meshes, one of each 30 | 31 | # pack as many boats as possible into the printer volume, given a few different arrangements 32 | binpack 1 3DBenchy.stl 2 3DBenchy-x2.stl 4 3DBenchy-x4.stl 33 | ``` 34 | 35 | ### Examples 36 | 37 | 113 3DBenchy tug boats packed tightly 38 | 39 | ![3DBenchy](http://i.imgur.com/adjchjy.png) 40 | 41 | 27 R2-D2 droids, 8 parts each 42 | 43 | ![R2-D2](http://i.imgur.com/qE90ijK.png) 44 | -------------------------------------------------------------------------------- /binpack/pack.go: -------------------------------------------------------------------------------- 1 | package binpack 2 | 3 | type Axis int 4 | 5 | const ( 6 | AxisX Axis = iota 7 | AxisY 8 | AxisZ 9 | ) 10 | 11 | type Item struct { 12 | ID int 13 | Score int 14 | Size Vector 15 | } 16 | 17 | type Box struct { 18 | Origin Vector 19 | Size Vector 20 | } 21 | 22 | func (box Box) Cut(axis Axis, offset int) (Box, Box) { 23 | o1 := box.Origin 24 | o2 := box.Origin 25 | s1 := box.Size 26 | s2 := box.Size 27 | switch axis { 28 | case AxisX: 29 | s1.X = offset 30 | s2.X -= offset 31 | o2.X += offset 32 | case AxisY: 33 | s1.Y = offset 34 | s2.Y -= offset 35 | o2.Y += offset 36 | case AxisZ: 37 | s1.Z = offset 38 | s2.Z -= offset 39 | o2.Z += offset 40 | } 41 | return Box{o1, s1}, Box{o2, s2} 42 | } 43 | 44 | func (box Box) Cuts(a1, a2, a3 Axis, s1, s2, s3 int) (Box, Box, Box) { 45 | b := box 46 | b, b1 := b.Cut(a1, s1) 47 | b, b2 := b.Cut(a2, s2) 48 | _, b3 := b.Cut(a3, s3) 49 | return b1, b2, b3 50 | } 51 | 52 | type Placement struct { 53 | Item Item 54 | Position Vector 55 | } 56 | 57 | type Result struct { 58 | Score int 59 | Placements []Placement 60 | } 61 | 62 | func MakeResult(r0, r1, r2 Result, item Item, position Vector) Result { 63 | r3 := Result{item.Score, []Placement{{item, position}}} 64 | score := r0.Score + r1.Score + r2.Score + r3.Score 65 | n := len(r0.Placements) + len(r1.Placements) + len(r2.Placements) + len(r3.Placements) 66 | placements := make([]Placement, 0, n) 67 | placements = append(placements, r0.Placements...) 68 | placements = append(placements, r1.Placements...) 69 | placements = append(placements, r2.Placements...) 70 | placements = append(placements, r3.Placements...) 71 | return Result{score, placements} 72 | } 73 | 74 | func (result Result) Translate(offset Vector) Result { 75 | placements := make([]Placement, len(result.Placements)) 76 | for i, p := range result.Placements { 77 | p.Position = p.Position.Add(offset) 78 | placements[i] = p 79 | } 80 | return Result{result.Score, placements} 81 | } 82 | 83 | func Pack(items []Item, box Box) Result { 84 | hash := NewSpatialHash(1000) 85 | minVolume := items[0].Size.Sort() 86 | for _, item := range items { 87 | minVolume = minVolume.Min(item.Size.Sort()) 88 | } 89 | return pack(items, box, hash, minVolume) 90 | } 91 | 92 | func pack(items []Item, box Box, hash *SpatialHash, minVolume Vector) Result { 93 | bs := box.Size 94 | if !bs.Sort().Fits(minVolume) { 95 | return Result{} 96 | } 97 | if result, ok := hash.Get(bs); ok { 98 | return result 99 | } 100 | best := Result{} 101 | for _, item := range items { 102 | s := item.Size 103 | if s.X > bs.X || s.Y > bs.Y || s.Z > bs.Z { 104 | continue 105 | } 106 | var b [6][3]Box 107 | b[0][0], b[0][1], b[0][2] = box.Cuts(AxisX, AxisY, AxisZ, s.X, s.Y, s.Z) 108 | b[1][0], b[1][1], b[1][2] = box.Cuts(AxisX, AxisZ, AxisY, s.X, s.Z, s.Y) 109 | b[2][0], b[2][1], b[2][2] = box.Cuts(AxisY, AxisX, AxisZ, s.Y, s.X, s.Z) 110 | b[3][0], b[3][1], b[3][2] = box.Cuts(AxisY, AxisZ, AxisX, s.Y, s.Z, s.X) 111 | b[4][0], b[4][1], b[4][2] = box.Cuts(AxisZ, AxisX, AxisY, s.Z, s.X, s.Y) 112 | b[5][0], b[5][1], b[5][2] = box.Cuts(AxisZ, AxisY, AxisX, s.Z, s.Y, s.X) 113 | for i := 0; i < 6; i++ { 114 | var r [3]Result 115 | score := item.Score 116 | for j := 0; j < 3; j++ { 117 | r[j] = pack(items, b[i][j], hash, minVolume) 118 | score += r[j].Score 119 | } 120 | if score > best.Score { 121 | for j := 0; j < 3; j++ { 122 | r[j] = r[j].Translate(b[i][j].Origin) 123 | } 124 | best = MakeResult(r[0], r[1], r[2], item, box.Origin) 125 | } 126 | } 127 | } 128 | best = best.Translate(box.Origin.Negate()) 129 | var size Vector 130 | for _, p := range best.Placements { 131 | size = size.Max(p.Position.Add(p.Item.Size)) 132 | } 133 | hash.Add(size, bs, best) 134 | return best 135 | } 136 | -------------------------------------------------------------------------------- /binpack/spatial.go: -------------------------------------------------------------------------------- 1 | package binpack 2 | 3 | type SpatialHash struct { 4 | CellSize int 5 | Cells map[SpatialKey][]*SpatialValue 6 | } 7 | 8 | type SpatialKey struct { 9 | X, Y, Z int 10 | } 11 | 12 | type SpatialValue struct { 13 | Min, Max Vector 14 | Result Result 15 | } 16 | 17 | func NewSpatialHash(cellSize int) *SpatialHash { 18 | cells := make(map[SpatialKey][]*SpatialValue) 19 | return &SpatialHash{cellSize, cells} 20 | } 21 | 22 | func (h *SpatialHash) KeyForVector(v Vector) SpatialKey { 23 | x := v.X / h.CellSize 24 | y := v.Y / h.CellSize 25 | z := v.Z / h.CellSize 26 | return SpatialKey{x, y, z} 27 | } 28 | 29 | func (h *SpatialHash) Add(min, max Vector, result Result) { 30 | value := &SpatialValue{min, max, result} 31 | k1 := h.KeyForVector(min) 32 | k2 := h.KeyForVector(max) 33 | for x := k1.X; x <= k2.X; x++ { 34 | for y := k1.Y; y <= k2.Y; y++ { 35 | for z := k1.Z; z <= k2.Z; z++ { 36 | k := SpatialKey{x, y, z} 37 | h.Cells[k] = append(h.Cells[k], value) 38 | } 39 | } 40 | } 41 | } 42 | 43 | func (h *SpatialHash) Get(v Vector) (Result, bool) { 44 | k := h.KeyForVector(v) 45 | for _, value := range h.Cells[k] { 46 | if v.GreaterThanOrEqual(value.Min) && v.LessThanOrEqual(value.Max) { 47 | return value.Result, true 48 | } 49 | } 50 | return Result{}, false 51 | } 52 | -------------------------------------------------------------------------------- /binpack/vector.go: -------------------------------------------------------------------------------- 1 | package binpack 2 | 3 | type Vector struct { 4 | X, Y, Z int 5 | } 6 | 7 | func (a Vector) Add(b Vector) Vector { 8 | return Vector{a.X + b.X, a.Y + b.Y, a.Z + b.Z} 9 | } 10 | 11 | func (a Vector) Sub(b Vector) Vector { 12 | return Vector{a.X - b.X, a.Y - b.Y, a.Z - b.Z} 13 | } 14 | 15 | func (a Vector) Negate() Vector { 16 | return Vector{-a.X, -a.Y, -a.Z} 17 | } 18 | 19 | func (a Vector) Min(b Vector) Vector { 20 | if b.X < a.X { 21 | a.X = b.X 22 | } 23 | if b.Y < a.Y { 24 | a.Y = b.Y 25 | } 26 | if b.Z < a.Z { 27 | a.Z = b.Z 28 | } 29 | return a 30 | } 31 | 32 | func (a Vector) Max(b Vector) Vector { 33 | if b.X > a.X { 34 | a.X = b.X 35 | } 36 | if b.Y > a.Y { 37 | a.Y = b.Y 38 | } 39 | if b.Z > a.Z { 40 | a.Z = b.Z 41 | } 42 | return a 43 | } 44 | 45 | func (a Vector) Sort() Vector { 46 | if a.X > a.Z { 47 | a.X, a.Z = a.Z, a.X 48 | } 49 | if a.X > a.Y { 50 | a.X, a.Y = a.Y, a.X 51 | } 52 | if a.Y > a.Z { 53 | a.Y, a.Z = a.Z, a.Y 54 | } 55 | return a 56 | } 57 | 58 | func (a Vector) Fits(b Vector) bool { 59 | return a.X >= b.X && a.Y >= b.Y && a.Z >= b.Z 60 | } 61 | 62 | func (a Vector) GreaterThanOrEqual(b Vector) bool { 63 | return a.X >= b.X && a.Y >= b.Y && a.Z >= b.Z 64 | } 65 | 66 | func (a Vector) LessThanOrEqual(b Vector) bool { 67 | return a.X <= b.X && a.Y <= b.Y && a.Z <= b.Z 68 | } 69 | -------------------------------------------------------------------------------- /cmd/binpack/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "os" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/fogleman/fauxgl" 11 | "github.com/fogleman/pack3d/binpack" 12 | ) 13 | 14 | const ( 15 | SizeX = 165 16 | SizeY = 165 17 | SizeZ = 320 18 | ) 19 | 20 | var Rotations []fauxgl.Matrix 21 | 22 | func init() { 23 | for i := 0; i < 2; i++ { 24 | for j := 0; j < 3; j++ { 25 | m := fauxgl.Rotate(fauxgl.Vector{0, 0, 1}, float64(i)*math.Pi/2) 26 | switch j { 27 | case 1: 28 | m = m.Rotate(fauxgl.Vector{1, 0, 0}, math.Pi/2) 29 | case 2: 30 | m = m.Rotate(fauxgl.Vector{0, 1, 0}, math.Pi/2) 31 | } 32 | Rotations = append(Rotations, m) 33 | } 34 | } 35 | } 36 | 37 | func timed(name string) func() { 38 | if len(name) > 0 { 39 | fmt.Printf("%s... ", name) 40 | } 41 | start := time.Now() 42 | return func() { 43 | fmt.Println(time.Since(start)) 44 | } 45 | } 46 | 47 | func main() { 48 | const S = 100 49 | const P = 2.5 50 | 51 | var items []binpack.Item 52 | var meshes []*fauxgl.Mesh 53 | 54 | var done func() 55 | 56 | score := 1 57 | ok := false 58 | for _, arg := range os.Args[1:] { 59 | _score, err := strconv.ParseInt(arg, 0, 0) 60 | if err == nil { 61 | score = int(_score) 62 | continue 63 | } 64 | 65 | done = timed("loading mesh") 66 | mesh, err := fauxgl.LoadMesh(arg) 67 | if err != nil { 68 | panic(err) 69 | } 70 | done() 71 | 72 | i := len(meshes) 73 | meshes = append(meshes, mesh) 74 | box := mesh.BoundingBox() 75 | for j, m := range Rotations { 76 | id := i*len(Rotations) + j 77 | s := box.Transform(m).Size() 78 | sx := int(math.Ceil((s.X + P*2) * S)) 79 | sy := int(math.Ceil((s.Y + P*2) * S)) 80 | sz := int(math.Ceil((s.Z + P*2) * S)) 81 | items = append(items, binpack.Item{id, score, binpack.Vector{sx, sy, sz}}) 82 | } 83 | ok = true 84 | } 85 | 86 | if !ok { 87 | fmt.Println("Usage: binpack N1 mesh1.stl N2 mesh2.stl ...") 88 | fmt.Println(" - Packs as many items into the volume as possible.") 89 | fmt.Println(" - N specifies how many items the mesh contains.") 90 | fmt.Println(" - Provide multiple pack3d meshes for best results.") 91 | return 92 | } 93 | 94 | done = timed("bin packing") 95 | box := binpack.Box{binpack.Vector{}, binpack.Vector{SizeX * S, SizeY * S, SizeZ * S}} 96 | result := binpack.Pack(items, box) 97 | done() 98 | 99 | fmt.Printf("packed %d items\n", result.Score) 100 | 101 | done = timed("building result") 102 | mesh := fauxgl.NewEmptyMesh() 103 | for _, placement := range result.Placements { 104 | p := placement.Position 105 | v := fauxgl.Vector{float64(p.X)/S + P, float64(p.Y)/S + P, float64(p.Z)/S + P} 106 | i := placement.Item.ID / len(Rotations) 107 | j := placement.Item.ID % len(Rotations) 108 | m := meshes[i].Copy() 109 | m.Transform(Rotations[j]) 110 | m.MoveTo(v, fauxgl.Vector{}) 111 | mesh.Add(m) 112 | } 113 | mesh.MoveTo(fauxgl.Vector{}, fauxgl.Vector{}) 114 | done() 115 | 116 | done = timed("writing stl file") 117 | mesh.SaveSTL("binpack.stl") 118 | done() 119 | 120 | // for _, p := range result.Placements { 121 | // fmt.Println(p) 122 | // } 123 | // fmt.Println(result.Score) 124 | } 125 | -------------------------------------------------------------------------------- /cmd/bvh/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "os" 7 | "path" 8 | "strconv" 9 | 10 | . "github.com/fogleman/fauxgl" 11 | "github.com/fogleman/pack3d/pack3d" 12 | ) 13 | 14 | func main() { 15 | detail := 8 16 | for _, arg := range os.Args[1:] { 17 | _detail, err := strconv.ParseInt(arg, 0, 0) 18 | if err == nil { 19 | detail = int(_detail) 20 | continue 21 | } 22 | mesh, err := LoadMesh(arg) 23 | if err != nil { 24 | panic(err) 25 | } 26 | tree := pack3d.NewTreeForMesh(mesh, detail) 27 | mesh = NewEmptyMesh() 28 | n := int(math.Pow(2, float64(detail))) 29 | for _, box := range tree[len(tree)-n:] { 30 | mesh.Add(NewCubeForBox(box)) 31 | } 32 | ext := path.Ext(arg) 33 | mesh.SaveSTL(fmt.Sprintf(arg[:len(arg)-len(ext)]+".bvh.%d.stl", detail)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cmd/meshinfo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | . "github.com/fogleman/fauxgl" 8 | ) 9 | 10 | func main() { 11 | for _, path := range os.Args[1:] { 12 | fmt.Println(path) 13 | mesh, err := LoadMesh(path) 14 | if err != nil { 15 | fmt.Println(err) 16 | continue 17 | } 18 | box := mesh.BoundingBox() 19 | size := box.Size() 20 | volume := mesh.Volume() 21 | aabbVolume := size.X * size.Y * size.Z 22 | center := box.Anchor(V(0.5, 0.5, 0.5)) 23 | fmt.Printf(" triangles = %d\n", len(mesh.Triangles)) 24 | fmt.Printf(" x range = %g to %g\n", box.Min.X, box.Max.X) 25 | fmt.Printf(" y range = %g to %g\n", box.Min.Y, box.Max.Y) 26 | fmt.Printf(" z range = %g to %g\n", box.Min.Z, box.Max.Z) 27 | fmt.Printf(" center = %g, %g, %g\n", center.X, center.Y, center.Z) 28 | fmt.Printf(" size = %g x %g x %g\n", size.X, size.Y, size.Z) 29 | fmt.Printf(" volume = %g\n", volume) 30 | fmt.Printf(" aabb vol = %g\n", aabbVolume) 31 | fmt.Println() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cmd/pack3d/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/rand" 7 | "os" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/fogleman/fauxgl" 12 | "github.com/fogleman/pack3d/pack3d" 13 | ) 14 | 15 | const ( 16 | bvhDetail = 8 17 | annealingIterations = 2000000 18 | ) 19 | 20 | func timed(name string) func() { 21 | if len(name) > 0 { 22 | fmt.Printf("%s... ", name) 23 | } 24 | start := time.Now() 25 | return func() { 26 | fmt.Println(time.Since(start)) 27 | } 28 | } 29 | 30 | func main() { 31 | var done func() 32 | 33 | rand.Seed(time.Now().UTC().UnixNano()) 34 | 35 | model := pack3d.NewModel() 36 | count := 1 37 | ok := false 38 | var totalVolume float64 39 | for _, arg := range os.Args[1:] { 40 | _count, err := strconv.ParseInt(arg, 0, 0) 41 | if err == nil { 42 | count = int(_count) 43 | continue 44 | } 45 | 46 | done = timed(fmt.Sprintf("loading mesh %s", arg)) 47 | mesh, err := fauxgl.LoadMesh(arg) 48 | if err != nil { 49 | panic(err) 50 | } 51 | done() 52 | 53 | totalVolume += mesh.BoundingBox().Volume() 54 | size := mesh.BoundingBox().Size() 55 | fmt.Printf(" %d triangles\n", len(mesh.Triangles)) 56 | fmt.Printf(" %g x %g x %g\n", size.X, size.Y, size.Z) 57 | 58 | done = timed("centering mesh") 59 | mesh.Center() 60 | done() 61 | 62 | done = timed("building bvh tree") 63 | model.Add(mesh, bvhDetail, count) 64 | ok = true 65 | done() 66 | } 67 | 68 | if !ok { 69 | fmt.Println("Usage: pack3d N1 mesh1.stl N2 mesh2.stl ...") 70 | fmt.Println(" - Packs N copies of each mesh into as small of a volume as possible.") 71 | fmt.Println(" - Runs forever, looking for the best packing.") 72 | fmt.Println(" - Results are written to disk whenever a new best is found.") 73 | return 74 | } 75 | 76 | side := math.Pow(totalVolume, 1.0/3) 77 | model.Deviation = side / 32 78 | 79 | best := 1e9 80 | for { 81 | model = model.Pack(annealingIterations, nil) 82 | score := model.Energy() 83 | if score < best { 84 | best = score 85 | done = timed("writing mesh") 86 | model.Mesh().SaveSTL(fmt.Sprintf("pack3d-%.3f.stl", score)) 87 | // model.TreeMesh().SaveSTL(fmt.Sprintf("out%dtree.stl", int(score*100000))) 88 | done() 89 | } 90 | model.Reset() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /cmd/render/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | . "github.com/fogleman/fauxgl" 8 | "github.com/nfnt/resize" 9 | ) 10 | 11 | const ( 12 | N = 48 13 | scale = 4 // optional supersampling 14 | width = 1200 // output width in pixels 15 | height = 1600 // output height in pixels 16 | fovy = 18 // vertical field of view in degrees 17 | near = 100 // near clipping plane 18 | far = 10000 // far clipping plane 19 | ) 20 | 21 | var ( 22 | eye = V(1000, 1000, 160) // camera position 23 | center = V(165/2.0, 165/2.0, 160) // view center position 24 | up = V(0, 0, 1) // up vector 25 | light = V(0.75, 0.25, 1).Normalize() // light direction 26 | ) 27 | 28 | func timed(name string) func() { 29 | if len(name) > 0 { 30 | fmt.Printf("%s... ", name) 31 | } 32 | start := time.Now() 33 | return func() { 34 | fmt.Println(time.Since(start)) 35 | } 36 | } 37 | 38 | func main() { 39 | var done func() 40 | 41 | done = timed("loading mesh") 42 | fuse, err := LoadMesh("fuse2.stl") 43 | if err != nil { 44 | panic(err) 45 | } 46 | done() 47 | 48 | // load a mesh 49 | done = timed("loading mesh") 50 | mesh, err := LoadMesh("giraffe48.stl") 51 | if err != nil { 52 | panic(err) 53 | } 54 | done() 55 | 56 | t := len(mesh.Triangles) / N 57 | 58 | // create a rendering context 59 | context := NewContext(width*scale, height*scale) 60 | context.ClearColorBufferWith(HexColor("#FFFFFF")) 61 | 62 | // create transformation matrix and light direction 63 | aspect := float64(width) / float64(height) 64 | matrix := LookAt(eye, center, up).Perspective(fovy, aspect, near, far) 65 | 66 | // render 67 | shader := NewPhongShader(matrix, light, eye) 68 | context.Shader = shader 69 | 70 | done = timed("rendering fuse") 71 | shader.ObjectColor = HexColor("#2A2C2B") 72 | context.DrawMesh(fuse) 73 | done() 74 | 75 | for i := 0; i <= N; i++ { 76 | if i > 0 { 77 | j := N - i + 1 78 | done = timed("rendering boats") 79 | shader.ObjectColor = HexColor("#7E827A") 80 | context.DrawTriangles(mesh.Triangles[t*(j-1) : t*j]) 81 | done() 82 | } 83 | 84 | // downsample image for antialiasing 85 | done = timed("downsampling image") 86 | image := context.Image() 87 | image = resize.Resize(width, height, image, resize.Bilinear) 88 | done() 89 | 90 | // save image 91 | done = timed("writing output") 92 | SavePNG(fmt.Sprintf("out%03d.png", i), image) 93 | done() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /cmd/render/render.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | . "github.com/fogleman/fauxgl" 9 | "github.com/nfnt/resize" 10 | ) 11 | 12 | const ( 13 | scale = 4 // optional supersampling 14 | width = 2048 // output width in pixels 15 | height = 2048 // output height in pixels 16 | fovy = 35 // vertical field of view in degrees 17 | near = 1 // near clipping plane 18 | far = 1000 // far clipping plane 19 | ) 20 | 21 | var ( 22 | eye = V(100, 200, 100) // camera position 23 | center = V(0, 0, 0) // view center position 24 | up = V(0, 0, 1) // up vector 25 | light = V(0.75, 1, 0.25).Normalize() // light direction 26 | color = HexColor("#468966") // object color 27 | background = HexColor("#FFF8E3") // background color 28 | ) 29 | 30 | func timed(name string) func() { 31 | if len(name) > 0 { 32 | fmt.Printf("%s... ", name) 33 | } 34 | start := time.Now() 35 | return func() { 36 | fmt.Println(time.Since(start)) 37 | } 38 | } 39 | 40 | func swap(v Vector, n int) Vector { 41 | switch n { 42 | case 0: 43 | return Vector{v.X, v.Y, v.Z} 44 | case 1: 45 | return Vector{v.X, v.Z, v.Y} 46 | case 2: 47 | return Vector{v.Y, v.X, v.Z} 48 | case 3: 49 | return Vector{v.Y, v.Z, v.X} 50 | case 4: 51 | return Vector{v.Z, v.X, v.Y} 52 | case 5: 53 | return Vector{v.Z, v.Y, v.X} 54 | } 55 | return v 56 | } 57 | 58 | func main() { 59 | var done func() 60 | 61 | for _, path := range os.Args[1:] { 62 | for i := 0; i < 6; i++ { 63 | 64 | // load a mesh 65 | done = timed("loading mesh") 66 | mesh, err := LoadMesh(path) 67 | if err != nil { 68 | panic(err) 69 | } 70 | done() 71 | // mesh.Transform(Rotate(up, Radians(180))) 72 | 73 | // fit mesh in a bi-unit cube centered at the origin 74 | done = timed("transforming mesh") 75 | mesh.MoveTo(Vector{}, Vector{0.5, 0.5, 0.5}) 76 | done() 77 | 78 | _eye := swap(eye, i) 79 | _center := swap(center, i) 80 | _up := swap(up, i) 81 | _light := swap(light, i) 82 | 83 | // create a rendering context 84 | context := NewContext(width*scale, height*scale) 85 | context.ClearColorBufferWith(background) 86 | 87 | // create transformation matrix and light direction 88 | aspect := float64(width) / float64(height) 89 | matrix := LookAt(_eye, _center, _up).Perspective(fovy, aspect, near, far) 90 | 91 | // render 92 | shader := NewPhongShader(matrix, _light, _eye) 93 | shader.ObjectColor = color 94 | context.Shader = shader 95 | done = timed("rendering mesh") 96 | context.DrawMesh(mesh) 97 | done() 98 | 99 | context.Shader = NewSolidColorShader(matrix, Black) 100 | context.LineWidth = scale * 3 101 | context.DrawMesh(NewCubeOutlineForBox(mesh.BoundingBox())) 102 | 103 | // downsample image for antialiasing 104 | done = timed("downsampling image") 105 | image := context.Image() 106 | image = resize.Resize(width, height, image, resize.Bilinear) 107 | done() 108 | 109 | // save image 110 | done = timed("writing output") 111 | SavePNG(fmt.Sprintf("%s.%d.png", path, i), image) 112 | done() 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /cmd/separate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | . "github.com/fogleman/fauxgl" 9 | ) 10 | 11 | func separate(filename string) { 12 | mesh, err := LoadMesh(filename) 13 | if err != nil { 14 | panic(err) 15 | } 16 | 17 | lookup := make(map[Vector][]*Triangle) 18 | for _, t := range mesh.Triangles { 19 | lookup[t.V1.Position] = append(lookup[t.V1.Position], t) 20 | lookup[t.V2.Position] = append(lookup[t.V2.Position], t) 21 | lookup[t.V3.Position] = append(lookup[t.V3.Position], t) 22 | } 23 | 24 | var groups [][]*Triangle 25 | seen := make(map[*Triangle]bool) 26 | done := false 27 | for !done { 28 | done = true 29 | var q []*Triangle 30 | for _, t := range mesh.Triangles { 31 | if !seen[t] { 32 | q = append(q, t) 33 | done = false 34 | break 35 | } 36 | } 37 | var group []*Triangle 38 | for len(q) > 0 { 39 | var t *Triangle 40 | t, q = q[len(q)-1], q[:len(q)-1] 41 | if seen[t] { 42 | continue 43 | } 44 | group = append(group, t) 45 | seen[t] = true 46 | for _, v := range []Vertex{t.V1, t.V2, t.V3} { 47 | for _, u := range lookup[v.Position] { 48 | if !seen[u] { 49 | q = append(q, u) 50 | } 51 | } 52 | 53 | } 54 | } 55 | if len(group) > 0 { 56 | groups = append(groups, group) 57 | } 58 | } 59 | for i, group := range groups { 60 | fmt.Println(len(group)) 61 | mesh := NewTriangleMesh(group) 62 | ext := path.Ext(filename) 63 | mesh.SaveSTL(fmt.Sprintf(filename[:len(filename)-len(ext)]+".%d.stl", i)) 64 | } 65 | } 66 | 67 | func main() { 68 | for _, filename := range os.Args[1:] { 69 | separate(filename) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cmd/sort/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | "strconv" 8 | 9 | . "github.com/fogleman/fauxgl" 10 | ) 11 | 12 | func main() { 13 | path := os.Args[1] 14 | _count, _ := strconv.ParseInt(os.Args[2], 0, 0) 15 | count := int(_count) 16 | 17 | mesh, err := LoadMesh(path) 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | mesh.MoveTo(Vector{}, Vector{}) 23 | 24 | meshes := make([]*Mesh, 0, count) 25 | n := len(mesh.Triangles) / count 26 | for i := 0; i < len(mesh.Triangles); i += n { 27 | m := NewTriangleMesh(mesh.Triangles[i : i+n]) 28 | meshes = append(meshes, m) 29 | } 30 | 31 | sort.Slice(meshes, func(i, j int) bool { 32 | a := meshes[i].BoundingBox().Min 33 | b := meshes[j].BoundingBox().Min 34 | a = Vector{a.Z, a.X, a.Y} 35 | b = Vector{b.Z, b.X, b.Y} 36 | return a.Less(b) 37 | }) 38 | 39 | result := NewEmptyMesh() 40 | for _, mesh := range meshes { 41 | result.Add(mesh) 42 | fmt.Println(mesh.BoundingBox()) 43 | } 44 | result.SaveSTL("out.stl") 45 | } 46 | -------------------------------------------------------------------------------- /pack3d/anneal.go: -------------------------------------------------------------------------------- 1 | package pack3d 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/rand" 7 | "time" 8 | ) 9 | 10 | type AnnealCallback func(Annealable) 11 | 12 | type Annealable interface { 13 | Energy() float64 14 | DoMove() Undo 15 | UndoMove(Undo) 16 | Copy() Annealable 17 | } 18 | 19 | func Anneal(state Annealable, maxTemp, minTemp float64, steps int, callback AnnealCallback) Annealable { 20 | start := time.Now() 21 | factor := -math.Log(maxTemp / minTemp) 22 | state = state.Copy() 23 | bestState := state.Copy() 24 | if callback != nil { 25 | callback(bestState) 26 | } 27 | bestEnergy := state.Energy() 28 | previousEnergy := bestEnergy 29 | rate := steps / 200 30 | for step := 0; step < steps; step++ { 31 | pct := float64(step) / float64(steps-1) 32 | temp := maxTemp * math.Exp(factor*pct) 33 | if step%rate == 0 { 34 | showProgress(step, steps, bestEnergy, time.Since(start).Seconds()) 35 | } 36 | undo := state.DoMove() 37 | energy := state.Energy() 38 | change := energy - previousEnergy 39 | if change > 0 && math.Exp(-change/temp) < rand.Float64() { 40 | state.UndoMove(undo) 41 | } else { 42 | previousEnergy = energy 43 | if energy < bestEnergy { 44 | bestEnergy = energy 45 | bestState = state.Copy() 46 | if callback != nil { 47 | callback(bestState) 48 | } 49 | } 50 | } 51 | } 52 | showProgress(steps, steps, bestEnergy, time.Since(start).Seconds()) 53 | fmt.Println() 54 | return bestState 55 | } 56 | 57 | func showProgress(i, n int, e, d float64) { 58 | pct := int(100 * float64(i) / float64(n)) 59 | fmt.Printf(" %3d%% [", pct) 60 | for p := 0; p < 100; p += 3 { 61 | if pct > p { 62 | fmt.Print("=") 63 | } else { 64 | fmt.Print(" ") 65 | } 66 | } 67 | fmt.Printf("] %.6f %.3fs \r", e, d) 68 | } 69 | -------------------------------------------------------------------------------- /pack3d/axis.go: -------------------------------------------------------------------------------- 1 | package pack3d 2 | 3 | import "github.com/fogleman/fauxgl" 4 | 5 | type Axis uint8 6 | 7 | const ( 8 | AxisNone Axis = iota 9 | AxisX 10 | AxisY 11 | AxisZ 12 | ) 13 | 14 | func (a Axis) Vector() fauxgl.Vector { 15 | switch a { 16 | case AxisX: 17 | return fauxgl.Vector{1, 0, 0} 18 | case AxisY: 19 | return fauxgl.Vector{0, 1, 0} 20 | case AxisZ: 21 | return fauxgl.Vector{0, 0, 1} 22 | } 23 | return fauxgl.Vector{} 24 | } 25 | -------------------------------------------------------------------------------- /pack3d/bvh.go: -------------------------------------------------------------------------------- 1 | package pack3d 2 | 3 | import "github.com/fogleman/fauxgl" 4 | 5 | type Tree []fauxgl.Box 6 | 7 | func NewTreeForMesh(mesh *fauxgl.Mesh, depth int) Tree { 8 | mesh = mesh.Copy() 9 | mesh.Center() 10 | boxes := make([]fauxgl.Box, len(mesh.Triangles)) 11 | for i, t := range mesh.Triangles { 12 | boxes[i] = t.BoundingBox() 13 | } 14 | root := NewNode(boxes, depth) 15 | tree := make(Tree, 1<= len(a) && j1 >= len(b) { 41 | return true 42 | } else if i1 >= len(a) { 43 | return a.intersects(b, t1, t2, i, j1) || a.intersects(b, t1, t2, i, j2) 44 | } else if j1 >= len(b) { 45 | return a.intersects(b, t1, t2, i1, j) || a.intersects(b, t1, t2, i2, j) 46 | } else { 47 | return a.intersects(b, t1, t2, i1, j1) || 48 | a.intersects(b, t1, t2, i1, j2) || 49 | a.intersects(b, t1, t2, i2, j1) || 50 | a.intersects(b, t1, t2, i2, j2) 51 | } 52 | } 53 | 54 | func boxesIntersect(b1, b2 fauxgl.Box, t1, t2 fauxgl.Vector) bool { 55 | if b1 == fauxgl.EmptyBox || b2 == fauxgl.EmptyBox { 56 | return false 57 | } 58 | return !(b1.Min.X+t1.X > b2.Max.X+t2.X || 59 | b1.Max.X+t1.X < b2.Min.X+t2.X || 60 | b1.Min.Y+t1.Y > b2.Max.Y+t2.Y || 61 | b1.Max.Y+t1.Y < b2.Min.Y+t2.Y || 62 | b1.Min.Z+t1.Z > b2.Max.Z+t2.Z || 63 | b1.Max.Z+t1.Z < b2.Min.Z+t2.Z) 64 | } 65 | 66 | type Node struct { 67 | Box fauxgl.Box 68 | Left *Node 69 | Right *Node 70 | } 71 | 72 | func NewNode(boxes []fauxgl.Box, depth int) *Node { 73 | box := fauxgl.BoxForBoxes(boxes).Offset(2.5) 74 | node := &Node{box, nil, nil} 75 | node.Split(boxes, depth) 76 | return node 77 | } 78 | 79 | func (a *Node) Flatten(tree Tree, i int) { 80 | tree[i] = a.Box 81 | if a.Left != nil { 82 | a.Left.Flatten(tree, i*2+1) 83 | } 84 | if a.Right != nil { 85 | a.Right.Flatten(tree, i*2+2) 86 | } 87 | } 88 | 89 | func (node *Node) Split(boxes []fauxgl.Box, depth int) { 90 | if depth == 0 { 91 | return 92 | } 93 | box := node.Box 94 | best := box.Volume() 95 | bestAxis := AxisNone 96 | bestPoint := 0.0 97 | bestSide := false 98 | const N = 16 99 | for s := 0; s < 2; s++ { 100 | side := s == 1 101 | for i := 1; i < N; i++ { 102 | p := float64(i) / N 103 | x := box.Min.X + (box.Max.X-box.Min.X)*p 104 | y := box.Min.Y + (box.Max.Y-box.Min.Y)*p 105 | z := box.Min.Z + (box.Max.Z-box.Min.Z)*p 106 | sx := partitionScore(boxes, AxisX, x, side) 107 | if sx < best { 108 | best = sx 109 | bestAxis = AxisX 110 | bestPoint = x 111 | bestSide = side 112 | } 113 | sy := partitionScore(boxes, AxisY, y, side) 114 | if sy < best { 115 | best = sy 116 | bestAxis = AxisY 117 | bestPoint = y 118 | bestSide = side 119 | } 120 | sz := partitionScore(boxes, AxisZ, z, side) 121 | if sz < best { 122 | best = sz 123 | bestAxis = AxisZ 124 | bestPoint = z 125 | bestSide = side 126 | } 127 | } 128 | } 129 | if bestAxis == AxisNone { 130 | return 131 | } 132 | l, r := partition(boxes, bestAxis, bestPoint, bestSide) 133 | node.Left = NewNode(l, depth-1) 134 | node.Right = NewNode(r, depth-1) 135 | } 136 | 137 | func partitionBox(box fauxgl.Box, axis Axis, point float64) (left, right bool) { 138 | switch axis { 139 | case AxisX: 140 | left = box.Min.X <= point 141 | right = box.Max.X >= point 142 | case AxisY: 143 | left = box.Min.Y <= point 144 | right = box.Max.Y >= point 145 | case AxisZ: 146 | left = box.Min.Z <= point 147 | right = box.Max.Z >= point 148 | } 149 | return 150 | } 151 | 152 | func partitionScore(boxes []fauxgl.Box, axis Axis, point float64, side bool) float64 { 153 | var major fauxgl.Box 154 | for _, box := range boxes { 155 | l, r := partitionBox(box, axis, point) 156 | if (l && r) || (l && side) || (r && !side) { 157 | major = major.Extend(box) 158 | } 159 | } 160 | var minor fauxgl.Box 161 | for _, box := range boxes { 162 | if !major.ContainsBox(box) { 163 | minor = minor.Extend(box) 164 | } 165 | } 166 | return major.Volume() + minor.Volume() - major.Intersection(minor).Volume() 167 | } 168 | 169 | func partition(boxes []fauxgl.Box, axis Axis, point float64, side bool) (left, right []fauxgl.Box) { 170 | var major fauxgl.Box 171 | for _, box := range boxes { 172 | l, r := partitionBox(box, axis, point) 173 | if (l && r) || (l && side) || (r && !side) { 174 | major = major.Extend(box) 175 | } 176 | } 177 | for _, box := range boxes { 178 | if major.ContainsBox(box) { 179 | left = append(left, box) 180 | } else { 181 | right = append(right, box) 182 | } 183 | } 184 | if !side { 185 | left, right = right, left 186 | } 187 | return 188 | } 189 | -------------------------------------------------------------------------------- /pack3d/model.go: -------------------------------------------------------------------------------- 1 | package pack3d 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | 7 | "github.com/fogleman/fauxgl" 8 | ) 9 | 10 | var Rotations []fauxgl.Matrix 11 | 12 | func init() { 13 | for i := 0; i < 4; i++ { 14 | for s := -1; s <= 1; s += 2 { 15 | for a := 1; a <= 3; a++ { 16 | up := AxisZ.Vector() 17 | m := fauxgl.Rotate(up, float64(i)*fauxgl.Radians(90)) 18 | m = m.RotateTo(up, Axis(a).Vector().MulScalar(float64(s))) 19 | Rotations = append(Rotations, m) 20 | } 21 | } 22 | } 23 | } 24 | 25 | type Undo struct { 26 | Index int 27 | Rotation int 28 | Translation fauxgl.Vector 29 | } 30 | 31 | type Item struct { 32 | Mesh *fauxgl.Mesh 33 | Trees []Tree 34 | Rotation int 35 | Translation fauxgl.Vector 36 | } 37 | 38 | func (item *Item) Matrix() fauxgl.Matrix { 39 | return Rotations[item.Rotation].Translate(item.Translation) 40 | } 41 | 42 | func (item *Item) Copy() *Item { 43 | dup := *item 44 | return &dup 45 | } 46 | 47 | type Model struct { 48 | Items []*Item 49 | MinVolume float64 50 | MaxVolume float64 51 | Deviation float64 52 | } 53 | 54 | func NewModel() *Model { 55 | return &Model{nil, 0, 0, 1} 56 | } 57 | 58 | func (m *Model) Add(mesh *fauxgl.Mesh, detail, count int) { 59 | tree := NewTreeForMesh(mesh, detail) 60 | trees := make([]Tree, len(Rotations)) 61 | for i, m := range Rotations { 62 | trees[i] = tree.Transform(m) 63 | } 64 | for i := 0; i < count; i++ { 65 | m.add(mesh, trees) 66 | } 67 | } 68 | 69 | func (m *Model) add(mesh *fauxgl.Mesh, trees []Tree) { 70 | index := len(m.Items) 71 | item := Item{mesh, trees, 0, fauxgl.Vector{}} 72 | m.Items = append(m.Items, &item) 73 | d := 1.0 74 | for !m.ValidChange(index) { 75 | item.Rotation = rand.Intn(len(Rotations)) 76 | item.Translation = fauxgl.RandomUnitVector().MulScalar(d) 77 | d *= 1.2 78 | } 79 | tree := trees[0] 80 | m.MinVolume = math.Max(m.MinVolume, tree[0].Volume()) 81 | m.MaxVolume += tree[0].Volume() 82 | } 83 | 84 | func (m *Model) Reset() { 85 | items := m.Items 86 | m.Items = nil 87 | m.MinVolume = 0 88 | m.MaxVolume = 0 89 | for _, item := range items { 90 | m.add(item.Mesh, item.Trees) 91 | } 92 | } 93 | 94 | func (m *Model) Pack(iterations int, callback AnnealCallback) *Model { 95 | e := 0.5 96 | return Anneal(m, 1e0*e, 1e-4*e, iterations, callback).(*Model) 97 | } 98 | 99 | func (m *Model) Meshes() []*fauxgl.Mesh { 100 | result := make([]*fauxgl.Mesh, len(m.Items)) 101 | for i, item := range m.Items { 102 | mesh := item.Mesh.Copy() 103 | mesh.Transform(item.Matrix()) 104 | result[i] = mesh 105 | } 106 | return result 107 | } 108 | 109 | func (m *Model) Mesh() *fauxgl.Mesh { 110 | result := fauxgl.NewEmptyMesh() 111 | for _, mesh := range m.Meshes() { 112 | result.Add(mesh) 113 | } 114 | return result 115 | } 116 | 117 | func (m *Model) TreeMeshes() []*fauxgl.Mesh { 118 | result := make([]*fauxgl.Mesh, len(m.Items)) 119 | for i, item := range m.Items { 120 | mesh := fauxgl.NewEmptyMesh() 121 | tree := item.Trees[item.Rotation] 122 | for _, box := range tree[len(tree)/2:] { 123 | mesh.Add(fauxgl.NewCubeForBox(box)) 124 | } 125 | mesh.Transform(fauxgl.Translate(item.Translation)) 126 | result[i] = mesh 127 | } 128 | return result 129 | } 130 | 131 | func (m *Model) TreeMesh() *fauxgl.Mesh { 132 | result := fauxgl.NewEmptyMesh() 133 | for _, mesh := range m.TreeMeshes() { 134 | result.Add(mesh) 135 | } 136 | return result 137 | } 138 | 139 | func (m *Model) ValidChange(i int) bool { 140 | item1 := m.Items[i] 141 | tree1 := item1.Trees[item1.Rotation] 142 | for j := 0; j < len(m.Items); j++ { 143 | if j == i { 144 | continue 145 | } 146 | item2 := m.Items[j] 147 | tree2 := item2.Trees[item2.Rotation] 148 | if tree1.Intersects(tree2, item1.Translation, item2.Translation) { 149 | return false 150 | } 151 | } 152 | return true 153 | } 154 | 155 | func (m *Model) BoundingBox() fauxgl.Box { 156 | box := fauxgl.EmptyBox 157 | for _, item := range m.Items { 158 | tree := item.Trees[item.Rotation] 159 | box = box.Extend(tree[0].Translate(item.Translation)) 160 | } 161 | return box 162 | } 163 | 164 | func (m *Model) Volume() float64 { 165 | return m.BoundingBox().Volume() 166 | } 167 | 168 | func (m *Model) Energy() float64 { 169 | return m.Volume() / m.MaxVolume 170 | } 171 | 172 | func (m *Model) DoMove() Undo { 173 | i := rand.Intn(len(m.Items)) 174 | item := m.Items[i] 175 | undo := Undo{i, item.Rotation, item.Translation} 176 | for { 177 | if rand.Intn(4) == 0 { 178 | // rotate 179 | item.Rotation = rand.Intn(len(Rotations)) 180 | } else { 181 | // translate 182 | offset := Axis(rand.Intn(3) + 1).Vector() 183 | offset = offset.MulScalar(rand.NormFloat64() * m.Deviation) 184 | item.Translation = item.Translation.Add(offset) 185 | } 186 | if m.ValidChange(i) { 187 | break 188 | } 189 | item.Rotation = undo.Rotation 190 | item.Translation = undo.Translation 191 | } 192 | return undo 193 | } 194 | 195 | func (m *Model) UndoMove(undo Undo) { 196 | item := m.Items[undo.Index] 197 | item.Rotation = undo.Rotation 198 | item.Translation = undo.Translation 199 | } 200 | 201 | func (m *Model) Copy() Annealable { 202 | items := make([]*Item, len(m.Items)) 203 | for i, item := range m.Items { 204 | items[i] = item.Copy() 205 | } 206 | return &Model{items, m.MinVolume, m.MaxVolume, m.Deviation} 207 | } 208 | --------------------------------------------------------------------------------