From 0f6262da8feba206a285ba8799341455f87fd4dd Mon Sep 17 00:00:00 2001 From: ArtMares Date: Fri, 19 Jul 2024 10:21:26 +0300 Subject: [PATCH] feat: test tool write --- .gitignore | 4 + cmd/decoder/main.go | 220 +++++++++++ cmd/testgltf/main.go | 45 +++ go.mod | 12 + go.sum | 20 + internal/pkg/helpers/buffer.go | 21 ++ internal/services/exporter/cache.go | 41 +++ internal/services/exporter/exporter.go | 229 ++++++++++++ internal/services/exporter/model.go | 141 +++++++ models/.keep | 0 pkg/threedb/animation.go | 11 + pkg/threedb/coordinate.go | 12 + pkg/threedb/coordinate_test.go | 14 + pkg/threedb/decoder.go | 490 +++++++++++++++++++++++++ pkg/threedb/material.go | 6 + pkg/threedb/mesh.go | 9 + pkg/threedb/meshlink.go | 10 + pkg/threedb/model.go | 58 +++ pkg/threedb/vector.go | 32 ++ pkg/threedb/vector_test.go | 17 + 20 files changed, 1392 insertions(+) create mode 100644 cmd/decoder/main.go create mode 100644 cmd/testgltf/main.go create mode 100644 go.sum create mode 100644 internal/pkg/helpers/buffer.go create mode 100644 internal/services/exporter/cache.go create mode 100644 internal/services/exporter/exporter.go create mode 100644 internal/services/exporter/model.go create mode 100644 models/.keep create mode 100644 pkg/threedb/animation.go create mode 100644 pkg/threedb/coordinate.go create mode 100644 pkg/threedb/coordinate_test.go create mode 100644 pkg/threedb/decoder.go create mode 100644 pkg/threedb/material.go create mode 100644 pkg/threedb/mesh.go create mode 100644 pkg/threedb/meshlink.go create mode 100644 pkg/threedb/model.go create mode 100644 pkg/threedb/vector.go create mode 100644 pkg/threedb/vector_test.go diff --git a/.gitignore b/.gitignore index 967689f..06bc52e 100644 --- a/.gitignore +++ b/.gitignore @@ -154,3 +154,7 @@ Network Trash Folder Temporary Items .apdisk +# Ignore models +models/* +!models/.keep + diff --git a/cmd/decoder/main.go b/cmd/decoder/main.go new file mode 100644 index 0000000..a82d4dd --- /dev/null +++ b/cmd/decoder/main.go @@ -0,0 +1,220 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "math/rand/v2" + "os" + "path" + + "github.com/gabriel-vasile/mimetype" + "github.com/qmuntal/gltf" + "github.com/qmuntal/gltf/modeler" + + "git.influ.su/artmares/digglestool/internal/services/exporter" + "git.influ.su/artmares/digglestool/pkg/threedb" +) + +const ( + outputPath = "./models" + + texturesCacheFile = "./textures.cache" +) + +func main() { + texturesPath := os.Getenv("TEXTURES_PATH") + if texturesPath == "" { + log.Println("TEXTURES_PATH environment variable not set") + return + } + + inputPath := "./data" + inputModel := "produktionsstaetten" + + f, err := os.Open(path.Join(inputPath, inputModel+".3db")) + if err != nil { + log.Fatal(err) + } + defer f.Close() + + model := threedb.Model{} + err = threedb.NewDecoder(f).Decode(&model) + if err != nil { + log.Println(err) + return + } + //log.Printf("DB Version: %v\n", model.DBVersion) + //log.Printf("Name: %v\n", model.Name) + //log.Printf("Materials: %d\n", len(model.Materials)) + //for _, material := range model.Materials { + // log.Printf("\t: %v\n", material) + //} + //log.Printf("Meshes: %d\n", len(model.Meshes)) + //log.Printf("Objects: %+v\n", model.Objects) + //log.Printf("Animations: %d\n", len(model.Animations)) + //for _, animation := range model.Animations { + // log.Printf("\t: %+v\n", animation) + //} + //log.Printf("Triangles: %d\n", len(model.Triangles)) + //log.Printf("Texture Coordinates: %d\n", len(model.TextureCoordinates)) + //log.Printf("Points: %d\n", len(model.Points)) + //log.Printf("Brightness: %d\n", len(model.Brightness)) + + logFile, err := os.OpenFile(fmt.Sprintf("./%s.json", inputModel), os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Println(err) + return + } + defer logFile.Close() + enc := json.NewEncoder(logFile) + enc.SetIndent("", " ") + if err = enc.Encode(model); err != nil { + log.Println(err) + return + } + + export := exporter.NewExporter( + exporter.WithTexturesBasePath(texturesPath), + exporter.WithOnlyBaseMesh(), + exporter.WithAnimation(), + ) + if err = export.Export(outputPath, &model); err != nil { + log.Println(err) + return + } + + //if err = export(&model, inputModel, texturesCache); err != nil { + // log.Println(err) + // return + //} +} + +func exportFn(model *threedb.Model, inputModel string, cache map[string]string) error { + //maxMeshIndex := 133 + //meshIndex := randRange(0, len(model.Meshes)-1) + meshIndex := 0 + //log.Println("Mesh Index:", meshIndex) + binaryCache := make(map[string][]byte) + + //for meshIndex := 0; meshIndex < len(model.Meshes); meshIndex++ { + doc := gltf.NewDocument() + //doc.Materials = []*gltf.Material{{ + // Name: "Default", AlphaMode: gltf.AlphaOpaque, AlphaCutoff: gltf.Float(0.5), + // PBRMetallicRoughness: &gltf.PBRMetallicRoughness{BaseColorFactor: &[4]float64{0.8, 0.8, 0.8, 1}, MetallicFactor: gltf.Float(0.1), RoughnessFactor: gltf.Float(0.99)}, + //}} + for _, material := range model.Materials { + data, ok := binaryCache[material.Name] + if !ok { + texturePath, ok := cache[material.Name] + if !ok { + log.Printf("Invalid texture cache %q", material.Name) + continue + return fmt.Errorf("invalid texture cache %q", material.Name) + } + data, err := os.ReadFile(texturePath) + if err != nil { + return err + } + binaryCache[material.Name] = data + } + mt := mimetype.Detect(data) + if mt == nil { + return fmt.Errorf("can't geting mimetype %q", material.Name) + } + imageIndex, err := modeler.WriteImage(doc, material.Name+".tga", mt.String(), bytes.NewBuffer(data)) + if err != nil { + return err + } + doc.Textures = append(doc.Textures, &gltf.Texture{ + Name: material.Name, + //Sampler: gltf.Index(0), + Source: gltf.Index(imageIndex), + }) + doc.Materials = append(doc.Materials, &gltf.Material{ + Name: material.Name, + AlphaMode: gltf.AlphaOpaque, + AlphaCutoff: gltf.Float(0.5), + PBRMetallicRoughness: &gltf.PBRMetallicRoughness{ + BaseColorTexture: &gltf.TextureInfo{Index: imageIndex}, + BaseColorFactor: &[4]float64{0.8, 0.8, 0.8, 1}, + MetallicFactor: gltf.Float(0.1), + RoughnessFactor: gltf.Float(0.99), + }, + }) + } + mesh := model.Meshes[meshIndex] + for _, meshLink := range mesh.Links { + triangles := model.Triangles[meshLink.Triangles] + points := model.Points[meshLink.Points] + textureCoordinates := model.TextureCoordinates[meshLink.TextureCoordinates] + material := model.Materials[meshLink.Material] + var vertices [][3]float32 + var mins []float64 + var maxs []float64 + for _, point := range points { + vec := point.Transform(100) + vec = vec.Normalize() + vertices = append(vertices, vec) + mins = append(mins, float64(vec.Min())) + maxs = append(maxs, float64(vec.Max())) + } + verticesIndex := modeler.WriteAccessor(doc, gltf.TargetElementArrayBuffer, vertices) + var textureCoords [][2]float32 + for _, txc := range textureCoordinates { + textureCoords = append(textureCoords, txc) + } + textureIndex := modeler.WriteAccessor(doc, gltf.TargetArrayBuffer, textureCoords) + trianglesIndex := modeler.WriteAccessor(doc, gltf.TargetArrayBuffer, triangles) + materialIndex := len(doc.Materials) + doc.Meshes = append(doc.Meshes, &gltf.Mesh{ + Name: fmt.Sprintf("%s-%d", model.Name, len(doc.Meshes)), + Primitives: []*gltf.Primitive{{ + Attributes: gltf.PrimitiveAttributes{ + gltf.POSITION: verticesIndex, + gltf.TEXCOORD_0: textureIndex, + }, + Indices: gltf.Index(trianglesIndex), + Material: gltf.Index(materialIndex), + }}, + }) + + //imageIndex, err := modeler.WriteImage(doc) + doc.Materials = append(doc.Materials, &gltf.Material{ + Name: material.Name, + AlphaMode: gltf.AlphaOpaque, + AlphaCutoff: gltf.Float(0.5), + PBRMetallicRoughness: &gltf.PBRMetallicRoughness{ + //BaseColorTexture: &gltf.TextureInfo{Index: imageIndex}, + BaseColorFactor: &[4]float64{0.8, 0.8, 0.8, 1}, + MetallicFactor: gltf.Float(0.1), + RoughnessFactor: gltf.Float(0.99), + }, + }) + doc.Nodes = append(doc.Nodes, &gltf.Node{ + Mesh: gltf.Index(len(doc.Meshes) - 1), + }) + } + var nodes []int + for index := range doc.Nodes { + nodes = append(nodes, index) + } + + doc.Scenes = []*gltf.Scene{ + {Name: "Root Scene", Nodes: nodes}, + } + if err := gltf.Save(doc, path.Join(outputPath, fmt.Sprintf("%s-%d.gltf", inputModel, meshIndex))); err != nil { + return err + } + //if err := gltf.SaveBinary(doc, fmt.Sprintf("./%s.glb", inputModel)); err != nil { + // return err + //} + //} + + return nil +} + +func randRange(min, max int) int { + return rand.IntN(max-min) + min +} diff --git a/cmd/testgltf/main.go b/cmd/testgltf/main.go new file mode 100644 index 0000000..ed3654c --- /dev/null +++ b/cmd/testgltf/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "log" + + "github.com/qmuntal/gltf" +) + +func main() { + doc := &gltf.Document{ + Accessors: []*gltf.Accessor{ + {BufferView: gltf.Index(0), ComponentType: gltf.ComponentUshort, Count: 36, Type: gltf.AccessorScalar}, + {BufferView: gltf.Index(1), ComponentType: gltf.ComponentFloat, Count: 24, Max: []float64{0.5, 0.5, 0.5}, Min: []float64{-0.5, -0.5, -0.5}, Type: gltf.AccessorVec3}, + {BufferView: gltf.Index(2), ComponentType: gltf.ComponentFloat, Count: 24, Type: gltf.AccessorVec3}, + {BufferView: gltf.Index(3), ComponentType: gltf.ComponentFloat, Count: 24, Type: gltf.AccessorVec4}, + {BufferView: gltf.Index(4), ComponentType: gltf.ComponentFloat, Count: 24, Type: gltf.AccessorVec2}, + }, + Asset: gltf.Asset{Version: "2.0", Generator: "FBX2glTF"}, + BufferViews: []*gltf.BufferView{ + {Buffer: 0, ByteLength: 72, ByteOffset: 0, Target: gltf.TargetElementArrayBuffer}, + {Buffer: 0, ByteLength: 288, ByteOffset: 72, Target: gltf.TargetArrayBuffer}, + {Buffer: 0, ByteLength: 288, ByteOffset: 360, Target: gltf.TargetArrayBuffer}, + {Buffer: 0, ByteLength: 384, ByteOffset: 648, Target: gltf.TargetArrayBuffer}, + {Buffer: 0, ByteLength: 192, ByteOffset: 1032, Target: gltf.TargetArrayBuffer}, + }, + Buffers: []*gltf.Buffer{{ByteLength: 1224, Data: []byte{97, 110, 121, 32, 99, 97, 114, 110, 97, 108, 32, 112, 108, 101, 97, 115}}}, + Materials: []*gltf.Material{{ + Name: "Default", AlphaMode: gltf.AlphaOpaque, AlphaCutoff: gltf.Float(0.5), + PBRMetallicRoughness: &gltf.PBRMetallicRoughness{BaseColorFactor: &[4]float64{0.8, 0.8, 0.8, 1}, MetallicFactor: gltf.Float(0.1), RoughnessFactor: gltf.Float(0.99)}, + }}, + Meshes: []*gltf.Mesh{{Name: "Cube", Primitives: []*gltf.Primitive{{Indices: gltf.Index(0), Material: gltf.Index(0), Mode: gltf.PrimitiveTriangles, Attributes: gltf.PrimitiveAttributes{gltf.POSITION: 1, gltf.COLOR_0: 3, gltf.NORMAL: 2, gltf.TEXCOORD_0: 4}}}}}, + Nodes: []*gltf.Node{ + {Name: "RootNode", Children: []int{1, 2, 3}}, + {Name: "Mesh"}, + {Name: "Cube", Mesh: gltf.Index(0)}, + {Name: "Texture Group"}, + }, + Samplers: []*gltf.Sampler{{WrapS: gltf.WrapRepeat, WrapT: gltf.WrapRepeat}}, + Scene: gltf.Index(0), + Scenes: []*gltf.Scene{{Name: "Root Scene", Nodes: []int{0}}}, + } + if err := gltf.Save(doc, "./test.gltf"); err != nil { + log.Println(err) + } +} diff --git a/go.mod b/go.mod index 01a4b13..d4b0cbf 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,15 @@ module git.influ.su/artmares/digglestool go 1.22.3 + +require github.com/stretchr/testify v1.9.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/ftrvxmtrx/tga v0.0.0-20150524081124-bd8e8d5be13a // indirect + github.com/gabriel-vasile/mimetype v1.4.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/qmuntal/gltf v0.26.1-0.20240704075444-782e57e021e1 // indirect + golang.org/x/net v0.25.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5485900 --- /dev/null +++ b/go.sum @@ -0,0 +1,20 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ftrvxmtrx/tga v0.0.0-20150524081124-bd8e8d5be13a h1:eSqaRmdlZ9JsJ7JuWfDr3ym3monToXRczohBOL+heVQ= +github.com/ftrvxmtrx/tga v0.0.0-20150524081124-bd8e8d5be13a/go.mod h1:US5WvgEHtG+BvWNNs6gk937h0QL2g2x+r7RH8m3g80Y= +github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= +github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/qmuntal/gltf v0.26.0 h1:geVxZPBJ43f0ouLyseqMuzgKG6m6DkNH+gI3A63PFas= +github.com/qmuntal/gltf v0.26.0/go.mod h1:YoXZOt0Nc0kIfSKOLZIRoV4FycdC+GzE+3JgiAGYoMs= +github.com/qmuntal/gltf v0.26.1-0.20240704075444-782e57e021e1 h1:E2QnY6J3DP8tSyAE28BiGcbPFpbIDDtvq3zB6KyBVSI= +github.com/qmuntal/gltf v0.26.1-0.20240704075444-782e57e021e1/go.mod h1:YoXZOt0Nc0kIfSKOLZIRoV4FycdC+GzE+3JgiAGYoMs= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/pkg/helpers/buffer.go b/internal/pkg/helpers/buffer.go new file mode 100644 index 0000000..a3626ef --- /dev/null +++ b/internal/pkg/helpers/buffer.go @@ -0,0 +1,21 @@ +package helpers + +import ( + "bytes" + "sync" +) + +var pool sync.Pool + +func NewBuffer() (*bytes.Buffer, func(*bytes.Buffer)) { + if v := pool.Get(); v != nil { + b, _ := v.(*bytes.Buffer) + b.Reset() + return b, putter + } + return &bytes.Buffer{}, putter +} + +func putter(buff *bytes.Buffer) { + pool.Put(buff) +} diff --git a/internal/services/exporter/cache.go b/internal/services/exporter/cache.go new file mode 100644 index 0000000..900187e --- /dev/null +++ b/internal/services/exporter/cache.go @@ -0,0 +1,41 @@ +package exporter + +import ( + "bytes" + "strings" + "sync" +) + +type Cache[T any] struct { + data map[string]T + mutex sync.RWMutex +} + +func NewCache[T any]() *Cache[T] { + return &Cache[T]{ + data: make(map[string]T), + } +} + +func (c *Cache[T]) Set(key string, value T) { + key = strings.ToLower(key) + c.mutex.Lock() + defer c.mutex.Unlock() + + c.data[key] = value +} + +func (c *Cache[T]) Get(key string) (T, bool) { + key = strings.ToLower(key) + c.mutex.RLock() + defer c.mutex.RUnlock() + + value, ok := c.data[key] + return value, ok +} + +type CacheItem struct { + Path string + Size int64 + Bytes *bytes.Buffer +} diff --git a/internal/services/exporter/exporter.go b/internal/services/exporter/exporter.go new file mode 100644 index 0000000..014f3f7 --- /dev/null +++ b/internal/services/exporter/exporter.go @@ -0,0 +1,229 @@ +package exporter + +import ( + "errors" + "fmt" + _ "image/png" + "log" + "os" + "path" + "path/filepath" + "strings" + + _ "github.com/ftrvxmtrx/tga" + "github.com/qmuntal/gltf" + + "git.influ.su/artmares/digglestool/pkg/threedb" +) + +type Exporter struct { + onlyBaseMesh bool + animation bool + texturesBasePath *string + cache *Cache[*CacheItem] +} + +type Option func(*Exporter) + +func WithOnlyBaseMesh() Option { + return func(e *Exporter) { + e.onlyBaseMesh = true + } +} + +func WithTexturesBasePath(basePath string) Option { + return func(e *Exporter) { + if basePath != "" { + e.texturesBasePath = &basePath + } + } +} + +func WithAnimation() Option { + return func(e *Exporter) { + e.animation = true + } +} + +func NewExporter(options ...Option) *Exporter { + e := &Exporter{ + cache: NewCache[*CacheItem](), + } + for _, option := range options { + option(e) + } + return e +} + +func (e *Exporter) Export(basePath string, model *threedb.Model) error { + if model == nil { + return errors.New("model is nil") + } + meshesLen := len(model.Meshes) + if meshesLen == 0 { + return errors.New("no meshes to export") + } + if err := e.prepareTextures(); err != nil { + return err + } + if e.onlyBaseMesh { + log.Printf("Export Base Model: %s", model.Name) + doc, err := e.generate(model, 0) + if err != nil { + return err + } + if err = gltf.Save(doc, path.Join(basePath, fmt.Sprintf("%s-base-model.gltf", model.Name))); err != nil { + return err + } + } + if e.animation { + dirPath := path.Join(basePath, "animation", model.Name) + if _, err := os.Stat(dirPath); os.IsNotExist(err) { + if err = os.MkdirAll(dirPath, os.ModeDir); err != nil { + return err + } + } + log.Printf("Export Animation to %s\n", dirPath) + length := len(model.Animations) + for index, animation := range model.Animations { + log.Printf("Export progress: %d/%d\r", index+1, length) + indexes := make([]int, len(animation.MeshIndexes)) + for _, index := range animation.MeshIndexes { + indexes = append(indexes, int(index)) + } + doc, err := e.generate(model, indexes...) + if err != nil { + return err + } + if err = gltf.Save(doc, path.Join(dirPath, fmt.Sprintf("%s-%s.gltf", model.Name, animation.Name))); err != nil { + return err + } + } + } + + return nil +} + +func (e *Exporter) generate(model *threedb.Model, meshIndexes ...int) (*gltf.Document, error) { + return new(Model).Generate(model, e.cache, meshIndexes...) + //var err error + //doc := gltf.NewDocument() + //log.Println("Generate", meshIndexes) + //materialsMap := make(map[uint16]int) + //for _, meshIndex := range meshIndexes { + // mesh := model.Meshes[meshIndex] + // var nodeIndexes []int + // for index, meshLink := range mesh.Links { + // points := model.Points[meshLink.Points] + // var vertices [][3]float32 + // for _, point := range points { + // vec := point.Transform(100) + // vec = vec.Normalize() + // vertices = append(vertices, vec) + // } + // verticesIndex := modeler.WriteAccessor(doc, gltf.TargetElementArrayBuffer, vertices) + // textureCoordinates := model.TextureCoordinates[meshLink.TextureCoordinates] + // var textureCoord [][2]float32 + // for _, txc := range textureCoordinates { + // textureCoord = append(textureCoord, txc) + // } + // textureCoordIndex := modeler.WriteAccessor(doc, gltf.TargetArrayBuffer, textureCoord) + // trianglesIndex := modeler.WriteAccessor(doc, gltf.TargetArrayBuffer, model.Triangles[meshLink.Triangles]) + // materialIndex, ok := materialsMap[meshLink.Material] + // if !ok { + // material := model.Materials[meshLink.Material] + // log.Println("Material", material.Name) + // data := e.cache.Bytes(material.Name) + // if data == nil { + // var f *os.File + // if f, err = os.Open(e.cache.Path(material.Name)); err != nil { + // return nil, err + // } + // img, _, err := image.Decode(f) + // if err != nil { + // return nil, err + // } + // buff, putter := helpers.NewBuffer() + // if err = png.Encode(buff, img); err != nil { + // return nil, err + // } + // data = buff.Bytes() + // log.Println(material.Name, len(data)) + // putter(buff) + // e.cache.AddBytes(material.Name, data) + // } + // log.Println(material.Name, len(data)) + // var imageIndex int + // if imageIndex, err = modeler.WriteImage(doc, material.Name, "image/png", bytes.NewBuffer(data)); err != nil { + // return nil, err + // } + // doc.Textures = append(doc.Textures, &gltf.Texture{ + // Name: material.Name, + // Sampler: gltf.Index(0), + // Source: gltf.Index(imageIndex), + // }) + // materialIndex = len(doc.Materials) + // doc.Materials = append(doc.Materials, &gltf.Material{ + // Name: material.Name, + // AlphaMode: gltf.AlphaOpaque, + // AlphaCutoff: gltf.Float(0.5), + // PBRMetallicRoughness: &gltf.PBRMetallicRoughness{ + // BaseColorTexture: &gltf.TextureInfo{Index: imageIndex}, + // BaseColorFactor: &[4]float64{0.8, 0.8, 0.8, 1}, + // MetallicFactor: gltf.Float(0.1), + // RoughnessFactor: gltf.Float(0.99), + // }, + // }) + // materialsMap[meshLink.Material] = materialIndex + // } + // log.Println("materialIndex", materialIndex) + // doc.Meshes = append(doc.Meshes, &gltf.Mesh{ + // Name: fmt.Sprintf("%s-%d", model.Name, index), + // Primitives: []*gltf.Primitive{{ + // Attributes: gltf.PrimitiveAttributes{ + // gltf.POSITION: verticesIndex, + // gltf.TEXCOORD_0: textureCoordIndex, + // }, + // Indices: gltf.Index(trianglesIndex), + // Material: gltf.Index(materialIndex), + // }}, + // }) + // nodeIndexes = append(nodeIndexes, len(doc.Nodes)) + // doc.Nodes = append(doc.Nodes, &gltf.Node{ + // Mesh: gltf.Index(len(doc.Meshes) - 1), + // }) + // } + // doc.Scenes = []*gltf.Scene{{ + // Name: "Root Scene", + // Nodes: nodeIndexes, + // }} + // doc.Samplers = []*gltf.Sampler{{}} + //} + //return doc, nil +} + +func (e *Exporter) prepareTextures() error { + if e.texturesBasePath == nil { + return nil + } + if err := filepath.Walk(*e.texturesBasePath, func(pathFile string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if strings.HasSuffix(info.Name(), ".tga") { + key := strings.TrimSuffix(info.Name(), ".tga") + item, ok := e.cache.Get(key) + if !ok { + item = &CacheItem{} + } + if info.Size() > item.Size { + item.Path = pathFile + } + e.cache.Set(key, item) + } + return nil + }); err != nil { + return err + } + return nil +} diff --git a/internal/services/exporter/model.go b/internal/services/exporter/model.go new file mode 100644 index 0000000..51ba53d --- /dev/null +++ b/internal/services/exporter/model.go @@ -0,0 +1,141 @@ +package exporter + +import ( + "bytes" + "fmt" + "image" + "image/png" + "log" + "os" + + _ "github.com/ftrvxmtrx/tga" + "github.com/qmuntal/gltf" + "github.com/qmuntal/gltf/modeler" + + "git.influ.su/artmares/digglestool/pkg/threedb" +) + +type Model struct { + doc *gltf.Document + materialMap map[uint16]int +} + +func (m *Model) Generate(model *threedb.Model, cache *Cache[*CacheItem], meshIndexes ...int) (*gltf.Document, error) { + m.doc = gltf.NewDocument() + m.doc.Scenes = []*gltf.Scene{} + m.materialMap = make(map[uint16]int) + log.Println("Generate use mesh indexes", len(meshIndexes)) + for i, meshIndex := range meshIndexes { + mesh := model.Meshes[meshIndex] + var nodeIndexes []int + for index, link := range mesh.Links { + materialIndex, err := m.materialIndex(model, cache, link.Material) + if err != nil { + return nil, err + } + verticesIndex := m.generateVertices(model.Points[link.Points]) + textureCoordIndex := m.generateTextureCoordinate(model.TextureCoordinates[link.TextureCoordinates]) + trianglesIndex := modeler.WriteAccessor(m.doc, gltf.TargetArrayBuffer, model.Triangles[link.Triangles]) + m.doc.Meshes = append(m.doc.Meshes, &gltf.Mesh{ + Name: fmt.Sprintf("%s-%d-%d", model.Name, i, index), + Primitives: []*gltf.Primitive{{ + Attributes: gltf.PrimitiveAttributes{ + gltf.POSITION: verticesIndex, + gltf.TEXCOORD_0: textureCoordIndex, + }, + Indices: gltf.Index(trianglesIndex), + Material: gltf.Index(materialIndex), + }}, + }) + nodeIndexes = append(nodeIndexes, len(m.doc.Nodes)) + m.doc.Nodes = append(m.doc.Nodes, &gltf.Node{ + Mesh: gltf.Index(len(m.doc.Meshes) - 1), + }) + } + m.doc.Scenes = append(m.doc.Scenes, &gltf.Scene{ + Name: fmt.Sprintf("Root Scene-%d", meshIndex), + Nodes: nodeIndexes, + }) + } + m.doc.Samplers = []*gltf.Sampler{{}} + return m.doc, nil +} + +func (m *Model) generateVertices(vectors []threedb.Vector) int { + var vertices [][3]float32 + for _, vector := range vectors { + vector = vector.Transform(100) + vertices = append(vertices, vector.Normalize()) + } + + return modeler.WriteAccessor(m.doc, gltf.TargetElementArrayBuffer, vertices) +} + +func (m *Model) generateTextureCoordinate(coordinates []threedb.Coordinate) int { + var coords [][2]float32 + for _, coordinate := range coordinates { + coords = append(coords, coordinate) + } + return modeler.WriteAccessor(m.doc, gltf.TargetArrayBuffer, coords) +} + +func (m *Model) materialIndex(model *threedb.Model, cache *Cache[*CacheItem], meshMaterialIndex uint16) (int, error) { + //logger, logPutter := helpers.NewBuffer() + //defer func() { + // logPutter(logger) + //}() + materialIndex, ok := m.materialMap[meshMaterialIndex] + //logger.WriteString(fmt.Sprintf("Try find material index %d\n", meshMaterialIndex)) + if !ok { + //logger.WriteString(fmt.Sprintf("Material index %d not found\n", meshMaterialIndex)) + material := model.Materials[meshMaterialIndex] + item, ok := cache.Get(material.Name) + if !ok { + return 0, nil + } + //logger.WriteString(fmt.Sprintf("Try found material name %s\n", material.Name)) + if item.Bytes == nil { + //logger.WriteString(fmt.Sprintf("Material name %s not found in cache\n", material.Name)) + f, err := os.Open(item.Path) + if err != nil { + return 0, err + } + defer f.Close() + img, _, err := image.Decode(f) + if err != nil { + return 0, err + } + item.Bytes = &bytes.Buffer{} + if err = png.Encode(item.Bytes, img); err != nil { + return 0, err + } + cache.Set(material.Name, item) + } + //log.Printf("material index: %d, material name: %s, data: %d", meshMaterialIndex, material.Name, len(data)) + imageIndex, err := modeler.WriteImage(m.doc, material.Name, "image/png", item.Bytes) + if err != nil { + return 0, err + } + m.doc.Textures = append(m.doc.Textures, &gltf.Texture{ + Name: material.Name, + Sampler: gltf.Index(0), + Source: gltf.Index(imageIndex), + }) + materialIndex = len(m.doc.Materials) + m.doc.Materials = append(m.doc.Materials, &gltf.Material{ + Name: material.Name, + AlphaMode: gltf.AlphaOpaque, + AlphaCutoff: gltf.Float(0.5), + PBRMetallicRoughness: &gltf.PBRMetallicRoughness{ + BaseColorTexture: &gltf.TextureInfo{Index: imageIndex}, + BaseColorFactor: &[4]float64{0.8, 0.8, 0.8, 1}, + MetallicFactor: gltf.Float(0.1), + RoughnessFactor: gltf.Float(0.99), + }, + }) + m.materialMap[meshMaterialIndex] = materialIndex + } + //log.Println(logger.String()) + + return materialIndex, nil +} diff --git a/models/.keep b/models/.keep new file mode 100644 index 0000000..e69de29 diff --git a/pkg/threedb/animation.go b/pkg/threedb/animation.go new file mode 100644 index 0000000..21216fe --- /dev/null +++ b/pkg/threedb/animation.go @@ -0,0 +1,11 @@ +package threedb + +type Animation struct { + Name string + MeshIndexes []uint32 + Unknown, Unknown1, Unknown2 uint16 + //Unknown1 float32 + Unknown3 string + MoveVector *Vector + RotationVector *Vector +} diff --git a/pkg/threedb/coordinate.go b/pkg/threedb/coordinate.go new file mode 100644 index 0000000..52accbb --- /dev/null +++ b/pkg/threedb/coordinate.go @@ -0,0 +1,12 @@ +package threedb + +import "fmt" + +type Coordinate [2]float32 + +func (m Coordinate) X() float32 { return m[0] } +func (m Coordinate) Y() float32 { return m[1] } +func (m *Coordinate) Set(x, y float32) { m[0], m[1] = x, y } +func (m *Coordinate) SetX(x float32) { m[0] = x } +func (m *Coordinate) SetY(y float32) { m[1] = y } +func (m Coordinate) String() string { return fmt.Sprintf("x:%f,y:%f", m[0], m[1]) } diff --git a/pkg/threedb/coordinate_test.go b/pkg/threedb/coordinate_test.go new file mode 100644 index 0000000..fd22666 --- /dev/null +++ b/pkg/threedb/coordinate_test.go @@ -0,0 +1,14 @@ +package threedb + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCoordinate_Set(t *testing.T) { + expect := Coordinate{1, 1} + coord := Coordinate{} + coord.Set(1, 1) + require.Equal(t, expect, coord) +} diff --git a/pkg/threedb/decoder.go b/pkg/threedb/decoder.go new file mode 100644 index 0000000..04cbcbb --- /dev/null +++ b/pkg/threedb/decoder.go @@ -0,0 +1,490 @@ +package threedb + +import ( + "encoding/binary" + "errors" + "io" + "log" +) + +type Decoder struct { + reader io.ReadSeeker +} + +func NewDecoder(reader io.ReadSeeker) *Decoder { + return &Decoder{reader: reader} +} + +func (dec *Decoder) Decode(model *Model) error { + if model == nil { + return errors.New("model is nil") + } + var err error + if model.DBVersion, err = dec.readString(); err != nil { + return err + } + if model.Name, err = dec.readString(); err != nil { + return err + } + if err = dec.readMaterials(model); err != nil { + return err + } + if err = dec.readMeshes(model); err != nil { + return err + } + if err = dec.readObjects(model); err != nil { + return err + } + if err = dec.readAnimations(model); err != nil { + return err + } + if err = dec.readShadows(model); err != nil { + return err + } + if err = dec.readCubeMaps(model); err != nil { + return err + } + if err = dec.readData(model); err != nil { + return err + } + + return nil +} + +func (dec *Decoder) seek(offset int64) (err error) { + _, err = dec.reader.Seek(offset, io.SeekCurrent) + return +} + +func (dec *Decoder) read(dst any) error { + return binary.Read(dec.reader, binary.LittleEndian, dst) +} + +func (dec *Decoder) readUInt8() (result uint8, err error) { + err = dec.read(&result) + return +} + +func (dec *Decoder) readUInt16() (result uint16, err error) { + err = dec.read(&result) + return +} + +func (dec *Decoder) readUInt32() (result uint32, err error) { + err = dec.read(&result) + return +} + +func (dec *Decoder) readFloat32() (result float32, err error) { + err = dec.read(&result) + return +} + +func (dec *Decoder) readString() (string, error) { + length, err := dec.readUInt32() + if err != nil { + return "", err + } + buf := make([]byte, length) + if err = dec.read(&buf); err != nil { + return "", err + } + + return string(buf), nil +} + +func (dec *Decoder) readVector() (*Vector, error) { + var vec Vector + var err error + if vec[0], err = dec.readFloat32(); err != nil { + return nil, err + } + if vec[1], err = dec.readFloat32(); err != nil { + return nil, err + } + if vec[2], err = dec.readFloat32(); err != nil { + return nil, err + } + return &vec, nil +} + +func (dec *Decoder) readMaterials(model *Model) error { + materialCount, err := dec.readUInt16() + if err != nil { + return err + } + count := int(materialCount) + for i := 0; i < count; i++ { + material := Material{} + if material.Name, err = dec.readString(); err != nil { + return err + } + if material.Path, err = dec.readString(); err != nil { + return err + } + if material.Unknown, err = dec.readUInt32(); err != nil { + return err + } + model.Materials = append(model.Materials, material) + } + + return nil +} + +func (dec *Decoder) readMeshes(model *Model) error { + meshCount, err := dec.readUInt32() + if err != nil { + return err + } + count := int(meshCount) + for i := 0; i < count; i++ { + mesh := Mesh{} + if err = dec.readMeshLink(&mesh); err != nil { + return err + } + if mesh.Vector1, err = dec.readVector(); err != nil { + return err + } + if mesh.Vector2, err = dec.readVector(); err != nil { + return err + } + _ = dec.seek(0x80) + if mesh.Shadow, err = dec.readUInt16(); err != nil { + return err + } // Mesh Shadow Index ? + _ = dec.seek(0x30) + if mesh.CMap, err = dec.readUInt16(); err != nil { + return err + } // Mesh CMap Index ? + model.Meshes = append(model.Meshes, mesh) + } + return nil +} + +func (dec *Decoder) readMeshLink(mesh *Mesh) error { + meshLinkCount, err := dec.readUInt16() + if err != nil { + return err + } + count := int(meshLinkCount) + for i := 0; i < count; i++ { + meshLink := MeshLink{} + if meshLink.Material, err = dec.readUInt16(); err != nil { + return err + } + if meshLink.Unknown, err = dec.readUInt16(); err != nil { + return err + } + if meshLink.Triangles, err = dec.readUInt16(); err != nil { + return err + } + if meshLink.TextureCoordinates, err = dec.readUInt16(); err != nil { + return err + } + if meshLink.Points, err = dec.readUInt16(); err != nil { + return err + } + if meshLink.Brightness, err = dec.readUInt16(); err != nil { + return err + } + mesh.Links = append(mesh.Links, meshLink) + } + return nil +} + +func (dec *Decoder) readObjects(model *Model) error { + keyValuePairCount, err := dec.readUInt16() + if err != nil { + return err + } + count := int(keyValuePairCount) + if count > 0 && model.Objects == nil { + model.Objects = make(map[string][]uint32) + } + for i := 0; i < count; i++ { + var key string + if key, err = dec.readString(); err != nil { + return err + } + var objectCount uint16 + if objectCount, err = dec.readUInt16(); err != nil { + return err + } + model.Objects[key] = make([]uint32, objectCount) + for j := 0; j < int(objectCount); j++ { + var n uint32 + if n, err = dec.readUInt32(); err != nil { + return err + } + model.Objects[key] = append(model.Objects[key], n) + } + } + return nil +} + +func (dec *Decoder) readAnimations(model *Model) error { + animationCount, err := dec.readUInt16() + if err != nil { + return err + } + count := int(animationCount) + for i := 0; i < count; i++ { + animation := Animation{} + if animation.Name, err = dec.readString(); err != nil { + return err + } + + var meshIndexesCount uint16 + if meshIndexesCount, err = dec.readUInt16(); err != nil { + return err + } + animation.MeshIndexes = make([]uint32, meshIndexesCount) + for j := 0; j < int(meshIndexesCount); j++ { + var n uint32 + if n, err = dec.readUInt32(); err != nil { + return err + } + animation.MeshIndexes[j] = n + } + if animation.Unknown, err = dec.readUInt16(); err != nil { + return err + } + if animation.Unknown1, err = dec.readUInt16(); err != nil { + return err + } + if animation.Unknown2, err = dec.readUInt16(); err != nil { + return err + } + //if animation.Unknown1, err = dec.readFloat32(); err != nil { + // return err + //} + if animation.Unknown3, err = dec.readString(); err != nil { + return err + } + if animation.MoveVector, err = dec.readVector(); err != nil { + return err + } + if animation.RotationVector, err = dec.readVector(); err != nil { + return err + } + model.Animations = append(model.Animations, animation) + } + return nil +} + +func (dec *Decoder) readShadows(_ *Model) error { + shadowCount, err := dec.readUInt16() + if err != nil { + return err + } + count := int(shadowCount) + for i := 0; i < count; i++ { + // Skip // TODO: возможно это картинка 32х32 + _ = dec.seek(32 * 32) + } + return nil +} + +func (dec *Decoder) readCubeMaps(_ *Model) error { + cubeMapCount, err := dec.readUInt16() + if err != nil { + return err + } + count := int(cubeMapCount) + for i := 0; i < count; i++ { + var width, height uint16 + if width, err = dec.readUInt16(); err != nil { + return err + } + if height, err = dec.readUInt16(); err != nil { + return err + } + _, _ = dec.readUInt16() + _, _ = dec.readUInt16() + // Skip pixel data + _ = dec.seek(int64(width * height)) + } + return nil +} + +func (dec *Decoder) readData(model *Model) error { + var ( + triangleCount uint16 + trianglesCounts []uint16 + textureCoordCount uint16 + textureCoordCounts []uint16 + pointCount uint16 + pointCounts []uint16 + brightnessCount uint16 + brightnessCounts []uint16 + unknownCount uint32 + + cnt uint16 + err error + ) + if triangleCount, err = dec.readUInt16(); err != nil { + return err + } + if textureCoordCount, err = dec.readUInt16(); err != nil { + return err + } + if pointCount, err = dec.readUInt16(); err != nil { + return err + } + if brightnessCount, err = dec.readUInt16(); err != nil { + return err + } + if unknownCount, err = dec.readUInt32(); err != nil { + return err + } + log.Println("unknownCount:", unknownCount) + for i := 0; i < int(triangleCount); i++ { + if cnt, err = dec.readUInt16(); err != nil { + return err + } + trianglesCounts = append(trianglesCounts, cnt) + } + for i := 0; i < int(textureCoordCount); i++ { + if cnt, err = dec.readUInt16(); err != nil { + return err + } + textureCoordCounts = append(textureCoordCounts, cnt) + } + for i := 0; i < int(pointCount); i++ { + if cnt, err = dec.readUInt16(); err != nil { + return err + } + pointCounts = append(pointCounts, cnt) + } + for i := 0; i < int(brightnessCount); i++ { + if cnt, err = dec.readUInt16(); err != nil { + return err + } + brightnessCounts = append(brightnessCounts, cnt) + } + for i := 0; i < int(unknownCount); i++ { + //x, err := dec.readFloat32() + //if err != nil { + // return err + //} + //y, err := dec.readFloat32() + //if err != nil { + // return err + //} + //vec, err := dec.readVector() + //if err != nil { + // return err + //} + //log.Println(Coordinate{x, y}, vec) + //if cnt, err = dec.readUInt16(); err != nil { + // return err + //} + //log.Println("cnt", cnt) + //buff := make([]byte, 20) + //err = dec.read(&buff) + //if err != nil { + // return err + //} + //log.Println(buff) + //vec, err := dec.readVector() + //if err != nil { + // return err + //} + //log.Println(vec) + _ = dec.seek(20) + } + if err = dec.readTriangles(model, int(triangleCount), trianglesCounts); err != nil { + return err + } + if err = dec.readTextureCoordinates(model, int(textureCoordCount), textureCoordCounts); err != nil { + return err + } + if err = dec.readPoint(model, int(pointCount), pointCounts); err != nil { + return err + } + if err = dec.readBrightness(model, int(brightnessCount), brightnessCounts); err != nil { + return err + } + + return nil +} + +func (dec *Decoder) readTriangles(model *Model, count int, counts []uint16) error { + for i := 0; i < count; i++ { + cnt := int(counts[i]) + var triangles []uint16 + for j := 0; j < cnt; j++ { + n, err := dec.readUInt16() + if err != nil { + return err + } + triangles = append(triangles, n) + } + model.Triangles = append(model.Triangles, triangles) + } + return nil +} + +func (dec *Decoder) readTextureCoordinates(model *Model, count int, counts []uint16) error { + for i := 0; i < count; i++ { + cnt := int(counts[i]) + var cords []Coordinate + for j := 0; j < cnt; j++ { + cord := Coordinate{} + u, err := dec.readFloat32() + if err != nil { + return err + } + v, err := dec.readFloat32() + if err != nil { + return err + } + cord.Set(u, v) + cords = append(cords, cord) + } + model.TextureCoordinates = append(model.TextureCoordinates, cords) + } + return nil +} + +func (dec *Decoder) readPoint(model *Model, count int, counts []uint16) error { + for i := 0; i < count; i++ { + cnt := int(counts[i]) + var vectors []Vector + for j := 0; j < cnt; j++ { + vec := Vector{} + ux, err := dec.readUInt16() + if err != nil { + return err + } + uy, err := dec.readUInt16() + if err != nil { + return err + } + uz, err := dec.readUInt16() + if err != nil { + return err + } + vec.Set(float32(ux)/float32(0xffff), float32(uy)/float32(0xffff), float32(uz)/float32(0xffff)) + vectors = append(vectors, vec) + } + model.Points = append(model.Points, vectors) + } + return nil +} + +func (dec *Decoder) readBrightness(model *Model, count int, counts []uint16) error { + for i := 0; i < count; i++ { + cnt := int(counts[i]) + var brightness []byte + for j := 0; j < cnt; j++ { + b, err := dec.readUInt8() + if err != nil { + return err + } + brightness = append(brightness, b) + } + model.Brightness = append(model.Brightness, brightness) + } + return nil +} diff --git a/pkg/threedb/material.go b/pkg/threedb/material.go new file mode 100644 index 0000000..434d0da --- /dev/null +++ b/pkg/threedb/material.go @@ -0,0 +1,6 @@ +package threedb + +type Material struct { + Name, Path string + Unknown uint32 +} diff --git a/pkg/threedb/mesh.go b/pkg/threedb/mesh.go new file mode 100644 index 0000000..389fb26 --- /dev/null +++ b/pkg/threedb/mesh.go @@ -0,0 +1,9 @@ +package threedb + +type Mesh struct { + Links []MeshLink + Vector1 *Vector + Vector2 *Vector + Shadow uint16 + CMap uint16 +} diff --git a/pkg/threedb/meshlink.go b/pkg/threedb/meshlink.go new file mode 100644 index 0000000..de08216 --- /dev/null +++ b/pkg/threedb/meshlink.go @@ -0,0 +1,10 @@ +package threedb + +type MeshLink struct { + Material uint16 + Triangles uint16 + TextureCoordinates uint16 + Points uint16 + Brightness uint16 + Unknown uint16 +} diff --git a/pkg/threedb/model.go b/pkg/threedb/model.go new file mode 100644 index 0000000..21aaebc --- /dev/null +++ b/pkg/threedb/model.go @@ -0,0 +1,58 @@ +package threedb + +import "encoding/json" + +type Model struct { + DBVersion, Name string + Materials []Material + Meshes []Mesh + Objects map[string][]uint32 + Animations []Animation + Triangles [][]uint16 + TextureCoordinates [][]Coordinate + Points [][]Vector + Brightness [][]byte + + view ModelView +} + +type ModelView interface { + Prepare(*Model) +} + +func (m Model) MarshalJSON() ([]byte, error) { + if m.view == nil { + m.view = &DefaultModelView{} + } + m.view.Prepare(&m) + return json.Marshal(m.view) +} + +type DefaultModelView struct { + DBVersion, Name string + MeshesCount int + Meshes []int + FirstMesh Mesh + Materials []Material + Animations []Animation + Triangles int + TextureCoordinates int + Points int + Brightness int +} + +func (v *DefaultModelView) Prepare(m *Model) { + v.DBVersion = m.DBVersion + v.Name = m.Name + v.MeshesCount = len(m.Meshes) + for _, mesh := range m.Meshes { + v.Meshes = append(v.Meshes, len(mesh.Links)) + } + v.FirstMesh = m.Meshes[0] + v.Materials = m.Materials + v.Animations = m.Animations + v.Triangles = len(m.Triangles) + v.TextureCoordinates = len(m.TextureCoordinates) + v.Points = len(m.Points) + v.Brightness = len(m.Brightness) +} diff --git a/pkg/threedb/vector.go b/pkg/threedb/vector.go new file mode 100644 index 0000000..8d56fd2 --- /dev/null +++ b/pkg/threedb/vector.go @@ -0,0 +1,32 @@ +package threedb + +import ( + "fmt" + "slices" +) + +type Vector [3]float32 + +func (m Vector) X() float32 { return m[0] } +func (m Vector) Y() float32 { return m[1] } +func (m Vector) Z() float32 { return m[2] } +func (m *Vector) Set(x, y, z float32) { m[0], m[1], m[2] = x, y, z } +func (m *Vector) SetX(x float32) { m[0] = x } +func (m *Vector) SetY(y float32) { m[1] = y } +func (m *Vector) SetZ(z float32) { m[2] = z } +func (m Vector) String() string { return fmt.Sprintf("x:%f,y:%f,z:%f", m[0], m[1], m[2]) } + +func (m Vector) Transform(scale float32) Vector { + return Vector{(m[0] - .5) * scale, (m[1] - .5) * scale, (m[2] - .5) * scale} +} + +func (m Vector) Normalize() Vector { + return Vector{m[0] * -1, m[1] * -1, m[2]} +} + +func (m Vector) Min() float32 { return slices.Min([]float32{m[0], m[1], m[2]}) } +func (m Vector) Max() float32 { return slices.Max([]float32{m[0], m[1], m[2]}) } + +func (m Vector) MarshalText() ([]byte, error) { + return []byte(m.String()), nil +} diff --git a/pkg/threedb/vector_test.go b/pkg/threedb/vector_test.go new file mode 100644 index 0000000..f1e3684 --- /dev/null +++ b/pkg/threedb/vector_test.go @@ -0,0 +1,17 @@ +package threedb + +import ( + "fmt" + "testing" +) + +func TestVector_Array(t *testing.T) { + var vecs [][3]float32 + vecs = append(vecs, Vector{1.0, 2.0, 3.0}) + vecs = append(vecs, Vector{1.0, 2.0, 3.0}) + vecs = append(vecs, Vector{1.0, 2.0, 3.0}) + vecs = append(vecs, Vector{1.0, 2.0, 3.0}) + fmt.Println(vecs) + vec := Vector{1.0, 2.0, 3.0} + fmt.Println([3]float32(vec)) +}