From 26f84bdc1b9c0eeb2656e5b344f7956eda1a35c3 Mon Sep 17 00:00:00 2001 From: sentientstardust Date: Thu, 21 May 2026 06:01:34 +0100 Subject: [PATCH] Implement GLB/glTF 3D model import --- localization/i18n/OrcaSlicer.pot | 2 +- localization/i18n/ca/OrcaSlicer_ca.po | 4 +- localization/i18n/cs/OrcaSlicer_cs.po | 4 +- localization/i18n/de/OrcaSlicer_de.po | 4 +- localization/i18n/en/OrcaSlicer_en.po | 2 +- localization/i18n/es/OrcaSlicer_es.po | 4 +- localization/i18n/fr/OrcaSlicer_fr.po | 4 +- localization/i18n/hu/OrcaSlicer_hu.po | 4 +- localization/i18n/it/OrcaSlicer_it.po | 4 +- localization/i18n/ja/OrcaSlicer_ja.po | 4 +- localization/i18n/ko/OrcaSlicer_ko.po | 4 +- localization/i18n/lt/OrcaSlicer_lt.po | 4 +- localization/i18n/nl/OrcaSlicer_nl.po | 4 +- localization/i18n/pl/OrcaSlicer_pl.po | 4 +- localization/i18n/pt_BR/OrcaSlicer_pt_BR.po | 4 +- localization/i18n/ru/OrcaSlicer_ru.po | 4 +- localization/i18n/sv/OrcaSlicer_sv.po | 4 +- localization/i18n/tr/OrcaSlicer_tr.po | 4 +- localization/i18n/uk/OrcaSlicer_uk.po | 4 +- localization/i18n/vi/OrcaSlicer_vi.po | 4 +- localization/i18n/zh_CN/OrcaSlicer_zh_CN.po | 4 +- localization/i18n/zh_TW/OrcaSlicer_zh_TW.po | 4 +- src/libslic3r/CMakeLists.txt | 5 + src/libslic3r/Format/GLTF.cpp | 994 ++++++++++++++++++++ src/libslic3r/Format/GLTF.hpp | 33 + src/libslic3r/Format/ImportedTexture.cpp | 365 +++++++ src/libslic3r/Format/ImportedTexture.hpp | 44 + src/libslic3r/Model.cpp | 329 +++---- src/slic3r/GUI/GUI_App.cpp | 4 +- src/slic3r/GUI/MainFrame.cpp | 4 +- src/slic3r/GUI/Plater.cpp | 19 +- 31 files changed, 1630 insertions(+), 251 deletions(-) create mode 100644 src/libslic3r/Format/GLTF.cpp create mode 100644 src/libslic3r/Format/GLTF.hpp create mode 100644 src/libslic3r/Format/ImportedTexture.cpp create mode 100644 src/libslic3r/Format/ImportedTexture.hpp diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index 66c91929919..9876af112b1 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -5355,7 +5355,7 @@ msgstr "" msgid "Save current project as" msgstr "" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgstr "" msgid "Load a model" diff --git a/localization/i18n/ca/OrcaSlicer_ca.po b/localization/i18n/ca/OrcaSlicer_ca.po index 7960f651a62..c578d84eb50 100644 --- a/localization/i18n/ca/OrcaSlicer_ca.po +++ b/localization/i18n/ca/OrcaSlicer_ca.po @@ -5833,8 +5833,8 @@ msgstr "Desa el projecte com a" msgid "Save current project as" msgstr "Desar el projecte actual com" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "Importar 3MF STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "Importar 3MF STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "Carregar un model" diff --git a/localization/i18n/cs/OrcaSlicer_cs.po b/localization/i18n/cs/OrcaSlicer_cs.po index 2ce84a0cde1..086774ce375 100644 --- a/localization/i18n/cs/OrcaSlicer_cs.po +++ b/localization/i18n/cs/OrcaSlicer_cs.po @@ -5740,8 +5740,8 @@ msgstr "Uložit projekt jako" msgid "Save current project as" msgstr "Uložit aktuální projekt jako" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "Importovat 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "Importovat 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "Načíst model" diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index 8614a865d5a..4148d775e65 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -5886,8 +5886,8 @@ msgstr "Projekt speichern als" msgid "Save current project as" msgstr "Aktuelles Projekt speichern als" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "Importiere 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "Importiere 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "Lade ein Modell" diff --git a/localization/i18n/en/OrcaSlicer_en.po b/localization/i18n/en/OrcaSlicer_en.po index 96d1f5c588c..15c339ff3e0 100644 --- a/localization/i18n/en/OrcaSlicer_en.po +++ b/localization/i18n/en/OrcaSlicer_en.po @@ -5412,7 +5412,7 @@ msgstr "" msgid "Save current project as" msgstr "" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgstr "" msgid "Load a model" diff --git a/localization/i18n/es/OrcaSlicer_es.po b/localization/i18n/es/OrcaSlicer_es.po index 142ada0d46b..a46d0a22fbc 100644 --- a/localization/i18n/es/OrcaSlicer_es.po +++ b/localization/i18n/es/OrcaSlicer_es.po @@ -5873,8 +5873,8 @@ msgstr "Guardar proyecto como" msgid "Save current project as" msgstr "Guardar el proyecto actual como" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "Importar 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "Importar 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "Cargar un modelo" diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po index 3e285b8c4a7..8e271782297 100644 --- a/localization/i18n/fr/OrcaSlicer_fr.po +++ b/localization/i18n/fr/OrcaSlicer_fr.po @@ -5883,8 +5883,8 @@ msgstr "Enregistrer le projet sous" msgid "Save current project as" msgstr "Enregistrer le projet actuel sous" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "Importer des fichiers 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "Importer des fichiers 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "Charger un modèle" diff --git a/localization/i18n/hu/OrcaSlicer_hu.po b/localization/i18n/hu/OrcaSlicer_hu.po index 80d440642ef..5766d5e49c7 100644 --- a/localization/i18n/hu/OrcaSlicer_hu.po +++ b/localization/i18n/hu/OrcaSlicer_hu.po @@ -5813,8 +5813,8 @@ msgstr "Projekt mentése másként" msgid "Save current project as" msgstr "Jelenlegi projekt mentése másként" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "Importálás 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "Importálás 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "Modell betöltése" diff --git a/localization/i18n/it/OrcaSlicer_it.po b/localization/i18n/it/OrcaSlicer_it.po index b33ae653af4..ea716b5af09 100644 --- a/localization/i18n/it/OrcaSlicer_it.po +++ b/localization/i18n/it/OrcaSlicer_it.po @@ -5865,8 +5865,8 @@ msgstr "Salva Progetto come" msgid "Save current project as" msgstr "Salva Progetto corrente come" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "Importa 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "Importa 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "Carica modello" diff --git a/localization/i18n/ja/OrcaSlicer_ja.po b/localization/i18n/ja/OrcaSlicer_ja.po index 7a3d446f822..03d9baa5397 100644 --- a/localization/i18n/ja/OrcaSlicer_ja.po +++ b/localization/i18n/ja/OrcaSlicer_ja.po @@ -5691,8 +5691,8 @@ msgstr "プロジェクトを名前を付けて保存" msgid "Save current project as" msgstr "プロジェクトを名前を付けて保存" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "3MF/STL/STEP/SVG/OBJ/AMFをインポート" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMFをインポート" msgid "Load a model" msgstr "モデルを読み込む" diff --git a/localization/i18n/ko/OrcaSlicer_ko.po b/localization/i18n/ko/OrcaSlicer_ko.po index ec0168dd4c9..5e57c876d6b 100644 --- a/localization/i18n/ko/OrcaSlicer_ko.po +++ b/localization/i18n/ko/OrcaSlicer_ko.po @@ -5647,8 +5647,8 @@ msgstr "프로젝트 다른 이름으로 저장" msgid "Save current project as" msgstr "현재 프로젝트 다른 이름으로 저장" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "3MF/STL/STEP/SVG/OBJ/AMF 가져오기" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF 가져오기" msgid "Load a model" msgstr "모델 불러오기" diff --git a/localization/i18n/lt/OrcaSlicer_lt.po b/localization/i18n/lt/OrcaSlicer_lt.po index d5d01908b30..9c618f73ca1 100644 --- a/localization/i18n/lt/OrcaSlicer_lt.po +++ b/localization/i18n/lt/OrcaSlicer_lt.po @@ -5796,8 +5796,8 @@ msgstr "Išsaugoti projektą kaip" msgid "Save current project as" msgstr "Išsaugoti dabartinį projektą kaip" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "Importuoti 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "Importuoti 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "Įkelti modelį" diff --git a/localization/i18n/nl/OrcaSlicer_nl.po b/localization/i18n/nl/OrcaSlicer_nl.po index 7266a862b7b..e6051156535 100644 --- a/localization/i18n/nl/OrcaSlicer_nl.po +++ b/localization/i18n/nl/OrcaSlicer_nl.po @@ -5783,8 +5783,8 @@ msgstr "Bewaar project als" msgid "Save current project as" msgstr "Bewaar huidig project als" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "3MF/STL/STEP/SVG/OBJ/AMF importeren" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF importeren" msgid "Load a model" msgstr "Laad een model" diff --git a/localization/i18n/pl/OrcaSlicer_pl.po b/localization/i18n/pl/OrcaSlicer_pl.po index b991f18681e..9f8f485b45c 100644 --- a/localization/i18n/pl/OrcaSlicer_pl.po +++ b/localization/i18n/pl/OrcaSlicer_pl.po @@ -5638,8 +5638,8 @@ msgstr "Zapisz projekt jako" msgid "Save current project as" msgstr "Zapisz bieżący projekt jako" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "Importuj 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "Importuj 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "Wczytaj model" diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index 4c2bc0d1382..e35adf38ac9 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -5831,8 +5831,8 @@ msgstr "Salvar projeto como" msgid "Save current project as" msgstr "Salvar o projeto atual como" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "Importar 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "Importar 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "Carregar um modelo" diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po index c344999ab37..d4a8a9bda3c 100644 --- a/localization/i18n/ru/OrcaSlicer_ru.po +++ b/localization/i18n/ru/OrcaSlicer_ru.po @@ -5977,8 +5977,8 @@ msgstr "Сохранить проект как" msgid "Save current project as" msgstr "Сохранить текущий проект как" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "Импорт 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "Импорт 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "Загрузка модели" diff --git a/localization/i18n/sv/OrcaSlicer_sv.po b/localization/i18n/sv/OrcaSlicer_sv.po index 4a7e8b3acc7..32e0bf6b7ca 100644 --- a/localization/i18n/sv/OrcaSlicer_sv.po +++ b/localization/i18n/sv/OrcaSlicer_sv.po @@ -5752,8 +5752,8 @@ msgstr "Spara Projekt som" msgid "Save current project as" msgstr "Spara nuvarande projekt som" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "Importera 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "Importera 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "Ladda modell" diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index 84238e9b72f..1356785b7fc 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -5381,8 +5381,8 @@ msgstr "Projeyi farklı kaydet" msgid "Save current project as" msgstr "Mevcut projeyi farklı kaydet" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "3MF/STL/STEP/SVG/OBJ/AMF'yi içe aktar" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF'yi içe aktar" msgid "Load a model" msgstr "Model yükle" diff --git a/localization/i18n/uk/OrcaSlicer_uk.po b/localization/i18n/uk/OrcaSlicer_uk.po index 56737be7e8b..325968edb07 100644 --- a/localization/i18n/uk/OrcaSlicer_uk.po +++ b/localization/i18n/uk/OrcaSlicer_uk.po @@ -5638,8 +5638,8 @@ msgstr "Зберегти проєкт як" msgid "Save current project as" msgstr "Зберегти поточний проєкт як" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "Імпорт 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "Імпорт 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "Завантажте модель" diff --git a/localization/i18n/vi/OrcaSlicer_vi.po b/localization/i18n/vi/OrcaSlicer_vi.po index 5ece6f7a7bf..dfb8da79498 100644 --- a/localization/i18n/vi/OrcaSlicer_vi.po +++ b/localization/i18n/vi/OrcaSlicer_vi.po @@ -5713,8 +5713,8 @@ msgstr "Lưu dự án thành" msgid "Save current project as" msgstr "Lưu dự án hiện tại thành" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "Nhập 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "Nhập 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "Tải model" diff --git a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po index c1af59fa3cb..53443bb7b32 100644 --- a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po +++ b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po @@ -5525,8 +5525,8 @@ msgstr "项目另存为" msgid "Save current project as" msgstr "项目另存为" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "导入 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "导入 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "加载模型" diff --git a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po index 035ef1d0e25..ad647c68ec7 100644 --- a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po +++ b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po @@ -5594,8 +5594,8 @@ msgstr "另存專案為" msgid "Save current project as" msgstr "將目前專案另存為" -msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" -msgstr "匯入 3MF/STL/STEP/SVG/OBJ/AMF" +msgid "Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" +msgstr "匯入 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF" msgid "Load a model" msgstr "載入模型" diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 44bb48ff04a..50b0695f880 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -200,6 +200,10 @@ set(lisbslic3r_sources Format/DRC.hpp Format/bbs_3mf.cpp Format/bbs_3mf.hpp + Format/GLTF.cpp + Format/GLTF.hpp + Format/ImportedTexture.cpp + Format/ImportedTexture.hpp format.hpp Format/OBJ.cpp Format/OBJ.hpp @@ -623,6 +627,7 @@ target_link_libraries(libslic3r qhull qoi semver + tinygltf_v3 TBB::tbb TBB::tbbmalloc ZLIB::ZLIB diff --git a/src/libslic3r/Format/GLTF.cpp b/src/libslic3r/Format/GLTF.cpp new file mode 100644 index 00000000000..f4f8f070b25 --- /dev/null +++ b/src/libslic3r/Format/GLTF.cpp @@ -0,0 +1,994 @@ +// original author: sentientstardust + +#include "GLTF.hpp" + +#include "libslic3r/Model.hpp" +#include "libslic3r/TriangleMesh.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace Slic3r { +namespace { + +static std::string percent_decode(const std::string &value); + +static std::string tg3_string(const tg3_str &str) +{ + return str.data == nullptr ? std::string() : std::string(str.data, str.len); +} + +static uint32_t rgba_to_packed(const RGBA &color) +{ + const uint32_t r = uint32_t(std::lround(std::clamp(color[0], 0.f, 1.f) * 255.f)) & 0xFFu; + const uint32_t g = uint32_t(std::lround(std::clamp(color[1], 0.f, 1.f) * 255.f)) & 0xFFu; + const uint32_t b = uint32_t(std::lround(std::clamp(color[2], 0.f, 1.f) * 255.f)) & 0xFFu; + const uint32_t a = uint32_t(std::lround(std::clamp(color[3], 0.f, 1.f) * 255.f)) & 0xFFu; + return (r << 24) | (g << 16) | (b << 8) | a; +} + +static int32_t tg3_read_file(uint8_t **out_data, uint64_t *out_size, const char *path, uint32_t path_len, void *) +{ + if (out_data == nullptr || out_size == nullptr || path == nullptr) + return 0; + + *out_data = nullptr; + *out_size = 0; + + const std::string filename = percent_decode(std::string(path, path_len)); + boost::nowide::ifstream ifs(filename, std::ios::binary | std::ios::ate); + if (!ifs.is_open()) + return 0; + + const std::streamoff stream_size = ifs.tellg(); + if (stream_size <= 0) + return 0; + ifs.seekg(0, std::ios::beg); + + uint8_t *data = static_cast(std::malloc(size_t(stream_size))); + if (data == nullptr) + return 0; + + if (!ifs.read(reinterpret_cast(data), stream_size)) { + std::free(data); + return 0; + } + + *out_data = data; + *out_size = uint64_t(stream_size); + return 1; +} + +static void tg3_free_file(uint8_t *data, uint64_t, void *) +{ + std::free(data); +} + +static int attribute_index(const tg3_primitive &primitive, const char *name) +{ + const size_t name_len = std::strlen(name); + for (uint32_t i = 0; i < primitive.attributes_count; ++i) { + const tg3_str &key = primitive.attributes[i].key; + if (key.len == name_len && key.data != nullptr && std::memcmp(key.data, name, name_len) == 0) + return primitive.attributes[i].value; + } + return -1; +} + +static size_t component_size(int32_t component_type) +{ + switch (component_type) { + case TG3_COMPONENT_TYPE_BYTE: + case TG3_COMPONENT_TYPE_UNSIGNED_BYTE: + return 1; + case TG3_COMPONENT_TYPE_SHORT: + case TG3_COMPONENT_TYPE_UNSIGNED_SHORT: + return 2; + case TG3_COMPONENT_TYPE_INT: + case TG3_COMPONENT_TYPE_UNSIGNED_INT: + case TG3_COMPONENT_TYPE_FLOAT: + return 4; + case TG3_COMPONENT_TYPE_DOUBLE: + return 8; + default: + return 0; + } +} + +static size_t accessor_component_count(int32_t type) +{ + switch (type) { + case TG3_TYPE_SCALAR: + return 1; + case TG3_TYPE_VEC2: + return 2; + case TG3_TYPE_VEC3: + return 3; + case TG3_TYPE_VEC4: + return 4; + default: + return 0; + } +} + +struct AccessorView +{ + const tg3_accessor *accessor{nullptr}; + const tg3_buffer_view *buffer_view{nullptr}; + const tg3_buffer *buffer{nullptr}; + size_t component_size{0}; + size_t component_count{0}; + size_t element_size{0}; + size_t stride{0}; + uint64_t base_offset{0}; +}; + +static bool make_accessor_view(const tg3_model &model, int accessor_idx, AccessorView &view) +{ + view = {}; + if (accessor_idx < 0 || uint32_t(accessor_idx) >= model.accessors_count) + return false; + + const tg3_accessor &accessor = model.accessors[accessor_idx]; + if (accessor.buffer_view < 0 || uint32_t(accessor.buffer_view) >= model.buffer_views_count || accessor.sparse.is_sparse) + return false; + + const tg3_buffer_view &buffer_view = model.buffer_views[accessor.buffer_view]; + if (buffer_view.buffer < 0 || uint32_t(buffer_view.buffer) >= model.buffers_count) + return false; + + const tg3_buffer &buffer = model.buffers[buffer_view.buffer]; + const size_t comp_size = component_size(accessor.component_type); + const size_t comp_count = accessor_component_count(accessor.type); + if (comp_size == 0 || comp_count == 0) + return false; + + const size_t element_size = comp_size * comp_count; + const size_t stride = buffer_view.byte_stride == 0 ? element_size : size_t(buffer_view.byte_stride); + if (stride < element_size) + return false; + if (buffer_view.byte_offset > std::numeric_limits::max() - accessor.byte_offset) + return false; + + const uint64_t base_offset = buffer_view.byte_offset + accessor.byte_offset; + if (base_offset > buffer.data.count) + return false; + if (accessor.count > 0) { + const uint64_t last_offset = base_offset + uint64_t(stride) * (accessor.count - 1); + if (last_offset < base_offset || last_offset > buffer.data.count || uint64_t(element_size) > buffer.data.count - last_offset) + return false; + } + + view.accessor = &accessor; + view.buffer_view = &buffer_view; + view.buffer = &buffer; + view.component_size = comp_size; + view.component_count = comp_count; + view.element_size = element_size; + view.stride = stride; + view.base_offset = base_offset; + return true; +} + +template static T read_unaligned(const uint8_t *data) +{ + T value; + std::memcpy(&value, data, sizeof(T)); + return value; +} + +static double normalized_signed(double value, double max_value) +{ + return std::max(value / max_value, -1.0); +} + +static bool read_component(const AccessorView &view, uint64_t element_idx, size_t component_idx, bool force_normalized, double &out) +{ + if (view.accessor == nullptr || element_idx >= view.accessor->count || component_idx >= view.component_count) + return false; + + const uint8_t *ptr = view.buffer->data.data + view.base_offset + element_idx * view.stride + component_idx * view.component_size; + const bool normalized = force_normalized || view.accessor->normalized != 0; + + switch (view.accessor->component_type) { + case TG3_COMPONENT_TYPE_BYTE: { + const int8_t v = read_unaligned(ptr); + out = normalized ? normalized_signed(double(v), 127.0) : double(v); + return true; + } + case TG3_COMPONENT_TYPE_UNSIGNED_BYTE: { + const uint8_t v = read_unaligned(ptr); + out = normalized ? double(v) / 255.0 : double(v); + return true; + } + case TG3_COMPONENT_TYPE_SHORT: { + const int16_t v = read_unaligned(ptr); + out = normalized ? normalized_signed(double(v), 32767.0) : double(v); + return true; + } + case TG3_COMPONENT_TYPE_UNSIGNED_SHORT: { + const uint16_t v = read_unaligned(ptr); + out = normalized ? double(v) / 65535.0 : double(v); + return true; + } + case TG3_COMPONENT_TYPE_INT: { + const int32_t v = read_unaligned(ptr); + out = normalized ? normalized_signed(double(v), 2147483647.0) : double(v); + return true; + } + case TG3_COMPONENT_TYPE_UNSIGNED_INT: { + const uint32_t v = read_unaligned(ptr); + out = normalized ? double(v) / 4294967295.0 : double(v); + return true; + } + case TG3_COMPONENT_TYPE_FLOAT: + out = double(read_unaligned(ptr)); + return true; + case TG3_COMPONENT_TYPE_DOUBLE: + out = read_unaligned(ptr); + return true; + default: + return false; + } +} + +static bool read_index(const AccessorView &view, uint64_t element_idx, uint32_t &out) +{ + if (view.accessor == nullptr || view.accessor->type != TG3_TYPE_SCALAR || element_idx >= view.accessor->count) + return false; + + const uint8_t *ptr = view.buffer->data.data + view.base_offset + element_idx * view.stride; + switch (view.accessor->component_type) { + case TG3_COMPONENT_TYPE_UNSIGNED_BYTE: + out = read_unaligned(ptr); + return true; + case TG3_COMPONENT_TYPE_UNSIGNED_SHORT: + out = read_unaligned(ptr); + return true; + case TG3_COMPONENT_TYPE_UNSIGNED_INT: + out = read_unaligned(ptr); + return true; + default: + return false; + } +} + +static bool read_vec3(const AccessorView &view, uint64_t element_idx, Vec3f &out) +{ + double x = 0.0; + double y = 0.0; + double z = 0.0; + if (view.component_count < 3 || + !read_component(view, element_idx, 0, false, x) || + !read_component(view, element_idx, 1, false, y) || + !read_component(view, element_idx, 2, false, z)) + return false; + out = Vec3f(float(x), float(y), float(z)); + return true; +} + +static bool read_vec2(const AccessorView &view, uint64_t element_idx, Vec2f &out) +{ + double x = 0.0; + double y = 0.0; + if (view.component_count < 2 || + !read_component(view, element_idx, 0, false, x) || + !read_component(view, element_idx, 1, false, y)) + return false; + out = Vec2f(float(x), float(y)); + return true; +} + +static RGBA read_color_or_white(const AccessorView *view, uint64_t element_idx) +{ + if (view == nullptr) + return {1.f, 1.f, 1.f, 1.f}; + + double r = 1.0; + double g = 1.0; + double b = 1.0; + double a = 1.0; + const bool force_normalized = view->accessor->component_type != TG3_COMPONENT_TYPE_FLOAT && + view->accessor->component_type != TG3_COMPONENT_TYPE_DOUBLE; + if (view->component_count < 3 || + !read_component(*view, element_idx, 0, force_normalized, r) || + !read_component(*view, element_idx, 1, force_normalized, g) || + !read_component(*view, element_idx, 2, force_normalized, b)) + return {1.f, 1.f, 1.f, 1.f}; + + if (view->component_count >= 4) + read_component(*view, element_idx, 3, force_normalized, a); + + return {float(std::clamp(r, 0.0, 1.0)), + float(std::clamp(g, 0.0, 1.0)), + float(std::clamp(b, 0.0, 1.0)), + float(std::clamp(a, 0.0, 1.0))}; +} + +static bool buffer_view_bytes(const tg3_model &model, int buffer_view_idx, const uint8_t *&data, size_t &size) +{ + data = nullptr; + size = 0; + if (buffer_view_idx < 0 || uint32_t(buffer_view_idx) >= model.buffer_views_count) + return false; + const tg3_buffer_view &buffer_view = model.buffer_views[buffer_view_idx]; + if (buffer_view.buffer < 0 || uint32_t(buffer_view.buffer) >= model.buffers_count) + return false; + const tg3_buffer &buffer = model.buffers[buffer_view.buffer]; + if (buffer_view.byte_offset > buffer.data.count || buffer_view.byte_length > buffer.data.count - buffer_view.byte_offset) + return false; + data = buffer.data.data + buffer_view.byte_offset; + size = size_t(buffer_view.byte_length); + return true; +} + +static int hex_value(char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return 10 + c - 'a'; + if (c >= 'A' && c <= 'F') + return 10 + c - 'A'; + return -1; +} + +static std::string percent_decode(const std::string &value) +{ + std::string out; + out.reserve(value.size()); + for (size_t i = 0; i < value.size(); ++i) { + if (value[i] == '%' && i + 2 < value.size()) { + const int hi = hex_value(value[i + 1]); + const int lo = hex_value(value[i + 2]); + if (hi >= 0 && lo >= 0) { + out.push_back(char((hi << 4) | lo)); + i += 2; + continue; + } + } + out.push_back(value[i]); + } + return out; +} + +static int base64_value(char c) +{ + if (c >= 'A' && c <= 'Z') + return c - 'A'; + if (c >= 'a' && c <= 'z') + return 26 + c - 'a'; + if (c >= '0' && c <= '9') + return 52 + c - '0'; + if (c == '+') + return 62; + if (c == '/') + return 63; + return -1; +} + +static bool decode_base64(const std::string &input, std::vector &out) +{ + out.clear(); + uint32_t val = 0; + int valb = -8; + for (const char c : input) { + if (std::isspace(static_cast(c))) + continue; + if (c == '=') + break; + const int decoded = base64_value(c); + if (decoded < 0) + return false; + val = (val << 6) + decoded; + valb += 6; + if (valb >= 0) { + out.push_back(uint8_t((val >> valb) & 0xFF)); + valb -= 8; + } + } + return !out.empty(); +} + +static bool decode_data_uri(const std::string &uri, std::vector &bytes, std::string &mime_type) +{ + bytes.clear(); + mime_type.clear(); + + if (uri.rfind("data:", 0) != 0) + return false; + + const size_t comma = uri.find(','); + if (comma == std::string::npos) + return false; + + const std::string meta = uri.substr(5, comma - 5); + const std::string payload = uri.substr(comma + 1); + const bool is_base64 = meta.find(";base64") != std::string::npos; + const size_t semicolon = meta.find(';'); + mime_type = semicolon == std::string::npos ? meta : meta.substr(0, semicolon); + + if (!is_base64) + return false; + + return decode_base64(payload, bytes); +} + +struct GltfTextureState +{ + std::vector image_to_imported; + std::vector texture_to_imported; +}; + +static bool decode_gltf_image(const tg3_model &model, + const boost::filesystem::path &base_dir, + const tg3_image &image, + ImportedTextureImage &out) +{ + out = {}; + out.name = tg3_string(image.name); + + if (image.buffer_view >= 0) { + const uint8_t *data = nullptr; + size_t data_size = 0; + if (!buffer_view_bytes(model, image.buffer_view, data, data_size)) + return false; + return decode_image_texture_rgba_from_memory(data, + data_size, + tg3_string(image.mime_type), + out.rgba, + out.width, + out.height); + } + + const std::string uri = tg3_string(image.uri); + if (uri.empty()) + return false; + + if (uri.rfind("data:", 0) == 0) { + std::vector bytes; + std::string mime_type; + if (!decode_data_uri(uri, bytes, mime_type)) + return false; + return decode_image_texture_rgba_from_memory(bytes.data(), + bytes.size(), + mime_type, + out.rgba, + out.width, + out.height); + } + + if (uri.find(':') != std::string::npos && !boost::algorithm::istarts_with(uri, "file:")) + return false; + + std::string decoded_uri = percent_decode(uri); + if (boost::algorithm::istarts_with(decoded_uri, "file://")) + decoded_uri = decoded_uri.substr(7); + else if (boost::algorithm::istarts_with(decoded_uri, "file:")) + decoded_uri = decoded_uri.substr(5); + + boost::filesystem::path image_path(decoded_uri); + if (!image_path.is_absolute()) + image_path = base_dir / image_path; + + out.name = image_path.string(); + return decode_image_texture_rgba_from_file(image_path.lexically_normal().string(), out.rgba, out.width, out.height); +} + +static int imported_texture_index_for_gltf_texture(const tg3_model &model, + const boost::filesystem::path &base_dir, + int texture_idx, + GltfTextureState &texture_state, + GltfImportInfo &info) +{ + if (texture_idx < 0 || uint32_t(texture_idx) >= model.textures_count) + return -1; + + if (texture_state.texture_to_imported.empty()) + texture_state.texture_to_imported.assign(model.textures_count, -2); + if (texture_state.image_to_imported.empty()) + texture_state.image_to_imported.assign(model.images_count, -2); + + int &cached_texture = texture_state.texture_to_imported[texture_idx]; + if (cached_texture != -2) + return cached_texture; + + const tg3_texture &texture = model.textures[texture_idx]; + if (texture.source < 0 || uint32_t(texture.source) >= model.images_count) { + cached_texture = -1; + return -1; + } + + int &cached_image = texture_state.image_to_imported[texture.source]; + if (cached_image != -2) { + cached_texture = cached_image; + return cached_texture; + } + + ImportedTextureImage imported; + if (!decode_gltf_image(model, base_dir, model.images[texture.source], imported)) { + BOOST_LOG_TRIVIAL(error) << "glTF material texture image failed to decode image_index=" << texture.source; + cached_image = -1; + cached_texture = -1; + return -1; + } + + cached_image = int(info.textures.size()); + info.textures.emplace_back(std::move(imported)); + cached_texture = cached_image; + return cached_texture; +} + +static RGBA material_base_color(const tg3_material *material) +{ + if (material == nullptr) + return {1.f, 1.f, 1.f, 1.f}; + + return { + float(std::clamp(material->pbr_metallic_roughness.base_color_factor[0], 0.0, 1.0)), + float(std::clamp(material->pbr_metallic_roughness.base_color_factor[1], 0.0, 1.0)), + float(std::clamp(material->pbr_metallic_roughness.base_color_factor[2], 0.0, 1.0)), + float(std::clamp(material->pbr_metallic_roughness.base_color_factor[3], 0.0, 1.0)) + }; +} + +static const tg3_value *find_object_value(const tg3_value &value, const char *key) +{ + if (value.type != TG3_VALUE_OBJECT || key == nullptr) + return nullptr; + + const size_t key_len = std::strlen(key); + for (uint32_t i = 0; i < value.object_count; ++i) { + const tg3_kv_pair &pair = value.object_data[i]; + if (pair.key.data != nullptr && pair.key.len == key_len && std::memcmp(pair.key.data, key, key_len) == 0) + return &pair.value; + } + return nullptr; +} + +static const tg3_value *find_extension_value(const tg3_extras_ext &ext, const char *name) +{ + if (name == nullptr) + return nullptr; + + const size_t name_len = std::strlen(name); + for (uint32_t i = 0; i < ext.extensions_count; ++i) { + const tg3_extension &extension = ext.extensions[i]; + if (extension.name.data != nullptr && extension.name.len == name_len && std::memcmp(extension.name.data, name, name_len) == 0) + return &extension.value; + } + return nullptr; +} + +static bool value_to_int(const tg3_value &value, int &out) +{ + if (value.type != TG3_VALUE_INT) + return false; + if (value.int_val < std::numeric_limits::min() || value.int_val > std::numeric_limits::max()) + return false; + out = int(value.int_val); + return true; +} + +static bool texture_info_from_extension_value(const tg3_value *value, int &texture_idx, int &tex_coord) +{ + if (value == nullptr || value->type != TG3_VALUE_OBJECT) + return false; + + const tg3_value *index_value = find_object_value(*value, "index"); + if (index_value == nullptr || !value_to_int(*index_value, texture_idx)) + return false; + + tex_coord = 0; + if (const tg3_value *tex_coord_value = find_object_value(*value, "texCoord")) + value_to_int(*tex_coord_value, tex_coord); + + return true; +} + +static bool diffuse_factor_from_extension_value(const tg3_value *value, RGBA &out) +{ + if (value == nullptr || value->type != TG3_VALUE_ARRAY || value->array_count < 3) + return false; + + std::array factor{1.f, 1.f, 1.f, 1.f}; + for (uint32_t i = 0; i < std::min(value->array_count, 4); ++i) { + const tg3_value &component = value->array_data[i]; + if (component.type == TG3_VALUE_REAL) + factor[i] = float(std::clamp(component.real_val, 0.0, 1.0)); + else if (component.type == TG3_VALUE_INT) + factor[i] = float(std::clamp(double(component.int_val), 0.0, 1.0)); + else + return false; + } + + out = factor; + return true; +} + +struct MaterialTextureSource +{ + RGBA color{1.f, 1.f, 1.f, 1.f}; + int texture_idx{-1}; + int tex_coord{0}; +}; + +static MaterialTextureSource material_texture_source(const tg3_material *material) +{ + MaterialTextureSource source; + if (material == nullptr) + return source; + + source.color = material_base_color(material); + source.texture_idx = material->pbr_metallic_roughness.base_color_texture.index; + source.tex_coord = material->pbr_metallic_roughness.base_color_texture.tex_coord; + + const tg3_value *spec_gloss = find_extension_value(material->ext, "KHR_materials_pbrSpecularGlossiness"); + if (spec_gloss != nullptr) { + RGBA diffuse_factor; + if (diffuse_factor_from_extension_value(find_object_value(*spec_gloss, "diffuseFactor"), diffuse_factor)) + source.color = diffuse_factor; + + int diffuse_texture_idx = -1; + int diffuse_tex_coord = 0; + if (source.texture_idx < 0 && + texture_info_from_extension_value(find_object_value(*spec_gloss, "diffuseTexture"), diffuse_texture_idx, diffuse_tex_coord)) { + source.texture_idx = diffuse_texture_idx; + source.tex_coord = diffuse_tex_coord; + } + } + + return source; +} + +static Eigen::Matrix4d node_transform(const tg3_node &node) +{ + if (node.has_matrix) { + Eigen::Matrix4d matrix; + for (int col = 0; col < 4; ++col) + for (int row = 0; row < 4; ++row) + matrix(row, col) = node.matrix[col * 4 + row]; + return matrix; + } + + Eigen::Affine3d transform = Eigen::Affine3d::Identity(); + transform.translate(Eigen::Vector3d(node.translation[0], node.translation[1], node.translation[2])); + Eigen::Quaterniond rotation(node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]); + if (rotation.norm() > 0.0) + transform.rotate(rotation.normalized()); + transform.scale(Eigen::Vector3d(node.scale[0], node.scale[1], node.scale[2])); + return transform.matrix(); +} + +static bool build_primitive_indices(const tg3_model &model, const tg3_primitive &primitive, const AccessorView &positions, std::vector &indices) +{ + indices.clear(); + if (primitive.indices >= 0) { + AccessorView index_view; + if (!make_accessor_view(model, primitive.indices, index_view)) + return false; + indices.reserve(size_t(index_view.accessor->count)); + for (uint64_t i = 0; i < index_view.accessor->count; ++i) { + uint32_t index = 0; + if (!read_index(index_view, i, index) || index >= positions.accessor->count) + return false; + indices.emplace_back(index); + } + } else { + if (positions.accessor->count > std::numeric_limits::max()) + return false; + indices.reserve(size_t(positions.accessor->count)); + for (uint64_t i = 0; i < positions.accessor->count; ++i) + indices.emplace_back(uint32_t(i)); + } + return true; +} + +static void triangle_source_indices(int mode, const std::vector &indices, size_t triangle_idx, std::array &out) +{ + if (mode == TG3_MODE_TRIANGLES || mode == -1) { + out = {indices[triangle_idx * 3], indices[triangle_idx * 3 + 1], indices[triangle_idx * 3 + 2]}; + } else if (mode == TG3_MODE_TRIANGLE_STRIP) { + if (triangle_idx % 2 == 0) + out = {indices[triangle_idx], indices[triangle_idx + 1], indices[triangle_idx + 2]}; + else + out = {indices[triangle_idx + 1], indices[triangle_idx], indices[triangle_idx + 2]}; + } else { + out = {indices[0], indices[triangle_idx + 1], indices[triangle_idx + 2]}; + } +} + +static size_t triangle_count_for_mode(int mode, size_t index_count) +{ + if (mode == TG3_MODE_TRIANGLES || mode == -1) + return index_count / 3; + if (mode == TG3_MODE_TRIANGLE_STRIP || mode == TG3_MODE_TRIANGLE_FAN) + return index_count >= 3 ? index_count - 2 : 0; + return 0; +} + +static void append_mesh_primitive(const tg3_model &model, + const boost::filesystem::path &base_dir, + const tg3_primitive &primitive, + const Eigen::Matrix4d &transform, + indexed_triangle_set &its, + GltfTextureState &texture_state, + GltfImportInfo &info) +{ + const int mode = primitive.mode == -1 ? TG3_MODE_TRIANGLES : primitive.mode; + if (mode != TG3_MODE_TRIANGLES && mode != TG3_MODE_TRIANGLE_STRIP && mode != TG3_MODE_TRIANGLE_FAN) + return; + + AccessorView positions; + if (!make_accessor_view(model, attribute_index(primitive, "POSITION"), positions) || + positions.accessor->type != TG3_TYPE_VEC3) + return; + + std::vector source_indices; + if (!build_primitive_indices(model, primitive, positions, source_indices)) + return; + + const tg3_material *material = nullptr; + bool has_material = false; + int imported_texture_idx = -1; + int texture_coord = 0; + if (primitive.material >= 0 && uint32_t(primitive.material) < model.materials_count) { + material = &model.materials[primitive.material]; + has_material = true; + } + + const MaterialTextureSource texture_source = material_texture_source(material); + if (material != nullptr) { + texture_coord = std::max(texture_source.tex_coord, 0); + imported_texture_idx = imported_texture_index_for_gltf_texture(model, base_dir, texture_source.texture_idx, texture_state, info); + } + + const RGBA face_color = texture_source.color; + const int color_accessor_idx = attribute_index(primitive, "COLOR_0"); + AccessorView color_view_storage; + AccessorView *color_view = nullptr; + if (make_accessor_view(model, color_accessor_idx, color_view_storage) && + (color_view_storage.accessor->type == TG3_TYPE_VEC3 || color_view_storage.accessor->type == TG3_TYPE_VEC4)) { + color_view = &color_view_storage; + info.has_vertex_colors = true; + } + + std::string uv_attr = "TEXCOORD_" + std::to_string(texture_coord); + const int uv_accessor_idx = attribute_index(primitive, uv_attr.c_str()); + AccessorView uv_view_storage; + AccessorView *uv_view = nullptr; + if (imported_texture_idx >= 0 && + make_accessor_view(model, uv_accessor_idx, uv_view_storage) && + uv_view_storage.accessor->type == TG3_TYPE_VEC2) + uv_view = &uv_view_storage; + + const size_t tri_count = triangle_count_for_mode(mode, source_indices.size()); + for (size_t tri_idx = 0; tri_idx < tri_count; ++tri_idx) { + std::array src{}; + triangle_source_indices(mode, source_indices, tri_idx, src); + if (src[0] == src[1] || src[0] == src[2] || src[1] == src[2]) + continue; + + std::array tri_positions; + std::array tri_colors; + std::array tri_uv{Vec2f(0.f, 0.f), Vec2f(0.f, 0.f), Vec2f(0.f, 0.f)}; + bool valid_triangle = true; + bool valid_uv = uv_view != nullptr; + for (int corner = 0; corner < 3; ++corner) { + Vec3f position; + if (!read_vec3(positions, src[corner], position)) { + valid_triangle = false; + break; + } + + const Eigen::Vector4d transformed = transform * Eigen::Vector4d(position.x(), position.y(), position.z(), 1.0); + tri_positions[corner] = Vec3f(float(transformed.x()), float(transformed.y()), float(transformed.z())); + tri_colors[corner] = read_color_or_white(color_view, src[corner]); + + if (uv_view != nullptr) { + if (read_vec2(*uv_view, src[corner], tri_uv[corner])) + tri_uv[corner].y() = 1.f - tri_uv[corner].y(); + else + valid_uv = false; + } + } + + if (!valid_triangle) + continue; + + Vec3i32 face; + for (int corner = 0; corner < 3; ++corner) { + face[corner] = int(its.vertices.size()); + its.vertices.emplace_back(tri_positions[corner]); + info.vertex_colors.emplace_back(tri_colors[corner]); + info.vertex_colors_rgba.emplace_back(rgba_to_packed(tri_colors[corner])); + } + + its.indices.emplace_back(face); + info.triangle_uvs.emplace_back(tri_uv); + info.triangle_uvs_valid.emplace_back(valid_uv ? uint8_t(1) : uint8_t(0)); + info.triangle_texture_indices.emplace_back(valid_uv ? imported_texture_idx : -1); + info.material_colors.emplace_back(face_color); + info.has_material_colors = info.has_material_colors || has_material; + } +} + +static void append_node_meshes(const tg3_model &model, + const boost::filesystem::path &base_dir, + int node_idx, + const Eigen::Matrix4d &parent_transform, + std::vector &visited, + indexed_triangle_set &its, + GltfTextureState &texture_state, + GltfImportInfo &info) +{ + if (node_idx < 0 || uint32_t(node_idx) >= model.nodes_count) + return; + if (visited[node_idx] != 0) + return; + visited[node_idx] = 1; + + const tg3_node &node = model.nodes[node_idx]; + const Eigen::Matrix4d transform = parent_transform * node_transform(node); + if (node.mesh >= 0 && uint32_t(node.mesh) < model.meshes_count) { + const tg3_mesh &mesh = model.meshes[node.mesh]; + for (uint32_t primitive_idx = 0; primitive_idx < mesh.primitives_count; ++primitive_idx) + append_mesh_primitive(model, base_dir, mesh.primitives[primitive_idx], transform, its, texture_state, info); + } + + for (uint32_t child_idx = 0; child_idx < node.children_count; ++child_idx) + append_node_meshes(model, base_dir, node.children[child_idx], transform, visited, its, texture_state, info); + + visited[node_idx] = 0; +} + +static std::vector scene_root_nodes(const tg3_model &model) +{ + if (model.scenes_count > 0) { + const int scene_idx = model.default_scene >= 0 && uint32_t(model.default_scene) < model.scenes_count ? model.default_scene : 0; + const tg3_scene &scene = model.scenes[scene_idx]; + return std::vector(scene.nodes, scene.nodes + scene.nodes_count); + } + + std::vector is_child(model.nodes_count, 0); + for (uint32_t node_idx = 0; node_idx < model.nodes_count; ++node_idx) { + const tg3_node &node = model.nodes[node_idx]; + for (uint32_t child_idx = 0; child_idx < node.children_count; ++child_idx) + if (node.children[child_idx] >= 0 && uint32_t(node.children[child_idx]) < model.nodes_count) + is_child[node.children[child_idx]] = 1; + } + + std::vector roots; + for (uint32_t node_idx = 0; node_idx < model.nodes_count; ++node_idx) + if (is_child[node_idx] == 0) + roots.emplace_back(int(node_idx)); + return roots; +} + +static std::string first_error_message(const tg3_error_stack &errors) +{ + const uint32_t count = tg3_errors_count(&errors); + if (count == 0) + return {}; + + const tg3_error_entry *entry = tg3_errors_get(&errors, 0); + if (entry == nullptr || entry->message == nullptr) + return {}; + + return entry->message; +} + +static void finish_import_info(GltfImportInfo &info) +{ + if (!info.has_vertex_colors) { + info.vertex_colors.clear(); + info.vertex_colors_rgba.clear(); + } + + if (!info.has_material_colors) { + info.material_colors.clear(); + info.is_single_material_color = false; + return; + } + + info.is_single_material_color = !info.material_colors.empty(); + for (size_t i = 1; i < info.material_colors.size(); ++i) { + if (!color_is_equal(info.material_colors.front(), info.material_colors[i])) { + info.is_single_material_color = false; + break; + } + } +} + +} // namespace + +bool load_gltf(const char *path, Model *model, GltfImportInfo &import_info, std::string &message) +{ + import_info = {}; + message.clear(); + if (path == nullptr || model == nullptr) + return false; + + tg3_model parsed_model{}; + tg3_error_stack errors{}; + tg3_error_stack_init(&errors); + + tg3_parse_options options{}; + tg3_parse_options_init(&options); + options.images_as_is = 1; + options.validate_indices = 1; + options.max_external_file_size = 1024ull * 1024ull * 1024ull; + options.fs.read_file = tg3_read_file; + options.fs.free_file = tg3_free_file; + + const std::string input_path(path); + const tg3_error_code parse_result = tg3_parse_file(&parsed_model, + &errors, + path, + uint32_t(input_path.size()), + &options); + if (parse_result != TG3_OK) { + message = first_error_message(errors); + if (message.empty()) + message = "load_gltf: failed to parse"; + tg3_model_free(&parsed_model); + tg3_error_stack_free(&errors); + return false; + } + + indexed_triangle_set its; + GltfTextureState texture_state; + const boost::filesystem::path input_fs_path(input_path); + const boost::filesystem::path base_dir = input_fs_path.parent_path(); + const std::vector roots = scene_root_nodes(parsed_model); + std::vector visited(parsed_model.nodes_count, 0); + for (int node_idx : roots) + append_node_meshes(parsed_model, + base_dir, + node_idx, + Eigen::Matrix4d::Identity(), + visited, + its, + texture_state, + import_info); + + if (its.indices.empty() || its.vertices.empty()) { + message = "load_gltf: file contains no supported mesh triangles"; + tg3_model_free(&parsed_model); + tg3_error_stack_free(&errors); + return false; + } + + TriangleMesh mesh(std::move(its)); + if (mesh.volume() < 0.0) { + mesh.flip_triangles(); + for (std::array &uvs : import_info.triangle_uvs) + std::swap(uvs[1], uvs[2]); + } + + const std::string object_name = input_fs_path.stem().empty() ? input_fs_path.filename().string() : input_fs_path.stem().string(); + model->add_object(object_name.c_str(), path, std::move(mesh)); + + finish_import_info(import_info); + + tg3_model_free(&parsed_model); + tg3_error_stack_free(&errors); + return true; +} + +} // namespace Slic3r diff --git a/src/libslic3r/Format/GLTF.hpp b/src/libslic3r/Format/GLTF.hpp new file mode 100644 index 00000000000..efc6373659d --- /dev/null +++ b/src/libslic3r/Format/GLTF.hpp @@ -0,0 +1,33 @@ +#ifndef slic3r_Format_GLTF_hpp_ +#define slic3r_Format_GLTF_hpp_ + +#include "ImportedTexture.hpp" +#include "OBJ.hpp" + +#include +#include +#include + +namespace Slic3r { + +class Model; + +struct GltfImportInfo +{ + std::vector vertex_colors; + std::vector vertex_colors_rgba; + bool has_vertex_colors{false}; + std::vector material_colors; + bool has_material_colors{false}; + bool is_single_material_color{false}; + std::vector> triangle_uvs; + std::vector triangle_uvs_valid; + std::vector triangle_texture_indices; + std::vector textures; +}; + +bool load_gltf(const char *path, Model *model, GltfImportInfo &import_info, std::string &message); + +} // namespace Slic3r + +#endif diff --git a/src/libslic3r/Format/ImportedTexture.cpp b/src/libslic3r/Format/ImportedTexture.cpp new file mode 100644 index 00000000000..ce63b2b1015 --- /dev/null +++ b/src/libslic3r/Format/ImportedTexture.cpp @@ -0,0 +1,365 @@ +#include "ImportedTexture.hpp" + +#include "libslic3r/PNGReadWrite.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace Slic3r { + +bool checked_rgba_buffer_size(size_t width, size_t height, size_t &buffer_size) +{ + buffer_size = 0; + if (width == 0 || height == 0) + return false; + if (width > std::numeric_limits::max() / height) + return false; + const size_t pixel_count = width * height; + if (pixel_count > std::numeric_limits::max() / 4) + return false; + buffer_size = pixel_count * 4; + return true; +} + +static bool decode_png_texture_rgba_from_memory(const uint8_t *data, + size_t data_size, + std::vector &out_rgba, + uint32_t &out_width, + uint32_t &out_height) +{ + out_rgba.clear(); + out_width = 0; + out_height = 0; + + if (data == nullptr || data_size == 0) + return false; + + png::ReadBuf rb{reinterpret_cast(data), data_size}; + png::ImageColorscale img; + if (!png::decode_colored_png(rb, img)) + return false; + + if (img.cols == 0 || img.rows == 0 || (img.bytes_per_pixel != 3 && img.bytes_per_pixel != 4)) + return false; + + size_t rgba_size = 0; + if (!checked_rgba_buffer_size(img.cols, img.rows, rgba_size)) + return false; + + const size_t row_stride = img.cols * size_t(img.bytes_per_pixel); + if (img.buf.size() < img.rows * row_stride) + return false; + + out_rgba.assign(rgba_size, 255); + for (size_t y = 0; y < img.rows; ++y) { + const size_t src_row_off = y * row_stride; + const size_t dst_row_off = y * img.cols * 4; + for (size_t x = 0; x < img.cols; ++x) { + const size_t src = src_row_off + x * size_t(img.bytes_per_pixel); + const size_t dst = dst_row_off + x * 4; + out_rgba[dst + 0] = img.buf[src + 0]; + out_rgba[dst + 1] = img.buf[src + 1]; + out_rgba[dst + 2] = img.buf[src + 2]; + out_rgba[dst + 3] = (img.bytes_per_pixel == 4) ? img.buf[src + 3] : uint8_t(255); + } + } + + out_width = uint32_t(img.cols); + out_height = uint32_t(img.rows); + return true; +} + +struct JpegDecodeErrorManager +{ + jpeg_error_mgr pub; + jmp_buf setjmp_buffer; +}; + +static void jpeg_decode_error_exit(j_common_ptr cinfo) +{ + auto *err = reinterpret_cast(cinfo->err); + longjmp(err->setjmp_buffer, 1); +} + +static bool decode_jpeg_texture_rgba_from_memory(const uint8_t *data, + size_t data_size, + std::vector &out_rgba, + uint32_t &out_width, + uint32_t &out_height) +{ + out_rgba.clear(); + out_width = 0; + out_height = 0; + + if (data == nullptr || data_size == 0) + return false; + + jpeg_decompress_struct cinfo{}; + JpegDecodeErrorManager jerr{}; + cinfo.err = jpeg_std_error(&jerr.pub); + jerr.pub.error_exit = jpeg_decode_error_exit; + bool jpeg_created = false; + auto destroy_jpeg = [&cinfo, &jpeg_created]() { + if (jpeg_created) { + jpeg_destroy_decompress(&cinfo); + jpeg_created = false; + } + }; + + if (setjmp(jerr.setjmp_buffer)) { + destroy_jpeg(); + return false; + } + + jpeg_create_decompress(&cinfo); + jpeg_created = true; + jpeg_mem_src(&cinfo, data, static_cast(data_size)); + + if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) { + destroy_jpeg(); + return false; + } + + if (!jpeg_start_decompress(&cinfo)) { + destroy_jpeg(); + return false; + } + + const uint32_t width = cinfo.output_width; + const uint32_t height = cinfo.output_height; + const int components = cinfo.output_components; + size_t rgba_size = 0; + const size_t scanline_stride = size_t(width) * size_t(std::max(components, 0)); + if (!checked_rgba_buffer_size(width, height, rgba_size) || + components <= 0 || + scanline_stride > std::numeric_limits::max()) { + jpeg_finish_decompress(&cinfo); + destroy_jpeg(); + return false; + } + + out_rgba.assign(rgba_size, uint8_t(255)); + JSAMPARRAY scanline = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, + JPOOL_IMAGE, + JDIMENSION(scanline_stride), + 1); + + uint32_t y = 0; + while (cinfo.output_scanline < cinfo.output_height) { + jpeg_read_scanlines(&cinfo, scanline, 1); + const unsigned char *src = scanline[0]; + const uint32_t dst_y = height - 1 - y; + for (uint32_t x = 0; x < width; ++x) { + const size_t dst = (size_t(dst_y) * size_t(width) + size_t(x)) * 4; + if (components >= 3) { + const size_t s = size_t(x) * size_t(components); + out_rgba[dst + 0] = src[s + 0]; + out_rgba[dst + 1] = src[s + 1]; + out_rgba[dst + 2] = src[s + 2]; + } else { + const unsigned char g = src[x]; + out_rgba[dst + 0] = g; + out_rgba[dst + 1] = g; + out_rgba[dst + 2] = g; + } + out_rgba[dst + 3] = 255; + } + ++y; + } + + if (!jpeg_finish_decompress(&cinfo)) { + destroy_jpeg(); + return false; + } + destroy_jpeg(); + + out_width = width; + out_height = height; + return true; +} + +static bool has_png_signature(const uint8_t *data, size_t data_size) +{ + static constexpr uint8_t signature[] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}; + return data_size >= sizeof(signature) && std::equal(std::begin(signature), std::end(signature), data); +} + +static bool has_jpeg_signature(const uint8_t *data, size_t data_size) +{ + return data_size >= 3 && data[0] == 0xff && data[1] == 0xd8 && data[2] == 0xff; +} + +bool decode_image_texture_rgba_from_file(const std::string &texture_path, + std::vector &out_rgba, + uint32_t &out_width, + uint32_t &out_height) +{ + out_rgba.clear(); + out_width = 0; + out_height = 0; + + if (!is_supported_image_texture_path(texture_path)) + return false; + + boost::nowide::ifstream ifs(texture_path, std::ios::binary); + if (!ifs.is_open()) + return false; + + std::string encoded_data((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + if (encoded_data.empty()) + return false; + + return decode_image_texture_rgba_from_memory(reinterpret_cast(encoded_data.data()), + encoded_data.size(), + texture_path, + out_rgba, + out_width, + out_height); +} + +bool decode_image_texture_rgba_from_memory(const uint8_t *data, + size_t data_size, + const std::string &mime_type_or_name, + std::vector &out_rgba, + uint32_t &out_width, + uint32_t &out_height) +{ + out_rgba.clear(); + out_width = 0; + out_height = 0; + + if (data == nullptr || data_size == 0) + return false; + + if (boost::algorithm::iequals(mime_type_or_name, "image/png") || + boost::algorithm::iends_with(mime_type_or_name, ".png") || + has_png_signature(data, data_size)) + return decode_png_texture_rgba_from_memory(data, data_size, out_rgba, out_width, out_height); + + if (boost::algorithm::iequals(mime_type_or_name, "image/jpeg") || + boost::algorithm::iequals(mime_type_or_name, "image/jpg") || + boost::algorithm::iends_with(mime_type_or_name, ".jpg") || + boost::algorithm::iends_with(mime_type_or_name, ".jpeg") || + has_jpeg_signature(data, data_size)) + return decode_jpeg_texture_rgba_from_memory(data, data_size, out_rgba, out_width, out_height); + + return false; +} + +bool is_supported_image_texture_path(const std::string &texture_path) +{ + return boost::algorithm::iends_with(texture_path, ".png") || + boost::algorithm::iends_with(texture_path, ".jpg") || + boost::algorithm::iends_with(texture_path, ".jpeg"); +} + +struct ImportedTextureAtlasEntry +{ + uint32_t x_offset{0}; + uint32_t width{0}; + uint32_t height{0}; +}; + +bool build_imported_texture_atlas(const std::vector &textures, + const std::vector &triangle_texture_indices, + std::vector> &triangle_uvs, + std::vector &triangle_uv_valid, + std::vector &atlas_rgba, + uint32_t &atlas_width, + uint32_t &atlas_height) +{ + atlas_rgba.clear(); + atlas_width = 0; + atlas_height = 0; + + if (textures.empty() || triangle_uvs.size() != triangle_uv_valid.size() || triangle_uvs.size() != triangle_texture_indices.size()) + return false; + + std::vector placements(textures.size()); + for (size_t i = 0; i < textures.size(); ++i) { + const ImportedTextureImage &texture = textures[i]; + if (texture.width == 0 || texture.height == 0 || texture.rgba.empty()) + return false; + size_t texture_rgba_size = 0; + if (!checked_rgba_buffer_size(texture.width, texture.height, texture_rgba_size) || + texture.rgba.size() < texture_rgba_size || + texture.width > std::numeric_limits::max() - atlas_width) + return false; + + placements[i].x_offset = atlas_width; + placements[i].width = texture.width; + placements[i].height = texture.height; + atlas_width += texture.width; + atlas_height = std::max(atlas_height, texture.height); + } + + size_t atlas_rgba_size = 0; + if (atlas_width == 0 || atlas_height == 0 || !checked_rgba_buffer_size(atlas_width, atlas_height, atlas_rgba_size)) + return false; + + atlas_rgba.assign(atlas_rgba_size, uint8_t(0)); + for (size_t i = 0; i < textures.size(); ++i) { + const ImportedTextureImage &texture = textures[i]; + const ImportedTextureAtlasEntry &entry = placements[i]; + for (uint32_t y = 0; y < texture.height; ++y) { + const size_t src_off = size_t(y) * size_t(texture.width) * 4; + const size_t dst_off = (size_t(y) * size_t(atlas_width) + size_t(entry.x_offset)) * 4; + std::copy(texture.rgba.begin() + src_off, + texture.rgba.begin() + src_off + size_t(texture.width) * 4, + atlas_rgba.begin() + dst_off); + } + } + + auto wrap_uv = [](float value) { + if (!std::isfinite(value)) + return 0.f; + + constexpr float k_uv_epsilon = 1e-6f; + if (value >= -k_uv_epsilon && value <= 1.f + k_uv_epsilon) + return std::clamp(value, 0.f, 1.f); + + const float wrapped = value - std::floor(value); + return wrapped < 0.f ? wrapped + 1.f : wrapped; + }; + + auto remap_uv = [&wrap_uv, &atlas_width, &atlas_height](const Vec2f &uv, const ImportedTextureAtlasEntry &entry) { + const float u = wrap_uv(uv.x()); + const float v = wrap_uv(uv.y()); + return Vec2f((float(entry.x_offset) + u * float(entry.width)) / float(atlas_width), + v * float(entry.height) / float(atlas_height)); + }; + + bool has_any_textured_triangle = false; + for (size_t tri_idx = 0; tri_idx < triangle_uvs.size(); ++tri_idx) { + triangle_uv_valid[tri_idx] = 0; + const int texture_idx = triangle_texture_indices[tri_idx]; + if (texture_idx < 0 || size_t(texture_idx) >= textures.size()) + continue; + + const ImportedTextureAtlasEntry &entry = placements[size_t(texture_idx)]; + triangle_uvs[tri_idx][0] = remap_uv(triangle_uvs[tri_idx][0], entry); + triangle_uvs[tri_idx][1] = remap_uv(triangle_uvs[tri_idx][1], entry); + triangle_uvs[tri_idx][2] = remap_uv(triangle_uvs[tri_idx][2], entry); + triangle_uv_valid[tri_idx] = 1; + has_any_textured_triangle = true; + } + + if (!has_any_textured_triangle) { + atlas_rgba.clear(); + atlas_width = 0; + atlas_height = 0; + return false; + } + + return true; +} + +} // namespace Slic3r diff --git a/src/libslic3r/Format/ImportedTexture.hpp b/src/libslic3r/Format/ImportedTexture.hpp new file mode 100644 index 00000000000..815fc8633bf --- /dev/null +++ b/src/libslic3r/Format/ImportedTexture.hpp @@ -0,0 +1,44 @@ +#ifndef slic3r_Format_ImportedTexture_hpp_ +#define slic3r_Format_ImportedTexture_hpp_ + +#include "libslic3r/Point.hpp" + +#include +#include +#include +#include +#include + +namespace Slic3r { + +struct ImportedTextureImage +{ + std::string name; + std::vector rgba; + uint32_t width{0}; + uint32_t height{0}; +}; + +bool checked_rgba_buffer_size(size_t width, size_t height, size_t &buffer_size); +bool decode_image_texture_rgba_from_file(const std::string &texture_path, + std::vector &out_rgba, + uint32_t &out_width, + uint32_t &out_height); +bool decode_image_texture_rgba_from_memory(const uint8_t *data, + size_t data_size, + const std::string &mime_type_or_name, + std::vector &out_rgba, + uint32_t &out_width, + uint32_t &out_height); +bool is_supported_image_texture_path(const std::string &texture_path); +bool build_imported_texture_atlas(const std::vector &textures, + const std::vector &triangle_texture_indices, + std::vector> &triangle_uvs, + std::vector &triangle_uv_valid, + std::vector &atlas_rgba, + uint32_t &atlas_width, + uint32_t &atlas_height); + +} // namespace Slic3r + +#endif diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 3fad532b7c2..077d80fd8bd 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -11,12 +11,12 @@ #include "TriangleMeshSlicer.hpp" #include "TriangleSelector.hpp" #include "MaterialType.hpp" -#include "PNGReadWrite.hpp" #include "Format/AMF.hpp" #include "Format/svg.hpp" #include "Format/bbs_3mf.hpp" #include "Format/DRC.hpp" +#include "Format/GLTF.hpp" // BBS #include "FaceDetector.hpp" @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include @@ -34,11 +33,8 @@ #include #include #include -#include #include -#include - #include "SVG.hpp" #include #include @@ -147,198 +143,6 @@ static std::vector resolve_obj_texture_path_candidates(const std::s return candidates; } -static bool decode_png_texture_rgba(const std::string &texture_path, - std::vector &out_rgba, - uint32_t &out_width, - uint32_t &out_height) -{ - out_rgba.clear(); - out_width = 0; - out_height = 0; - - boost::nowide::ifstream ifs(texture_path, std::ios::binary); - if (!ifs.is_open()) - return false; - - std::string encoded_data((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); - if (encoded_data.empty()) - return false; - - png::ReadBuf rb{encoded_data.data(), encoded_data.size()}; - png::ImageColorscale img; - if (!png::decode_colored_png(rb, img)) - return false; - - if (img.cols == 0 || img.rows == 0 || (img.bytes_per_pixel != 3 && img.bytes_per_pixel != 4)) - return false; - - size_t rgba_size = 0; - if (!checked_rgba_buffer_size(img.cols, img.rows, rgba_size)) - return false; - - const size_t row_stride = img.cols * size_t(img.bytes_per_pixel); - if (img.buf.size() < img.rows * row_stride) - return false; - - out_rgba.assign(rgba_size, 255); - for (size_t y = 0; y < img.rows; ++y) { - const size_t src_row_off = y * row_stride; - const size_t dst_row_off = y * img.cols * 4; - for (size_t x = 0; x < img.cols; ++x) { - const size_t src = src_row_off + x * size_t(img.bytes_per_pixel); - const size_t dst = dst_row_off + x * 4; - out_rgba[dst + 0] = img.buf[src + 0]; - out_rgba[dst + 1] = img.buf[src + 1]; - out_rgba[dst + 2] = img.buf[src + 2]; - out_rgba[dst + 3] = (img.bytes_per_pixel == 4) ? img.buf[src + 3] : uint8_t(255); - } - } - - out_width = uint32_t(img.cols); - out_height = uint32_t(img.rows); - return true; -} - -struct JpegDecodeErrorManager -{ - jpeg_error_mgr pub; - jmp_buf setjmp_buffer; -}; - -static void jpeg_decode_error_exit(j_common_ptr cinfo) -{ - auto *err = reinterpret_cast(cinfo->err); - longjmp(err->setjmp_buffer, 1); -} - -static bool decode_jpeg_texture_rgba(const std::string &texture_path, - std::vector &out_rgba, - uint32_t &out_width, - uint32_t &out_height) -{ - out_rgba.clear(); - out_width = 0; - out_height = 0; - - boost::nowide::ifstream ifs(texture_path, std::ios::binary); - if (!ifs.is_open()) - return false; - - std::string encoded_data((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); - if (encoded_data.empty()) - return false; - - jpeg_decompress_struct cinfo{}; - JpegDecodeErrorManager jerr{}; - cinfo.err = jpeg_std_error(&jerr.pub); - jerr.pub.error_exit = jpeg_decode_error_exit; - bool jpeg_created = false; - auto destroy_jpeg = [&cinfo, &jpeg_created]() { - if (jpeg_created) { - jpeg_destroy_decompress(&cinfo); - jpeg_created = false; - } - }; - - if (setjmp(jerr.setjmp_buffer)) { - destroy_jpeg(); - return false; - } - - jpeg_create_decompress(&cinfo); - jpeg_created = true; - jpeg_mem_src(&cinfo, - reinterpret_cast(encoded_data.data()), - static_cast(encoded_data.size())); - - if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) { - destroy_jpeg(); - return false; - } - - if (!jpeg_start_decompress(&cinfo)) { - destroy_jpeg(); - return false; - } - - const uint32_t width = cinfo.output_width; - const uint32_t height = cinfo.output_height; - const int components = cinfo.output_components; - size_t rgba_size = 0; - const size_t scanline_stride = size_t(width) * size_t(std::max(components, 0)); - if (!checked_rgba_buffer_size(width, height, rgba_size) || - components <= 0 || - scanline_stride > std::numeric_limits::max()) { - jpeg_finish_decompress(&cinfo); - destroy_jpeg(); - return false; - } - - out_rgba.assign(rgba_size, uint8_t(255)); - JSAMPARRAY scanline = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, - JPOOL_IMAGE, - JDIMENSION(scanline_stride), - 1); - - uint32_t y = 0; - while (cinfo.output_scanline < cinfo.output_height) { - jpeg_read_scanlines(&cinfo, scanline, 1); - const unsigned char *src = scanline[0]; - const uint32_t dst_y = height - 1 - y; - for (uint32_t x = 0; x < width; ++x) { - const size_t dst = (size_t(dst_y) * size_t(width) + size_t(x)) * 4; - if (components >= 3) { - const size_t s = size_t(x) * size_t(components); - out_rgba[dst + 0] = src[s + 0]; - out_rgba[dst + 1] = src[s + 1]; - out_rgba[dst + 2] = src[s + 2]; - } else { - const unsigned char g = src[x]; - out_rgba[dst + 0] = g; - out_rgba[dst + 1] = g; - out_rgba[dst + 2] = g; - } - out_rgba[dst + 3] = 255; - } - ++y; - } - - if (!jpeg_finish_decompress(&cinfo)) { - destroy_jpeg(); - return false; - } - destroy_jpeg(); - - out_width = width; - out_height = height; - return true; -} - -static bool decode_image_texture_rgba(const std::string &texture_path, - std::vector &out_rgba, - uint32_t &out_width, - uint32_t &out_height) -{ - out_rgba.clear(); - out_width = 0; - out_height = 0; - - if (boost::algorithm::iends_with(texture_path, ".png")) - return decode_png_texture_rgba(texture_path, out_rgba, out_width, out_height); - - if (boost::algorithm::iends_with(texture_path, ".jpg") || boost::algorithm::iends_with(texture_path, ".jpeg")) - return decode_jpeg_texture_rgba(texture_path, out_rgba, out_width, out_height); - - return false; -} - -static bool is_supported_obj_texture_path(const std::string &texture_path) -{ - return boost::algorithm::iends_with(texture_path, ".png") || - boost::algorithm::iends_with(texture_path, ".jpg") || - boost::algorithm::iends_with(texture_path, ".jpeg"); -} - struct ObjTextureImage { std::string resolved_path; @@ -381,7 +185,7 @@ static size_t count_resolved_obj_albedo_texture_references(const std::string &ob const std::string texture_ref = extract_obj_texture_reference(map_kd_raw); const auto candidates = resolve_obj_texture_path_candidates(obj_path, texture_ref); for (const std::string &candidate : candidates) { - if (!is_supported_obj_texture_path(candidate) || !boost::filesystem::exists(candidate)) + if (!is_supported_image_texture_path(candidate) || !boost::filesystem::exists(candidate)) continue; const std::string normalized = boost::filesystem::path(candidate).lexically_normal().string(); @@ -422,7 +226,7 @@ static ObjTextureImportData load_obj_albedo_textures(const std::string &obj_path ObjTextureImage image; image.resolved_path = candidate; - if (!decode_image_texture_rgba(candidate, image.rgba, image.width, image.height)) { + if (!decode_image_texture_rgba_from_file(candidate, image.rgba, image.width, image.height)) { had_decode_failure = true; last_failed_path = candidate; continue; @@ -938,6 +742,133 @@ Model Model::read_from_file(const std::string& } } } + else if (boost::algorithm::iends_with(input_file, ".gltf") || boost::algorithm::iends_with(input_file, ".glb")) { + GltfImportInfo gltf_info; + result = load_gltf(input_file.c_str(), &model, gltf_info, message); + if (result) { + for (ModelObject *obj : model.objects) + if (obj != nullptr) + obj->rotate(Geometry::deg2rad(90.0), Axis::X); + + if (!message.empty()) + BOOST_LOG_TRIVIAL(error) << message; + + const bool has_valid_texture_uvs = std::any_of(gltf_info.triangle_uvs_valid.begin(), gltf_info.triangle_uvs_valid.end(), [](uint8_t uv_valid) { + return uv_valid != 0; + }); + const ObjImportCapabilities capabilities{ + gltf_info.has_vertex_colors, + gltf_info.has_material_colors, + gltf_info.is_single_material_color, + gltf_info.textures.size(), + has_valid_texture_uvs + }; + const bool has_mode_selection = bool(objModeFn); + ObjImportMode import_mode = ObjImportMode::UseDefault; + const bool has_usable_uv_texture_data = capabilities.texture_count > 0 && capabilities.has_valid_texture_uvs; + if (has_mode_selection) { + import_mode = objModeFn(capabilities); + if (import_mode == ObjImportMode::UseDefault) + import_mode = has_usable_uv_texture_data ? ObjImportMode::ImportTextures : ObjImportMode::ImportPaintedRegions; + } + + if (!is_cb_cancel && model.objects.size() == 1) { + ModelObject *obj = model.objects.front(); + if (obj != nullptr && obj->volumes.size() == 1 && obj->volumes.front() != nullptr) { + ModelVolume *volume = obj->volumes.front(); + + volume->imported_vertex_colors_rgba.clear(); + volume->imported_texture_uvs_per_face.clear(); + volume->imported_texture_uv_valid.clear(); + volume->imported_texture_rgba.clear(); + volume->imported_texture_raw_filament_offsets.clear(); + volume->imported_texture_width = 0; + volume->imported_texture_height = 0; + volume->imported_texture_raw_channels = 0; + volume->imported_texture_raw_metadata_json.clear(); + volume->uv_map_generator_version = 0; + bool has_imported_usable_uv_texture_data = false; + + const size_t triangle_count = volume->mesh().its.indices.size(); + if (triangle_count == gltf_info.triangle_uvs.size() && + triangle_count == gltf_info.triangle_uvs_valid.size() && + triangle_count == gltf_info.triangle_texture_indices.size()) { + std::vector> triangle_uvs = gltf_info.triangle_uvs; + std::vector triangle_uv_valid = gltf_info.triangle_uvs_valid; + + const bool import_textures = has_mode_selection ? + (import_mode == ObjImportMode::ImportTextures) : + true; + + if (import_textures) { + std::vector atlas_rgba; + uint32_t atlas_width = 0; + uint32_t atlas_height = 0; + if (build_imported_texture_atlas(gltf_info.textures, + gltf_info.triangle_texture_indices, + triangle_uvs, + triangle_uv_valid, + atlas_rgba, + atlas_width, + atlas_height)) { + volume->imported_texture_uvs_per_face.reserve(triangle_count * 6); + volume->imported_texture_uv_valid.reserve(triangle_count); + bool has_any_valid_uv_face = false; + for (size_t face_idx = 0; face_idx < triangle_count; ++face_idx) { + const std::array &uv = triangle_uvs[face_idx]; + volume->imported_texture_uvs_per_face.emplace_back(uv[0].x()); + volume->imported_texture_uvs_per_face.emplace_back(uv[0].y()); + volume->imported_texture_uvs_per_face.emplace_back(uv[1].x()); + volume->imported_texture_uvs_per_face.emplace_back(uv[1].y()); + volume->imported_texture_uvs_per_face.emplace_back(uv[2].x()); + volume->imported_texture_uvs_per_face.emplace_back(uv[2].y()); + volume->imported_texture_uv_valid.emplace_back(triangle_uv_valid[face_idx]); + has_any_valid_uv_face = has_any_valid_uv_face || (triangle_uv_valid[face_idx] != 0); + } + volume->imported_texture_width = atlas_width; + volume->imported_texture_height = atlas_height; + volume->imported_texture_rgba = std::move(atlas_rgba); + volume->imported_texture_raw_filament_offsets.clear(); + volume->imported_texture_raw_channels = 0; + volume->imported_texture_raw_metadata_json.clear(); + volume->uv_map_generator_version = 0; + has_imported_usable_uv_texture_data = has_any_valid_uv_face; + } + } + } + + const bool import_vertex_colors = has_mode_selection ? + (import_mode == ObjImportMode::ImportPaintedRegions || + (import_mode == ObjImportMode::ImportTextures && !has_imported_usable_uv_texture_data)) : + true; + if (import_vertex_colors && + gltf_info.has_vertex_colors && + volume->mesh().its.vertices.size() == gltf_info.vertex_colors_rgba.size()) + volume->imported_vertex_colors_rgba = gltf_info.vertex_colors_rgba; + } + } + + const bool import_painted_regions = has_mode_selection ? + (import_mode == ObjImportMode::ImportPaintedRegions) : + true; + + if (!is_cb_cancel && import_painted_regions && objFn) { + ObjDialogInOut in_out; + in_out.model = &model; + if (gltf_info.has_vertex_colors && !gltf_info.vertex_colors.empty()) { + in_out.input_colors = gltf_info.vertex_colors; + in_out.is_single_color = false; + in_out.deal_vertex_color = true; + objFn(in_out); + } else if (gltf_info.has_material_colors && !gltf_info.material_colors.empty()) { + in_out.input_colors = gltf_info.material_colors; + in_out.is_single_color = gltf_info.is_single_material_color; + in_out.deal_vertex_color = false; + objFn(in_out); + } + } + } + } else if (boost::algorithm::iends_with(input_file, ".svg")) result = load_svg(input_file.c_str(), &model, message); //BBS: remove the old .amf.xml files diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 2d0dcaa817d..a273c7716d0 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -563,10 +563,10 @@ static const FileWildcards file_wildcards_by_type[FT_SIZE] = { /* FT_GCODE */ { L("G-code files"), { ".gcode"sv} }, #ifdef __APPLE__ /* FT_MODEL */ - {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}}, + {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}}, #else /* FT_MODEL */ - {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".drc"sv}}, + {L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".drc"sv}}, #endif /* FT_ZIP */ { L("ZIP files"), { ".zip"sv } }, /* FT_PROJECT */ { L("Project files"), { ".3mf"sv} }, diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 2c8fd7f2f85..3c634c9fb11 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -2717,13 +2717,13 @@ void MainFrame::init_menubar_as_editor() // BBS wxMenu *import_menu = new wxMenu(); #ifndef __APPLE__ - append_menu_item(import_menu, wxID_ANY, _L("Import 3MF/STL/STEP/SVG/OBJ/AMF") + dots + "\t" + ctrl + "I", _L("Load a model"), + append_menu_item(import_menu, wxID_ANY, _L("Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF") + dots + "\t" + ctrl + "I", _L("Load a model"), [this](wxCommandEvent&) { if (m_plater) { m_plater->add_file(); } }, "menu_import", nullptr, [this](){return can_add_models(); }, this); #else - append_menu_item(import_menu, wxID_ANY, _L("Import 3MF/STL/STEP/SVG/OBJ/AMF") + dots + "\t" + ctrl + "I", _L("Load a model"), + append_menu_item(import_menu, wxID_ANY, _L("Import 3MF/STL/STEP/SVG/OBJ/GLTF/GLB/AMF") + dots + "\t" + ctrl + "I", _L("Load a model"), [this](wxCommandEvent&) { if (m_plater) { m_plater->add_model(); } }, "", nullptr, [this](){return can_add_models(); }, this); #endif diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 14eb732e7b8..24891a620a1 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -411,11 +411,18 @@ wxString sanitize_window_layout_for_wayland(const wxString& layout, bool* remove } #endif +static bool is_color_import_choice_file(const std::string &path) +{ + return boost::iends_with(path, ".obj") || + boost::iends_with(path, ".gltf") || + boost::iends_with(path, ".glb"); +} + static ObjImportMode show_obj_import_choice_dialog(wxWindow *parent, const ObjImportCapabilities &capabilities) { wxDialog dialog(parent ? parent : static_cast(wxGetApp().mainframe), wxID_ANY, - _L("OBJ import"), + _L("Color import"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE); @@ -426,7 +433,7 @@ static ObjImportMode show_obj_import_choice_dialog(wxWindow *parent, const ObjIm line_top->SetBackgroundColour(wxColour(166, 169, 170)); main_sizer->Add(line_top, 0, wxEXPAND, 0); - auto *message = new wxStaticText(&dialog, wxID_ANY, _L("Choose how to import this OBJ file.")); + auto *message = new wxStaticText(&dialog, wxID_ANY, _L("Choose how to import this model's color data.")); message->Wrap(dialog.FromDIP(420)); main_sizer->Add(message, 0, wxALL | wxEXPAND, dialog.FromDIP(16)); @@ -9271,7 +9278,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ bool obj_imported_for_texture_mapping = false; auto obj_color_fun = [this, &path](ObjDialogInOut &in_out) { - if (!boost::iends_with(path.string(), ".obj")) { return; } + if (!is_color_import_choice_file(path.string())) { return; } const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); ObjColorDialog color_dlg(nullptr, in_out, extruder_colours); if (color_dlg.ShowModal() != wxID_OK) { @@ -9279,7 +9286,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ } }; auto obj_import_mode_fun = [this, &path, &obj_imported_for_texture_mapping](const ObjImportCapabilities &capabilities) -> ObjImportMode { - if (!boost::iends_with(path.string(), ".obj")) + if (!is_color_import_choice_file(path.string())) return ObjImportMode::UseDefault; const ObjImportMode mode = show_obj_import_choice_dialog(q, capabilities); obj_imported_for_texture_mapping = mode == ObjImportMode::ImportTextures; @@ -11492,7 +11499,7 @@ void Plater::priv::reload_from_disk() const auto& path = input_paths[i].string(); bool obj_imported_for_texture_mapping = false; auto obj_color_fun = [this, &path](ObjDialogInOut &in_out) { - if (!boost::iends_with(path, ".obj")) { return; } + if (!is_color_import_choice_file(path)) { return; } const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); ObjColorDialog color_dlg(nullptr, in_out, extruder_colours); if (color_dlg.ShowModal() != wxID_OK) { @@ -11500,7 +11507,7 @@ void Plater::priv::reload_from_disk() } }; auto obj_import_mode_fun = [this, &path, &obj_imported_for_texture_mapping](const ObjImportCapabilities &capabilities) -> ObjImportMode { - if (!boost::iends_with(path, ".obj")) + if (!is_color_import_choice_file(path)) return ObjImportMode::UseDefault; const ObjImportMode mode = show_obj_import_choice_dialog(q, capabilities); obj_imported_for_texture_mapping = mode == ObjImportMode::ImportTextures;