From 907dea02d943b2178e8b35af2adfec2b2141156f Mon Sep 17 00:00:00 2001 From: ArtMares Date: Wed, 6 Aug 2025 16:09:32 +0300 Subject: [PATCH] feat: update AI --- .gitignore | 2 + cmd/decoder/main.go | 16 +- cmd/testgltf/main.go | 6 +- docs/3db_format_specification.md | 214 ++++++++++++++ docs/tasks.md | 91 ++++++ internal/pkg/logger/logger.go | 236 +++++++++++++++ internal/services/exporter/exporter.go | 8 +- internal/services/exporter/model.go | 4 +- pkg/threedb/decoder.go | 379 +++++++++++++++++++++---- 9 files changed, 891 insertions(+), 65 deletions(-) create mode 100644 docs/3db_format_specification.md create mode 100644 docs/tasks.md create mode 100644 internal/pkg/logger/logger.go diff --git a/.gitignore b/.gitignore index 06bc52e..1ca96f9 100644 --- a/.gitignore +++ b/.gitignore @@ -157,4 +157,6 @@ Temporary Items # Ignore models models/* !models/.keep +data +textures diff --git a/cmd/decoder/main.go b/cmd/decoder/main.go index a82d4dd..1a1ac73 100644 --- a/cmd/decoder/main.go +++ b/cmd/decoder/main.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - "log" "math/rand/v2" "os" "path" @@ -13,6 +12,7 @@ import ( "github.com/qmuntal/gltf" "github.com/qmuntal/gltf/modeler" + "git.influ.su/artmares/digglestool/internal/pkg/logger" "git.influ.su/artmares/digglestool/internal/services/exporter" "git.influ.su/artmares/digglestool/pkg/threedb" ) @@ -26,7 +26,7 @@ const ( func main() { texturesPath := os.Getenv("TEXTURES_PATH") if texturesPath == "" { - log.Println("TEXTURES_PATH environment variable not set") + logger.Info("TEXTURES_PATH environment variable not set") return } @@ -35,14 +35,14 @@ func main() { f, err := os.Open(path.Join(inputPath, inputModel+".3db")) if err != nil { - log.Fatal(err) + logger.Fatal(err) } defer f.Close() model := threedb.Model{} err = threedb.NewDecoder(f).Decode(&model) if err != nil { - log.Println(err) + logger.Error(err) return } //log.Printf("DB Version: %v\n", model.DBVersion) @@ -64,14 +64,14 @@ func main() { 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) + logger.Error(err) return } defer logFile.Close() enc := json.NewEncoder(logFile) enc.SetIndent("", " ") if err = enc.Encode(model); err != nil { - log.Println(err) + logger.Error(err) return } @@ -81,7 +81,7 @@ func main() { exporter.WithAnimation(), ) if err = export.Export(outputPath, &model); err != nil { - log.Println(err) + logger.Error(err) return } @@ -109,7 +109,7 @@ func exportFn(model *threedb.Model, inputModel string, cache map[string]string) if !ok { texturePath, ok := cache[material.Name] if !ok { - log.Printf("Invalid texture cache %q", material.Name) + logger.Warnf("Invalid texture cache %q", material.Name) continue return fmt.Errorf("invalid texture cache %q", material.Name) } diff --git a/cmd/testgltf/main.go b/cmd/testgltf/main.go index ed3654c..fda4c17 100644 --- a/cmd/testgltf/main.go +++ b/cmd/testgltf/main.go @@ -1,9 +1,9 @@ package main import ( - "log" - "github.com/qmuntal/gltf" + + "git.influ.su/artmares/digglestool/internal/pkg/logger" ) func main() { @@ -40,6 +40,6 @@ func main() { Scenes: []*gltf.Scene{{Name: "Root Scene", Nodes: []int{0}}}, } if err := gltf.Save(doc, "./test.gltf"); err != nil { - log.Println(err) + logger.Error(err) } } diff --git a/docs/3db_format_specification.md b/docs/3db_format_specification.md new file mode 100644 index 0000000..d599b1e --- /dev/null +++ b/docs/3db_format_specification.md @@ -0,0 +1,214 @@ +# Спецификация формата файла .3db + +## Обзор + +Формат файла .3db используется для хранения трехмерных моделей, включая геометрию, текстуры, анимации и другие данные. Этот документ описывает структуру и формат файла .3db на основе реализации декодера в проекте Digglestool. + +## Общая структура файла + +Файл .3db имеет бинарный формат с порядком байтов little-endian. Файл состоит из нескольких последовательных секций: + +1. Заголовок файла +2. Секция материалов +3. Секция мешей +4. Секция объектов +5. Секция анимаций +6. Секция теней +7. Секция кубических карт +8. Секция данных (треугольники, текстурные координаты, точки, яркость) + +## Типы данных + +### Базовые типы + +- **uint8**: 8-битное беззнаковое целое число +- **uint16**: 16-битное беззнаковое целое число +- **uint32**: 32-битное беззнаковое целое число +- **float32**: 32-битное число с плавающей точкой + +### Строки + +Строки хранятся в следующем формате: +- **uint32**: длина строки в байтах +- **byte[]**: массив байтов, представляющий строку + +### Векторы и координаты + +- **Vector**: массив из трех float32 значений [X, Y, Z] +- **Coordinate**: массив из двух float32 значений [U, V] + +## Детальное описание секций + +### 1. Заголовок файла + +Заголовок файла содержит: +- **string**: версия базы данных (DBVersion) +- **string**: имя модели (Name) + +### 2. Секция материалов + +Секция материалов начинается с: +- **uint16**: количество материалов + +За этим следуют записи материалов, каждая из которых содержит: +- **string**: имя материала +- **string**: путь к текстуре материала +- **uint32**: неизвестное значение + +### 3. Секция мешей + +Секция мешей начинается с: +- **uint32**: количество мешей + +За этим следуют записи мешей, каждая из которых содержит: +- **Секция связей меша**: + - **uint16**: количество связей + - Для каждой связи: + - **uint16**: индекс материала + - **uint16**: неизвестное значение + - **uint16**: индекс массива треугольников + - **uint16**: индекс массива текстурных координат + - **uint16**: индекс массива точек + - **uint16**: индекс массива яркости +- **Vector**: первый вектор (возможно, позиция или ориентация) +- **Vector**: второй вектор (возможно, масштаб или ограничивающий бокс) +- **пропуск 0x80 байт**: неиспользуемые данные +- **uint16**: индекс тени +- **пропуск 0x30 байт**: неиспользуемые данные +- **uint16**: индекс кубической карты + +### 4. Секция объектов + +Секция объектов начинается с: +- **uint16**: количество пар ключ-значение + +За этим следуют пары ключ-значение, каждая из которых содержит: +- **string**: ключ +- **uint16**: количество объектов +- **uint32[]**: массив индексов объектов + +### 5. Секция анимаций + +Секция анимаций начинается с: +- **uint16**: количество анимаций + +За этим следуют записи анимаций, каждая из которых содержит: +- **string**: имя анимации +- **uint16**: количество индексов мешей +- **uint32[]**: массив индексов мешей +- **uint16**: неизвестное значение 1 +- **uint16**: неизвестное значение 2 +- **uint16**: неизвестное значение 3 +- **string**: неизвестная строка +- **Vector**: вектор перемещения +- **Vector**: вектор вращения + +### 6. Секция теней + +Секция теней начинается с: +- **uint16**: количество теней + +За этим следуют данные теней, каждая из которых содержит: +- **пропуск 32*32 байт**: данные изображения тени (предположительно 32x32 пикселя) + +### 7. Секция кубических карт + +Секция кубических карт начинается с: +- **uint16**: количество кубических карт + +За этим следуют записи кубических карт, каждая из которых содержит: +- **uint16**: ширина +- **uint16**: высота +- **uint16**: неизвестное значение 1 +- **uint16**: неизвестное значение 2 +- **пропуск width*height байт**: данные пикселей + +### 8. Секция данных + +Секция данных содержит: +- **uint16**: количество массивов треугольников +- **uint16**: количество массивов текстурных координат +- **uint16**: количество массивов точек +- **uint16**: количество массивов яркости +- **uint32**: неизвестное количество + +Затем следуют размеры массивов: +- **uint16[]**: размеры массивов треугольников +- **uint16[]**: размеры массивов текстурных координат +- **uint16[]**: размеры массивов точек +- **uint16[]**: размеры массивов яркости + +Затем следует пропуск неизвестных данных: +- **пропуск 20*unknownCount байт**: неизвестные данные + +Затем следуют данные массивов: +- **Массивы треугольников**: для каждого массива: + - **uint16[]**: индексы вершин треугольников +- **Массивы текстурных координат**: для каждого массива: + - **Coordinate[]**: текстурные координаты (U, V) +- **Массивы точек**: для каждого массива: + - **Vector[]**: точки (X, Y, Z), хранятся как нормализованные uint16 значения +- **Массивы яркости**: для каждого массива: + - **uint8[]**: значения яркости + +## Структуры данных + +### Material (Материал) +``` +type Material struct { + Name, Path string + Unknown uint32 +} +``` + +### Mesh (Меш) +``` +type Mesh struct { + Links []MeshLink + Vector1 *Vector + Vector2 *Vector + Shadow uint16 + CMap uint16 +} +``` + +### MeshLink (Связь меша) +``` +type MeshLink struct { + Material uint16 + Triangles uint16 + TextureCoordinates uint16 + Points uint16 + Brightness uint16 + Unknown uint16 +} +``` + +### Animation (Анимация) +``` +type Animation struct { + Name string + MeshIndexes []uint32 + Unknown, Unknown1, Unknown2 uint16 + Unknown3 string + MoveVector *Vector + RotationVector *Vector +} +``` + +### Vector (Вектор) +``` +type Vector [3]float32 +``` + +### Coordinate (Координата) +``` +type Coordinate [2]float32 +``` + +## Примечания по реализации + +1. Все строки хранятся с предшествующей длиной (uint32). +2. Векторы точек хранятся как нормализованные значения в диапазоне [0, 1], которые затем преобразуются в координаты модели. +3. Некоторые поля помечены как "неизвестные", так как их точное назначение не определено в текущей реализации. +4. Файл использует порядок байтов little-endian для всех числовых значений. diff --git a/docs/tasks.md b/docs/tasks.md new file mode 100644 index 0000000..3c4a435 --- /dev/null +++ b/docs/tasks.md @@ -0,0 +1,91 @@ +# Задачи по улучшению Digglestool + +Этот документ содержит приоритезированный список задач по улучшению проекта Digglestool. Каждая задача отмечена флажком, который можно отметить при выполнении. + +## Архитектура и структура + +- [ ] Создать подробный README.md с описанием проекта, инструкциями по установке и примерами использования +- [x] Добавить надлежащую документацию для спецификации формата файла .3db +- [ ] Внедрить единую стратегию обработки ошибок в кодовой базе +- [ ] Рефакторинг жестко закодированных путей в decoder/main.go для использования конфигурационных файлов или аргументов командной строки +- [x] Создать единую систему логирования вместо прямого использования log.Println +- [ ] Реализовать правильный CLI-интерфейс с флагами и командами, используя библиотеку типа cobra или urfave/cli +- [ ] Добавить информацию о версии в приложение +- [ ] Разделить декодер и экспортер на отдельные команды с общими библиотеками + +## Качество кода + +- [x] Добавить более подробные комментарии для объяснения сложных алгоритмов, особенно в декодере +- [ ] Удалить закомментированный код в decoder/main.go и других файлах +- [ ] Внедрить единые шаблоны обработки ошибок в кодовой базе +- [ ] Добавить поддержку контекста для операций, которые могут занять много времени +- [ ] Рефакторинг функции exportFn в decoder/main.go для большей модульности +- [ ] Улучшить именование переменных для лучшей читаемости кода +- [ ] Добавить правильную валидацию для входных файлов и параметров +- [ ] Последовательно реализовать правильную очистку ресурсов с помощью операторов defer + +## Тестирование + +- [ ] Добавить модульные тесты для пакета threedb +- [ ] Добавить модульные тесты для сервиса экспортера +- [ ] Создать интеграционные тесты для полного процесса конвертации +- [ ] Добавить бенчмарки для критически важных с точки зрения производительности частей кода +- [ ] Реализовать тестовые фикстуры с примерами файлов .3db +- [ ] Добавить CI/CD-конвейер для автоматизированного тестирования +- [ ] Реализовать отчеты о покрытии кода + +## Производительность + +- [ ] Профилировать приложение для выявления узких мест производительности +- [ ] Оптимизировать использование памяти при загрузке и конвертации моделей +- [ ] Реализовать параллельную обработку для работы с несколькими моделями +- [ ] Улучшить реализацию пула буферов для обработки буферов разных размеров +- [ ] Добавить отчеты о прогрессе для длительных операций +- [ ] Реализовать механизмы кэширования для часто используемых ресурсов +- [ ] Оптимизировать загрузку и обработку текстур + +## Функциональность + +- [ ] Добавить поддержку пакетной обработки нескольких файлов .3db +- [ ] Реализовать режим предварительного просмотра для быстрой визуализации моделей без полного экспорта +- [ ] Добавить поддержку большего количества форматов экспорта помимо GLTF +- [ ] Реализовать генератор текстурных атласов для оптимизации использования текстур +- [ ] Добавить поддержку оптимизации моделей (уменьшение количества полигонов и т.д.) +- [ ] Реализовать улучшения экспорта анимации +- [ ] Добавить поддержку пользовательских свойств материалов +- [ ] Создать простой веб-интерфейс для конвертации моделей + +## Безопасность + +- [ ] Реализовать правильную валидацию входных данных для предотвращения потенциальных проблем безопасности +- [ ] Добавить ограничения размера файлов для предотвращения атак типа "отказ в обслуживании" +- [ ] Реализовать безопасную обработку путей к файлам для предотвращения атак с обходом пути +- [ ] Добавить контрольные суммы для проверки целостности файлов + +## Документация + +- [ ] Создать документацию API для всех публичных пакетов +- [ ] Добавить примеры для типичных случаев использования +- [ ] Документировать процесс конвертации моделей +- [ ] Создать руководства по использованию инструмента +- [ ] Добавить встроенную документацию для сложных алгоритмов +- [ ] Документировать процесс обработки текстур +- [ ] Создать журнал изменений для отслеживания изменений версий + +## Сборка и распространение + +- [ ] Создать правильные сборки релизов для нескольких платформ +- [ ] Реализовать стратегию версионирования +- [ ] Добавить установочные скрипты или пакеты +- [ ] Создать Docker-контейнеры для простого развертывания +- [ ] Реализовать улучшения управления зависимостями +- [ ] Добавить скрипты сборки для обеспечения единообразия сборок в разных средах + +## Пользовательский опыт + +- [ ] Улучшить сообщения об ошибках, сделав их более понятными для пользователя +- [ ] Добавить индикаторы прогресса для длительных операций +- [ ] Реализовать улучшенное логирование с различными уровнями детализации +- [ ] Создать простой графический интерфейс для нетехнических пользователей +- [ ] Добавить поддержку конфигурационных файлов для постоянных настроек +- [ ] Реализовать автодополнение команд для CLI diff --git a/internal/pkg/logger/logger.go b/internal/pkg/logger/logger.go new file mode 100644 index 0000000..f2505cd --- /dev/null +++ b/internal/pkg/logger/logger.go @@ -0,0 +1,236 @@ +package logger + +import ( + "fmt" + "io" + "log" + "os" + "sync" +) + +// Level represents the severity level of a log message +type Level int + +const ( + // DEBUG level for detailed troubleshooting information + DEBUG Level = iota + // INFO level for general operational information + INFO + // WARN level for warning conditions + WARN + // ERROR level for error conditions + ERROR + // FATAL level for critical errors that cause the program to exit + FATAL +) + +var levelNames = map[Level]string{ + DEBUG: "DEBUG", + INFO: "INFO", + WARN: "WARN", + ERROR: "ERROR", + FATAL: "FATAL", +} + +// String returns the string representation of the log level +func (l Level) String() string { + if name, ok := levelNames[l]; ok { + return name + } + return fmt.Sprintf("LEVEL(%d)", l) +} + +// Logger is the interface that defines the logging methods +type Logger interface { + Debug(v ...interface{}) + Debugf(format string, v ...interface{}) + Info(v ...interface{}) + Infof(format string, v ...interface{}) + Warn(v ...interface{}) + Warnf(format string, v ...interface{}) + Error(v ...interface{}) + Errorf(format string, v ...interface{}) + Fatal(v ...interface{}) + Fatalf(format string, v ...interface{}) + SetLevel(level Level) + GetLevel() Level + SetOutput(w io.Writer) +} + +// DefaultLogger is the standard implementation of the Logger interface +type DefaultLogger struct { + mu sync.Mutex + logger *log.Logger + level Level +} + +// NewDefaultLogger creates a new DefaultLogger with the specified level +func NewDefaultLogger(level Level) *DefaultLogger { + return &DefaultLogger{ + logger: log.New(os.Stdout, "", log.LstdFlags), + level: level, + } +} + +// SetOutput sets the output destination for the logger +func (l *DefaultLogger) SetOutput(w io.Writer) { + l.mu.Lock() + defer l.mu.Unlock() + l.logger.SetOutput(w) +} + +// SetLevel sets the minimum log level that will be output +func (l *DefaultLogger) SetLevel(level Level) { + l.mu.Lock() + defer l.mu.Unlock() + l.level = level +} + +// GetLevel returns the current log level +func (l *DefaultLogger) GetLevel() Level { + l.mu.Lock() + defer l.mu.Unlock() + return l.level +} + +// log logs a message at the specified level +func (l *DefaultLogger) log(level Level, v ...interface{}) { + if level < l.level { + return + } + l.mu.Lock() + defer l.mu.Unlock() + l.logger.Print(fmt.Sprintf("[%s] ", level), fmt.Sprint(v...)) +} + +// logf logs a formatted message at the specified level +func (l *DefaultLogger) logf(level Level, format string, v ...interface{}) { + if level < l.level { + return + } + l.mu.Lock() + defer l.mu.Unlock() + l.logger.Print(fmt.Sprintf("[%s] %s", level, fmt.Sprintf(format, v...))) +} + +// Debug logs a message at DEBUG level +func (l *DefaultLogger) Debug(v ...interface{}) { + l.log(DEBUG, v...) +} + +// Debugf logs a formatted message at DEBUG level +func (l *DefaultLogger) Debugf(format string, v ...interface{}) { + l.logf(DEBUG, format, v...) +} + +// Info logs a message at INFO level +func (l *DefaultLogger) Info(v ...interface{}) { + l.log(INFO, v...) +} + +// Infof logs a formatted message at INFO level +func (l *DefaultLogger) Infof(format string, v ...interface{}) { + l.logf(INFO, format, v...) +} + +// Warn logs a message at WARN level +func (l *DefaultLogger) Warn(v ...interface{}) { + l.log(WARN, v...) +} + +// Warnf logs a formatted message at WARN level +func (l *DefaultLogger) Warnf(format string, v ...interface{}) { + l.logf(WARN, format, v...) +} + +// Error logs a message at ERROR level +func (l *DefaultLogger) Error(v ...interface{}) { + l.log(ERROR, v...) +} + +// Errorf logs a formatted message at ERROR level +func (l *DefaultLogger) Errorf(format string, v ...interface{}) { + l.logf(ERROR, format, v...) +} + +// Fatal logs a message at FATAL level and then exits the program +func (l *DefaultLogger) Fatal(v ...interface{}) { + l.log(FATAL, v...) + os.Exit(1) +} + +// Fatalf logs a formatted message at FATAL level and then exits the program +func (l *DefaultLogger) Fatalf(format string, v ...interface{}) { + l.logf(FATAL, format, v...) + os.Exit(1) +} + +// Global logger instance +var ( + defaultLogger Logger = NewDefaultLogger(INFO) +) + +// SetDefaultLogger sets the global default logger +func SetDefaultLogger(logger Logger) { + defaultLogger = logger +} + +// GetDefaultLogger returns the global default logger +func GetDefaultLogger() Logger { + return defaultLogger +} + +// SetLevel sets the log level for the default logger +func SetLevel(level Level) { + defaultLogger.SetLevel(level) +} + +// Debug logs a message at DEBUG level using the default logger +func Debug(v ...interface{}) { + defaultLogger.Debug(v...) +} + +// Debugf logs a formatted message at DEBUG level using the default logger +func Debugf(format string, v ...interface{}) { + defaultLogger.Debugf(format, v...) +} + +// Info logs a message at INFO level using the default logger +func Info(v ...interface{}) { + defaultLogger.Info(v...) +} + +// Infof logs a formatted message at INFO level using the default logger +func Infof(format string, v ...interface{}) { + defaultLogger.Infof(format, v...) +} + +// Warn logs a message at WARN level using the default logger +func Warn(v ...interface{}) { + defaultLogger.Warn(v...) +} + +// Warnf logs a formatted message at WARN level using the default logger +func Warnf(format string, v ...interface{}) { + defaultLogger.Warnf(format, v...) +} + +// Error logs a message at ERROR level using the default logger +func Error(v ...interface{}) { + defaultLogger.Error(v...) +} + +// Errorf logs a formatted message at ERROR level using the default logger +func Errorf(format string, v ...interface{}) { + defaultLogger.Errorf(format, v...) +} + +// Fatal logs a message at FATAL level using the default logger and then exits +func Fatal(v ...interface{}) { + defaultLogger.Fatal(v...) +} + +// Fatalf logs a formatted message at FATAL level using the default logger and then exits +func Fatalf(format string, v ...interface{}) { + defaultLogger.Fatalf(format, v...) +} diff --git a/internal/services/exporter/exporter.go b/internal/services/exporter/exporter.go index 014f3f7..aa59bfc 100644 --- a/internal/services/exporter/exporter.go +++ b/internal/services/exporter/exporter.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" _ "image/png" - "log" "os" "path" "path/filepath" @@ -13,6 +12,7 @@ import ( _ "github.com/ftrvxmtrx/tga" "github.com/qmuntal/gltf" + "git.influ.su/artmares/digglestool/internal/pkg/logger" "git.influ.su/artmares/digglestool/pkg/threedb" ) @@ -67,7 +67,7 @@ func (e *Exporter) Export(basePath string, model *threedb.Model) error { return err } if e.onlyBaseMesh { - log.Printf("Export Base Model: %s", model.Name) + logger.Infof("Export Base Model: %s", model.Name) doc, err := e.generate(model, 0) if err != nil { return err @@ -83,10 +83,10 @@ func (e *Exporter) Export(basePath string, model *threedb.Model) error { return err } } - log.Printf("Export Animation to %s\n", dirPath) + logger.Infof("Export Animation to %s", dirPath) length := len(model.Animations) for index, animation := range model.Animations { - log.Printf("Export progress: %d/%d\r", index+1, length) + logger.Infof("Export progress: %d/%d\r", index+1, length) indexes := make([]int, len(animation.MeshIndexes)) for _, index := range animation.MeshIndexes { indexes = append(indexes, int(index)) diff --git a/internal/services/exporter/model.go b/internal/services/exporter/model.go index 51ba53d..8278846 100644 --- a/internal/services/exporter/model.go +++ b/internal/services/exporter/model.go @@ -5,13 +5,13 @@ import ( "fmt" "image" "image/png" - "log" "os" _ "github.com/ftrvxmtrx/tga" "github.com/qmuntal/gltf" "github.com/qmuntal/gltf/modeler" + "git.influ.su/artmares/digglestool/internal/pkg/logger" "git.influ.su/artmares/digglestool/pkg/threedb" ) @@ -24,7 +24,7 @@ func (m *Model) Generate(model *threedb.Model, cache *Cache[*CacheItem], meshInd m.doc = gltf.NewDocument() m.doc.Scenes = []*gltf.Scene{} m.materialMap = make(map[uint16]int) - log.Println("Generate use mesh indexes", len(meshIndexes)) + logger.Info("Generate use mesh indexes", len(meshIndexes)) for i, meshIndex := range meshIndexes { mesh := model.Meshes[meshIndex] var nodeIndexes []int diff --git a/pkg/threedb/decoder.go b/pkg/threedb/decoder.go index 04cbcbb..10d1e83 100644 --- a/pkg/threedb/decoder.go +++ b/pkg/threedb/decoder.go @@ -1,31 +1,59 @@ +// Package threedb provides functionality for working with .3db file format. +// This package contains the decoder for parsing 3D model data from the proprietary .3db format +// used in the Diggles game. The format stores mesh data, materials, animations, and other +// 3D model information in a binary structure. package threedb import ( "encoding/binary" "errors" "io" - "log" + + "git.influ.su/artmares/digglestool/internal/pkg/logger" ) +// Decoder is responsible for reading and parsing .3db file format. +// It uses an io.ReadSeeker to navigate through the binary file structure +// and extract model data according to the .3db format specification. type Decoder struct { - reader io.ReadSeeker + reader io.ReadSeeker // The source from which the 3D model data is read } +// NewDecoder creates a new Decoder instance with the provided reader. +// The reader must implement io.ReadSeeker interface to allow both reading +// and seeking within the file. func NewDecoder(reader io.ReadSeeker) *Decoder { return &Decoder{reader: reader} } +// Decode parses the entire .3db file and populates the provided Model structure. +// The .3db file format has a specific structure with sections for different types of data: +// 1. File header (version and model name) +// 2. Materials list +// 3. Meshes with their properties +// 4. Object definitions +// 5. Animation data +// 6. Shadow maps +// 7. Cube maps (environment textures) +// 8. Geometry data (triangles, texture coordinates, vertices, etc.) +// +// This method reads each section in sequence, following the file format specification. +// If any error occurs during reading, the decoding process is aborted. func (dec *Decoder) Decode(model *Model) error { if model == nil { return errors.New("model is nil") } var err error + + // Read file header information if model.DBVersion, err = dec.readString(); err != nil { return err } if model.Name, err = dec.readString(); err != nil { return err } + + // Read all model components in the order they appear in the file if err = dec.readMaterials(model); err != nil { return err } @@ -44,6 +72,8 @@ func (dec *Decoder) Decode(model *Model) error { if err = dec.readCubeMaps(model); err != nil { return err } + + // Read the actual geometry data (triangles, texture coordinates, vertices, etc.) if err = dec.readData(model); err != nil { return err } @@ -51,40 +81,58 @@ func (dec *Decoder) Decode(model *Model) error { return nil } +// seek advances the reader position by the specified offset. +// This is used to skip over sections of the file that are not needed +// or not yet understood in the file format. func (dec *Decoder) seek(offset int64) (err error) { _, err = dec.reader.Seek(offset, io.SeekCurrent) return } +// read is a helper method that reads binary data from the reader into the provided destination. +// All data in the .3db format is stored in little-endian byte order. func (dec *Decoder) read(dst any) error { return binary.Read(dec.reader, binary.LittleEndian, dst) } +// readUInt8 reads an unsigned 8-bit integer from the binary stream. func (dec *Decoder) readUInt8() (result uint8, err error) { err = dec.read(&result) return } +// readUInt16 reads an unsigned 16-bit integer from the binary stream. +// These are commonly used for indices, counts, and references in the .3db format. func (dec *Decoder) readUInt16() (result uint16, err error) { err = dec.read(&result) return } +// readUInt32 reads an unsigned 32-bit integer from the binary stream. +// These are used for larger indices and counts in the .3db format. func (dec *Decoder) readUInt32() (result uint32, err error) { err = dec.read(&result) return } +// readFloat32 reads a 32-bit floating point number from the binary stream. +// These are used for coordinates, vectors, and other numeric values in the .3db format. func (dec *Decoder) readFloat32() (result float32, err error) { err = dec.read(&result) return } +// readString reads a string from the binary stream. +// In the .3db format, strings are stored as a 32-bit length followed by the string data. +// This is used for names, paths, and other textual information in the model. func (dec *Decoder) readString() (string, error) { + // First read the length of the string length, err := dec.readUInt32() if err != nil { return "", err } + + // Then read the string data buf := make([]byte, length) if err = dec.read(&buf); err != nil { return "", err @@ -93,9 +141,14 @@ func (dec *Decoder) readString() (string, error) { return string(buf), nil } +// readVector reads a 3D vector (x, y, z coordinates) from the binary stream. +// Vectors are used for positions, directions, and other spatial information in the model. +// Each component of the vector is stored as a 32-bit floating point number. func (dec *Decoder) readVector() (*Vector, error) { var vec Vector var err error + + // Read the x, y, and z components of the vector if vec[0], err = dec.readFloat32(); err != nil { return nil, err } @@ -105,76 +158,126 @@ func (dec *Decoder) readVector() (*Vector, error) { if vec[2], err = dec.readFloat32(); err != nil { return nil, err } + return &vec, nil } +// readMaterials reads the materials section of the .3db file. +// Materials define the visual appearance of the model's surfaces and include +// information about textures, colors, and other surface properties. +// Each material has a name, a path to its texture file, and some additional properties. func (dec *Decoder) readMaterials(model *Model) error { + // Read the number of materials in the file materialCount, err := dec.readUInt16() if err != nil { return err } count := int(materialCount) + + // Read each material's data for i := 0; i < count; i++ { material := Material{} + + // Read the material name (usually corresponds to a texture name) if material.Name, err = dec.readString(); err != nil { return err } + + // Read the path to the material's texture file if material.Path, err = dec.readString(); err != nil { return err } + + // Read an unknown property (possibly related to material properties or flags) if material.Unknown, err = dec.readUInt32(); err != nil { return err } + + // Add the material to the model's materials list model.Materials = append(model.Materials, material) } return nil } +// readMeshes reads the meshes section of the .3db file. +// Meshes are the 3D objects that make up the model. Each mesh consists of +// a collection of triangles, texture coordinates, and vertices, organized into "links". +// Meshes also contain transformation data and references to shadows and cube maps. func (dec *Decoder) readMeshes(model *Model) error { + // Read the number of meshes in the file meshCount, err := dec.readUInt32() if err != nil { return err } count := int(meshCount) + + // Read each mesh's data for i := 0; i < count; i++ { mesh := Mesh{} + + // Read the mesh links (connections to materials, triangles, etc.) if err = dec.readMeshLink(&mesh); err != nil { return err } + + // Read two vectors that might represent position, scale, or other transformation data if mesh.Vector1, err = dec.readVector(); err != nil { return err } if mesh.Vector2, err = dec.readVector(); err != nil { return err } + + // Skip 128 bytes of unknown data _ = dec.seek(0x80) + + // Read the shadow index (reference to a shadow in the shadows section) if mesh.Shadow, err = dec.readUInt16(); err != nil { return err - } // Mesh Shadow Index ? + } + + // Skip 48 bytes of unknown data _ = dec.seek(0x30) + + // Read the cube map index (reference to a cube map in the cube maps section) if mesh.CMap, err = dec.readUInt16(); err != nil { return err - } // Mesh CMap Index ? + } + + // Add the mesh to the model's meshes list model.Meshes = append(model.Meshes, mesh) } return nil } +// readMeshLink reads the links section of a mesh. +// Links connect a mesh to its materials, triangles, texture coordinates, vertices, and brightness values. +// Each mesh can have multiple links, typically one for each material used in the mesh. func (dec *Decoder) readMeshLink(mesh *Mesh) error { + // Read the number of links in the mesh meshLinkCount, err := dec.readUInt16() if err != nil { return err } count := int(meshLinkCount) + + // Read each link's data for i := 0; i < count; i++ { meshLink := MeshLink{} + + // Read the material index (reference to a material in the materials section) if meshLink.Material, err = dec.readUInt16(); err != nil { return err } + + // Read an unknown property if meshLink.Unknown, err = dec.readUInt16(); err != nil { return err } + + // Read indices to geometry data that will be read later in readData + // These are indices into arrays of triangles, texture coordinates, points, and brightness values if meshLink.Triangles, err = dec.readUInt16(); err != nil { return err } @@ -187,30 +290,48 @@ func (dec *Decoder) readMeshLink(mesh *Mesh) error { if meshLink.Brightness, err = dec.readUInt16(); err != nil { return err } + + // Add the link to the mesh's links list mesh.Links = append(mesh.Links, meshLink) } return nil } +// readObjects reads the objects section of the .3db file. +// Objects are named collections of mesh indices that group meshes together +// for logical organization or animation purposes. For example, all meshes +// that make up a character's arm might be grouped under an "arm" object. func (dec *Decoder) readObjects(model *Model) error { + // Read the number of key-value pairs (object definitions) keyValuePairCount, err := dec.readUInt16() if err != nil { return err } count := int(keyValuePairCount) + + // Initialize the objects map if there are objects to read if count > 0 && model.Objects == nil { model.Objects = make(map[string][]uint32) } + + // Read each object definition for i := 0; i < count; i++ { + // Read the object name (key) var key string if key, err = dec.readString(); err != nil { return err } + + // Read the number of mesh indices in this object var objectCount uint16 if objectCount, err = dec.readUInt16(); err != nil { return err } + + // Initialize the array for this object's mesh indices model.Objects[key] = make([]uint32, objectCount) + + // Read each mesh index for j := 0; j < int(objectCount); j++ { var n uint32 if n, err = dec.readUInt32(); err != nil { @@ -222,23 +343,36 @@ func (dec *Decoder) readObjects(model *Model) error { return nil } +// readAnimations reads the animations section of the .3db file. +// Animations define how meshes move and rotate over time. Each animation +// has a name, a list of mesh indices it affects, and transformation data. func (dec *Decoder) readAnimations(model *Model) error { + // Read the number of animations animationCount, err := dec.readUInt16() if err != nil { return err } count := int(animationCount) + + // Read each animation for i := 0; i < count; i++ { animation := Animation{} + + // Read the animation name if animation.Name, err = dec.readString(); err != nil { return err } + // Read the number of mesh indices this animation affects var meshIndexesCount uint16 if meshIndexesCount, err = dec.readUInt16(); err != nil { return err } + + // Initialize the array for mesh indices animation.MeshIndexes = make([]uint32, meshIndexesCount) + + // Read each mesh index for j := 0; j < int(meshIndexesCount); j++ { var n uint32 if n, err = dec.readUInt32(); err != nil { @@ -246,6 +380,9 @@ func (dec *Decoder) readAnimations(model *Model) error { } animation.MeshIndexes[j] = n } + + // Read animation properties (some are not fully understood) + // These might control timing, interpolation, or other animation parameters if animation.Unknown, err = dec.readUInt16(); err != nil { return err } @@ -255,43 +392,60 @@ func (dec *Decoder) readAnimations(model *Model) error { if animation.Unknown2, err = dec.readUInt16(); err != nil { return err } - //if animation.Unknown1, err = dec.readFloat32(); err != nil { - // return err - //} + + // Read additional animation data if animation.Unknown3, err = dec.readString(); err != nil { return err } + + // Read movement and rotation vectors that define the animation transformation if animation.MoveVector, err = dec.readVector(); err != nil { return err } if animation.RotationVector, err = dec.readVector(); err != nil { return err } + + // Add the animation to the model's animations list model.Animations = append(model.Animations, animation) } return nil } +// readShadows reads the shadows section of the .3db file. +// Shadows appear to be stored as 32x32 pixel images, but this implementation +// currently just skips over them without processing the data. func (dec *Decoder) readShadows(_ *Model) error { + // Read the number of shadows shadowCount, err := dec.readUInt16() if err != nil { return err } count := int(shadowCount) + + // Skip over each shadow's data + // Each shadow appears to be a 32x32 pixel image (1024 bytes) for i := 0; i < count; i++ { - // Skip // TODO: возможно это картинка 32х32 + // Skip the shadow data _ = dec.seek(32 * 32) } return nil } +// readCubeMaps reads the cube maps section of the .3db file. +// Cube maps are used for environment reflections and are stored as +// rectangular images with dimensions specified in the file. func (dec *Decoder) readCubeMaps(_ *Model) error { + // Read the number of cube maps cubeMapCount, err := dec.readUInt16() if err != nil { return err } count := int(cubeMapCount) + + // Read each cube map for i := 0; i < count; i++ { + // Read the dimensions of the cube map image var width, height uint16 if width, err = dec.readUInt16(); err != nil { return err @@ -299,29 +453,48 @@ func (dec *Decoder) readCubeMaps(_ *Model) error { if height, err = dec.readUInt16(); err != nil { return err } + + // Read two unknown values (possibly format or flags) _, _ = dec.readUInt16() _, _ = dec.readUInt16() - // Skip pixel data + + // Skip the pixel data (width * height bytes) _ = dec.seek(int64(width * height)) } return nil } +// readData reads the geometry data section of the .3db file. +// This is the most complex part of the file format, containing all the actual 3D geometry +// information that defines the model's shape and appearance. The data is organized into +// several arrays: +// +// 1. Triangles: Define the faces of the 3D model by referencing vertices +// 2. Texture Coordinates: Define how textures are mapped onto the model's surface +// 3. Points: The actual 3D vertices that make up the model's shape +// 4. Brightness: Per-vertex lighting information +// +// The function first reads counts for each type of data, then reads arrays of counts +// that indicate how many elements are in each sub-array. Finally, it reads the actual +// geometry data by calling specialized functions for each data type. 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 + // These variables store the number of sub-arrays for each data type + triangleCount uint16 // Number of triangle arrays + trianglesCounts []uint16 // Number of triangles in each array + textureCoordCount uint16 // Number of texture coordinate arrays + textureCoordCounts []uint16 // Number of texture coordinates in each array + pointCount uint16 // Number of point (vertex) arrays + pointCounts []uint16 // Number of points in each array + brightnessCount uint16 // Number of brightness arrays + brightnessCounts []uint16 // Number of brightness values in each array + unknownCount uint32 // Number of unknown data blocks to skip - cnt uint16 + cnt uint16 // Temporary variable for reading counts err error ) + + // Read the number of arrays for each data type if triangleCount, err = dec.readUInt16(); err != nil { return err } @@ -334,74 +507,77 @@ func (dec *Decoder) readData(model *Model) error { if brightnessCount, err = dec.readUInt16(); err != nil { return err } + + // Read the count of unknown data blocks (possibly animation or physics related) if unknownCount, err = dec.readUInt32(); err != nil { return err } - log.Println("unknownCount:", unknownCount) + logger.Debug("unknownCount:", unknownCount) + + // Read the number of elements in each triangle array + // Each value tells us how many triangles are in the corresponding array for i := 0; i < int(triangleCount); i++ { if cnt, err = dec.readUInt16(); err != nil { return err } trianglesCounts = append(trianglesCounts, cnt) } + + // Read the number of elements in each texture coordinate array + // Each value tells us how many texture coordinates are in the corresponding array for i := 0; i < int(textureCoordCount); i++ { if cnt, err = dec.readUInt16(); err != nil { return err } textureCoordCounts = append(textureCoordCounts, cnt) } + + // Read the number of elements in each point (vertex) array + // Each value tells us how many vertices are in the corresponding array for i := 0; i < int(pointCount); i++ { if cnt, err = dec.readUInt16(); err != nil { return err } pointCounts = append(pointCounts, cnt) } + + // Read the number of elements in each brightness array + // Each value tells us how many brightness values are in the corresponding array for i := 0; i < int(brightnessCount); i++ { if cnt, err = dec.readUInt16(); err != nil { return err } brightnessCounts = append(brightnessCounts, cnt) } + + // Skip over blocks of unknown data + // Each block is 20 bytes long and may contain additional model information + // that is not currently used by this decoder 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) + // Note: There was experimental code here to try to decode this data, + // but it's been commented out as the format is not fully understood. + // Each block appears to be 20 bytes in size. _ = dec.seek(20) } + + // Now read the actual geometry data using the counts we've collected + + // Read triangle indices that define the model's faces if err = dec.readTriangles(model, int(triangleCount), trianglesCounts); err != nil { return err } + + // Read texture coordinates that define how textures map onto the model if err = dec.readTextureCoordinates(model, int(textureCoordCount), textureCoordCounts); err != nil { return err } + + // Read 3D points (vertices) that define the model's shape if err = dec.readPoint(model, int(pointCount), pointCounts); err != nil { return err } + + // Read brightness values that define per-vertex lighting if err = dec.readBrightness(model, int(brightnessCount), brightnessCounts); err != nil { return err } @@ -409,10 +585,27 @@ func (dec *Decoder) readData(model *Model) error { return nil } +// readTriangles reads the triangle data from the .3db file. +// Triangles are the fundamental building blocks of 3D models, defining the faces +// that make up the model's surface. Each triangle is defined by three indices that +// reference vertices in the corresponding point array. +// +// The function reads 'count' arrays of triangles, where each array contains a variable +// number of triangle indices as specified in the 'counts' slice. Each triangle index +// is stored as a 16-bit unsigned integer. +// +// In 3D graphics, triangles are used because they are always planar (flat) and can +// approximate any 3D surface when used in sufficient numbers. The triangles in this +// format appear to reference vertices in the corresponding point arrays, allowing +// the renderer to construct the 3D mesh. func (dec *Decoder) readTriangles(model *Model, count int, counts []uint16) error { + // Process each array of triangles for i := 0; i < count; i++ { + // Get the number of triangle indices in this array cnt := int(counts[i]) var triangles []uint16 + + // Read each triangle index for j := 0; j < cnt; j++ { n, err := dec.readUInt16() if err != nil { @@ -420,70 +613,160 @@ func (dec *Decoder) readTriangles(model *Model, count int, counts []uint16) erro } triangles = append(triangles, n) } + + // Add the array of triangles to the model model.Triangles = append(model.Triangles, triangles) } return nil } +// readTextureCoordinates reads the texture coordinate data from the .3db file. +// Texture coordinates (also known as UV coordinates) define how 2D textures are mapped +// onto the 3D model's surface. Each texture coordinate is a 2D point (u,v) that maps +// a vertex of the 3D model to a specific point on the texture image. +// +// The function reads 'count' arrays of texture coordinates, where each array contains +// a variable number of coordinates as specified in the 'counts' slice. Each coordinate +// consists of two 32-bit floating point values (u,v) that range from 0.0 to 1.0, +// representing relative positions on the texture image: +// - u: Horizontal position (0.0 = left edge, 1.0 = right edge) +// - v: Vertical position (0.0 = top edge, 1.0 = bottom edge) +// +// Texture mapping is a critical part of 3D rendering as it allows detailed 2D images +// to be applied to 3D surfaces, giving models realistic appearance without requiring +// extremely complex geometry. func (dec *Decoder) readTextureCoordinates(model *Model, count int, counts []uint16) error { + // Process each array of texture coordinates for i := 0; i < count; i++ { + // Get the number of texture coordinates in this array cnt := int(counts[i]) var cords []Coordinate + + // Read each texture coordinate (u,v pair) for j := 0; j < cnt; j++ { cord := Coordinate{} + + // Read the u coordinate (horizontal position on texture) u, err := dec.readFloat32() if err != nil { return err } + + // Read the v coordinate (vertical position on texture) v, err := dec.readFloat32() if err != nil { return err } + + // Set the coordinate values and add to the array cord.Set(u, v) cords = append(cords, cord) } + + // Add the array of texture coordinates to the model model.TextureCoordinates = append(model.TextureCoordinates, cords) } return nil } +// readPoint reads the vertex data from the .3db file. +// Points (or vertices) are the fundamental 3D coordinates that define the shape of the model. +// Each point is a 3D vector with x, y, and z coordinates that specify its position in 3D space. +// +// The function reads 'count' arrays of points, where each array contains a variable +// number of vertices as specified in the 'counts' slice. Interestingly, in the .3db format, +// each coordinate is stored as a 16-bit unsigned integer (0-65535) rather than a floating point, +// which is then normalized to a floating point value between 0.0 and 1.0 by dividing by 0xFFFF (65535). +// +// This compression technique reduces file size while maintaining reasonable precision. +// The actual world-space coordinates are likely calculated by applying transformations +// (scaling, rotation, translation) to these normalized coordinates during rendering. +// +// The normalization formula is: +// +// float_value = uint16_value / 65535.0 +// +// This gives a value in the range [0.0, 1.0] which can later be transformed to the +// appropriate scale and position in the 3D world. func (dec *Decoder) readPoint(model *Model, count int, counts []uint16) error { + // Process each array of points (vertices) for i := 0; i < count; i++ { + // Get the number of points in this array cnt := int(counts[i]) var vectors []Vector + + // Read each point (3D vertex) for j := 0; j < cnt; j++ { vec := Vector{} + + // Read the x coordinate as a 16-bit unsigned integer ux, err := dec.readUInt16() if err != nil { return err } + + // Read the y coordinate as a 16-bit unsigned integer uy, err := dec.readUInt16() if err != nil { return err } + + // Read the z coordinate as a 16-bit unsigned integer uz, err := dec.readUInt16() if err != nil { return err } + + // Convert the integer coordinates to normalized floating point values [0.0, 1.0] + // by dividing by the maximum 16-bit value (0xFFFF = 65535) vec.Set(float32(ux)/float32(0xffff), float32(uy)/float32(0xffff), float32(uz)/float32(0xffff)) vectors = append(vectors, vec) } + + // Add the array of points to the model model.Points = append(model.Points, vectors) } return nil } +// readBrightness reads the brightness (lighting) data from the .3db file. +// Brightness values represent the lighting or shading information for each vertex +// in the model. This allows for pre-calculated lighting effects that don't need +// to be computed at runtime, which was important for older 3D engines with limited +// processing power. +// +// The function reads 'count' arrays of brightness values, where each array contains +// a variable number of values as specified in the 'counts' slice. Each brightness +// value is stored as an 8-bit unsigned integer (0-255), where: +// - 0 represents complete darkness (black) +// - 255 represents maximum brightness (white) +// - Values in between represent varying levels of gray +// +// These brightness values are typically used during rendering to modulate the color +// of each vertex, creating lighting effects like shadows, highlights, and ambient +// occlusion. The values might be applied directly to vertex colors or used as +// multipliers for texture colors. +// +// This approach to lighting was common in older 3D games where dynamic lighting +// was computationally expensive, so pre-calculated lighting was stored in the model. func (dec *Decoder) readBrightness(model *Model, count int, counts []uint16) error { + // Process each array of brightness values for i := 0; i < count; i++ { + // Get the number of brightness values in this array cnt := int(counts[i]) var brightness []byte + + // Read each brightness value for j := 0; j < cnt; j++ { + // Read the brightness as an 8-bit unsigned integer (0-255) b, err := dec.readUInt8() if err != nil { return err } brightness = append(brightness, b) } + + // Add the array of brightness values to the model model.Brightness = append(model.Brightness, brightness) } return nil