From de11c3a58c21bde483e6ac88fa619693acf72d6d Mon Sep 17 00:00:00 2001 From: Jordan Bonecutter Date: Fri, 21 Nov 2025 14:19:39 -0800 Subject: [PATCH 1/3] Add LoadOBJReader function to load an obj from an io.ReadCloser --- obj.go | 9 +++++++-- util.go | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/obj.go b/obj.go index e1a7129..0619d74 100644 --- a/obj.go +++ b/obj.go @@ -2,6 +2,7 @@ package fauxgl import ( "bufio" + "io" "os" "strconv" "strings" @@ -21,12 +22,16 @@ func LoadOBJ(path string) (*Mesh, error) { if err != nil { return nil, err } - defer file.Close() + return LoadOBJReader(file) +} + +func LoadOBJReader(r io.ReadCloser) (*Mesh, error) { + defer r.Close() vs := make([]Vector, 1, 1024) // 1-based indexing vts := make([]Vector, 1, 1024) // 1-based indexing vns := make([]Vector, 1, 1024) // 1-based indexing var triangles []*Triangle - scanner := bufio.NewScanner(file) + scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() fields := strings.Fields(line) diff --git a/util.go b/util.go index 50837b6..12b009c 100644 --- a/util.go +++ b/util.go @@ -5,6 +5,7 @@ import ( "image" _ "image/jpeg" "image/png" + "io" "math" "os" "path/filepath" From b4c189c52732f08ec1371720cb73702fda5a519b Mon Sep 17 00:00:00 2001 From: Jordan Bonecutter Date: Sat, 24 Jan 2026 13:34:22 -0800 Subject: [PATCH 2/3] add tests, support readers on all mesh types, fix broken packages --- 3ds.go | 58 ++++++++++++++++---------- 3ds_test.go | 43 +++++++++++++++++++ box.3ds | Bin 0 -> 595 bytes box.ascii.stl | 30 ++++++++++++++ box.obj | 33 +++++++++++++++ box.stl | Bin 0 -> 684 bytes cmd/meshmontage/main.go | 2 +- cmd/meshrepeat/main.go | 2 +- cmd/meshsample/main.go | 2 +- cmd/meshscale/main.go | 2 +- go.mod | 14 +++++++ go.sum | 23 ++++++++++ obj.go | 85 ++++++++++++++++++++++++++++++++----- obj_test.go | 35 ++++++++++++++++ ply.go | 31 +++++++------- ply_test.go | 23 ++++++++++ pyramid.obj | 16 +++++++ pyramid.ply | 21 ++++++++++ stl.go | 54 ++++++++++++++---------- stl_test.go | 37 +++++++++++++++++ util.go | 90 ++++++++++++++++++++++++++++++++++++---- 21 files changed, 517 insertions(+), 84 deletions(-) create mode 100644 3ds_test.go create mode 100644 box.3ds create mode 100644 box.ascii.stl create mode 100644 box.obj create mode 100644 box.stl create mode 100644 go.mod create mode 100644 go.sum create mode 100644 obj_test.go create mode 100644 ply_test.go create mode 100644 pyramid.obj create mode 100644 pyramid.ply create mode 100644 stl_test.go diff --git a/3ds.go b/3ds.go index d67e06b..8b3e756 100644 --- a/3ds.go +++ b/3ds.go @@ -7,23 +7,26 @@ import ( ) func Load3DS(filename string) (*Mesh, error) { - type ChunkHeader struct { - ChunkID uint16 - Length uint32 - } - file, err := os.Open(filename) if err != nil { return nil, err } defer file.Close() + return Load3DSReader(file) +} + +func Load3DSReader(r io.Reader) (*Mesh, error) { + type ChunkHeader struct { + ChunkID uint16 + Length uint32 + } var vertices []Vector var faces []*Triangle var triangles []*Triangle for { header := ChunkHeader{} - if err := binary.Read(file, binary.LittleEndian, &header); err != nil { + if err := binary.Read(r, binary.LittleEndian, &header); err != nil { if err == io.EOF { break } @@ -33,26 +36,26 @@ func Load3DS(filename string) (*Mesh, error) { case 0x4D4D: case 0x3D3D: case 0x4000: - _, err := readNullTerminatedString(file) + _, err := readNullTerminatedString(r) if err != nil { return nil, err } case 0x4100: case 0x4110: - v, err := readVertexList(file) + v, err := readVertexList(r) if err != nil { return nil, err } vertices = v case 0x4120: - f, err := readFaceList(file, vertices) + f, err := readFaceList(r, vertices) if err != nil { return nil, err } faces = f triangles = append(triangles, faces...) case 0x4150: - err := readSmoothingGroups(file, faces) + err := readSmoothingGroups(r, faces) if err != nil { return nil, err } @@ -65,16 +68,25 @@ func Load3DS(filename string) (*Mesh, error) { // vertices[i] = matrix.MulPosition(v) // } default: - file.Seek(int64(header.Length-6), 1) + var err error + if seeker, canSeek := r.(io.Seeker); canSeek { + _, err = seeker.Seek(int64(header.Length-6), io.SeekCurrent) + } else { + _, err = io.CopyN(io.Discard, r, int64(header.Length-6)) + } + + if err != nil { + return nil, err + } } } return NewTriangleMesh(triangles), nil } -func readSmoothingGroups(file *os.File, triangles []*Triangle) error { +func readSmoothingGroups(r io.Reader, triangles []*Triangle) error { groups := make([]uint32, len(triangles)) - if err := binary.Read(file, binary.LittleEndian, &groups); err != nil { + if err := binary.Read(r, binary.LittleEndian, &groups); err != nil { return err } var tables [32]map[Vector][]Vector @@ -117,9 +129,9 @@ func readSmoothingGroups(file *os.File, triangles []*Triangle) error { return nil } -func readLocalAxis(file *os.File) (Matrix, error) { +func readLocalAxis(r io.Reader) (Matrix, error) { var m [4][3]float32 - if err := binary.Read(file, binary.LittleEndian, &m); err != nil { + if err := binary.Read(r, binary.LittleEndian, &m); err != nil { return Matrix{}, err } matrix := Matrix{ @@ -131,15 +143,15 @@ func readLocalAxis(file *os.File) (Matrix, error) { return matrix, nil } -func readVertexList(file *os.File) ([]Vector, error) { +func readVertexList(r io.Reader) ([]Vector, error) { var count uint16 - if err := binary.Read(file, binary.LittleEndian, &count); err != nil { + if err := binary.Read(r, binary.LittleEndian, &count); err != nil { return nil, err } result := make([]Vector, count) for i := range result { var v [3]float32 - if err := binary.Read(file, binary.LittleEndian, &v); err != nil { + if err := binary.Read(r, binary.LittleEndian, &v); err != nil { return nil, err } result[i] = Vector{float64(v[0]), float64(v[1]), float64(v[2])} @@ -147,15 +159,15 @@ func readVertexList(file *os.File) ([]Vector, error) { return result, nil } -func readFaceList(file *os.File, vertices []Vector) ([]*Triangle, error) { +func readFaceList(r io.Reader, vertices []Vector) ([]*Triangle, error) { var count uint16 - if err := binary.Read(file, binary.LittleEndian, &count); err != nil { + if err := binary.Read(r, binary.LittleEndian, &count); err != nil { return nil, err } result := make([]*Triangle, count) for i := range result { var v [4]uint16 - if err := binary.Read(file, binary.LittleEndian, &v); err != nil { + if err := binary.Read(r, binary.LittleEndian, &v); err != nil { return nil, err } result[i] = NewTriangleForPoints( @@ -164,11 +176,11 @@ func readFaceList(file *os.File, vertices []Vector) ([]*Triangle, error) { return result, nil } -func readNullTerminatedString(file *os.File) (string, error) { +func readNullTerminatedString(r io.Reader) (string, error) { var bytes []byte buf := make([]byte, 1) for { - n, err := file.Read(buf) + n, err := r.Read(buf) if err != nil { return "", err } else if n == 1 { diff --git a/3ds_test.go b/3ds_test.go new file mode 100644 index 0000000..ca45d67 --- /dev/null +++ b/3ds_test.go @@ -0,0 +1,43 @@ +package fauxgl_test + +import ( + "bytes" + _ "embed" + "io" + "testing" + + "github.com/fogleman/fauxgl" +) + +//go:embed box.3ds +var box3DS []byte + +func TestLoad3DS(t *testing.T) { + mesh, err := fauxgl.Load3DSReader(bytes.NewReader(box3DS)) + if err != nil { + t.Fatalf("failed loading box.3ds mesh: %s", err) + } + + if len(mesh.Triangles) != 12 { + t.Fatalf("failed loading triangles, expected %d", len(mesh.Triangles)) + } +} + +type noseek struct { + rs io.ReadSeeker +} + +func (ns noseek) Read(p []byte) (n int, err error) { + return ns.rs.Read(p) +} + +func TestLoad3DSNoSeek(t *testing.T) { + mesh, err := fauxgl.Load3DSReader(noseek{bytes.NewReader(box3DS)}) + if err != nil { + t.Fatalf("failed loading box.3ds mesh: %s", err) + } + + if len(mesh.Triangles) != 12 { + t.Fatalf("failed loading triangles, expected %d", len(mesh.Triangles)) + } +} diff --git a/box.3ds b/box.3ds new file mode 100644 index 0000000000000000000000000000000000000000..e0000997fed4c2070aac9624717357ba09a362ee GIT binary patch literal 595 zcmZ`$F;2rk5FDGB7%M^+5jBP)1vH_dGw~UvbcB>91xOSWDUj$8Wr}pE=%}bu-WT8- z{Dox3&MHKRvu1a8c5ZXItEzDbgh&AF5qUlhfa~091Y*E!fMUc>`TW|y1YAj;x+yy` zdN0Y>NoV;f-_QF+XtE&Lggmoo8f4PN0zZaWYki1;F|~Fc)w)b_pw??{So8lISOF4>edRy jnIZE%((Td(H0$?8o%(Z=>%4XC$SltOoyQ;L<1|$Ijr$7Fs6gv^D%)BH+mUw= len(indexArr) { + return Vector{}, fmt.Errorf("found out of bounds %s index: %d (len=%d)", desc, idx, len(indexArr)) + } + + vIdx := indexArr[idx] + if vIdx < 0 || vIdx >= len(s) { + return Vector{}, fmt.Errorf("found out of bounds %s vector index: %d (len=%d)", desc, vIdx, len(s)) + } + + return s[vIdx], nil +} diff --git a/obj_test.go b/obj_test.go new file mode 100644 index 0000000..a96e319 --- /dev/null +++ b/obj_test.go @@ -0,0 +1,35 @@ +package fauxgl_test + +import ( + "bytes" + _ "embed" + "testing" + + "github.com/fogleman/fauxgl" +) + +//go:embed box.obj +var boxOBJ []byte + +func TestLoadOBJ(t *testing.T) { + mesh, err := fauxgl.LoadOBJReader(bytes.NewReader(boxOBJ)) + if err != nil { + t.Fatalf("failed loading box.obj mesh: %s", err) + } + + if len(mesh.Triangles) != 12 { + t.Fatalf("failed loading triangles, expected %d", len(mesh.Triangles)) + } +} + + +//go:embed pyramid.obj +var pyramidOBJ []byte + +func TestLoadOBJBad(t *testing.T) { + _, err := fauxgl.LoadOBJReader(bytes.NewReader(pyramidOBJ)) + if err == nil { + t.Fatalf("expected failure loading pyramid.obj mesh: %s", err) + } +} + diff --git a/ply.go b/ply.go index 3ef1840..753e711 100644 --- a/ply.go +++ b/ply.go @@ -3,6 +3,7 @@ package fauxgl import ( "bufio" "encoding/binary" + "io" "os" "strconv" "strings" @@ -76,8 +77,12 @@ func LoadPLY(path string) (*Mesh, error) { } defer file.Close() + return LoadPLYReader(file) +} + +func LoadPLYReader(r io.Reader) (*Mesh, error) { // read header - reader := bufio.NewReader(file) + reader := bufio.NewReader(r) var element plyElement var elements []plyElement format := plyAscii @@ -126,20 +131,18 @@ func LoadPLY(path string) (*Mesh, error) { } } - file.Seek(int64(bytes), 0) - switch format { case plyBinaryBigEndian: - return loadPlyBinary(file, elements, binary.BigEndian) + return loadPlyBinary(reader, elements, binary.BigEndian) case plyBinaryLittleEndian: - return loadPlyBinary(file, elements, binary.LittleEndian) + return loadPlyBinary(reader, elements, binary.LittleEndian) default: - return loadPlyAscii(file, elements) + return loadPlyAscii(reader, elements) } } -func loadPlyAscii(file *os.File, elements []plyElement) (*Mesh, error) { - scanner := bufio.NewScanner(file) +func loadPlyAscii(r io.Reader, elements []plyElement) (*Mesh, error) { + scanner := bufio.NewScanner(r) var vertexes []Vector var triangles []*Triangle for _, element := range elements { @@ -181,7 +184,7 @@ func loadPlyAscii(file *os.File, elements []plyElement) (*Mesh, error) { return NewTriangleMesh(triangles), nil } -func loadPlyBinary(file *os.File, elements []plyElement, order binary.ByteOrder) (*Mesh, error) { +func loadPlyBinary(r io.Reader, elements []plyElement, order binary.ByteOrder) (*Mesh, error) { var vertexes []Vector var triangles []*Triangle for _, element := range elements { @@ -190,7 +193,7 @@ func loadPlyBinary(file *os.File, elements []plyElement, order binary.ByteOrder) var points []Vector for _, property := range element.properties { if property.countType == plyNone { - value, err := readPlyFloat(file, order, property.dataType) + value, err := readPlyFloat(r, order, property.dataType) if err != nil { return nil, err } @@ -204,12 +207,12 @@ func loadPlyBinary(file *os.File, elements []plyElement, order binary.ByteOrder) vertex.Z = value } } else { - count, err := readPlyInt(file, order, property.countType) + count, err := readPlyInt(r, order, property.countType) if err != nil { return nil, err } for j := 0; j < count; j++ { - value, err := readPlyInt(file, order, property.dataType) + value, err := readPlyInt(r, order, property.dataType) if err != nil { return nil, err } @@ -235,12 +238,12 @@ func loadPlyBinary(file *os.File, elements []plyElement, order binary.ByteOrder) return NewTriangleMesh(triangles), nil } -func readPlyInt(file *os.File, order binary.ByteOrder, dataType plyDataType) (int, error) { +func readPlyInt(file io.Reader, order binary.ByteOrder, dataType plyDataType) (int, error) { value, err := readPlyFloat(file, order, dataType) return int(value), err } -func readPlyFloat(file *os.File, order binary.ByteOrder, dataType plyDataType) (float64, error) { +func readPlyFloat(file io.Reader, order binary.ByteOrder, dataType plyDataType) (float64, error) { switch dataType { case plyInt8: var value int8 diff --git a/ply_test.go b/ply_test.go new file mode 100644 index 0000000..f93a3e4 --- /dev/null +++ b/ply_test.go @@ -0,0 +1,23 @@ +package fauxgl_test + +import ( + "bytes" + _ "embed" + "testing" + + "github.com/fogleman/fauxgl" +) + +//go:embed pyramid.ply +var pyramidPLY []byte + +func TestLoadPLY(t *testing.T) { + mesh, err := fauxgl.LoadPLYReader(bytes.NewReader(pyramidPLY)) + if err != nil { + t.Fatalf("failed loading box.ply mesh: %s", err) + } + + if len(mesh.Triangles) != 6 { + t.Fatalf("failed loading triangles, expected %d", len(mesh.Triangles)) + } +} diff --git a/pyramid.obj b/pyramid.obj new file mode 100644 index 0000000..ad98750 --- /dev/null +++ b/pyramid.obj @@ -0,0 +1,16 @@ +# OBJ file created by ply_to_obj.c +# +g Object001 + +v 0 0 0 +v 1 0 0 +v 1 1 0 +v 0 1 0 +v 0.5 0.5 1.6 + +f 5 2 3 +f 4 5 3 +f 6 3 2 +f 5 6 2 +f 4 6 5 +f 6 4 3 diff --git a/pyramid.ply b/pyramid.ply new file mode 100644 index 0000000..1f34868 --- /dev/null +++ b/pyramid.ply @@ -0,0 +1,21 @@ +ply +format ascii 1.0 +comment created by MATLAB ply_write +element vertex 5 +property float x +property float y +property float z +element face 6 +property list uchar char vertex_indices +end_header +0.000000 0.000000 0.000000 +1.000000 0.000000 0.000000 +1.000000 1.000000 0.000000 +0.000000 1.000000 0.000000 +0.500000 0.500000 1.600000 +3 1 0 3 +3 1 3 2 +3 0 1 4 +3 0 4 3 +3 3 4 2 +3 1 2 4 diff --git a/stl.go b/stl.go index d33ebb3..b12cb0f 100644 --- a/stl.go +++ b/stl.go @@ -2,6 +2,7 @@ package fauxgl import ( "bufio" + "bytes" "encoding/binary" "io" "math" @@ -10,9 +11,14 @@ import ( "strings" ) -type STLHeader struct { - _ [80]uint8 - Count uint32 +type STLHeader [84]byte + +func (s *STLHeader) Count() uint32 { + return binary.LittleEndian.Uint32(s[80:]) +} + +func (s *STLHeader) SetCount(count uint32) { + binary.LittleEndian.AppendUint32(s[80:80], count) } type STLTriangle struct { @@ -35,30 +41,33 @@ func LoadSTL(path string) (*Mesh, error) { } size := info.Size() + return LoadSTLReader(file, size) +} + +func LoadSTLReader(r io.Reader, size int64) (*Mesh, error) { // read header, get expected binary size header := STLHeader{} - if err := binary.Read(file, binary.LittleEndian, &header); err != nil { - return nil, err - } - expectedSize := int64(header.Count)*50 + 84 - - // rewind to start of file - _, err = file.Seek(0, 0) - if err != nil { + if _, err := io.ReadFull(r, header[:]); err != nil { return nil, err } + expectedSize := int64(header.Count())*50 + 84 // parse ascii or binary stl if size == expectedSize { - return loadSTLB(file) + return loadSTLB( + &header, + r, + ) } else { - return loadSTLA(file) + return loadSTLA( + io.MultiReader(bytes.NewReader(header[:]), r), + ) } } -func loadSTLA(file *os.File) (*Mesh, error) { +func loadSTLA(r io.Reader) (*Mesh, error) { var vertexes []Vector - scanner := bufio.NewScanner(file) + scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() fields := strings.Fields(line) @@ -83,16 +92,15 @@ func makeFloat(b []byte) float64 { return float64(math.Float32frombits(binary.LittleEndian.Uint32(b))) } -func loadSTLB(file *os.File) (*Mesh, error) { - r := bufio.NewReader(file) - header := STLHeader{} - if err := binary.Read(r, binary.LittleEndian, &header); err != nil { - return nil, err - } - count := int(header.Count) +func loadSTLB( + header *STLHeader, + reader io.Reader, +) (*Mesh, error) { + count := int(header.Count()) triangles := make([]*Triangle, count) _triangles := make([]Triangle, count) b := make([]byte, count*50) + r := bufio.NewReader(reader) _, err := io.ReadFull(r, b) if err != nil { return nil, err @@ -161,7 +169,7 @@ func SaveSTL(path string, mesh *Mesh) error { defer file.Close() w := bufio.NewWriter(file) header := STLHeader{} - header.Count = uint32(len(mesh.Triangles)) + header.SetCount(uint32(len(mesh.Triangles))) if err := binary.Write(w, binary.LittleEndian, &header); err != nil { return err } diff --git a/stl_test.go b/stl_test.go new file mode 100644 index 0000000..7adc09e --- /dev/null +++ b/stl_test.go @@ -0,0 +1,37 @@ +package fauxgl_test + +import ( + "bytes" + _ "embed" + "testing" + + "github.com/fogleman/fauxgl" +) + +//go:embed box.stl +var boxSTL []byte + +//go:embed box.ascii.stl +var boxSTLA []byte + +func TestLoadSTL(t *testing.T) { + mesh, err := fauxgl.LoadSTLReader(bytes.NewReader(boxSTL), int64(len(boxSTL))) + if err != nil { + t.Fatalf("failed loading box.stl mesh: %s", err) + } + + if len(mesh.Triangles) != 12 { + t.Fatalf("failed loading triangles, expected %d", len(mesh.Triangles)) + } +} + +func TestLoadSTLA(t *testing.T) { + mesh, err := fauxgl.LoadSTLReader(bytes.NewReader(boxSTLA), int64(len(boxSTLA))) + if err != nil { + t.Fatalf("failed loading box.ascii.stl mesh: %s", err) + } + + if len(mesh.Triangles) != 4 { + t.Fatalf("failed loading triangles, expected %d", len(mesh.Triangles)) + } +} diff --git a/util.go b/util.go index 12b009c..06001e6 100644 --- a/util.go +++ b/util.go @@ -29,19 +29,91 @@ func LatLngToXYZ(lat, lng float64) Vector { return Vector{x, y, z} } +type SeekReadCloser interface { + io.ReaderFrom +} + func LoadMesh(path string) (*Mesh, error) { + meshType, err := MeshTypeFromPath(path) + if err != nil { + return nil, err + } + + fi, err := os.Open(path) + if err != nil { + return nil, err + } + defer fi.Close() + + stat, err := fi.Stat() + if err != nil { + return nil, err + } + + return LoadMeshSizedReader(meshType, fi, stat.Size()) +} + +type MeshType string + +const ( + MeshTypeUnknown = "" + MeshTypeSTL = ".stl" + MeshTypeOBJ = ".obj" + MeshTypePLY = ".ply" + MeshType3DS = ".3ds" +) + +func MeshTypeFromPath(path string) (MeshType, error) { ext := strings.ToLower(filepath.Ext(path)) switch ext { - case ".stl": - return LoadSTL(path) - case ".obj": - return LoadOBJ(path) - case ".ply": - return LoadPLY(path) - case ".3ds": - return Load3DS(path) + case MeshTypeSTL: + return MeshTypeSTL, nil + case MeshTypeOBJ: + return MeshTypeOBJ, nil + case MeshTypePLY: + return MeshTypePLY, nil + case MeshType3DS: + return MeshType3DS, nil + } + + return MeshTypeUnknown, fmt.Errorf("unrecognized mesh type: %s", ext) +} + +func LoadMeshSizedReader( + meshType MeshType, + r io.Reader, + size int64, +) (*Mesh, error) { + switch meshType { + case MeshTypeSTL: + return LoadSTLReader(r, size) + case MeshTypeOBJ: + return LoadOBJReader(r) + case MeshTypePLY: + return LoadPLYReader(r) + case MeshType3DS: + return Load3DSReader(r) } - return nil, fmt.Errorf("unrecognized mesh extension: %s", ext) + + return nil, fmt.Errorf("unsupported mesh type: %s", meshType) +} + +func LoadMeshReader( + meshType MeshType, + r io.Reader, +) (*Mesh, error) { + switch meshType { + case MeshTypeSTL: + return nil, fmt.Errorf("cannot load an stl mesh from an unsized io.Reader") + case MeshTypeOBJ: + return LoadOBJReader(r) + case MeshTypePLY: + return LoadPLYReader(r) + case MeshType3DS: + return Load3DSReader(r) + } + + return nil, fmt.Errorf("unsupported mesh type: %s", meshType) } func LoadImage(path string) (image.Image, error) { From 03f189157fd7dc44dcf809b71ff9504d861e3531 Mon Sep 17 00:00:00 2001 From: Jordan Bonecutter Date: Sat, 24 Jan 2026 14:10:34 -0800 Subject: [PATCH 3/3] add render command to manually check file loading --- cmd/render/main.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 cmd/render/main.go diff --git a/cmd/render/main.go b/cmd/render/main.go new file mode 100644 index 0000000..c5facc5 --- /dev/null +++ b/cmd/render/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + "image/png" + "os" + + "github.com/fogleman/fauxgl" +) + +func main() { + if len(os.Args) != 3 { + fmt.Printf("usage: %s \n", os.Args[0]) + return + } + + mesh, err := fauxgl.LoadMesh(os.Args[1]) + if err != nil { + fmt.Println("failed loading mesh:", err.Error()) + return + } + + mesh.Transform(fauxgl.Rotate(fauxgl.V(0, 1, 0), fauxgl.Radians(45))) + mesh.Transform(fauxgl.Rotate(fauxgl.V(0, 0, 1), fauxgl.Radians(30))) + orientFrontFacing(mesh) + mesh.BiUnitCube() + + backside := mesh.Copy() + backside.ReverseWinding() + + mat := fauxgl.LookAt(fauxgl.V(3, 0, 0), fauxgl.V(0.0, 0.0, 0.0), fauxgl.V(0, 0, 1)).Orthographic(-1.1, 1.1, -1.1, 1.1, 1, 50) + context := fauxgl.NewContext(1920, 1920) + context.Cull = fauxgl.CullBack + context.ClearColorBufferWith(fauxgl.HexColor("#00000000")) + shader := fauxgl.NewPhongShader(mat, fauxgl.V(1, 0, 0), fauxgl.V(17, 0, 0)) + shader.ObjectColor = fauxgl.Gray(0.5).Opaque() + shader.AmbientColor = fauxgl.Gray(0.25).Opaque() + shader.DiffuseColor = fauxgl.Gray(1).Opaque() + shader.SpecularColor = fauxgl.Gray(0.15).Opaque() + shader.SpecularPower = 32 + context.Shader = shader + context.DrawMesh(mesh) + context.DrawMesh(backside) + + edges := mesh.SharpEdges(fauxgl.Radians(70)) + context.Shader = fauxgl.NewSolidColorShader(mat, fauxgl.Black) + context.DrawMesh(edges) + + img := context.Image() + out, err := os.Create(os.Args[2]) + if err != nil { + fmt.Println("failed creating output file:", err.Error()) + return + } + defer out.Close() + + if err := png.Encode(out, img); err != nil { + fmt.Println("failed writing output file:", err.Error()) + return + } +} + +func orientFrontFacing(mesh *fauxgl.Mesh) { + mesh.Transform(fauxgl.Matrix{ + 1, 0, 0, 0, + 0, 0, -1, 0, + 0, 1, 0, 0, + 0, 0, 0, 1, + }) +}