package main import ( "bytes" "encoding/json" "fmt" "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/pkg/errors" "git.influ.su/artmares/digglestool/internal/pkg/logger" "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 == "" { logger.Info("TEXTURES_PATH environment variable not set") return } inputPath := "./data" inputModel := "produktionsstaetten" f, err := os.Open(path.Join(inputPath, inputModel+".3db")) if err != nil { errors.LogFatal(errors.Wrap(err, "failed to open input file")) } defer f.Close() model := threedb.Model{} err = threedb.NewDecoder(f).Decode(&model) if err != nil { errors.LogError(errors.Wrap(err, "failed to decode model")) 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 { errors.LogError(errors.Wrap(err, "failed to create log file")) return } defer logFile.Close() enc := json.NewEncoder(logFile) enc.SetIndent("", " ") if err = enc.Encode(model); err != nil { errors.LogError(errors.Wrap(err, "failed to encode model to JSON")) return } export := exporter.NewExporter( exporter.WithTexturesBasePath(texturesPath), exporter.WithOnlyBaseMesh(), exporter.WithAnimation(), ) if err = export.Export(outputPath, &model); err != nil { errors.LogError(errors.Wrap(err, "failed to export model")) return } //if err = export(&model, inputModel, texturesCache); err != nil { // log.Println(err) // return //} } // exportFn exports a 3DB model to GLTF format func exportFn(model *threedb.Model, inputModel string, cache map[string]string) error { meshIndex := 0 doc := gltf.NewDocument() // Process materials and textures binaryCache := make(map[string][]byte) if err := processMaterials(doc, model, cache, binaryCache); err != nil { return err } // Process mesh data mesh := model.Meshes[meshIndex] if err := processMesh(doc, model, mesh); err != nil { return err } // Setup scene setupScene(doc) // Save the GLTF document if err := gltf.Save(doc, path.Join(outputPath, fmt.Sprintf("%s-%d.gltf", inputModel, meshIndex))); err != nil { return errors.Wrap(err, "failed to save GLTF document") } return nil } // processMaterials processes all materials in the model and adds them to the GLTF document func processMaterials(doc *gltf.Document, model *threedb.Model, cache map[string]string, binaryCache map[string][]byte) error { for _, material := range model.Materials { data, ok := binaryCache[material.Name] if !ok { texturePath, ok := cache[material.Name] if !ok { errors.LogWarn(errors.New("invalid texture cache: " + material.Name)) continue } var err error data, err = os.ReadFile(texturePath) if err != nil { return errors.Wrap(err, "failed to read texture file: "+texturePath) } binaryCache[material.Name] = data } if err := addMaterialToDocument(doc, material, data); err != nil { return err } } return nil } // addMaterialToDocument adds a material and its texture to the GLTF document func addMaterialToDocument(doc *gltf.Document, material threedb.Material, textureData []byte) error { mt := mimetype.Detect(textureData) if mt == nil { return errors.New("failed to detect mimetype for texture: " + material.Name) } imageIndex, err := modeler.WriteImage(doc, material.Name+".tga", mt.String(), bytes.NewBuffer(textureData)) if err != nil { return errors.Wrap(err, "failed to write image to GLTF document") } doc.Textures = append(doc.Textures, &gltf.Texture{ Name: material.Name, 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), }, }) return nil } // processMesh processes a mesh and adds it to the GLTF document func processMesh(doc *gltf.Document, model *threedb.Model, mesh threedb.Mesh) error { 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] // Process vertices vertices := processVertices(points) verticesIndex := modeler.WriteAccessor(doc, gltf.TargetElementArrayBuffer, vertices) // Process texture coordinates textureCoords := processTextureCoordinates(textureCoordinates) textureIndex := modeler.WriteAccessor(doc, gltf.TargetArrayBuffer, textureCoords) // Process triangles trianglesIndex := modeler.WriteAccessor(doc, gltf.TargetArrayBuffer, triangles) // Add mesh to document addMeshToDocument(doc, model, verticesIndex, textureIndex, trianglesIndex, material) } return nil } // processVertices processes the vertices of a mesh func processVertices(points []threedb.Vector) [][3]float32 { var vertices [][3]float32 for _, point := range points { vec := point.Transform(100) vec = vec.Normalize() vertices = append(vertices, vec) } return vertices } // processTextureCoordinates processes the texture coordinates of a mesh func processTextureCoordinates(textureCoordinates []threedb.Coordinate) [][2]float32 { var textureCoords [][2]float32 for _, txc := range textureCoordinates { textureCoords = append(textureCoords, txc) } return textureCoords } // addMeshToDocument adds a mesh to the GLTF document func addMeshToDocument(doc *gltf.Document, model *threedb.Model, verticesIndex, textureIndex, trianglesIndex int, material threedb.Material) { 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), }}, }) doc.Materials = append(doc.Materials, &gltf.Material{ Name: material.Name, 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), }, }) doc.Nodes = append(doc.Nodes, &gltf.Node{ Mesh: gltf.Index(len(doc.Meshes) - 1), }) } // setupScene sets up the scene in the GLTF document func setupScene(doc *gltf.Document) { var nodes []int for index := range doc.Nodes { nodes = append(nodes, index) } doc.Scenes = []*gltf.Scene{ {Name: "Root Scene", Nodes: nodes}, } } func randRange(min, max int) int { return rand.IntN(max-min) + min }