├── .gitignore ├── layer.go ├── README.md ├── triangle.go ├── path.go ├── LICENSE.md ├── slice.go └── cmd └── slicer └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | *.stl 3 | *.svg 4 | 5 | -------------------------------------------------------------------------------- /layer.go: -------------------------------------------------------------------------------- 1 | package slicer 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type Layer struct { 10 | Z float64 11 | Paths []Path 12 | } 13 | 14 | func (layer Layer) SVG() string { 15 | var buf bytes.Buffer 16 | for _, path := range layer.Paths { 17 | for i, point := range path { 18 | if i == 0 { 19 | buf.WriteString("M ") 20 | } else { 21 | buf.WriteString("L ") 22 | } 23 | buf.WriteString(fmt.Sprintf("%g %g ", point.X, point.Y)) 24 | } 25 | buf.WriteString("Z ") 26 | } 27 | return strings.TrimSpace(buf.String()) 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # slicer 2 | 3 | Fast 3D mesh slicer written in Go. Writes slices to grayscale PNG files. 4 | 5 | ### Install Go 6 | 7 | First, install Go, set your `GOPATH`, and make sure `$GOPATH/bin` is on your `PATH`. 8 | 9 | ```bash 10 | brew install go 11 | export GOPATH="$HOME/go" 12 | export PATH="$PATH:$GOPATH/bin" 13 | ``` 14 | 15 | ### Install Slicer 16 | 17 | ```bash 18 | $ go get -u github.com/fogleman/slicer/cmd/slicer 19 | ``` 20 | 21 | ### Example Usage 22 | 23 | ```bash 24 | $ slicer --help 25 | 26 | # slice model.stl with slices that are 0.1 units thick, rendering PNGs that 27 | # cover 100x100 units in size with resolution of 10 pixels per unit 28 | $ slicer -s 0.1 -w 100 -h 100 -x 10 model.stl 29 | ``` 30 | 31 | ### Example Slice 32 | 33 | ![Example](https://i.imgur.com/PWKUt1L.png) 34 | -------------------------------------------------------------------------------- /triangle.go: -------------------------------------------------------------------------------- 1 | package slicer 2 | 3 | import "github.com/fogleman/fauxgl" 4 | 5 | type triangle struct { 6 | N, V1, V2, V3 fauxgl.Vector 7 | MinZ, MaxZ float64 8 | } 9 | 10 | func newTriangle(ft *fauxgl.Triangle) *triangle { 11 | t := triangle{} 12 | // micro-adjust the z coordinates to avoid intersecting plane at exact tip 13 | t.V1 = ft.V1.Position.RoundPlaces(9) 14 | t.V2 = ft.V2.Position.RoundPlaces(9) 15 | t.V3 = ft.V3.Position.RoundPlaces(9) 16 | t.V1.Z += 5e-10 17 | t.V2.Z += 5e-10 18 | t.V3.Z += 5e-10 19 | // compute triangle normal 20 | e1 := t.V2.Sub(t.V1) 21 | e2 := t.V3.Sub(t.V1) 22 | t.N = e1.Cross(e2).Normalize() 23 | // compute min and max z value 24 | t.MinZ = t.V1.Z 25 | if t.V2.Z < t.MinZ { 26 | t.MinZ = t.V2.Z 27 | } 28 | if t.V3.Z < t.MinZ { 29 | t.MinZ = t.V3.Z 30 | } 31 | t.MaxZ = t.V1.Z 32 | if t.V2.Z > t.MaxZ { 33 | t.MaxZ = t.V2.Z 34 | } 35 | if t.V3.Z > t.MaxZ { 36 | t.MaxZ = t.V3.Z 37 | } 38 | return &t 39 | } 40 | -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | package slicer 2 | 3 | import "github.com/fogleman/fauxgl" 4 | 5 | type Path []fauxgl.Vector 6 | 7 | func joinPaths(paths []Path) []Path { 8 | lookup := make(map[fauxgl.Vector]Path, len(paths)) 9 | for _, path := range paths { 10 | lookup[path[0]] = path 11 | } 12 | var result []Path 13 | for len(lookup) > 0 { 14 | var v fauxgl.Vector 15 | for v = range lookup { 16 | break 17 | } 18 | var path Path 19 | for { 20 | path = append(path, v) 21 | if p, ok := lookup[v]; ok { 22 | delete(lookup, v) 23 | v = p[len(p)-1] 24 | } else { 25 | break 26 | } 27 | } 28 | result = append(result, path) 29 | } 30 | return result 31 | } 32 | 33 | func (p Path) Chop(step float64) Path { 34 | var result Path 35 | for i := 0; i < len(p)-1; i++ { 36 | a := p[i] 37 | b := p[i+1] 38 | v := b.Sub(a) 39 | l := v.Length() 40 | if i == 0 { 41 | result = append(result, a) 42 | } 43 | d := step 44 | for d < l { 45 | result = append(result, a.Add(v.MulScalar(d/l))) 46 | d += step 47 | } 48 | result = append(result, b) 49 | } 50 | return result 51 | } 52 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /slice.go: -------------------------------------------------------------------------------- 1 | package slicer 2 | 3 | import ( 4 | "math" 5 | "runtime" 6 | "sort" 7 | "sync" 8 | 9 | "github.com/fogleman/fauxgl" 10 | ) 11 | 12 | func SliceMesh(m *fauxgl.Mesh, step float64) []Layer { 13 | wn := runtime.NumCPU() 14 | minz := m.BoundingBox().Min.Z 15 | maxz := m.BoundingBox().Max.Z 16 | 17 | // copy triangles 18 | triangles := make([]*triangle, len(m.Triangles)) 19 | var wg sync.WaitGroup 20 | for wi := 0; wi < wn; wi++ { 21 | wg.Add(1) 22 | go func(wi int) { 23 | for i := wi; i < len(m.Triangles); i += wn { 24 | triangles[i] = newTriangle(m.Triangles[i]) 25 | } 26 | wg.Done() 27 | }(wi) 28 | } 29 | wg.Wait() 30 | 31 | // sort triangles 32 | sort.Slice(triangles, func(i, j int) bool { 33 | return triangles[i].MinZ < triangles[j].MinZ 34 | }) 35 | 36 | // create jobs for workers 37 | n := int(math.Ceil((maxz - minz) / step)) 38 | in := make(chan job, n) 39 | out := make(chan Layer, n) 40 | for wi := 0; wi < wn; wi++ { 41 | go worker(in, out) 42 | } 43 | index := 0 44 | var active []*triangle 45 | for i := 0; i < n; i++ { 46 | z := fauxgl.RoundPlaces(minz+step*float64(i)+step/2, 8) 47 | // remove triangles below plane 48 | newActive := active[:0] 49 | for _, t := range active { 50 | if t.MaxZ >= z { 51 | newActive = append(newActive, t) 52 | } 53 | } 54 | active = newActive 55 | // add triangles above plane 56 | for index < len(triangles) && triangles[index].MinZ <= z { 57 | active = append(active, triangles[index]) 58 | index++ 59 | } 60 | // copy triangles for worker job 61 | activeCopy := make([]*triangle, len(active)) 62 | copy(activeCopy, active) 63 | in <- job{z, activeCopy} 64 | } 65 | close(in) 66 | 67 | // read results from workers 68 | layers := make([]Layer, n) 69 | for i := 0; i < n; i++ { 70 | layers[i] = <-out 71 | } 72 | 73 | // sort layers 74 | sort.Slice(layers, func(i, j int) bool { 75 | return layers[i].Z < layers[j].Z 76 | }) 77 | 78 | // filter out empty layers 79 | if len(layers[0].Paths) == 0 { 80 | layers = layers[1:] 81 | } 82 | if len(layers[len(layers)-1].Paths) == 0 { 83 | layers = layers[:len(layers)-1] 84 | } 85 | 86 | return layers 87 | } 88 | 89 | type job struct { 90 | Z float64 91 | Triangles []*triangle 92 | } 93 | 94 | func worker(in chan job, out chan Layer) { 95 | var paths []Path 96 | for j := range in { 97 | paths = paths[:0] 98 | for _, t := range j.Triangles { 99 | if v1, v2, ok := intersectTriangle(j.Z, t); ok { 100 | paths = append(paths, Path{v1, v2}) 101 | } 102 | } 103 | out <- Layer{j.Z, joinPaths(paths)} 104 | } 105 | } 106 | 107 | func intersectSegment(z float64, v0, v1 fauxgl.Vector) (fauxgl.Vector, bool) { 108 | if v0.Z == v1.Z { 109 | return fauxgl.Vector{}, false 110 | } 111 | t := (z - v0.Z) / (v1.Z - v0.Z) 112 | if t < 0 || t > 1 { 113 | return fauxgl.Vector{}, false 114 | } 115 | v := v0.Add(v1.Sub(v0).MulScalar(t)) 116 | return v, true 117 | } 118 | 119 | func intersectTriangle(z float64, t *triangle) (fauxgl.Vector, fauxgl.Vector, bool) { 120 | v1, ok1 := intersectSegment(z, t.V1, t.V2) 121 | v2, ok2 := intersectSegment(z, t.V2, t.V3) 122 | v3, ok3 := intersectSegment(z, t.V3, t.V1) 123 | var p1, p2 fauxgl.Vector 124 | if ok1 && ok2 { 125 | p1, p2 = v1, v2 126 | } else if ok1 && ok3 { 127 | p1, p2 = v1, v3 128 | } else if ok2 && ok3 { 129 | p1, p2 = v2, v3 130 | } else { 131 | return fauxgl.Vector{}, fauxgl.Vector{}, false 132 | } 133 | p1 = p1.RoundPlaces(8) 134 | p2 = p2.RoundPlaces(8) 135 | if p1 == p2 { 136 | return fauxgl.Vector{}, fauxgl.Vector{}, false 137 | } 138 | n := fauxgl.Vector{p1.Y - p2.Y, p2.X - p1.X, 0} 139 | if n.Dot(t.N) < 0 { 140 | return p1, p2, true 141 | } else { 142 | return p2, p1, true 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /cmd/slicer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/png" 7 | "os" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | kingpin "gopkg.in/alecthomas/kingpin.v2" 15 | 16 | "github.com/fogleman/fauxgl" 17 | "github.com/fogleman/gg" 18 | "github.com/fogleman/slicer" 19 | ) 20 | 21 | var ( 22 | quiet = kingpin.Flag("quiet", "Run in silent mode.").Short('q').Bool() 23 | directory = kingpin.Flag("directory", "Set the output directory.").Short('d').ExistingDir() 24 | size = kingpin.Flag("size", "Set the slice thickness.").Required().Short('s').Float() 25 | subdivisions = kingpin.Flag("subdivisions", "Set the number of slice subdivisions.").Default("1").Short('b').Int() 26 | width = kingpin.Flag("width", "Set the raster width in model units.").Required().Short('w').Float() 27 | height = kingpin.Flag("height", "Set the raster height in model units.").Required().Short('h').Float() 28 | scale = kingpin.Flag("scale", "Set the raster scale.").Short('x').Required().Float() 29 | files = kingpin.Arg("files", "Mesh files to slice.").Required().ExistingFiles() 30 | ) 31 | 32 | func main() { 33 | kingpin.Parse() 34 | for _, filename := range *files { 35 | process(filename) 36 | } 37 | } 38 | 39 | func timed(name string) func() { 40 | if *quiet { 41 | return func() {} 42 | } 43 | fmt.Printf("%s... ", name) 44 | start := time.Now() 45 | return func() { 46 | fmt.Println(time.Since(start)) 47 | } 48 | } 49 | 50 | func log(text string) { 51 | if !*quiet { 52 | fmt.Println(text) 53 | } 54 | } 55 | 56 | func process(infile string) { 57 | var done func() 58 | 59 | log(fmt.Sprintf("input: %s", infile)) 60 | 61 | // load mesh 62 | done = timed("loading mesh") 63 | mesh, err := fauxgl.LoadMesh(infile) 64 | if err != nil { 65 | panic(err) 66 | } 67 | box := mesh.BoundingBox() 68 | done() 69 | 70 | // slice mesh 71 | done = timed("slicing mesh") 72 | step := *size / float64(*subdivisions) 73 | layers := slicer.SliceMesh(mesh, step) 74 | done() 75 | 76 | // determine output dir 77 | dir := "." 78 | if *directory != "" { 79 | dir = *directory 80 | } 81 | _, name := filepath.Split(infile) 82 | name = strings.TrimSuffix(name, filepath.Ext(name)) 83 | dir = filepath.Join(dir, name) 84 | os.MkdirAll(dir, os.ModePerm) 85 | 86 | // render pngs 87 | render(box, layers, dir) 88 | } 89 | 90 | type job struct { 91 | i int 92 | layers []slicer.Layer 93 | box fauxgl.Box 94 | } 95 | 96 | func render(box fauxgl.Box, layers []slicer.Layer, dir string) { 97 | b := *subdivisions 98 | wn := runtime.NumCPU() 99 | ch := make(chan job, len(layers)/b) 100 | var wg sync.WaitGroup 101 | for wi := 0; wi < wn; wi++ { 102 | wg.Add(1) 103 | go worker(ch, &wg, dir) 104 | } 105 | for i := 0; i < len(layers)/b; i++ { 106 | i0 := i * b 107 | i1 := i0 + b 108 | ch <- job{i, layers[i0:i1], box} 109 | } 110 | close(ch) 111 | wg.Wait() 112 | } 113 | 114 | func fastRGBAToGray(src *image.RGBA) *image.Gray { 115 | dst := image.NewGray(src.Bounds()) 116 | w := src.Bounds().Size().X 117 | h := src.Bounds().Size().Y 118 | for y := 0; y < h; y++ { 119 | i := src.PixOffset(0, y) 120 | j := dst.PixOffset(0, y) 121 | for x := 0; x < w; x++ { 122 | dst.Pix[j] = src.Pix[i] 123 | i += 4 124 | j++ 125 | } 126 | } 127 | return dst 128 | } 129 | 130 | func averageImages(images []*image.Gray) *image.Gray { 131 | if len(images) == 1 { 132 | return images[0] 133 | } 134 | dst := image.NewGray(images[0].Bounds()) 135 | for i := range dst.Pix { 136 | var sum int 137 | for _, im := range images { 138 | sum += int(im.Pix[i]) 139 | } 140 | dst.Pix[i] = uint8(sum / len(images)) 141 | } 142 | return dst 143 | } 144 | 145 | func savePNG(path string, im image.Image) error { 146 | file, err := os.Create(path) 147 | if err != nil { 148 | return err 149 | } 150 | defer file.Close() 151 | encoder := png.Encoder{ 152 | CompressionLevel: png.BestSpeed, 153 | } 154 | return encoder.Encode(file, im) 155 | } 156 | 157 | func worker(ch chan job, wg *sync.WaitGroup, dir string) { 158 | s := *scale 159 | w := *width * s 160 | h := *height * s 161 | for j := range ch { 162 | i := j.i 163 | layers := j.layers 164 | box := j.box 165 | center := box.Center() 166 | dc := gg.NewContext(int(w), int(h)) 167 | dc.InvertY() 168 | dc.Translate(w/2, h/2) 169 | dc.Scale(s, s) 170 | dc.Translate(-center.X, -center.Y) 171 | dc.SetFillRuleWinding() 172 | 173 | var images []*image.Gray 174 | for _, layer := range layers { 175 | dc.SetRGB(0, 0, 0) 176 | dc.Clear() 177 | for _, path := range layer.Paths { 178 | dc.NewSubPath() 179 | for _, point := range path { 180 | dc.LineTo(point.X, point.Y) 181 | } 182 | dc.ClosePath() 183 | } 184 | dc.SetRGB(1, 1, 1) 185 | dc.Fill() 186 | im := fastRGBAToGray(dc.Image().(*image.RGBA)) 187 | images = append(images, im) 188 | } 189 | dst := averageImages(images) 190 | 191 | path, _ := filepath.Abs(filepath.Join(dir, fmt.Sprintf("%04d.png", i))) 192 | savePNG(path, dst) 193 | fmt.Println(path) 194 | } 195 | wg.Done() 196 | } 197 | --------------------------------------------------------------------------------