├── README.md ├── cmd └── demsphere │ └── main.go ├── plane.go ├── shapes.go ├── stl.go ├── texture.go ├── triangle.go ├── triangulator.go └── vector.go /README.md: -------------------------------------------------------------------------------- 1 | # demsphere 2 | 3 | Generate 3D meshes of planets, moons, etc. from spherical DEMs. 4 | 5 | Example: Mars with 10x exaggerated elevation, accurate to 50 meters in elevation. 6 | 7 | ![Example](https://i.imgur.com/KXxcZfO.png) 8 | -------------------------------------------------------------------------------- /cmd/demsphere/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/fogleman/demsphere" 9 | "github.com/fogleman/fauxgl" 10 | kingpin "gopkg.in/alecthomas/kingpin.v2" 11 | ) 12 | 13 | var ( 14 | inputFile = kingpin.Flag("input", "Input DEM image to process.").Required().Short('i').ExistingFile() 15 | outputFile = kingpin.Flag("output", "Output STL file to write.").Required().Short('o').String() 16 | ) 17 | 18 | func timed(name string) func() { 19 | if len(name) > 0 { 20 | fmt.Printf("%s... ", name) 21 | } 22 | start := time.Now() 23 | return func() { 24 | fmt.Println(time.Since(start)) 25 | } 26 | } 27 | 28 | func main() { 29 | var done func() 30 | 31 | kingpin.Parse() 32 | 33 | done = timed("reading input") 34 | im, err := fauxgl.LoadImage(*inputFile) 35 | done() 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | 40 | // mercury 41 | // triangulator := demsphere.NewTriangulator( 42 | // im, 6, 12, 2439400, -10764, 8994, 50, 4, 1.0/2439400) 43 | 44 | // moon 45 | // triangulator := demsphere.NewTriangulator( 46 | // im, 6, 11, 1737400, -18257, 21563, 50, 3, 1.0/1737400) 47 | 48 | // mars 49 | triangulator := demsphere.NewTriangulator( 50 | im, 9, 12, 3396190, -8201, 21241, 50, 10, 1.0/3396190) 51 | 52 | // pluto 53 | // triangulator := demsphere.NewTriangulator( 54 | // im, 6, 12, 1188300, -4101, 6491, 50, 3, 1.0/1188300) 55 | 56 | done = timed("generating mesh") 57 | triangles := triangulator.Triangulate() 58 | done() 59 | 60 | fmt.Println(len(triangles)) 61 | 62 | // inner := fauxgl.NewSphere(4) 63 | // inner.Transform(fauxgl.Scale(fauxgl.V(0.85, 0.85, 0.85))) 64 | // inner.ReverseWinding() 65 | // for _, t := range inner.Triangles { 66 | // p1 := demsphere.Vector(t.V1.Position) 67 | // p2 := demsphere.Vector(t.V2.Position) 68 | // p3 := demsphere.Vector(t.V3.Position) 69 | // triangles = append(triangles, demsphere.Triangle{p1, p2, p3}) 70 | // } 71 | 72 | // fmt.Println(len(triangles)) 73 | 74 | done = timed("writing output") 75 | demsphere.WriteSTLFile(*outputFile, triangles) 76 | done() 77 | } 78 | 79 | // 4,5120,4.7372172692 80 | // 5,20480,2.3686086346009336 81 | // 6,81920,1.1844992435794788 82 | // 7,327680,0.5635352519913389 83 | // 8,1310720,0.2833191921173619 84 | // 9,5242880,0.14087309976888143 85 | // 10,20971520,0.07043426041304851 86 | // 11,83886080,0.036106462472517295 87 | // 12,335544320,0.018169800357500515 88 | -------------------------------------------------------------------------------- /plane.go: -------------------------------------------------------------------------------- 1 | package demsphere 2 | 3 | type Plane struct { 4 | N Vector 5 | D float64 6 | } 7 | 8 | func MakePlane(p1, p2, p3 Vector) Plane { 9 | n := p2.Sub(p1).Cross(p3.Sub(p1)).Normalize() 10 | d := n.Dot(p1) 11 | return Plane{n, d} 12 | } 13 | 14 | func (p *Plane) DistanceToPoint(q Vector) float64 { 15 | x := q.Dot(p.N) - p.D 16 | if x < 0 { 17 | return -x 18 | } 19 | return x 20 | } 21 | -------------------------------------------------------------------------------- /shapes.go: -------------------------------------------------------------------------------- 1 | package demsphere 2 | 3 | func NewIcosahedron() []Triangle { 4 | const a = 0.8506507174597755 5 | const b = 0.5257312591858783 6 | vertices := []Vector{ 7 | {-a, -b, 0}, 8 | {-a, b, 0}, 9 | {-b, 0, -a}, 10 | {-b, 0, a}, 11 | {0, -a, -b}, 12 | {0, -a, b}, 13 | {0, a, -b}, 14 | {0, a, b}, 15 | {b, 0, -a}, 16 | {b, 0, a}, 17 | {a, -b, 0}, 18 | {a, b, 0}, 19 | } 20 | indices := [][3]int{ 21 | {0, 3, 1}, 22 | {1, 3, 7}, 23 | {2, 0, 1}, 24 | {2, 1, 6}, 25 | {4, 0, 2}, 26 | {4, 5, 0}, 27 | {5, 3, 0}, 28 | {6, 1, 7}, 29 | {6, 7, 11}, 30 | {7, 3, 9}, 31 | {8, 2, 6}, 32 | {8, 4, 2}, 33 | {8, 6, 11}, 34 | {8, 10, 4}, 35 | {8, 11, 10}, 36 | {9, 3, 5}, 37 | {10, 5, 4}, 38 | {10, 9, 5}, 39 | {11, 7, 9}, 40 | {11, 9, 10}, 41 | } 42 | triangles := make([]Triangle, len(indices)) 43 | for i, idx := range indices { 44 | p1 := vertices[idx[0]] 45 | p2 := vertices[idx[1]] 46 | p3 := vertices[idx[2]] 47 | triangles[i] = Triangle{p1, p2, p3} 48 | } 49 | return triangles 50 | } 51 | -------------------------------------------------------------------------------- /stl.go: -------------------------------------------------------------------------------- 1 | package demsphere 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "io" 7 | "os" 8 | ) 9 | 10 | func WriteSTLFile(path string, triangles []Triangle) error { 11 | file, err := os.Create(path) 12 | if err != nil { 13 | return err 14 | } 15 | defer file.Close() 16 | 17 | w := bufio.NewWriter(file) 18 | defer w.Flush() 19 | 20 | return WriteSTL(w, triangles) 21 | } 22 | 23 | func WriteSTL(w io.Writer, triangles []Triangle) error { 24 | type STLHeader struct { 25 | _ [80]uint8 26 | Count uint32 27 | } 28 | 29 | type STLTriangle struct { 30 | N, V1, V2, V3 [3]float32 31 | _ uint16 32 | } 33 | 34 | header := STLHeader{} 35 | header.Count = uint32(len(triangles)) 36 | if err := binary.Write(w, binary.LittleEndian, &header); err != nil { 37 | return err 38 | } 39 | 40 | for _, triangle := range triangles { 41 | n := triangle.Normal() 42 | d := STLTriangle{} 43 | d.N[0] = float32(n.X) 44 | d.N[1] = float32(n.Y) 45 | d.N[2] = float32(n.Z) 46 | d.V1[0] = float32(triangle.A.X) 47 | d.V1[1] = float32(triangle.A.Y) 48 | d.V1[2] = float32(triangle.A.Z) 49 | d.V2[0] = float32(triangle.B.X) 50 | d.V2[1] = float32(triangle.B.Y) 51 | d.V2[2] = float32(triangle.B.Z) 52 | d.V3[0] = float32(triangle.C.X) 53 | d.V3[1] = float32(triangle.C.Y) 54 | d.V3[2] = float32(triangle.C.Z) 55 | if err := binary.Write(w, binary.LittleEndian, &d); err != nil { 56 | return err 57 | } 58 | } 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /texture.go: -------------------------------------------------------------------------------- 1 | package demsphere 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | "math" 7 | ) 8 | 9 | type Texture struct { 10 | W int 11 | H int 12 | Pix []float64 13 | } 14 | 15 | func NewTexture(im image.Image) *Texture { 16 | gray := ensureGray16(im) 17 | w := gray.Bounds().Size().X 18 | h := gray.Bounds().Size().Y 19 | data := gray16ToFloat64s(gray) 20 | return &Texture{w, h, data} 21 | } 22 | 23 | func (t *Texture) BilinearSample(u, v float64) float64 { 24 | u -= math.Floor(u) 25 | v -= math.Floor(v) 26 | x := u * float64(t.W) 27 | y := v * float64(t.H) 28 | x0 := int(x) 29 | y0 := int(y) 30 | x1 := x0 + 1 31 | y1 := y0 + 1 32 | x -= float64(x0) 33 | y -= float64(y0) 34 | if x0 >= t.W { 35 | x0 = 0 36 | } 37 | if y0 >= t.H { 38 | y0 = 0 39 | } 40 | if x1 >= t.W { 41 | x1 = 0 42 | } 43 | if y1 >= t.H { 44 | y1 = 0 45 | } 46 | var d float64 47 | d += t.Pix[x0+y0*t.W] * ((1 - x) * (1 - y)) 48 | d += t.Pix[x0+y1*t.W] * ((1 - x) * y) 49 | d += t.Pix[x1+y0*t.W] * (x * (1 - y)) 50 | d += t.Pix[x1+y1*t.W] * (x * y) 51 | return d 52 | } 53 | 54 | func (t *Texture) SphericalSample(spherical Vector) float64 { 55 | lat := math.Acos(spherical.Z) 56 | lng := math.Atan2(spherical.Y, spherical.X) 57 | u := (lng + math.Pi) / (2 * math.Pi) 58 | v := lat / math.Pi 59 | return t.BilinearSample(u, v) 60 | } 61 | 62 | func (t *Texture) Displace(spherical Vector, lo, hi float64) Vector { 63 | return spherical.MulScalar(lo + t.SphericalSample(spherical)*(hi-lo)) 64 | } 65 | 66 | func ensureGray16(im image.Image) *image.Gray16 { 67 | switch im := im.(type) { 68 | case *image.Gray16: 69 | return im 70 | default: 71 | dst := image.NewGray16(im.Bounds()) 72 | draw.Draw(dst, im.Bounds(), im, image.ZP, draw.Src) 73 | return dst 74 | } 75 | } 76 | 77 | func gray16ToFloat64s(im *image.Gray16) []float64 { 78 | w := im.Bounds().Size().X 79 | h := im.Bounds().Size().Y 80 | buf := make([]float64, w*h) 81 | index := 0 82 | for y := 0; y < h; y++ { 83 | i := im.PixOffset(0, y) 84 | for x := 0; x < w; x++ { 85 | v := (int(im.Pix[i]) << 8) | int(im.Pix[i+1]) 86 | buf[index] = float64(v) / 0xffff 87 | index += 1 88 | i += 2 89 | } 90 | } 91 | return buf 92 | } 93 | -------------------------------------------------------------------------------- /triangle.go: -------------------------------------------------------------------------------- 1 | package demsphere 2 | 3 | type Triangle struct { 4 | A, B, C Vector 5 | } 6 | 7 | func (t *Triangle) Normal() Vector { 8 | ab := t.B.Sub(t.A) 9 | ac := t.C.Sub(t.A) 10 | return ab.Cross(ac).Normalize() 11 | } 12 | -------------------------------------------------------------------------------- /triangulator.go: -------------------------------------------------------------------------------- 1 | package demsphere 2 | 3 | import ( 4 | "image" 5 | ) 6 | 7 | type Triangulator struct { 8 | texture *Texture 9 | 10 | minDetail int 11 | maxDetail int 12 | minRadius float64 13 | maxRadius float64 14 | minOutputRadius float64 15 | maxOutputRadius float64 16 | tolerance float64 17 | 18 | points map[Vector]Vector 19 | counts map[int]int 20 | 21 | temp []Triangle 22 | triangles []Triangle 23 | } 24 | 25 | func NewTriangulator(im image.Image, minDetail, maxDetail int, meanRadius, minElevation, maxElevation, tolerance, exaggeration, scale float64) *Triangulator { 26 | texture := NewTexture(im) 27 | minRadius := meanRadius + minElevation 28 | maxRadius := meanRadius + maxElevation 29 | minOutputRadius := (meanRadius + minElevation*exaggeration) * scale 30 | maxOutputRadius := (meanRadius + maxElevation*exaggeration) * scale 31 | points := make(map[Vector]Vector) 32 | counts := make(map[int]int) 33 | return &Triangulator{texture, minDetail, maxDetail, minRadius, maxRadius, minOutputRadius, maxOutputRadius, tolerance, points, counts, nil, nil} 34 | } 35 | 36 | func (tri *Triangulator) Triangulate() []Triangle { 37 | tri.temp = nil 38 | tri.triangles = nil 39 | for _, t := range NewIcosahedron() { 40 | tri.triangulate(0, t.A, t.B, t.C) 41 | } 42 | // for d := 0; d <= tri.maxDetail; d++ { 43 | // fmt.Println(d, tri.counts[d]) 44 | // } 45 | for _, t := range tri.temp { 46 | tri.split(t.A, t.B, t.C) 47 | } 48 | return tri.triangles 49 | } 50 | 51 | func (tri *Triangulator) split(v1, v2, v3 Vector) { 52 | v12 := bisect(v1, v2) 53 | v23 := bisect(v2, v3) 54 | v31 := bisect(v3, v1) 55 | if _, ok := tri.points[v12]; ok { 56 | tri.split(v1, v12, v3) 57 | tri.split(v12, v2, v3) 58 | } else if _, ok := tri.points[v23]; ok { 59 | tri.split(v1, v2, v23) 60 | tri.split(v23, v3, v1) 61 | } else if _, ok := tri.points[v31]; ok { 62 | tri.split(v1, v2, v31) 63 | tri.split(v31, v2, v3) 64 | } else { 65 | p1 := tri.points[v1] 66 | p2 := tri.points[v2] 67 | p3 := tri.points[v3] 68 | tri.triangles = append(tri.triangles, Triangle{p1, p2, p3}) 69 | } 70 | } 71 | 72 | func (tri *Triangulator) triangulate(detail int, v1, v2, v3 Vector) { 73 | if detail == tri.maxDetail { 74 | tri.leaf(v1, v2, v3) 75 | tri.counts[detail]++ 76 | return 77 | } 78 | 79 | v12 := bisect(v1, v2) 80 | v23 := bisect(v2, v3) 81 | v31 := bisect(v3, v1) 82 | 83 | if detail >= tri.minDetail { 84 | p1 := tri.texture.Displace(v1, tri.minRadius, tri.maxRadius) 85 | p2 := tri.texture.Displace(v2, tri.minRadius, tri.maxRadius) 86 | p3 := tri.texture.Displace(v3, tri.minRadius, tri.maxRadius) 87 | plane := MakePlane(p1, p2, p3) 88 | depth := tri.maxDetail - detail + 1 89 | if depth > 5 { 90 | depth = 5 91 | } 92 | if tri.withinTolerance(depth, plane, v1, v2, v3) { 93 | tri.leaf(v1, v2, v3) 94 | tri.counts[detail]++ 95 | return 96 | } 97 | } 98 | 99 | tri.triangulate(detail+1, v1, v12, v31) 100 | tri.triangulate(detail+1, v2, v23, v12) 101 | tri.triangulate(detail+1, v3, v31, v23) 102 | tri.triangulate(detail+1, v12, v23, v31) 103 | } 104 | 105 | func (tri *Triangulator) leaf(v1, v2, v3 Vector) { 106 | p1 := tri.texture.Displace(v1, tri.minOutputRadius, tri.maxOutputRadius) 107 | p2 := tri.texture.Displace(v2, tri.minOutputRadius, tri.maxOutputRadius) 108 | p3 := tri.texture.Displace(v3, tri.minOutputRadius, tri.maxOutputRadius) 109 | tri.points[v1] = p1 110 | tri.points[v2] = p2 111 | tri.points[v3] = p3 112 | tri.temp = append(tri.temp, Triangle{v1, v2, v3}) 113 | } 114 | 115 | func (tri *Triangulator) withinTolerance(depth int, plane Plane, v1, v2, v3 Vector) bool { 116 | if depth == 0 { 117 | return true 118 | } 119 | 120 | v12 := bisect(v1, v2) 121 | p12 := tri.texture.Displace(v12, tri.minRadius, tri.maxRadius) 122 | if plane.DistanceToPoint(p12) > tri.tolerance { 123 | return false 124 | } 125 | 126 | v23 := bisect(v2, v3) 127 | p23 := tri.texture.Displace(v23, tri.minRadius, tri.maxRadius) 128 | if plane.DistanceToPoint(p23) > tri.tolerance { 129 | return false 130 | } 131 | 132 | v31 := bisect(v3, v1) 133 | p13 := tri.texture.Displace(v31, tri.minRadius, tri.maxRadius) 134 | if plane.DistanceToPoint(p13) > tri.tolerance { 135 | return false 136 | } 137 | 138 | if depth == 1 { 139 | return true 140 | } 141 | 142 | return tri.withinTolerance(depth-1, plane, v1, v12, v31) && 143 | tri.withinTolerance(depth-1, plane, v2, v23, v12) && 144 | tri.withinTolerance(depth-1, plane, v3, v31, v23) && 145 | tri.withinTolerance(depth-1, plane, v12, v23, v31) 146 | } 147 | -------------------------------------------------------------------------------- /vector.go: -------------------------------------------------------------------------------- 1 | package demsphere 2 | 3 | import "math" 4 | 5 | func bisect(v1, v2 Vector) Vector { 6 | // v1.Add(v2).DivScalar(2).Normalize() 7 | x := (v1.X + v2.X) / 2 8 | y := (v1.Y + v2.Y) / 2 9 | z := (v1.Z + v2.Z) / 2 10 | r := 1 / math.Sqrt(x*x+y*y+z*z) 11 | return Vector{x * r, y * r, z * r} 12 | } 13 | 14 | type Vector struct { 15 | X, Y, Z float64 16 | } 17 | 18 | func (a Vector) Add(b Vector) Vector { 19 | return Vector{a.X + b.X, a.Y + b.Y, a.Z + b.Z} 20 | } 21 | 22 | func (a Vector) Cross(b Vector) Vector { 23 | x := a.Y*b.Z - a.Z*b.Y 24 | y := a.Z*b.X - a.X*b.Z 25 | z := a.X*b.Y - a.Y*b.X 26 | return Vector{x, y, z} 27 | } 28 | 29 | func (a Vector) DivScalar(b float64) Vector { 30 | return Vector{a.X / b, a.Y / b, a.Z / b} 31 | } 32 | 33 | func (a Vector) Dot(b Vector) float64 { 34 | return a.X*b.X + a.Y*b.Y + a.Z*b.Z 35 | } 36 | 37 | func (a Vector) MulScalar(b float64) Vector { 38 | return Vector{a.X * b, a.Y * b, a.Z * b} 39 | } 40 | 41 | func (a Vector) Normalize() Vector { 42 | r := 1 / math.Sqrt(a.X*a.X+a.Y*a.Y+a.Z*a.Z) 43 | return Vector{a.X * r, a.Y * r, a.Z * r} 44 | } 45 | 46 | func (a Vector) Sub(b Vector) Vector { 47 | return Vector{a.X - b.X, a.Y - b.Y, a.Z - b.Z} 48 | } 49 | --------------------------------------------------------------------------------