From 5bdfb53566f1431acf219b9a90ed5b5dd2ad78b6 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Thu, 24 Sep 2026 20:53:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20LOD=20=E7=BD=91=E6=A0=BC=E5=88=86?= =?UTF-8?q?=E7=BA=A7=E2=80=94=E2=80=94meshopt=20=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E6=9C=9F=E7=AE=80=E5=8C=96=20+=20=E6=8C=89=E8=B7=9D=E7=A6=BB/?= =?UTF-8?q?=E7=BA=A7=E8=81=94=E9=80=89=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vendored meshoptimizer 1.2(submodule 环境故障改直接 vendor)。Mesh 多 LOD:各级索引连续拼进同一 index buffer(共享顶点,LodRange 区间 选择),解析期 meshopt_simplify 生成链(LOD0=原始,每级目标减半, 相对误差 1e-2,小网格复用上级);都在 worker 线程,加载零额外卡顿。 SceneRenderer 按 (mesh, LOD) 聚合 batch(lookup key = 指针<<8|lod): 主视图按屏占比近似 radius/dist 选级(阈值 10/20 倍半径);阴影级联 按级联序降级(越远越简)。Draw 按 batch.lod 选索引区间。 A/B 对比(100 背包 10×10,RTX 5060 Laptop,vsync off): - 纯背包:LOD 开 93-95 fps / 关 90-107 fps —— 无显著差异 - 加 SunTemple:LOD 开 52-56 / 关 51-55 —— 无显著差异 结论:当前场景瓶颈不在顶点数(5060 顶点吞吐富余),LOD 基础设施 保留(几何复杂资产/低端 GPU/更大场景生效),帧率问题转向 Tracy GPU 时间线定位真实瓶颈。 坑:LodRange 记录"LOD 不足复用最后一级"时 first 不得继续累加 (buffer 无重复数据,累加必越界——VUID-08798 抓的)。 --- Engine/CMakeLists.txt | 6 + Engine/include/Engine/Renderer/Mesh.h | 26 +- Engine/include/Engine/Renderer/Model.h | 4 +- .../include/Engine/Renderer/SceneRenderer.h | 4 +- Engine/include/Engine/Renderer/SceneTypes.h | 4 +- Engine/src/Renderer/Mesh.cpp | 42 +- Engine/src/Renderer/Model.cpp | 37 +- Engine/src/Renderer/Passes/ForwardPass.cpp | 2 +- Engine/src/Renderer/Passes/ShadowPass.cpp | 2 +- Engine/src/Renderer/SceneRenderer.cpp | 27 +- Lib/meshoptimizer/.clang-format | 15 + Lib/meshoptimizer/.editorconfig | 11 + Lib/meshoptimizer/.git-blame-ignore-revs | 17 + .../.github/ISSUE_TEMPLATE/bug_report.md | 8 + .../.github/ISSUE_TEMPLATE/config.yml | 5 + .../.github/ISSUE_TEMPLATE/feature_request.md | 8 + Lib/meshoptimizer/.github/codecov.yml | 11 + Lib/meshoptimizer/.github/workflows/build.yml | 240 + .../.github/workflows/cifuzz.yml | 33 + .../.github/workflows/release.yml | 77 + Lib/meshoptimizer/.gitignore | 19 + Lib/meshoptimizer/.prettierrc | 16 + Lib/meshoptimizer/CMakeLists.txt | 227 + Lib/meshoptimizer/CONTRIBUTING.md | 63 + Lib/meshoptimizer/LICENSE.md | 21 + Lib/meshoptimizer/Makefile | 271 + Lib/meshoptimizer/README.md | 967 ++ Lib/meshoptimizer/demo/ansi.c | 2 + Lib/meshoptimizer/demo/clusterlod.h | 962 ++ Lib/meshoptimizer/demo/index.html | 148 + Lib/meshoptimizer/demo/main.cpp | 1978 +++ Lib/meshoptimizer/demo/meshletdec.slang | 209 + Lib/meshoptimizer/demo/nanite.cpp | 523 + Lib/meshoptimizer/demo/pirate.glb | Bin 0 -> 24696 bytes Lib/meshoptimizer/demo/pirate.obj | 11358 ++++++++++++++++ Lib/meshoptimizer/demo/remesh.html | 815 ++ Lib/meshoptimizer/demo/repaint.js | 593 + Lib/meshoptimizer/demo/simplify.html | 1102 ++ Lib/meshoptimizer/demo/tests.cpp | 3708 +++++ Lib/meshoptimizer/extern/.clang-format | 1 + Lib/meshoptimizer/extern/cgltf.h | 7240 ++++++++++ Lib/meshoptimizer/extern/fast_obj.h | 1611 +++ Lib/meshoptimizer/extern/sdefl.h | 800 ++ Lib/meshoptimizer/gltf/README.md | 133 + Lib/meshoptimizer/gltf/animation.cpp | 356 + Lib/meshoptimizer/gltf/cli.js | 25 + Lib/meshoptimizer/gltf/encodebasis.cpp | 214 + Lib/meshoptimizer/gltf/encodewebp.cpp | 169 + Lib/meshoptimizer/gltf/fileio.cpp | 115 + Lib/meshoptimizer/gltf/fuzz.dict | 303 + Lib/meshoptimizer/gltf/fuzz.glb | Bin 0 -> 6540 bytes Lib/meshoptimizer/gltf/gltfpack.cpp | 1871 +++ Lib/meshoptimizer/gltf/gltfpack.h | 449 + Lib/meshoptimizer/gltf/gltfpack.manifest | 9 + Lib/meshoptimizer/gltf/image.cpp | 372 + Lib/meshoptimizer/gltf/json.cpp | 92 + Lib/meshoptimizer/gltf/library.js | 355 + Lib/meshoptimizer/gltf/material.cpp | 637 + Lib/meshoptimizer/gltf/mesh.cpp | 1419 ++ Lib/meshoptimizer/gltf/node.cpp | 206 + Lib/meshoptimizer/gltf/package.json | 29 + Lib/meshoptimizer/gltf/parsegltf.cpp | 704 + Lib/meshoptimizer/gltf/parselib.cpp | 12 + Lib/meshoptimizer/gltf/parseobj.cpp | 326 + Lib/meshoptimizer/gltf/stream.cpp | 913 ++ Lib/meshoptimizer/gltf/wasistubs.cpp | 45 + Lib/meshoptimizer/gltf/wasistubs.txt | 2 + Lib/meshoptimizer/gltf/write.cpp | 1821 +++ Lib/meshoptimizer/js/README.md | 314 + Lib/meshoptimizer/js/benchmark.js | 150 + Lib/meshoptimizer/js/index.d.ts | 5 + Lib/meshoptimizer/js/index.js | 5 + Lib/meshoptimizer/js/meshopt_clusterizer.d.ts | 69 + Lib/meshoptimizer/js/meshopt_clusterizer.js | 381 + .../js/meshopt_clusterizer.test.js | 135 + Lib/meshoptimizer/js/meshopt_decoder.cjs | 199 + Lib/meshoptimizer/js/meshopt_decoder.d.ts | 15 + Lib/meshoptimizer/js/meshopt_decoder.mjs | 196 + Lib/meshoptimizer/js/meshopt_decoder.test.js | 356 + .../js/meshopt_decoder_reference.js | 472 + Lib/meshoptimizer/js/meshopt_encoder.d.ts | 23 + Lib/meshoptimizer/js/meshopt_encoder.js | 221 + Lib/meshoptimizer/js/meshopt_encoder.test.js | 243 + Lib/meshoptimizer/js/meshopt_simplifier.d.ts | 80 + Lib/meshoptimizer/js/meshopt_simplifier.js | 676 + .../js/meshopt_simplifier.test.js | 203 + Lib/meshoptimizer/js/meshopt_tangents.d.ts | 28 + Lib/meshoptimizer/js/meshopt_tangents.js | 220 + Lib/meshoptimizer/js/meshopt_tangents.test.js | 124 + Lib/meshoptimizer/js/package.json | 59 + Lib/meshoptimizer/js/wasi_trace.js | 54 + Lib/meshoptimizer/src/allocator.cpp | 21 + Lib/meshoptimizer/src/clusterizer.cpp | 1377 ++ Lib/meshoptimizer/src/indexanalyzer.cpp | 125 + Lib/meshoptimizer/src/indexcodec.cpp | 703 + Lib/meshoptimizer/src/indexgenerator.cpp | 825 ++ Lib/meshoptimizer/src/meshletcodec.cpp | 1051 ++ Lib/meshoptimizer/src/meshletutils.cpp | 619 + Lib/meshoptimizer/src/meshoptimizer.h | 1726 +++ Lib/meshoptimizer/src/opacitymap.cpp | 557 + Lib/meshoptimizer/src/overdrawoptimizer.cpp | 333 + Lib/meshoptimizer/src/partition.cpp | 621 + Lib/meshoptimizer/src/quantization.cpp | 127 + Lib/meshoptimizer/src/rasterizer.cpp | 289 + Lib/meshoptimizer/src/remesher.cpp | 751 + Lib/meshoptimizer/src/simplifier.cpp | 2947 ++++ Lib/meshoptimizer/src/spatialorder.cpp | 341 + Lib/meshoptimizer/src/stripifier.cpp | 296 + Lib/meshoptimizer/src/tangentspace.cpp | 730 + Lib/meshoptimizer/src/vcacheoptimizer.cpp | 467 + Lib/meshoptimizer/src/vertexcodec.cpp | 1880 +++ Lib/meshoptimizer/src/vertexfilter.cpp | 1503 ++ Lib/meshoptimizer/src/vfetchoptimizer.cpp | 71 + Lib/meshoptimizer/tools/bitmask.py | 20 + Lib/meshoptimizer/tools/clusterfuzz.cpp | 61 + Lib/meshoptimizer/tools/codecbench.cpp | 453 + Lib/meshoptimizer/tools/codecfuzz.cpp | 184 + Lib/meshoptimizer/tools/codectest.cpp | 233 + Lib/meshoptimizer/tools/gltfbasis.py | 77 + Lib/meshoptimizer/tools/objloader.cpp | 6 + Lib/meshoptimizer/tools/simplifyfuzz.cpp | 46 + Lib/meshoptimizer/tools/vcachetester.cpp | 513 + Lib/meshoptimizer/tools/vcachetuner.cpp | 502 + Lib/meshoptimizer/tools/wasmpack.py | 48 + Lib/meshoptimizer/tools/wasmstubs.cpp | 116 + 125 files changed, 67608 insertions(+), 29 deletions(-) create mode 100644 Lib/meshoptimizer/.clang-format create mode 100644 Lib/meshoptimizer/.editorconfig create mode 100644 Lib/meshoptimizer/.git-blame-ignore-revs create mode 100644 Lib/meshoptimizer/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 Lib/meshoptimizer/.github/ISSUE_TEMPLATE/config.yml create mode 100644 Lib/meshoptimizer/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 Lib/meshoptimizer/.github/codecov.yml create mode 100644 Lib/meshoptimizer/.github/workflows/build.yml create mode 100644 Lib/meshoptimizer/.github/workflows/cifuzz.yml create mode 100644 Lib/meshoptimizer/.github/workflows/release.yml create mode 100644 Lib/meshoptimizer/.gitignore create mode 100644 Lib/meshoptimizer/.prettierrc create mode 100644 Lib/meshoptimizer/CMakeLists.txt create mode 100644 Lib/meshoptimizer/CONTRIBUTING.md create mode 100644 Lib/meshoptimizer/LICENSE.md create mode 100644 Lib/meshoptimizer/Makefile create mode 100644 Lib/meshoptimizer/README.md create mode 100644 Lib/meshoptimizer/demo/ansi.c create mode 100644 Lib/meshoptimizer/demo/clusterlod.h create mode 100644 Lib/meshoptimizer/demo/index.html create mode 100644 Lib/meshoptimizer/demo/main.cpp create mode 100644 Lib/meshoptimizer/demo/meshletdec.slang create mode 100644 Lib/meshoptimizer/demo/nanite.cpp create mode 100644 Lib/meshoptimizer/demo/pirate.glb create mode 100644 Lib/meshoptimizer/demo/pirate.obj create mode 100644 Lib/meshoptimizer/demo/remesh.html create mode 100644 Lib/meshoptimizer/demo/repaint.js create mode 100644 Lib/meshoptimizer/demo/simplify.html create mode 100644 Lib/meshoptimizer/demo/tests.cpp create mode 100644 Lib/meshoptimizer/extern/.clang-format create mode 100644 Lib/meshoptimizer/extern/cgltf.h create mode 100644 Lib/meshoptimizer/extern/fast_obj.h create mode 100644 Lib/meshoptimizer/extern/sdefl.h create mode 100644 Lib/meshoptimizer/gltf/README.md create mode 100644 Lib/meshoptimizer/gltf/animation.cpp create mode 100644 Lib/meshoptimizer/gltf/cli.js create mode 100644 Lib/meshoptimizer/gltf/encodebasis.cpp create mode 100644 Lib/meshoptimizer/gltf/encodewebp.cpp create mode 100644 Lib/meshoptimizer/gltf/fileio.cpp create mode 100644 Lib/meshoptimizer/gltf/fuzz.dict create mode 100644 Lib/meshoptimizer/gltf/fuzz.glb create mode 100644 Lib/meshoptimizer/gltf/gltfpack.cpp create mode 100644 Lib/meshoptimizer/gltf/gltfpack.h create mode 100644 Lib/meshoptimizer/gltf/gltfpack.manifest create mode 100644 Lib/meshoptimizer/gltf/image.cpp create mode 100644 Lib/meshoptimizer/gltf/json.cpp create mode 100644 Lib/meshoptimizer/gltf/library.js create mode 100644 Lib/meshoptimizer/gltf/material.cpp create mode 100644 Lib/meshoptimizer/gltf/mesh.cpp create mode 100644 Lib/meshoptimizer/gltf/node.cpp create mode 100644 Lib/meshoptimizer/gltf/package.json create mode 100644 Lib/meshoptimizer/gltf/parsegltf.cpp create mode 100644 Lib/meshoptimizer/gltf/parselib.cpp create mode 100644 Lib/meshoptimizer/gltf/parseobj.cpp create mode 100644 Lib/meshoptimizer/gltf/stream.cpp create mode 100644 Lib/meshoptimizer/gltf/wasistubs.cpp create mode 100644 Lib/meshoptimizer/gltf/wasistubs.txt create mode 100644 Lib/meshoptimizer/gltf/write.cpp create mode 100644 Lib/meshoptimizer/js/README.md create mode 100644 Lib/meshoptimizer/js/benchmark.js create mode 100644 Lib/meshoptimizer/js/index.d.ts create mode 100644 Lib/meshoptimizer/js/index.js create mode 100644 Lib/meshoptimizer/js/meshopt_clusterizer.d.ts create mode 100644 Lib/meshoptimizer/js/meshopt_clusterizer.js create mode 100644 Lib/meshoptimizer/js/meshopt_clusterizer.test.js create mode 100644 Lib/meshoptimizer/js/meshopt_decoder.cjs create mode 100644 Lib/meshoptimizer/js/meshopt_decoder.d.ts create mode 100644 Lib/meshoptimizer/js/meshopt_decoder.mjs create mode 100644 Lib/meshoptimizer/js/meshopt_decoder.test.js create mode 100644 Lib/meshoptimizer/js/meshopt_decoder_reference.js create mode 100644 Lib/meshoptimizer/js/meshopt_encoder.d.ts create mode 100644 Lib/meshoptimizer/js/meshopt_encoder.js create mode 100644 Lib/meshoptimizer/js/meshopt_encoder.test.js create mode 100644 Lib/meshoptimizer/js/meshopt_simplifier.d.ts create mode 100644 Lib/meshoptimizer/js/meshopt_simplifier.js create mode 100644 Lib/meshoptimizer/js/meshopt_simplifier.test.js create mode 100644 Lib/meshoptimizer/js/meshopt_tangents.d.ts create mode 100644 Lib/meshoptimizer/js/meshopt_tangents.js create mode 100644 Lib/meshoptimizer/js/meshopt_tangents.test.js create mode 100644 Lib/meshoptimizer/js/package.json create mode 100644 Lib/meshoptimizer/js/wasi_trace.js create mode 100644 Lib/meshoptimizer/src/allocator.cpp create mode 100644 Lib/meshoptimizer/src/clusterizer.cpp create mode 100644 Lib/meshoptimizer/src/indexanalyzer.cpp create mode 100644 Lib/meshoptimizer/src/indexcodec.cpp create mode 100644 Lib/meshoptimizer/src/indexgenerator.cpp create mode 100644 Lib/meshoptimizer/src/meshletcodec.cpp create mode 100644 Lib/meshoptimizer/src/meshletutils.cpp create mode 100644 Lib/meshoptimizer/src/meshoptimizer.h create mode 100644 Lib/meshoptimizer/src/opacitymap.cpp create mode 100644 Lib/meshoptimizer/src/overdrawoptimizer.cpp create mode 100644 Lib/meshoptimizer/src/partition.cpp create mode 100644 Lib/meshoptimizer/src/quantization.cpp create mode 100644 Lib/meshoptimizer/src/rasterizer.cpp create mode 100644 Lib/meshoptimizer/src/remesher.cpp create mode 100644 Lib/meshoptimizer/src/simplifier.cpp create mode 100644 Lib/meshoptimizer/src/spatialorder.cpp create mode 100644 Lib/meshoptimizer/src/stripifier.cpp create mode 100644 Lib/meshoptimizer/src/tangentspace.cpp create mode 100644 Lib/meshoptimizer/src/vcacheoptimizer.cpp create mode 100644 Lib/meshoptimizer/src/vertexcodec.cpp create mode 100644 Lib/meshoptimizer/src/vertexfilter.cpp create mode 100644 Lib/meshoptimizer/src/vfetchoptimizer.cpp create mode 100644 Lib/meshoptimizer/tools/bitmask.py create mode 100644 Lib/meshoptimizer/tools/clusterfuzz.cpp create mode 100644 Lib/meshoptimizer/tools/codecbench.cpp create mode 100644 Lib/meshoptimizer/tools/codecfuzz.cpp create mode 100644 Lib/meshoptimizer/tools/codectest.cpp create mode 100644 Lib/meshoptimizer/tools/gltfbasis.py create mode 100644 Lib/meshoptimizer/tools/objloader.cpp create mode 100644 Lib/meshoptimizer/tools/simplifyfuzz.cpp create mode 100644 Lib/meshoptimizer/tools/vcachetester.cpp create mode 100644 Lib/meshoptimizer/tools/vcachetuner.cpp create mode 100644 Lib/meshoptimizer/tools/wasmpack.py create mode 100644 Lib/meshoptimizer/tools/wasmstubs.cpp diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 108c165..44987a8 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -47,4 +47,10 @@ target_compile_definitions(Engine PUBLIC TRACY_ON_DEMAND) find_package(Threads REQUIRED) target_link_libraries(Engine PUBLIC Threads::Threads) +# meshoptimizer(vendored,LOD 网格简化;submodule 机制故障改为直接 vendor) +file(GLOB MESHOPT_SOURCES ${CMAKE_SOURCE_DIR}/Lib/meshoptimizer/src/*.cpp) +add_library(meshopt STATIC ${MESHOPT_SOURCES}) +target_include_directories(meshopt PUBLIC ${CMAKE_SOURCE_DIR}/Lib/meshoptimizer/src) +target_link_libraries(Engine PUBLIC meshopt) + target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1) \ No newline at end of file diff --git a/Engine/include/Engine/Renderer/Mesh.h b/Engine/include/Engine/Renderer/Mesh.h index dc26873..f26e068 100644 --- a/Engine/include/Engine/Renderer/Mesh.h +++ b/Engine/include/Engine/Renderer/Mesh.h @@ -18,13 +18,18 @@ namespace Engine class Buffer; class Material; + // LOD 级数:LOD0 = 原始网格,越高越简(解析期 meshopt_simplify 生成, + // 共享顶点数据,只换索引区间) + inline constexpr u32 kMeshLodCount = 3; + class Mesh { public: // material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份) + // lods[0] 必须为原始索引(与 vertices 对应),各级简化链由解析期生成 Mesh(Device& device, MemoryAllocator& allocator, const std::vector& vertices, - const std::vector& indices, + const std::vector>& lods, Material* material); ~Mesh(); @@ -32,11 +37,16 @@ namespace Engine Mesh& operator=(const Mesh&) = delete; // instanceCount/firstInstance:GPUDriven 按 mesh 分 batch 后, - // 同 mesh 的 N 个实体一次 draw 完成,模型矩阵经实例索引取 - void Draw(vk::CommandBuffer cmd, u32 instanceCount = 1, u32 firstInstance = 0) const; + // 同 mesh 的 N 个实体一次 draw 完成,模型矩阵经实例索引取。 + // lod:索引区间选择(越界取最高级) + void Draw(vk::CommandBuffer cmd, u32 instanceCount = 1, + u32 firstInstance = 0, u32 lod = 0) const; Material* GetMaterial() const { return m_Material; } - u32 GetIndexCount() const { return m_IndexCount; } + u32 GetLodIndexCount(u32 lod) const + { + return m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1].indexCount; + } // 局部空间包围球(构造时从顶点算出),视锥剔除用 struct Bounds { @@ -46,10 +56,16 @@ namespace Engine const Bounds& GetBounds() const { return m_Bounds; } private: + // 同一 index buffer 内连续排布的各级索引区间 + struct LodRange { + u32 firstIndex = 0; + u32 indexCount = 0; + }; + Scope m_VertexBuffer; Scope m_IndexBuffer; + std::array m_Lods{}; Material* m_Material; - u32 m_IndexCount = 0; Bounds m_Bounds; u64 m_VertexBytes = 0; diff --git a/Engine/include/Engine/Renderer/Model.h b/Engine/include/Engine/Renderer/Model.h index b48645c..ad55e91 100644 --- a/Engine/include/Engine/Renderer/Model.h +++ b/Engine/include/Engine/Renderer/Model.h @@ -34,7 +34,9 @@ namespace Engine // worker 线程产出(ParseFromDisk),主线程消费(FromParsed)。 struct ParsedMesh { std::vector vertices; - std::vector indices; + // LOD 链([0] = 原始索引,越往后越简):解析期 meshopt_simplify + // 生成(worker 线程),消费端共享顶点只换索引 + std::vector> lods; std::array texPaths; MaterialParams params; }; diff --git a/Engine/include/Engine/Renderer/SceneRenderer.h b/Engine/include/Engine/Renderer/SceneRenderer.h index f38b825..5c0aaec 100644 --- a/Engine/include/Engine/Renderer/SceneRenderer.h +++ b/Engine/include/Engine/Renderer/SceneRenderer.h @@ -73,7 +73,9 @@ namespace Engine { // 时实例分散各处),扁平化成连续数组推迟到 appendView std::vector> batchWorlds; std::vector> batchPrevWorlds; - std::unordered_map lookup; + // key = (Mesh*, LOD):同 mesh 不同 LOD 索引区间不同, + // 必须分开成 batch(低 8 位 LOD,高位指针,指针 8 字节对齐安全) + std::unordered_map lookup; void Clear() { diff --git a/Engine/include/Engine/Renderer/SceneTypes.h b/Engine/include/Engine/Renderer/SceneTypes.h index 03e6033..834f515 100644 --- a/Engine/include/Engine/Renderer/SceneTypes.h +++ b/Engine/include/Engine/Renderer/SceneTypes.h @@ -19,11 +19,13 @@ namespace Engine { class Mesh; - // GPUDriven:按 mesh 聚合的绘制批 + // GPUDriven:按 (mesh, LOD) 聚合的绘制批——同 mesh 不同 LOD 的实例 + // 索引区间不同,不能并成一次 draw struct InstanceBatch { Mesh* mesh; u32 first; // 在实例矩阵数组中的起始下标 u32 count; // 实例数 + u32 lod = 0; // 绘制用索引区间(Mesh::Draw) }; // 一个渲染视图的剔除结果(相机视图 / 光源视图各一份) diff --git a/Engine/src/Renderer/Mesh.cpp b/Engine/src/Renderer/Mesh.cpp index d97e2b3..8b0b07a 100644 --- a/Engine/src/Renderer/Mesh.cpp +++ b/Engine/src/Renderer/Mesh.cpp @@ -17,13 +17,13 @@ namespace Engine { Mesh::Mesh(Device& device, MemoryAllocator& allocator, const std::vector& vertices, - const std::vector& indices, + const std::vector>& lods, Material* material) : m_Material(material) { - if (vertices.empty() || indices.empty()) + if (vertices.empty() || lods.empty() || lods[0].empty()) { - ENGINE_ERROR("Mesh: empty vertices or indices"); + ENGINE_ERROR("Mesh: empty vertices or LOD0 indices"); return; } @@ -35,15 +35,31 @@ namespace Engine vk::MemoryPropertyFlagBits::eHostCoherent); m_VertexBuffer->Upload(vertices.data(), vbSize); - u64 ibSize = sizeof(u32) * indices.size(); + // 各级索引连续拼进同一 index buffer(共享顶点,只换索引区间) + std::vector allIndices; + for (const auto& idx : lods) { + allIndices.insert(allIndices.end(), idx.begin(), idx.end()); + } + + u64 ibSize = sizeof(u32) * allIndices.size(); m_IndexBuffer = CreateScope( device, allocator, ibSize, vk::BufferUsageFlagBits::eIndexBuffer, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); - m_IndexBuffer->Upload(indices.data(), ibSize); + m_IndexBuffer->Upload(allIndices.data(), ibSize); - m_IndexCount = static_cast(indices.size()); + // 记录各级区间。LOD 不足的级复用最后一级的"已有区间" + //(first 不得继续累加——buffer 里没有重复数据,累加必越界) + u32 first = 0; + for (u32 lod = 0; lod < kMeshLodCount; ++lod) { + if (lod < lods.size()) { + m_Lods[lod] = {first, static_cast(lods[lod].size())}; + first += m_Lods[lod].indexCount; + } else { + m_Lods[lod] = m_Lods[lods.size() - 1]; + } + } // 局部空间包围球:顶点包围盒 → 中心 + 最大半径(视锥剔除用) { @@ -61,10 +77,12 @@ namespace Engine } } - ENGINE_INFO("Mesh created: {} vertices, {} indices", vertices.size(), indices.size()); + ENGINE_INFO("Mesh created: {} vertices, LOD0 {} indices", vertices.size(), lods[0].size()); m_VertexBytes = sizeof(Vertex) * vertices.size(); - m_IndexBytes = sizeof(u32) * indices.size(); + for (const auto& idx : lods) { + m_IndexBytes += sizeof(u32) * idx.size(); + } STATS.AddMesh(m_VertexBytes, m_IndexBytes); } @@ -73,10 +91,14 @@ namespace Engine STATS.RemoveMesh(m_VertexBytes, m_IndexBytes); } - void Mesh::Draw(vk::CommandBuffer cmd, u32 instanceCount, u32 firstInstance) const + void Mesh::Draw(vk::CommandBuffer cmd, u32 instanceCount, u32 firstInstance, + u32 lod) const { + const LodRange& range = + m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1]; cmd.bindVertexBuffers(0, m_VertexBuffer->Get(), {0}); cmd.bindIndexBuffer(m_IndexBuffer->Get(), 0, vk::IndexType::eUint32); - cmd.drawIndexed(m_IndexCount, instanceCount, 0, 0, firstInstance); + cmd.drawIndexed(range.indexCount, instanceCount, range.firstIndex, + 0, firstInstance); } } // namespace Engine diff --git a/Engine/src/Renderer/Model.cpp b/Engine/src/Renderer/Model.cpp index 5ec5ded..9be1986 100644 --- a/Engine/src/Renderer/Model.cpp +++ b/Engine/src/Renderer/Model.cpp @@ -14,6 +14,8 @@ #include #include +#include + #include #include @@ -81,13 +83,14 @@ Model::Model(Device& device, MemoryAllocator& allocator, {{ h, 0.0f, -h}, {0, 1, 0}, {0.0f, 0.0f}}, }; const std::vector indices = {0, 1, 2, 0, 2, 3}; + const std::vector> lodIndices = {indices}; // 材质/纹理生命周期随模型(缓存持有) std::string paths[Material::kSlotCount] = {"", "", "", ""}; Material* mat = GetOrCreateMaterial(paths, MaterialParams{}); // 全默认材质 m_Meshes.push_back(CreateScope( - m_Device, m_Allocator, vertices, indices, mat)); + m_Device, m_Allocator, vertices, lodIndices, mat)); } Scope Model::CreatePlane(Device& device, MemoryAllocator& allocator, @@ -158,7 +161,7 @@ Scope Model::FromParsed(Device& device, MemoryAllocator& allocator, for (auto& pm : parsed.meshes) { Material* mat = model->GetOrCreateMaterial(pm.texPaths.data(), pm.params); model->m_Meshes.push_back(CreateScope( - device, allocator, std::move(pm.vertices), std::move(pm.indices), mat)); + device, allocator, pm.vertices, pm.lods, mat)); } ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)", parsed.path, model->m_Meshes.size(), model->m_TextureCache.size()); @@ -177,7 +180,7 @@ void Model::LoadModel(const std::string& path) for (auto& pm : parsed.meshes) { Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params); m_Meshes.push_back(CreateScope( - m_Device, m_Allocator, std::move(pm.vertices), std::move(pm.indices), mat)); + m_Device, m_Allocator, pm.vertices, pm.lods, mat)); } ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)", path, m_Meshes.size(), m_TextureCache.size()); @@ -256,7 +259,7 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene, { ParsedMesh out; std::vector& vertices = out.vertices; - std::vector& indices = out.indices; + std::vector indices; vertices.reserve(mesh->mNumVertices); @@ -386,6 +389,32 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene, out.texPaths[slot] = std::move(texPaths[slot]); } out.params = params; + + // ---- LOD 链(解析期生成,worker 线程免费时间):---- + // LOD0 = 原始;每级目标索引数减半,相对误差 1e-2。顶点共享, + // 简化只重排索引(meshopt_simplify 从 LOD0 单次起点逐级向下) + out.lods.push_back(std::move(indices)); + for (u32 lod = 1; lod < kMeshLodCount; ++lod) { + const size_t target = out.lods[0].size() >> lod; + // 太小的网格不简化:简化收益低于误差风险,直接复用上一级 + if (target < 96 || out.lods[0].size() < 192) { + out.lods.push_back(out.lods.back()); + continue; + } + std::vector dest(out.lods[0].size()); + float error = 0.0f; + const size_t count = meshopt_simplify( + dest.data(), + out.lods[0].data(), out.lods[0].size(), + &out.vertices[0].position.x, out.vertices.size(), sizeof(Vertex), + target, 1e-2f, 0, &error); + if (count == 0) { + out.lods.push_back(out.lods.back()); // 简化失败兜底 + continue; + } + dest.resize(count); + out.lods.push_back(std::move(dest)); + } return out; } diff --git a/Engine/src/Renderer/Passes/ForwardPass.cpp b/Engine/src/Renderer/Passes/ForwardPass.cpp index bea2844..e343912 100644 --- a/Engine/src/Renderer/Passes/ForwardPass.cpp +++ b/Engine/src/Renderer/Passes/ForwardPass.cpp @@ -139,7 +139,7 @@ namespace Engine::Passes cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, m_Gpu.forwardLayout, 1, batch.mesh->GetMaterial()->GetDescriptorSet(), {}); - batch.mesh->Draw(cmd, batch.count, batch.first); + batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod); } cmd.endRendering(); diff --git a/Engine/src/Renderer/Passes/ShadowPass.cpp b/Engine/src/Renderer/Passes/ShadowPass.cpp index 7e2ecbc..3d7c2a9 100644 --- a/Engine/src/Renderer/Passes/ShadowPass.cpp +++ b/Engine/src/Renderer/Passes/ShadowPass.cpp @@ -101,7 +101,7 @@ namespace Engine::Passes &m_Frame.ubo.lightSpace[c]); for (const auto& batch : m_Frame.shadowCascades[c].batches) { - batch.mesh->Draw(cmd, batch.count, batch.first); + batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod); } cmd.endRendering(); diff --git a/Engine/src/Renderer/SceneRenderer.cpp b/Engine/src/Renderer/SceneRenderer.cpp index 1416f23..31d23a0 100644 --- a/Engine/src/Renderer/SceneRenderer.cpp +++ b/Engine/src/Renderer/SceneRenderer.cpp @@ -297,7 +297,19 @@ namespace Engine glm::vec3(t.World() * glm::vec4(bounds.center, 1.0f)); const f32 worldRadius = bounds.radius * worldScale; - auto tryAppend = [&](ViewScratch& s, const Frustum& frustum) { + // LOD 选级:屏占比近似 = radius/距离(主视图按实例距离; + // 阈值 10/20 倍半径,可调) + const f32 dist = + glm::length(worldCenter - camTransform->GetPosition()); + u32 mainLod = 0; + if (dist >= worldRadius * 20.0f) { + mainLod = 2; + } else if (dist >= worldRadius * 10.0f) { + mainLod = 1; + } + + auto tryAppend = [&](ViewScratch& s, const Frustum& frustum, + u32 lod) { if (!frustum.IntersectsSphere(worldCenter, worldRadius)) { return; } @@ -309,10 +321,12 @@ namespace Engine // 单实体时两序号恰好恒等,多实体共享 mesh 后分叉 // → batches[worldsIdx] 越界)。batch.first(GPU // firstInstance 基址)才是 worlds 索引,语义别混 + const u64 key = + (reinterpret_cast(mesh.get()) << 8) | lod; const auto [it, inserted] = s.lookup.emplace( - mesh.get(), static_cast(s.result.batches.size())); + key, static_cast(s.result.batches.size())); if (inserted) { - s.result.batches.push_back({mesh.get(), 0, 0}); + s.result.batches.push_back({mesh.get(), 0, 0, lod}); s.batchWorlds.emplace_back(); s.batchPrevWorlds.emplace_back(); } @@ -326,9 +340,12 @@ namespace Engine ++totalInstances; }; - tryAppend(m_MainScratch, mainFrustum); + tryAppend(m_MainScratch, mainFrustum, mainLod); for (u32 c = 0; c < kMaxCascades; ++c) { - tryAppend(m_CascScratch[c], cascFrustum[c]); + // 级联降级:越远的级联覆盖范围越大,用越低的 LOD + //(阴影本来就是低频信息,误差可接受) + tryAppend(m_CascScratch[c], cascFrustum[c], + std::min(c, kMeshLodCount - 1)); } } }); diff --git a/Lib/meshoptimizer/.clang-format b/Lib/meshoptimizer/.clang-format new file mode 100644 index 0000000..13b9079 --- /dev/null +++ b/Lib/meshoptimizer/.clang-format @@ -0,0 +1,15 @@ +Standard: Cpp03 +UseTab: ForIndentation +TabWidth: 4 +IndentWidth: 4 +AccessModifierOffset: -4 +BreakBeforeBraces: Allman +IndentCaseLabels: false +ColumnLimit: 0 +PointerAlignment: Left +BreakConstructorInitializersBeforeComma: true +NamespaceIndentation: None +AlignEscapedNewlines: DontAlign +AlignAfterOpenBracket: DontAlign +IndentExternBlock: NoIndent +Macros: [MESHOPTIMIZER_ALLOC_CALLCONV=&_&] diff --git a/Lib/meshoptimizer/.editorconfig b/Lib/meshoptimizer/.editorconfig new file mode 100644 index 0000000..288fe1c --- /dev/null +++ b/Lib/meshoptimizer/.editorconfig @@ -0,0 +1,11 @@ +# See https://editorconfig.org/ for more info + +[*] +charset = utf-8 +indent_style = tab +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +indent_style = space diff --git a/Lib/meshoptimizer/.git-blame-ignore-revs b/Lib/meshoptimizer/.git-blame-ignore-revs new file mode 100644 index 0000000..77aa716 --- /dev/null +++ b/Lib/meshoptimizer/.git-blame-ignore-revs @@ -0,0 +1,17 @@ +# This file contains a list of Git commit hashes that should be hidden from the +# regular Git history. Typically, this includes commits involving mass auto-formatting +# or other normalizations. Commit hashes *must* use the full 40-character notation. +# To apply the ignore list in your local Git client, you must run: +# +# git config blame.ignoreRevsFile .git-blame-ignore-revs +# +# This file is automatically used by GitHub.com's blame view. + +# Convert CRLF to LF everywhere +bb4aa0e1372751b74425e77c9a42f972971568bf + +# js: Reformat all sources with Prettier +3dea31b5c248594a62f49a3e41fc88d7ceae2de3 + +# Reformat workflow YAML files with Prettier +52e8e1f61712928a36b5257a894bb098a4a98b22 diff --git a/Lib/meshoptimizer/.github/ISSUE_TEMPLATE/bug_report.md b/Lib/meshoptimizer/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..6949faa --- /dev/null +++ b/Lib/meshoptimizer/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,8 @@ +--- +name: Bug report +about: Create a report if you've found a bug in this project; please use GitHub Discussions instead if you are uncertain or the bug may be in your code. +title: '' +labels: bug +assignees: '' + +--- diff --git a/Lib/meshoptimizer/.github/ISSUE_TEMPLATE/config.yml b/Lib/meshoptimizer/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3a7d1b5 --- /dev/null +++ b/Lib/meshoptimizer/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Questions and ideas + url: https://github.com/zeux/meshoptimizer/discussions + about: Please use GitHub Discussions if you have questions or ideas to discuss. diff --git a/Lib/meshoptimizer/.github/ISSUE_TEMPLATE/feature_request.md b/Lib/meshoptimizer/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..1dbda00 --- /dev/null +++ b/Lib/meshoptimizer/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,8 @@ +--- +name: Feature request +about: Suggest a feature for this project; please use GitHub Discussions if your idea is not sufficiently concrete. +title: '' +labels: enhancement +assignees: '' + +--- diff --git a/Lib/meshoptimizer/.github/codecov.yml b/Lib/meshoptimizer/.github/codecov.yml new file mode 100644 index 0000000..3ed6eb4 --- /dev/null +++ b/Lib/meshoptimizer/.github/codecov.yml @@ -0,0 +1,11 @@ +comment: false + +coverage: + status: + project: off + patch: off + +ignore: + - demo + - extern + - tools diff --git a/Lib/meshoptimizer/.github/workflows/build.yml b/Lib/meshoptimizer/.github/workflows/build.yml new file mode 100644 index 0000000..4760006 --- /dev/null +++ b/Lib/meshoptimizer/.github/workflows/build.yml @@ -0,0 +1,240 @@ +name: build + +on: + push: + branches: + - 'master' + paths-ignore: + - '*.md' + pull_request: + paths-ignore: + - '*.md' + +jobs: + unix: + strategy: + matrix: + os: + [ + { name: ubuntu, version: ubuntu-latest }, + { name: ubuntu-arm, version: ubuntu-24.04-arm }, + { name: ubuntu-clang, version: ubuntu-latest }, + { name: macos, version: macos-latest }, + ] + name: ${{matrix.os.name}} + runs-on: ${{matrix.os.version}} + env: + werror: 1 + steps: + - uses: actions/checkout@v6 + - name: work around ASLR+ASAN compatibility + run: sudo sysctl -w vm.mmap_rnd_bits=28 + if: matrix.os.name == 'ubuntu' + - name: enable clang + run: echo CXX=clang++ >Makefile.config + if: matrix.os.name == 'ubuntu-clang' + - name: make test + run: | + make -j2 config=sanitize test + make -j2 config=debug test + make -j2 config=release test + make -j2 config=coverage test + make -j2 config=coverage-scalar test + - name: make gltfpack + run: make -j2 config=release gltfpack + - name: upload coverage + run: | + find . -type f -name '*.gcno' -exec gcov -p {} + + sed -i -e "s/#####\(.*\)\(\/\/ unreachable.*\)/ -\1\2/" *.gcov + bash <(curl -s https://codecov.io/bash) -f './src*.gcov' -X search -t ${{secrets.CODECOV_TOKEN}} -B ${{github.ref}} + if: matrix.os.name != 'ubuntu-clang' + + windows: + strategy: + matrix: + arch: + [ + { name: windows, runner: windows-latest, arch: x64 }, + { name: windows-x86, runner: windows-latest, arch: Win32 }, + { name: windows-arm, runner: windows-11-arm, arch: ARM64 }, + ] + name: ${{matrix.arch.name}} + runs-on: ${{matrix.arch.runner}} + steps: + - uses: actions/checkout@v6 + - name: cmake configure + run: cmake . -DMESHOPT_BUILD_DEMO=ON -DMESHOPT_BUILD_GLTFPACK=ON -DMESHOPT_WERROR=ON -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$:Debug>" -A ${{matrix.arch.arch}} + - name: cmake test + shell: bash # necessary for fail-fast + run: | + cmake --build . -- -property:Configuration=Debug -verbosity:minimal + Debug/meshoptdemo.exe demo/pirate.obj + cmake --build . -- -property:Configuration=Release -verbosity:minimal + Release/meshoptdemo.exe demo/pirate.obj + + nodejs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: '16' + - name: test decoder + run: node js/meshopt_decoder.test.js + - name: test simd decoder + run: node --experimental-wasm-simd js/meshopt_decoder.test.js + - name: test encoder + run: node js/meshopt_encoder.test.js + - name: test simplifier + run: node js/meshopt_simplifier.test.js + - name: test clusterizer + run: node js/meshopt_clusterizer.test.js + - name: test tangents + run: node js/meshopt_tangents.test.js + - name: check typescript declarations + run: npx --yes -p typescript@6 tsc --noEmit --moduleResolution node16 --module node16 --strict js/index.d.ts + - name: check es5 + run: | + npm install -g es-check@7.2.1 + npx es-check --module es5 js/meshopt_decoder.mjs js/meshopt_encoder.js js/meshopt_simplifier.js js/meshopt_clusterizer.js js/meshopt_tangents.js + npx es-check --module es2020 gltf/library.js + + gltfpack: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/checkout@v6 + with: + repository: KhronosGroup/glTF-Sample-Assets + path: glTF-Sample-Assets + - name: work around ASLR+ASAN compatibility + run: sudo sysctl -w vm.mmap_rnd_bits=28 + - name: make + run: make -j2 config=sanitize gltfpack + - name: test + run: find glTF-Sample-Assets -name '*.gltf' -or -name '*.glb' | xargs -P 2 -L 16 -d '\n' ./gltfpack -cc -test + - name: pack + run: find glTF-Sample-Assets -name '*.gltf' | grep -v '\-Draco/' | xargs -P 2 -L 16 -d '\n' -I '{}' ./gltfpack -i '{}' -o '{}pack.gltf' + - name: validate + run: | + curl -sL $VALIDATOR | tar xJ + find glTF-Sample-Assets -name '*.gltfpack.gltf' | xargs -P 2 -L 1 -d '\n' ./gltf_validator -r -a + env: + VALIDATOR: https://github.com/KhronosGroup/glTF-Validator/releases/download/2.0.0-dev.3.10/gltf_validator-2.0.0-dev.3.10-linux64.tar.xz + + gltfpack-js: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: '18' + - name: install wasi + run: | + curl -sL https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$VERSION/wasi-sdk-$VERSION.0-x86_64-linux.deb > wasi-sdk.deb + sudo dpkg -i wasi-sdk.deb + env: + VERSION: 32 + - name: build + run: | + make -j2 -B gltf/library.wasm js + git status + - name: test + run: | + node gltf/cli.js -i demo/pirate.obj -o pirate.glb -v + node gltf/cli.js -i `pwd`/pirate.glb -o pirate-repack.glb -cc -v + wc -c pirate.glb pirate-repack.glb + node js/meshopt_decoder.test.js + node js/meshopt_encoder.test.js + node js/meshopt_simplifier.test.js + node js/meshopt_clusterizer.test.js + node js/meshopt_tangents.test.js + + gltfpack-full: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/checkout@v6 + with: + repository: zeux/basis_universal + ref: gltfpack + path: basis_universal + - uses: actions/checkout@v6 + with: + repository: webmproject/libwebp + ref: 1.6.0 + path: libwebp + - name: cmake configure + run: cmake . -DMESHOPT_BUILD_GLTFPACK=ON -DMESHOPT_GLTFPACK_BASISU_PATH=basis_universal -DMESHOPT_GLTFPACK_LIBWEBP_PATH=libwebp + - name: cmake build + run: cmake --build . --target gltfpack -j 4 + + gltfpack-coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/checkout@v6 + with: + repository: zeux/basis_universal + ref: gltfpack + path: basis_universal + - uses: actions/checkout@v6 + with: + repository: KhronosGroup/glTF-Sample-Assets + path: glTF-Sample-Assets + - name: build basisu + run: | + cd basis_universal + cmake . -D CMAKE_BUILD_TYPE=Debug + make -j4 basisu_encoder + - name: make + run: make -j2 config=coverage BASISU=basis_universal gltfpack + - name: test + run: | + find glTF-Sample-Assets -name '*.gltf' -or -name '*.glb' | xargs -P 2 -L 16 -d '\n' ./gltfpack -cc -test + ./gltfpack -test demo/pirate.obj -si 0.5 + ./gltfpack -test demo/pirate.obj -si 0.5 -slb -se 0.02 + ./gltfpack -test demo/pirate.obj -si 0.1 -sa + ./gltfpack -test demo/pirate.obj -noq + ./gltfpack -test demo/pirate.obj -vp 16 -vt 16 -vn 16 -vc 16 + ./gltfpack -test demo/pirate.obj -vpf -vtf -vnf + ./gltfpack -test demo/pirate.obj -vi -c + ./gltfpack -test glTF-Sample-Assets/Models/ABeautifulGame/glTF/ABeautifulGame.gltf -mi -c + ./gltfpack -test glTF-Sample-Assets/Models/ABeautifulGame/glTF/ABeautifulGame.gltf -kn -km -ke + ./gltfpack -test glTF-Sample-Assets/Models/ABeautifulGame/glTF/ABeautifulGame.gltf -si 0.1 -sp + ./gltfpack -test glTF-Sample-Assets/Models/ABeautifulGame/glTF/ABeautifulGame.gltf -si 0.1 -sv + ./gltfpack -test glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -vpf -vtf -c + ./gltfpack -test glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -vpf -vtf -cc + ./gltfpack -test glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -tc + ./gltfpack -test glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -tc -tq color 10 -tu normal,attrib -ts attrib 0.5 -tl color 512 + ./gltfpack -test glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -tr + ./gltfpack -test glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -tc -ts 0.5 -tl 64 + ./gltfpack -test glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -tc color -tfy -tq 4 -tj 1 + ./gltfpack -test glTF-Sample-Assets/Models/CesiumMan/glTF/CesiumMan.gltf -tu -ts 0.6 -tp -ar 0 + ./gltfpack -test glTF-Sample-Assets/Models/CesiumMan/glTF/CesiumMan.gltf -gt -kv + ./gltfpack -test glTF-Sample-Assets/Models/PrimitiveModeNormalsTest/glTF/PrimitiveModeNormalsTest.gltf -si 0.5 + ./gltfpack -test glTF-Sample-Assets/Models/VertexColorTest/glTF/VertexColorTest.gltf -si 0.5 -vc 12 + ./gltfpack -test glTF-Sample-Assets/Models/SimpleMeshes/glTF/SimpleMeshes.gltf -mm + ./gltfpack -test glTF-Sample-Assets/Models/SimpleMeshes/glTF/SimpleMeshes.gltf -kn + echo newmtl Leather > demo/pirate.mtl + echo map_Kd leather.jpg >> demo/pirate.mtl + echo map_d leather.jpg >> demo/pirate.mtl + ./gltfpack -test demo/pirate.obj + - name: test output + run: | + ./gltfpack || true + ./gltfpack -h || true + ./gltfpack -i glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -o box.glb -vv -r box.json + ./gltfpack -i glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -o box.gltf -cf + - name: upload coverage + run: | + find . -type f -name '*.gcno' -exec gcov -p {} + + sed -i -e "s/#####\(.*\)\(\/\/ unreachable.*\)/ -\1\2/" *.gcov + bash <(curl -s https://codecov.io/bash) -f './gltf*.gcov' -X search -t ${{secrets.CODECOV_TOKEN}} -B ${{github.ref}} + + macos-iphone: + runs-on: macos-14 + steps: + - uses: actions/checkout@v6 + - name: make + run: make -j2 config=iphone diff --git a/Lib/meshoptimizer/.github/workflows/cifuzz.yml b/Lib/meshoptimizer/.github/workflows/cifuzz.yml new file mode 100644 index 0000000..4ee4197 --- /dev/null +++ b/Lib/meshoptimizer/.github/workflows/cifuzz.yml @@ -0,0 +1,33 @@ +name: CIFuzz + +on: + push: + branches: + - 'master' + paths-ignore: + - '*.md' + +jobs: + Fuzzing: + runs-on: ubuntu-latest + steps: + - name: Build Fuzzers + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + oss-fuzz-project-name: 'meshoptimizer' + dry-run: false + language: c++ + - name: Run Fuzzers + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + oss-fuzz-project-name: 'meshoptimizer' + fuzz-seconds: 30 + dry-run: false + language: c++ + - name: Upload Crash + uses: actions/upload-artifact@v7 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts diff --git a/Lib/meshoptimizer/.github/workflows/release.yml b/Lib/meshoptimizer/.github/workflows/release.yml new file mode 100644 index 0000000..a485a29 --- /dev/null +++ b/Lib/meshoptimizer/.github/workflows/release.yml @@ -0,0 +1,77 @@ +name: release + +on: + push: + branches: + - 'master' + paths-ignore: + - '*.md' + +jobs: + gltfpack: + strategy: + matrix: + os: [{ name: windows, version: windows-latest }, { name: ubuntu, version: ubuntu-22.04 }, { name: macos, version: macos-14 }] + name: gltfpack-${{matrix.os.name}} + runs-on: ${{matrix.os.version}} + steps: + - uses: actions/checkout@v6 + - uses: actions/checkout@v6 + with: + repository: zeux/basis_universal + ref: gltfpack + path: basis_universal + - uses: actions/checkout@v6 + with: + repository: webmproject/libwebp + ref: 1.6.0 + path: libwebp + - name: cmake configure + run: cmake . -DMESHOPT_BUILD_GLTFPACK=ON -DMESHOPT_GLTFPACK_BASISU_PATH=basis_universal -D MESHOPT_GLTFPACK_LIBWEBP_PATH=libwebp -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded" -DCMAKE_BUILD_TYPE=Release + - name: cmake configure x64 + run: cmake . -DSSE=ON -DBASISU_SSE=ON + if: matrix.os.name == 'ubuntu' + - name: cmake build + run: cmake --build . --target gltfpack --config Release -j 3 + - uses: actions/upload-artifact@v7 + with: + name: gltfpack-windows + path: Release/gltfpack.exe + if: matrix.os.name == 'windows' + - uses: actions/upload-artifact@v7 + with: + name: gltfpack-${{matrix.os.name}} + path: gltfpack + if: matrix.os.name != 'windows' + + nodejs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: '18' + - name: install wasi + run: | + curl -sL https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$VERSION/wasi-sdk-$VERSION.0-x86_64-linux.deb > wasi-sdk.deb + sudo dpkg -i wasi-sdk.deb + env: + VERSION: 32 + - name: build + run: | + make -j2 -B gltf/library.wasm js + git status + - name: npm pack + run: | + cp LICENSE.md gltf/ + cp LICENSE.md js/ + cd gltf && npm pack && cd .. + cd js && npm pack && cd .. + - uses: actions/upload-artifact@v7 + with: + name: gltfpack-npm + path: gltf/gltfpack-*.tgz + - uses: actions/upload-artifact@v7 + with: + name: meshoptimizer-npm + path: js/meshoptimizer-*.tgz diff --git a/Lib/meshoptimizer/.gitignore b/Lib/meshoptimizer/.gitignore new file mode 100644 index 0000000..37533f7 --- /dev/null +++ b/Lib/meshoptimizer/.gitignore @@ -0,0 +1,19 @@ +# IDE integrations +/.cache/ +/.idea/ +/.vs/ +/.vscode/ +/.zed/ + +# Build files +/build/ +/cmake*/ +/out/ +/*.dSYM/ +/gltf/library.wasm +compile_commands.json + +# Test files +/data/ +/*.log +/perf.data* diff --git a/Lib/meshoptimizer/.prettierrc b/Lib/meshoptimizer/.prettierrc new file mode 100644 index 0000000..78f1f57 --- /dev/null +++ b/Lib/meshoptimizer/.prettierrc @@ -0,0 +1,16 @@ +{ + "useTabs": true, + "tabWidth": 4, + "semi": true, + "singleQuote": true, + "printWidth": 150, + "trailingComma": "es5", + "overrides": [ + { + "files": "*.yml", + "options": { + "tabWidth": 2 + } + }, + ] +} diff --git a/Lib/meshoptimizer/CMakeLists.txt b/Lib/meshoptimizer/CMakeLists.txt new file mode 100644 index 0000000..35047d1 --- /dev/null +++ b/Lib/meshoptimizer/CMakeLists.txt @@ -0,0 +1,227 @@ +cmake_minimum_required(VERSION 3.5...3.30) + +if(POLICY CMP0077) + cmake_policy(SET CMP0077 NEW) # Enables override of options from parent CMakeLists.txt +endif() + +if(POLICY CMP0091) + cmake_policy(SET CMP0091 NEW) # Enables use of MSVC_RUNTIME_LIBRARY +endif() + +if(POLICY CMP0092) + cmake_policy(SET CMP0092 NEW) # Enables clean /W4 override for MSVC +endif() + +project(meshoptimizer VERSION 1.2 LANGUAGES CXX) + +option(MESHOPT_BUILD_DEMO "Build demo" OFF) +option(MESHOPT_BUILD_GLTFPACK "Build gltfpack" OFF) +option(MESHOPT_BUILD_SHARED_LIBS "Build shared libraries" OFF) +option(MESHOPT_STABLE_EXPORTS "Only export stable APIs from shared library" OFF) +option(MESHOPT_WERROR "Treat warnings as errors" OFF) +option(MESHOPT_INSTALL "Install library" ON) + +# Optional gltfpack components for texture compression support +set(MESHOPT_GLTFPACK_BASISU_PATH "" CACHE STRING "") # Basis Universal, https://github.com/BinomialLLC/basis_universal +set(MESHOPT_GLTFPACK_LIBWEBP_PATH "" CACHE STRING "") # libwebp, https://github.com/webmproject/libwebp + +set(SOURCES + src/meshoptimizer.h + src/allocator.cpp + src/clusterizer.cpp + src/indexanalyzer.cpp + src/indexcodec.cpp + src/indexgenerator.cpp + src/meshletcodec.cpp + src/meshletutils.cpp + src/opacitymap.cpp + src/overdrawoptimizer.cpp + src/partition.cpp + src/quantization.cpp + src/rasterizer.cpp + src/remesher.cpp + src/simplifier.cpp + src/spatialorder.cpp + src/stripifier.cpp + src/tangentspace.cpp + src/vcacheoptimizer.cpp + src/vertexcodec.cpp + src/vertexfilter.cpp + src/vfetchoptimizer.cpp +) + +set(GLTF_SOURCES + gltf/animation.cpp + gltf/encodebasis.cpp + gltf/encodewebp.cpp + gltf/fileio.cpp + gltf/gltfpack.cpp + gltf/image.cpp + gltf/json.cpp + gltf/material.cpp + gltf/mesh.cpp + gltf/node.cpp + gltf/parseobj.cpp + gltf/parselib.cpp + gltf/parsegltf.cpp + gltf/stream.cpp + gltf/write.cpp +) + +if(WIN32) + list(APPEND GLTF_SOURCES gltf/gltfpack.manifest) +endif() + +if(MSVC) + add_compile_options(/W4) +else() + add_compile_options(-Wall -Wextra -Wshadow -Wno-missing-field-initializers) +endif() + +if(MESHOPT_WERROR) + if(MSVC) + add_compile_options(/WX) + else() + add_compile_options(-Werror) + endif() +endif() + +if(MESHOPT_BUILD_SHARED_LIBS) + add_library(meshoptimizer SHARED ${SOURCES}) +else() + add_library(meshoptimizer STATIC ${SOURCES}) +endif() + +target_include_directories(meshoptimizer INTERFACE "$") + +if(MESHOPT_BUILD_SHARED_LIBS) + set_target_properties(meshoptimizer PROPERTIES CXX_VISIBILITY_PRESET hidden) + set_target_properties(meshoptimizer PROPERTIES VISIBILITY_INLINES_HIDDEN ON) + + # soversion may be requested via -DMESHOPT_SOVERSION=n; note that experimental APIs (marked with MESHOPTIMIZER_EXPERIMENTAL) are not ABI-stable + if(MESHOPT_SOVERSION) + set_target_properties(meshoptimizer PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${MESHOPT_SOVERSION}) + endif() + + if(WIN32) + target_compile_definitions(meshoptimizer INTERFACE "MESHOPTIMIZER_API=__declspec(dllimport)") + target_compile_definitions(meshoptimizer PRIVATE "MESHOPTIMIZER_API=__declspec(dllexport)") + else() + target_compile_definitions(meshoptimizer PUBLIC "MESHOPTIMIZER_API=__attribute__((visibility(\"default\")))") + endif() + + target_compile_definitions(meshoptimizer PUBLIC MESHOPTIMIZER_ALLOC_EXPORT) + + if(MESHOPT_STABLE_EXPORTS) + target_compile_definitions(meshoptimizer PUBLIC "MESHOPTIMIZER_EXPERIMENTAL=") + endif() +endif() + +if(MESHOPT_BUILD_DEMO) + add_executable(demo demo/main.cpp demo/nanite.cpp demo/tests.cpp tools/objloader.cpp) + set_target_properties(demo PROPERTIES CXX_STANDARD 11) + set_target_properties(demo PROPERTIES OUTPUT_NAME meshoptdemo) + target_link_libraries(demo meshoptimizer) +endif() + +if(MESHOPT_BUILD_GLTFPACK) + add_executable(gltfpack ${GLTF_SOURCES}) + set_target_properties(gltfpack PROPERTIES CXX_STANDARD 11) + target_link_libraries(gltfpack meshoptimizer) + + if(MESHOPT_BUILD_SHARED_LIBS AND MESHOPT_STABLE_EXPORTS) + target_compile_definitions(gltfpack PRIVATE GLTFPACK_NO_EXPERIMENTAL) + endif() + + if(MESHOPT_BUILD_SHARED_LIBS) + string(CONCAT RPATH "$ORIGIN/../" ${CMAKE_INSTALL_LIBDIR}) + set_target_properties(gltfpack PROPERTIES INSTALL_RPATH ${RPATH}) + endif() + + if(NOT MESHOPT_GLTFPACK_BASISU_PATH STREQUAL "") + get_filename_component(BASISU_PATH ${MESHOPT_GLTFPACK_BASISU_PATH} ABSOLUTE) + if (NOT EXISTS ${BASISU_PATH}) + message(FATAL_ERROR "Basis Universal path ${BASISU_PATH} not found") + endif() + + if (NOT SSE AND NOT BASISU_SSE AND NOT MSVC AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") + message(WARNING "Building Basis Universal without SSE4.1 support; performance may be suboptimal") + endif() + + add_subdirectory(${BASISU_PATH} ${CMAKE_CURRENT_BINARY_DIR}/basisu EXCLUDE_FROM_ALL) + + target_compile_definitions(gltfpack PRIVATE WITH_BASISU) + target_link_libraries(gltfpack basisu_encoder) + set_source_files_properties(gltf/encodebasis.cpp PROPERTIES INCLUDE_DIRECTORIES ${BASISU_PATH}) # necessary because basisu_encoder doesn't export include directories + endif() + + if(NOT MESHOPT_GLTFPACK_LIBWEBP_PATH STREQUAL "") + get_filename_component(LIBWEBP_PATH ${MESHOPT_GLTFPACK_LIBWEBP_PATH} ABSOLUTE) + if (NOT EXISTS ${LIBWEBP_PATH}) + message(FATAL_ERROR "libwebp path ${LIBWEBP_PATH} not found") + endif() + + add_subdirectory(${LIBWEBP_PATH} ${CMAKE_CURRENT_BINARY_DIR}/libwebp EXCLUDE_FROM_ALL) + + target_compile_definitions(gltfpack PRIVATE WITH_LIBWEBP) + target_link_libraries(gltfpack webp) + + # when gltfpack is built with Basis Universal, we use Basis image decoders for PNG/JPEG support for ease of distribution + if(NOT MESHOPT_GLTFPACK_BASISU_PATH STREQUAL "") + target_compile_definitions(gltfpack PRIVATE WITH_LIBWEBP_BASIS) + else() + target_link_libraries(gltfpack imagedec) + endif() + endif() + + if(NOT MESHOPT_GLTFPACK_BASISU_PATH STREQUAL "" OR NOT MESHOPT_GLTFPACK_LIBWEBP_PATH STREQUAL "") + find_package(Threads REQUIRED) + target_link_libraries(gltfpack Threads::Threads) + endif() +endif() + +if(MESHOPT_INSTALL) + include(GNUInstallDirs) + + install(TARGETS meshoptimizer EXPORT meshoptimizerTargets + COMPONENT meshoptimizer + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + + if(MESHOPT_BUILD_GLTFPACK) + install(TARGETS gltfpack RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + endif() + + install(FILES src/meshoptimizer.h COMPONENT meshoptimizer DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + install(EXPORT meshoptimizerTargets COMPONENT meshoptimizer DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/meshoptimizer NAMESPACE meshoptimizer::) + + if(MSVC) + set(PDB_TARGETS meshoptimizer) + if(MESHOPT_BUILD_GLTFPACK) + list(APPEND PDB_TARGETS gltfpack) + endif() + + foreach(TARGET ${PDB_TARGETS}) + get_target_property(TARGET_TYPE ${TARGET} TYPE) + if(NOT ${TARGET_TYPE} STREQUAL "STATIC_LIBRARY") + install(FILES $ COMPONENT meshoptimizer DESTINATION ${CMAKE_INSTALL_BINDIR} OPTIONAL) + endif() + endforeach(TARGET) + endif() + + include(CMakePackageConfigHelpers) + + # CMake 3.18+ supports file(CONFIGURE OUTPUT file CONTENT content @ONLY) but we only require 3.5+ + file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/meshoptimizerConfig.cmake + "include(\"\${CMAKE_CURRENT_LIST_DIR}/meshoptimizerTargets.cmake\")\n") + + write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/meshoptimizerConfigVersion.cmake COMPATIBILITY ExactVersion) + + install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/meshoptimizerConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/meshoptimizerConfigVersion.cmake + COMPONENT meshoptimizer + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/meshoptimizer) +endif() diff --git a/Lib/meshoptimizer/CONTRIBUTING.md b/Lib/meshoptimizer/CONTRIBUTING.md new file mode 100644 index 0000000..63f54b3 --- /dev/null +++ b/Lib/meshoptimizer/CONTRIBUTING.md @@ -0,0 +1,63 @@ +Thanks for deciding to contribute to meshoptimizer! These guidelines will try to help make the process painless and efficient. + +## Questions + +If you have a question regarding the library usage, please [create a GitHub discussion](https://github.com/zeux/meshoptimizer/discussions/new/choose). +Some questions just need answers, but it's nice to keep them for future reference in case other people want to know the same thing. +Some questions help improve the library interface or documentation by inspiring future changes. + +## Bugs + +If the library doesn't compile on your system, compiles with warnings, doesn't seem to run correctly for your input data or if anything else is amiss, please [open a GitHub issue](https://github.com/zeux/meshoptimizer/issues/new). +It helps if you note the version of the library this issue happens in, the version of your compiler for compilation issues, and a reproduction case for runtime bugs. + +Of course, feel free to [create a pull request](https://help.github.com/articles/about-pull-requests/) to fix the bug yourself. + +## Features + +New algorithms and improvements to existing algorithms are always welcome; please open an issue describing an improvement you'd like to see or make before opening a pull request. + +This will give us a chance to discuss the idea before implementing it - some algorithms may not be easy to integrate into existing programs, may not be robust to arbitrary meshes or may be expensive to run or implement/maintain, so a discussion helps make sure these don't block the algorithm development. + +## Code style + +Contributions to this project are expected to follow the existing code style. +`.clang-format` file mostly defines syntactic styling rules (you can run `make format` to format the code accordingly). + +As for naming conventions, this library uses `snake_case` for variables, `lowerCamelCase` for functions, `UpperCamelCase` for types, `kCamelCase` for global constants and `SCARY_CASE` for macros. All public functions/types must additionally have an extra `meshopt_` prefix to avoid symbol conflicts. + +## Dependencies + +Please note that this library uses C89 interface for all APIs and a C++98 implementation - C++11 features can not be used. +This choice is made to maximize compatibility to make sure that any toolchain, including legacy proprietary gaming console toolchains, can compile this code. + +Additionally, the library code has zero external dependencies, does not depend on STL and does not use RTTI or exceptions. +This, again, maximizes compatibility and makes sure the library can be used in environments where STL use is discouraged or prohibited, as well as maximizing runtime performance and minimizing compilation times. + +The demo program uses STL since it serves as an example of usage and as a test harness, not as production-ready code. + +## Testing + +All pull requests will run through a continuous integration pipeline using GitHub Actions that will run the built-in unit tests and integration tests on Windows, macOS and Linux with gcc, clang and msvc compilers. +You can run the tests yourself using `make test` or building the demo program with `cmake -DBUILD_DEMO=ON` and running it. + +Unit tests can be found in `demo/tests.cpp` and functional tests - in `demo/main.cpp`; when making code changes please try to make sure they are covered by an existing test or add a new test accordingly. + +## Documentation + +Documentation for this library resides in the `meshoptimizer.h` header, with examples as part of a usage manual available in `README.md`. +Changes to documentation are always welcome and should use issues/pull requests as outlined above; please note that `README.md` only contains documentation for stable algorithms, as experimental algorithms may change the interface without concern for backwards compatibility. + +## Sensitive communication + +If you prefer to not disclose the issues or information relevant to the issue such as reproduction case to the public, you can always contact the author via e-mail (arseny.kapoulkine@gmail.com). + +## Contributor agreement + +Any code you submit will become part of the repository and be distributed under the [meshoptimizer license](https://github.com/zeux/meshoptimizer/blob/master/LICENSE.md). By submitting code to the project you agree that the code is your work and that you can give it to the project. + +You also agree by submitting your code that you grant all transferrable rights to the code to the project maintainer, including for example re-licensing the code, modifying the code, and distributing it in source or binary forms. Specifically, this includes a requirement that you assign copyright to the project maintainer. + +## AI + +AI-generated issues or pull requests (including pull request descriptions), will be closed. Repeat submissions may result in a ban. diff --git a/Lib/meshoptimizer/LICENSE.md b/Lib/meshoptimizer/LICENSE.md new file mode 100644 index 0000000..468827f --- /dev/null +++ b/Lib/meshoptimizer/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016-2026 Arseny Kapoulkine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Lib/meshoptimizer/Makefile b/Lib/meshoptimizer/Makefile new file mode 100644 index 0000000..1d17bab --- /dev/null +++ b/Lib/meshoptimizer/Makefile @@ -0,0 +1,271 @@ +MAKEFLAGS+=-r -j + +config=debug +files=demo/pirate.obj + +BUILD=build/$(config) + +LIBRARY_SOURCES=$(wildcard src/*.cpp) +LIBRARY_OBJECTS=$(LIBRARY_SOURCES:%=$(BUILD)/%.o) + +DEMO_SOURCES=$(wildcard demo/*.c demo/*.cpp) tools/objloader.cpp +DEMO_OBJECTS=$(DEMO_SOURCES:%=$(BUILD)/%.o) + +GLTFPACK_SOURCES=$(wildcard gltf/*.cpp) +GLTFPACK_OBJECTS=$(GLTFPACK_SOURCES:%=$(BUILD)/%.o) + +OBJECTS=$(LIBRARY_OBJECTS) $(DEMO_OBJECTS) $(GLTFPACK_OBJECTS) + +LIBRARY=$(BUILD)/libmeshoptimizer.a +DEMO=$(BUILD)/meshoptdemo + +CFLAGS=-g -Wall -Wextra -std=c89 +CXXFLAGS=-g -Wall -Wextra -Wshadow -Wno-missing-field-initializers +LDFLAGS= + +$(LIBRARY_OBJECTS): CXXFLAGS+=-std=gnu++98 +$(DEMO_OBJECTS): CXXFLAGS+=-std=c++11 +$(GLTFPACK_OBJECTS): CXXFLAGS+=-std=c++11 + +ifdef BASISU + $(GLTFPACK_OBJECTS): CXXFLAGS+=-DWITH_BASISU + $(BUILD)/gltf/encodebasis.cpp.o: CXXFLAGS+=-I$(BASISU) + gltfpack: LDFLAGS+=-lpthread $(BASISU)/libbasisu_encoder.a +endif + +WASI_SDK?=/opt/wasi-sdk +WASMCC?=$(WASI_SDK)/bin/clang++ +WASIROOT?=$(WASI_SDK)/share/wasi-sysroot + +WASM_FLAGS=--target=wasm32-wasi -Wno-deprecated --sysroot=$(WASIROOT) --no-wasm-opt +WASM_FLAGS+=-Wall -Wextra +WASM_FLAGS+=-O3 -DNDEBUG -DMESHOPTIMIZER_VERTEXCODEC_ZEROCOPY -nostartfiles -nostdlib -Wl,--no-entry -Wl,-s +WASM_FLAGS+=-mcpu=mvp # make sure clang doesn't use post-MVP features like sign extension +WASM_FLAGS+=-fno-slp-vectorize -fno-vectorize -fno-unroll-loops +WASM_FLAGS+=-Wl,-z -Wl,stack-size=36864 -Wl,--initial-memory=65536 +WASM_EXPORT_PREFIX=-Wl,--export + +WASM_DECODER_SOURCES=src/vertexcodec.cpp src/indexcodec.cpp src/vertexfilter.cpp tools/wasmstubs.cpp +WASM_DECODER_EXPORTS=meshopt_decodeVertexBuffer meshopt_decodeIndexBuffer meshopt_decodeIndexSequence meshopt_decodeFilterOct meshopt_decodeFilterQuat meshopt_decodeFilterExp meshopt_decodeFilterColor sbrk __wasm_call_ctors + +WASM_ENCODER_SOURCES=src/vertexcodec.cpp src/indexcodec.cpp src/vertexfilter.cpp src/vcacheoptimizer.cpp src/vfetchoptimizer.cpp src/spatialorder.cpp tools/wasmstubs.cpp +WASM_ENCODER_EXPORTS=meshopt_encodeVertexBuffer meshopt_encodeVertexBufferBound meshopt_encodeVertexBufferLevel meshopt_encodeIndexBuffer meshopt_encodeIndexBufferBound meshopt_encodeIndexSequence meshopt_encodeIndexSequenceBound meshopt_encodeVertexVersion meshopt_encodeIndexVersion meshopt_encodeFilterOct meshopt_encodeFilterQuat meshopt_encodeFilterExp meshopt_encodeFilterColor meshopt_optimizeVertexCache meshopt_optimizeVertexCacheStrip meshopt_optimizeVertexFetchRemap meshopt_spatialSortRemap sbrk __wasm_call_ctors + +WASM_SIMPLIFIER_SOURCES=src/simplifier.cpp src/remesher.cpp src/vfetchoptimizer.cpp src/indexgenerator.cpp tools/wasmstubs.cpp +WASM_SIMPLIFIER_EXPORTS=meshopt_simplify meshopt_simplifyWithAttributes meshopt_simplifyWithUpdate meshopt_simplifyScale meshopt_simplifyPoints meshopt_simplifySloppy meshopt_simplifyPrune meshopt_remesh meshopt_optimizeVertexFetchRemap meshopt_generatePositionRemap sbrk __wasm_call_ctors + +WASM_CLUSTERIZER_SOURCES=src/clusterizer.cpp src/meshletutils.cpp tools/wasmstubs.cpp +WASM_CLUSTERIZER_EXPORTS=meshopt_buildMeshletsBound meshopt_buildMeshletsFlex meshopt_buildMeshletsSpatial meshopt_computeClusterBounds meshopt_computeMeshletBounds meshopt_computeSphereBounds meshopt_optimizeMeshlet sbrk __wasm_call_ctors + +WASM_TANGENTS_SOURCES=src/tangentspace.cpp tools/wasmstubs.cpp +WASM_TANGENTS_EXPORTS=meshopt_generateTangents meshopt_generateNormals sbrk __wasm_call_ctors + +ifneq ($(werror),) + CFLAGS+=-Werror + CXXFLAGS+=-Werror +endif + +ifeq ($(config),iphone) + IPHONESDK=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk + CFLAGS+=-arch armv7 -arch arm64 -isysroot $(IPHONESDK) + CXXFLAGS+=-arch armv7 -arch arm64 -isysroot $(IPHONESDK) -stdlib=libc++ + LDFLAGS+=-arch armv7 -arch arm64 -isysroot $(IPHONESDK) -L $(IPHONESDK)/usr/lib -mios-version-min=7.0 +endif + +ifeq ($(config),trace) + CXXFLAGS+=-DTRACE=1 +endif + +ifeq ($(config),tracev) + CXXFLAGS+=-DTRACE=2 +endif + +ifeq ($(config),release) + CXXFLAGS+=-O3 -DNDEBUG +endif + +ifeq ($(config),coverage) + CXXFLAGS+=-coverage + LDFLAGS+=-coverage +endif + +ifeq ($(config),release-avx) + CXXFLAGS+=-O3 -DNDEBUG -mavx +endif + +ifeq ($(config),release-avx512) + CXXFLAGS+=-O3 -DNDEBUG -mavx512vl -mavx512vbmi -mavx512vbmi2 -mbmi2 +endif + +ifeq ($(config),release-scalar) + CXXFLAGS+=-O3 -DNDEBUG -DMESHOPTIMIZER_NO_SIMD +endif + +ifeq ($(config),coverage-scalar) + CXXFLAGS+=-coverage -DMESHOPTIMIZER_NO_SIMD + LDFLAGS+=-coverage +endif + +ifeq ($(config),sanitize) + CXXFLAGS+=-fsanitize=address,undefined -fsanitize-undefined-trap-on-error + LDFLAGS+=-fsanitize=address,undefined +endif + +ifeq ($(config),analyze) + CXXFLAGS+=--analyze +endif + +ifeq ($(config),fuzz) + CXXFLAGS+=-O1 -fsanitize=address,fuzzer + LDFLAGS+=-fsanitize=address,fuzzer + + $(GLTFPACK_OBJECTS): CXXFLAGS+=-DGLTFFUZZ +endif + +-include Makefile.config + +all: $(DEMO) + +test: $(DEMO) + $(DEMO) $(files) + +check: $(DEMO) + $(DEMO) + +dev: $(DEMO) + $(DEMO) -d $(files) + +nanite: $(DEMO) + $(DEMO) -n $(files) + +format: + clang-format -i src/*.h src/*.cpp demo/*.cpp gltf/*.h gltf/*.cpp + +formatjs: + prettier -w js/*.js gltf/*.js demo/*.html js/*.ts + +js: js/meshopt_decoder.cjs js/meshopt_decoder.mjs js/meshopt_encoder.js js/meshopt_simplifier.js js/meshopt_clusterizer.js js/meshopt_tangents.js + +symbols: $(BUILD)/amalgamated.so + nm $< -U -g + +gltfpack: $(BUILD)/gltfpack + ln -fs $^ $@ + +ifeq ($(config),fuzz) +gltffuzz: $(BUILD)/gltfpack + cp $^ $@ + mkdir -p /tmp/gltffuzz + cp gltf/fuzz.glb /tmp/gltffuzz/ + ./gltffuzz /tmp/gltffuzz -fork=16 -dict=gltf/fuzz.dict -ignore_crashes=1 -max_len=32768 +endif + +$(BUILD)/gltfpack: $(GLTFPACK_OBJECTS) $(LIBRARY) + $(CXX) $^ $(LDFLAGS) -o $@ + +gltfpack.wasm: gltf/library.wasm + +gltf/library.wasm: $(LIBRARY_SOURCES) $(GLTFPACK_SOURCES) + $(WASMCC) $^ -o $@ -Wall -Os -DNDEBUG --target=wasm32-wasi -Wno-deprecated --sysroot=$(WASIROOT) -nostartfiles --no-wasm-opt -Wl,--no-entry -Wl,--export=pack -Wl,--export=malloc -Wl,--export=free -Wl,--export=__wasm_call_ctors -Wl,-s -Wl,--allow-undefined-file=gltf/wasistubs.txt + +build/decoder_base.wasm: $(WASM_DECODER_SOURCES) + @mkdir -p build + $(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_DECODER_EXPORTS)) -o $@ + +build/decoder_simd.wasm: $(WASM_DECODER_SOURCES) + @mkdir -p build + $(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_DECODER_EXPORTS)) -o $@ -msimd128 -mbulk-memory -DMESHOPTIMIZER_VERTEXFILTER_SIMDNOTAIL + +build/encoder.wasm: $(WASM_ENCODER_SOURCES) + @mkdir -p build + $(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_ENCODER_EXPORTS)) -lc -o $@ + +build/simplifier.wasm: $(WASM_SIMPLIFIER_SOURCES) + @mkdir -p build + $(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_SIMPLIFIER_EXPORTS)) -lc -o $@ + +build/clusterizer.wasm: $(WASM_CLUSTERIZER_SOURCES) + @mkdir -p build + $(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_CLUSTERIZER_EXPORTS)) -lc -o $@ + +build/tangents.wasm: $(WASM_TANGENTS_SOURCES) + @mkdir -p build + $(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_TANGENTS_EXPORTS)) -lc -o $@ + +js/meshopt_decoder.mjs: build/decoder_base.wasm build/decoder_simd.wasm tools/wasmpack.py + sed -i "s#Built with clang.*#Built with $$($(WASMCC) --version | head -n 1 | sed 's/\s\+(.*//')#" $@ + sed -i "s#Built from meshoptimizer .*#Built from meshoptimizer $$(cat src/meshoptimizer.h | grep -Po '(?<=version )[0-9.]+')#" $@ + python3 tools/wasmpack.py patch $@ base $@ + echo "if (typeof exports === 'object' && typeof module === 'object') module.exports = MeshoptDecoder;" >>$@ + echo "else if (typeof define === 'function' && define['amd']) define([], function () { return MeshoptDecoder; });" >>$@ + echo "else if (typeof exports === 'object') exports['MeshoptDecoder'] = MeshoptDecoder;" >>$@ + echo "else (typeof self !== 'undefined' ? self : this).MeshoptDecoder = MeshoptDecoder;" >>$@ + +$(DEMO): $(DEMO_OBJECTS) $(LIBRARY) + $(CXX) $^ $(LDFLAGS) -o $@ + +vcachetuner: tools/vcachetuner.cpp tools/objloader.cpp $(LIBRARY) + $(CXX) $^ -fopenmp $(CXXFLAGS) -std=c++11 $(LDFLAGS) -o $@ + +codecbench: tools/codecbench.cpp $(LIBRARY) + $(CXX) $^ $(CXXFLAGS) $(LDFLAGS) -o $@ + +codecbench.js: tools/codecbench.cpp $(LIBRARY_SOURCES) + emcc $^ -O3 -g -DNDEBUG -s TOTAL_MEMORY=268435456 -s SINGLE_FILE=1 -o $@ + +codecbench-simd.js: tools/codecbench.cpp $(LIBRARY_SOURCES) + emcc $^ -O3 -g -DNDEBUG -s TOTAL_MEMORY=268435456 -s SINGLE_FILE=1 -msimd128 -o $@ + +codecbench.wasm: tools/codecbench.cpp $(LIBRARY_SOURCES) + $(WASMCC) $^ -fno-exceptions --target=wasm32-wasi --sysroot=$(WASIROOT) -lc++ -lc++abi -O3 -g -DNDEBUG -o $@ + +codecbench-simd.wasm: tools/codecbench.cpp $(LIBRARY_SOURCES) + $(WASMCC) $^ -fno-exceptions --target=wasm32-wasi --sysroot=$(WASIROOT) -lc++ -lc++abi -O3 -g -DNDEBUG -msimd128 -o $@ + +codectest: tools/codectest.cpp $(LIBRARY) + $(CXX) $^ $(CXXFLAGS) $(LDFLAGS) -o $@ + +codecfuzz: tools/codecfuzz.cpp src/vertexcodec.cpp src/indexcodec.cpp src/meshletcodec.cpp + $(CXX) $^ -fsanitize=fuzzer,address,undefined -O1 -g -o $@ + +clusterfuzz: tools/clusterfuzz.cpp src/clusterizer.cpp src/partition.cpp + $(CXX) $^ -fsanitize=fuzzer,address,undefined -O1 -g -o $@ + +simplifyfuzz: tools/simplifyfuzz.cpp src/simplifier.cpp + $(CXX) $^ -fsanitize=fuzzer,address,undefined -O1 -g -o $@ + +$(LIBRARY): $(LIBRARY_OBJECTS) + ar rcs $@ $^ + +$(BUILD)/amalgamated.so: $(LIBRARY_SOURCES) + @mkdir -p $(dir $@) + cat $^ | $(CXX) $(CXXFLAGS) -x c++ - -I src/ -o $@ -shared -fPIC + +$(BUILD)/%.cpp.o: %.cpp + @mkdir -p $(dir $@) + $(CXX) $< $(CXXFLAGS) -c -MMD -MP -o $@ + +$(BUILD)/%.c.o: %.c + @mkdir -p $(dir $@) + $(CC) $< $(CFLAGS) -c -MMD -MP -o $@ + +-include $(OBJECTS:.o=.d) + +clean: + rm -rf $(BUILD) + +.PHONY: all clean format js diff --git a/Lib/meshoptimizer/README.md b/Lib/meshoptimizer/README.md new file mode 100644 index 0000000..86d6442 --- /dev/null +++ b/Lib/meshoptimizer/README.md @@ -0,0 +1,967 @@ +# 🐇 meshoptimizer [![Actions Status](https://github.com/zeux/meshoptimizer/workflows/build/badge.svg)](https://github.com/zeux/meshoptimizer/actions) [![codecov.io](https://codecov.io/github/zeux/meshoptimizer/coverage.svg?branch=master)](https://codecov.io/github/zeux/meshoptimizer?branch=master) [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md) [![GitHub](https://img.shields.io/badge/repo-github-green.svg)](https://github.com/zeux/meshoptimizer) + +## Purpose + +When a GPU renders triangle meshes, various stages of the GPU pipeline have to process vertex and index data. The efficiency of these stages depends on the data you feed to them; this library provides algorithms to help optimize meshes for these stages, as well as algorithms to reduce the mesh complexity and storage overhead. + +The library provides a C and C++ interface for all algorithms; you can use it from C/C++ or from other languages via FFI (such as P/Invoke). If you want to use this library from Rust, you should use [meshopt crate](https://crates.io/crates/meshopt). JavaScript interface for some algorithms is available through [meshoptimizer.js](https://www.npmjs.com/package/meshoptimizer). + +Two companion projects are developed and distributed alongside the library: [gltfpack](./gltf/README.md), a command-line tool that automatically optimizes glTF files, and [clusterlod.h](./demo/clusterlod.h), a single-header C/C++ library for continuous level of detail using clustered simplification. + +## Installing + +meshoptimizer is hosted on GitHub; you can download the latest release using git: + +``` +git clone -b v1.2 https://github.com/zeux/meshoptimizer.git +``` + +Alternatively you can [download the .zip archive from GitHub](https://github.com/zeux/meshoptimizer/archive/v1.2.zip). + +The library is also available as a Linux package in several distributions ([ArchLinux](https://aur.archlinux.org/packages/meshoptimizer/), [Debian](https://packages.debian.org/libmeshoptimizer), [FreeBSD](https://www.freshports.org/misc/meshoptimizer/), [Nix](https://mynixos.com/nixpkgs/package/meshoptimizer), [Ubuntu](https://packages.ubuntu.com/libmeshoptimizer)), as well as a [Vcpkg port](https://github.com/microsoft/vcpkg/tree/master/ports/meshoptimizer) (see [installation instructions](https://learn.microsoft.com/en-us/vcpkg/get_started/get-started)) and a [Conan package](https://conan.io/center/recipes/meshoptimizer). + +[gltfpack](./gltf/README.md) is available as a pre-built binary on [Releases page](https://github.com/zeux/meshoptimizer/releases) or via [npm package](https://www.npmjs.com/package/gltfpack). Native binaries are recommended since they are more efficient and support texture compression. + +## Building + +meshoptimizer is distributed as a C/C++ header (`src/meshoptimizer.h`) and a set of C++ source files (`src/*.cpp`). To include it in your project, you can use one of two options: + +* Use CMake to build the library (either as a standalone project or as part of your project) +* Add source files to your project's build system + +The source files are organized in such a way that you don't need to change your build-system settings, and you only need to add the source files for the algorithms you use. They should build without warnings or special compilation options on all major compilers. If you prefer amalgamated builds, you can also concatenate the source files into a single `.cpp` file and build that instead. + +To use meshoptimizer functions, simply `#include` the header `meshoptimizer.h`; the library source is C++, but the header is C-compatible. + +## Core pipeline + +When optimizing a mesh, to maximize rendering efficiency you should typically feed it through a set of optimizations (the order is important!): + +1. Indexing +2. Vertex cache optimization +3. (optional) Overdraw optimization +4. Vertex fetch optimization +5. Vertex quantization +6. Index filtering +7. (optional) Shadow indexing + +### Indexing + +Most algorithms in this library assume that a mesh has a vertex buffer and an index buffer. For algorithms to work well and also for GPU to render your mesh efficiently, the vertex buffer has to have no redundant vertices; you can generate an index buffer from an unindexed vertex buffer or reindex an existing (potentially redundant) index buffer as follows: + +> Note: meshoptimizer generally works with 32-bit (`unsigned int`) indices, however when using C++ APIs you can use any integer type for index data by using the provided template overloads. By convention, remap tables always use `unsigned int`. + +First, generate a remap table from your existing vertex (and, optionally, index) data: + +```c++ +size_t index_count = face_count * 3; +size_t unindexed_vertex_count = face_count * 3; +std::vector remap(unindexed_vertex_count); // temporary remap table +size_t vertex_count = meshopt_generateVertexRemap(&remap[0], NULL, index_count, + &unindexed_vertices[0], unindexed_vertex_count, sizeof(Vertex)); +``` + +Note that in this case we only have an unindexed vertex buffer; when input mesh has an index buffer, it will need to be passed to `meshopt_generateVertexRemap` instead of `NULL`, along with the correct source vertex count. In either case, the remap table is generated based on binary equivalence of the input vertices, so the resulting mesh will render the same way. Binary equivalence considers all input bytes, including padding which should be zero-initialized if the vertex structure has gaps. + +After generating the remap table, you can allocate space for the target vertex buffer (`vertex_count` elements) and index buffer (`index_count` elements) and generate them: + +```c++ +meshopt_remapIndexBuffer(indices, NULL, index_count, &remap[0]); +meshopt_remapVertexBuffer(vertices, &unindexed_vertices[0], unindexed_vertex_count, sizeof(Vertex), &remap[0]); +``` + +You can then further optimize the resulting buffers by calling the other functions on them in-place. + +`meshopt_generateVertexRemap` uses binary equivalence of vertex data, which is generally a reasonable default; however, in some cases some attributes may have floating point drift causing extra vertices to be generated. For such cases, it may be necessary to quantize some attributes (most importantly, normals and tangents) before generating the remap, or use `meshopt_generateVertexRemapCustom` algorithm that allows comparing individual attributes with tolerance by providing a custom comparison function: + +```c++ +size_t vertex_count = meshopt_generateVertexRemapCustom(&remap[0], NULL, index_count, + &unindexed_vertices[0].px, unindexed_vertex_count, sizeof(Vertex), + [&](unsigned int lhs, unsigned int rhs) -> bool { + const Vertex& lv = unindexed_vertices[lhs]; + const Vertex& rv = unindexed_vertices[rhs]; + + return fabsf(lv.tx - rv.tx) < 1e-3f && fabsf(lv.ty - rv.ty) < 1e-3f; + }); +``` + +### Vertex cache optimization + +When the GPU renders the mesh, it runs the vertex shader for each vertex. Historically, GPUs used a small fixed-size post-transform cache (16-32 vertices) with different replacement policies to store the shader output and avoid redundant shader invocations. Modern GPUs still perform vertex reuse, but with substantially different mechanics: vertex invocations are batched into thread groups based on the input indices, and effective reuse depends on factors like vertex shader outputs and rasterizer throughput. To maximize the locality of reused vertex references, you have to reorder your triangles like so: + +```c++ +meshopt_optimizeVertexCache(indices, indices, index_count, vertex_count); +``` + +The details of vertex reuse vary between different GPU architectures, so vertex cache optimization uses an adaptive algorithm that produces a triangle sequence with good locality that works well across different GPUs. Alternatively, you can use an algorithm that optimizes specifically for fixed-size FIFO caches: `meshopt_optimizeVertexCacheFifo` (with a recommended cache size of 16). While it generally produces less performant results on most GPUs, it runs ~2x faster, which may benefit rapid content iteration. + +### Overdraw optimization + +After transforming the vertices, GPU sends the triangles for rasterization which results in generating pixels that are usually first run through the depth test, and pixels that pass it get the pixel shader executed to generate the final color. As pixel shaders get more expensive, it becomes more and more important to reduce overdraw. While in general improving overdraw requires view-dependent operations, this library provides an algorithm to reorder triangles to minimize the overdraw from all directions, which you can run after vertex cache optimization like this: + +```c++ +meshopt_optimizeOverdraw(indices, indices, index_count, &vertices[0].x, vertex_count, sizeof(Vertex), 1.05f); +``` + +The overdraw optimizer needs to read vertex positions as a float3 from the vertex; the code snippet above assumes that the vertex stores position as `float x, y, z`. + +When performing the overdraw optimization you have to specify a floating-point threshold parameter. The algorithm tries to maintain a balance between vertex cache efficiency and overdraw; the threshold determines how much the algorithm can compromise the vertex cache hit ratio, with 1.05 meaning that the resulting ratio should be at most 5% worse than before the optimization. + +Note that depending on the renderer structure and target hardware, the optimization may or may not be beneficial; for example, mobile GPUs with tiled deferred rendering (PowerVR, Apple) would not benefit from this optimization. For vertex heavy scenes it's recommended to measure the performance impact to ensure that the reduced vertex cache efficiency is outweighed by the reduced overdraw. + +### Vertex fetch optimization + +After the final triangle order has been established, we still can optimize the vertex buffer for memory efficiency. Before running the vertex shader GPU has to fetch the vertex attributes from the vertex buffer; the fetch is usually backed by a memory cache, and as such optimizing the data for the locality of memory access is important. You can do this by running this code: + +```c++ +meshopt_optimizeVertexFetch(vertices, indices, index_count, vertices, vertex_count, sizeof(Vertex)); +``` + +This will reorder the vertices in the vertex buffer to try to improve the locality of reference, and rewrite the indices in place to match; if the vertex data is stored using multiple streams, you should use `meshopt_optimizeVertexFetchRemap` instead. This optimization has to be performed on the final index buffer since the optimal vertex order depends on the triangle order. + +Note that the algorithm does not try to model cache replacement precisely and instead just orders vertices in the order of use, which generally produces results that are close to optimal. + +### Vertex quantization + +To optimize memory bandwidth when fetching the vertex data even further, and to reduce the amount of memory required to store the mesh, it is often beneficial to quantize the vertex attributes to smaller types. While this optimization can technically run at any part of the pipeline (and sometimes doing quantization as the first step can improve indexing by merging almost identical vertices), it generally is easier to run this after all other optimizations since some of them require access to float3 positions. + +Quantization is usually domain specific; it's common to quantize normals using 3 8-bit integers but you can use higher-precision quantization (for example using 10 bits per component in a 10_10_10_2 format), or a different encoding to use just 2 components. For positions and texture coordinate data the two most common storage formats are half precision floats, and 16-bit normalized integers that encode the position relative to the AABB of the mesh or the UV bounding rectangle. + +The number of possible combinations here is very large but this library does provide the building blocks, specifically functions to quantize floating point values to normalized integers, as well as half-precision floats. For example, here's how you can quantize a normal using 10-10-10 SNORM encoding: + +```c++ +unsigned int normal = + ((meshopt_quantizeSnorm(v.nx, 10) & 1023) << 20) | + ((meshopt_quantizeSnorm(v.ny, 10) & 1023) << 10) | + (meshopt_quantizeSnorm(v.nz, 10) & 1023); +``` + +and here's how you can quantize a position using half precision floats: + +```c++ +unsigned short px = meshopt_quantizeHalf(v.x); +unsigned short py = meshopt_quantizeHalf(v.y); +unsigned short pz = meshopt_quantizeHalf(v.z); +``` + +Since quantized vertex attributes often need to remain in their compact representations for efficient transfer and storage, they are usually dequantized during vertex processing by configuring the GPU vertex input correctly to expect normalized integers or half precision floats, which often needs no or minimal changes to the shader code. When CPU dequantization is required instead, `meshopt_dequantizeHalf` can be used to convert half precision values back to single precision; for normalized integer formats, the dequantization just requires dividing by 2^N-1 for unorm and 2^(N-1)-1 for snorm variants. For example, manually reversing `meshopt_quantizeUnorm(v, 10)` can be done by dividing by 1023. + +### Index filtering + +Some meshes may contain triangles that are processed during rendering but do not contribute to the rendered result. If any two vertices of a triangle result in the same position after vertex shader, the triangle is degenerate and will be skipped by the rasterizer. Some triangles may also be duplicates of an earlier triangle with the same post-transform positions and winding, in which case only one of the triangles will be visible depending on depth testing settings (assuming blending is disabled). In either case, such triangles require extra processing and removing them may improve rasterization or ray tracing performance; this library provides an algorithm that removes such triangles from the index buffer: + +```c++ +indices.resize(meshopt_filterIndexBuffer(&indices[0], &indices[0], indices.size(), &vertices[0].x, vertices.size(), sizeof(float) * 3, sizeof(Vertex))); +``` + +Note that the example above assumes only positions are relevant for transforming the vertices, but for deformable meshes skinning data may need to be added to the vertex portion used as a key; `meshopt_filterIndexBufferMulti` can be useful for these cases if the relevant data is not contiguous. + +Filtering after quantization is convenient because quantization may increase the number of redundant triangles if triangles had similar but not identical vertex positions before quantization. However, filtering can be done at any point in the pipeline as soon as the index buffer becomes available; you could also run vertex fetch optimization after filtering, since it will naturally filter out any vertices that may become unused after redundant triangles are eliminated, potentially saving extra memory. + +### Shadow indexing + +Many rendering pipelines require meshes to be rendered to depth-only targets, such as shadow maps or during a depth pre-pass, in addition to color/G-buffer targets. While using the same geometry data for both cases is possible, reducing the number of unique vertices for depth-only rendering can be beneficial, especially when the source geometry has many attribute seams due to faceted shading or lightmap texture seams. + +To achieve this, this library provides the `meshopt_generateShadowIndexBuffer` algorithm, which generates a second (shadow) index buffer that can be used with the original vertex data: + +```c++ +std::vector shadow_indices(index_count); +// note: this assumes Vertex starts with float3 positions and should be adjusted accordingly for quantized positions +meshopt_generateShadowIndexBuffer(&shadow_indices[0], indices, index_count, &vertices[0].x, vertex_count, sizeof(float) * 3, sizeof(Vertex)); +``` + +Because the vertex data is shared, shadow indexing should be done after other optimizations of the vertex/index data. However, it's possible (and recommended) to optimize the resulting shadow index buffer for vertex cache: + +```c++ +meshopt_optimizeVertexCache(&shadow_indices[0], &shadow_indices[0], index_count, vertex_count); +``` + +In some cases, it may be beneficial to split the vertex positions into a separate buffer to maximize efficiency for depth-only rendering. Note that the example above assumes only positions are relevant for shadow rendering, but more complex materials may require adding texture coordinates (for alpha testing) or skinning data to the vertex portion used as a key. `meshopt_generateShadowIndexBufferMulti` can be useful for these cases if the relevant data is not contiguous. + +Note that for meshes with optimal indexing and few attribute seams, the shadow index buffer will be very similar to the original index buffer, so it may not be always worth generating a separate shadow index buffer even if the rendering pipeline relies on depth-only passes. + +## Clusterization + +While traditionally meshes have served as a unit of rendering, new approaches to rendering and raytracing are starting to use a smaller unit of work, such as clusters or meshlets. This allows more freedom in how the geometry is processed, and can lead to better performance and more efficient use of GPU hardware. This section describes algorithms designed to work with meshes as sets of clusters. + +### Mesh shading + +Modern GPUs are beginning to deviate from the traditional rasterization model. NVidia GPUs starting from Turing and AMD GPUs starting from RDNA2 provide a new programmable geometry pipeline that, instead of being built around index buffers and vertex shaders, is built around mesh shaders - a new shader type that allows to provide a batch of work to the rasterizer. + +Using mesh shaders in context of traditional mesh rendering provides an opportunity to use a variety of optimization techniques, starting from more efficient vertex reuse, using various forms of culling (e.g. cluster frustum or occlusion culling) and in-memory compression to maximize the utilization of GPU hardware. Beyond traditional rendering mesh shaders provide a richer programming model that can synthesize new geometry more efficiently than common alternatives such as geometry shaders. Mesh shading can be accessed via Vulkan or Direct3D 12 APIs; please refer to [Introduction to Turing Mesh Shaders](https://developer.nvidia.com/blog/introduction-turing-mesh-shaders/) and [Mesh Shaders and Amplification Shaders: Reinventing the Geometry Pipeline](https://devblogs.microsoft.com/directx/coming-to-directx-12-mesh-shaders-and-amplification-shaders-reinventing-the-geometry-pipeline/) for additional information. + +To use mesh shaders for conventional rendering efficiently, geometry needs to be converted into a series of meshlets; each meshlet represents a small subset of the original mesh and comes with a small set of vertices and a separate micro-index buffer that references vertices in the meshlet. This information can be directly fed to the rasterizer from the mesh shader. This library provides algorithms to create meshlet data for a mesh, and - assuming geometry is static - can compute bounding information that can be used to perform cluster culling, rejecting meshlets that are invisible on screen. + +To generate meshlet data, this library provides `meshopt_buildMeshlets` algorithm, which tries to balance topological efficiency (by maximizing vertex reuse inside meshlets) with culling efficiency (by minimizing meshlet radius and triangle direction divergence) and produces GPU-friendly data. As an alternative (that can be useful for load-time processing), `meshopt_buildMeshletsScan` can create the meshlet data using a vertex cache-optimized index buffer as a starting point by greedily aggregating consecutive triangles until they go over the meshlet limits. `meshopt_buildMeshlets` is recommended for offline data processing even if cone culling is not used. + +```c++ +const size_t max_vertices = 64; +const size_t max_triangles = 126; // note: in v0.25 or prior, max_triangles needs to be divisible by 4 +const float cone_weight = 0.0f; + +size_t max_meshlets = meshopt_buildMeshletsBound(indices.size(), max_vertices, max_triangles); +std::vector meshlets(max_meshlets); +std::vector meshlet_vertices(indices.size()); +std::vector meshlet_triangles(indices.size()); // note: in v0.25 or prior, use indices.size() + max_meshlets * 3 + +size_t meshlet_count = meshopt_buildMeshlets(meshlets.data(), meshlet_vertices.data(), meshlet_triangles.data(), indices.data(), + indices.size(), &vertices[0].x, vertices.size(), sizeof(Vertex), max_vertices, max_triangles, cone_weight); +``` + +To generate the meshlet data, `max_vertices` and `max_triangles` need to be set within limits supported by the hardware; for NVidia the values of 64 and 126 are recommended. `cone_weight` should be left as 0 if cluster cone culling is not used, and set to a value between 0 and 1 to balance cone culling efficiency with other forms of culling like frustum or occlusion culling (`0.25` is a reasonable default). + +> Note that for earlier AMD GPUs, the best configurations tend to use the same limits for `max_vertices` and `max_triangles`, such as 64 and 64, or 128 and 128. Additionally, while NVidia recommends 64/126 as a good configuration, consider using a different configuration like `max_vertices 64, max_triangles 96`, to provide more realistic limits that are achievable on real-world meshes, and to reduce the overhead on other GPUs. + +Each resulting meshlet refers to a portion of `meshlet_vertices` and `meshlet_triangles` arrays; the arrays are overallocated for the worst case so it's recommended to trim them before saving them as an asset / uploading them to the GPU: + +```c++ +const meshopt_Meshlet& last = meshlets[meshlet_count - 1]; + +meshlet_vertices.resize(last.vertex_offset + last.vertex_count); +meshlet_triangles.resize(last.triangle_offset + last.triangle_count * 3); +meshlets.resize(meshlet_count); +``` + +Depending on the application, other strategies of storing the data can be useful; for example, `meshlet_vertices` serves as indices into the original vertex buffer but it might be worthwhile to generate a mini vertex buffer for each meshlet to remove the extra indirection when accessing vertex data, or it might be desirable to compress vertex data as vertices in each meshlet are likely to be very spatially coherent. + +> Some proprietary platforms have additional restrictions on the index range that can be referenced by a meshlet. Building the library with `MESHOPTIMIZER_CLUSTERIZER_INDEXLIMIT` defined will ensure that generated meshlets conform to these restrictions, but the resulting meshlets can *not* be processed further as additional reordering may break the limits. + +For optimal performance, it is recommended to further optimize each meshlet in isolation for better triangle and vertex locality by calling `meshopt_optimizeMeshlet` on vertex and index data like so: + +```c++ +meshopt_optimizeMeshlet(&meshlet_vertices[m.vertex_offset], &meshlet_triangles[m.triangle_offset], m.triangle_count, m.vertex_count); +``` + +Different applications will choose different strategies for rendering meshlets; on a GPU capable of mesh shading, meshlets can be rendered directly; for example, a basic GLSL shader for `VK_EXT_mesh_shader` extension could look like this (parts omitted for brevity): + +```glsl +layout(binding = 0) readonly buffer Meshlets { Meshlet meshlets[]; }; +layout(binding = 1) readonly buffer MeshletVertices { uint meshlet_vertices[]; }; +layout(binding = 2) readonly buffer MeshletTriangles { uint8_t meshlet_triangles[]; }; + +void main() { + Meshlet meshlet = meshlets[gl_WorkGroupID.x]; + SetMeshOutputsEXT(meshlet.vertex_count, meshlet.triangle_count); + + for (uint i = gl_LocalInvocationIndex; i < meshlet.vertex_count; i += gl_WorkGroupSize.x) { + uint index = meshlet_vertices[meshlet.vertex_offset + i]; + gl_MeshVerticesEXT[i].gl_Position = world_view_projection * vec4(vertex_positions[index], 1); + } + + for (uint i = gl_LocalInvocationIndex; i < meshlet.triangle_count; i += gl_WorkGroupSize.x) { + uint offset = meshlet.triangle_offset + i * 3; + gl_PrimitiveTriangleIndicesEXT[i] = uvec3( + meshlet_triangles[offset], meshlet_triangles[offset + 1], meshlet_triangles[offset + 2]); + } +} +``` + +> Note that DirectX 12 mesh shaders cannot index raw buffers using arbitrary byte offsets. Use a typed SRV buffer (`Buffer`) with `DXGI_FORMAT_R8_UINT` format, repack each triangle to 32 bits to be able to use aligned 32-bit loads with `ByteAddressBuffer`, or consider utilizing [16-bit scalar types](https://github.com/microsoft/DirectXShaderCompiler/wiki/16-Bit-Scalar-Types) to load a 3-byte triangle using two aligned 16-bit loads like so: `Buffer.Load(triangle_offset & ~1)`, then extracting indices with bitwise operations based on `triangle_offset & 1`. + +After generating the meshlet data, it's possible to generate extra data for each meshlet that can be saved and used at runtime to perform cluster culling, where each meshlet can be discarded if it's guaranteed to be invisible. To generate the data, `meshopt_computeMeshletBounds` can be used: + +```c++ +meshopt_Bounds bounds = meshopt_computeMeshletBounds(&meshlet_vertices[m.vertex_offset], &meshlet_triangles[m.triangle_offset], + m.triangle_count, &vertices[0].x, vertices.size(), sizeof(Vertex)); +``` + +The resulting `bounds` values can be used to perform frustum or occlusion culling using the bounding sphere, or cone culling using the cone axis/angle (which will reject the entire meshlet if all triangles are guaranteed to be back-facing from the camera point of view): + +```c++ +if (dot(normalize(cone_apex - camera_position), cone_axis) >= cone_cutoff) reject(); +``` + +Cluster culling should ideally run at a lower frequency than mesh shading, either using amplification/task shaders, or using a separate compute dispatch. + +By default, the meshlet builder tries to form complete meshlets even if that requires merging disconnected regions of the mesh into a single meshlet. In some cases, such as hierarchical level of detail, or when advanced culling is used, it may be beneficial to prioritize spatial locality of triangles in a meshlet even if that results in partially filled meshlets. To that end, `meshopt_buildMeshletsFlex` function can be used instead of `meshopt_buildMeshlets`; it provides two triangle limits, `min_triangles` and `max_triangles`, and uses an additional configuration parameter, `split_factor` (recommended value is 2.0), to decide whether increasing the meshlet radius is worth it to fit more triangles in the meshlet. When using this function, the worst case bound for the number of meshlets has to be computed using `meshopt_buildMeshletsBound` with `min_triangles` parameter instead of `max_triangles`. + +### Clustered raytracing + +In addition to rasterization, meshlets can also be used for ray tracing. NVidia GPUs starting from Turing with recent drivers provide support for cluster acceleration structures (via `VK_NV_cluster_acceleration_structure` extension / NVAPI); instead of building a traditional BLAS, a cluster acceleration structure can be built for each meshlet and combined into a single clustered BLAS. While this currently results in reduced ray tracing performance for static geometry (for which a traditional BLAS may be more suitable), it allows updating the individual clusters without having to rebuild or refit the entire BLAS, which can be useful for mesh deformation or hierarchical level of detail. + +When using meshlets for raytracing, the performance characteristics that matter differ from when rendering meshes with rasterization. For raytracing, clusters with optimal spatial division that minimize ray-triangle intersection tests are preferred, while for rasterization, clusters with maximum triangle count within vertex limits are ideal. + +To generate meshlets optimized for raytracing, this library provides `meshopt_buildMeshletsSpatial` algorithm, which builds clusters using surface area heuristic (SAH) to produce raytracing-friendly cluster distributions: + +```c++ +const size_t max_vertices = 64; +const size_t min_triangles = 16; +const size_t max_triangles = 64; +const float fill_weight = 0.5f; + +size_t max_meshlets = meshopt_buildMeshletsBound(indices.size(), max_vertices, min_triangles); // note: use min_triangles to compute worst case bound +std::vector meshlets(max_meshlets); +std::vector meshlet_vertices(indices.size()); +std::vector meshlet_triangles(indices.size()); // note: in v0.25 or prior, use indices.size() + max_meshlets * 3 + +size_t meshlet_count = meshopt_buildMeshletsSpatial(meshlets.data(), meshlet_vertices.data(), meshlet_triangles.data(), indices.data(), + indices.size(), &vertices[0].x, vertices.size(), sizeof(Vertex), max_vertices, min_triangles, max_triangles, fill_weight); +``` + +The algorithm recursively subdivides the triangles into a BVH-like hierarchy using SAH for optimal spatial partitioning while balancing cluster size; this results in clusters that are significantly more efficient to raytrace compared to clusters generated by `meshopt_buildMeshlets`, but can still be used for rasterization (for example, to build visibility buffers or G-buffers). + +The `min_triangles` and `max_triangles` parameters control the allowed range of triangles per cluster. For optimal raytracing performance, `min_triangles` should be at most `max_triangles/2` (or, ideally, `max_triangles/4`) to give the algorithm enough freedom to produce high-quality spatial partitioning. For meshes with few seams due to normal or UV discontinuities, using `max_vertices` equal to `max_triangles` is recommended when rasterization performance is a concern; for meshes with many seams or for renderers that primarily use meshlets for ray tracing, a higher `max_vertices` value should be used as it ensures that more clusters can fully utilize the triangle limit. + +The `fill_weight` parameter (typically between 0 and 1, although values higher than 1 could be used to prioritize cluster fill even more) controls the trade-off between pure SAH optimization and triangle utilization. A value of 0 will optimize purely for SAH, resulting in best raytracing performance but potentially smaller clusters. Values between 0.25 and 0.75 typically provide a good balance of SAH quality vs triangle count. + +When the resulting meshlets are used to generate hardware-specific acceleration structures, using fast trace (e.g. `VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR`) builds results in maximum performance; if build performance is important, using `meshopt_optimizeMeshlet` can help improve ray tracing performance when using fast build (e.g. `VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_BUILD_BIT_KHR`), although the tracing performance will still be lower than with fast trace builds. + +### Point cloud clusterization + +Both of the meshlet algorithms are designed to work with triangle meshes. In some cases, splitting a point cloud into fixed size clusters can be useful; the resulting point clusters could be rendered via mesh or compute shaders, or the resulting subdivision can be used to parallelize point processing while maintaining locality of points. To that end, this library provides `meshopt_spatialClusterPoints` algorithm: + +```c++ +const size_t cluster_size = 256; + +std::vector index(vertices.size()); +meshopt_spatialClusterPoints(&index[0], &vertices[0].px, vertices.size(), sizeof(Vertex), cluster_size); +``` + +The resulting index buffer could be used to process the points directly, or reorganize the point data into flat contiguous arrays. Every consecutive chunk of `cluster_size` points in the index buffer refers to a single cluster, with just the last cluster containing fewer points if the total number of points is not a multiple of `cluster_size`. Note that the index buffer is not a remap table, so `meshopt_remapVertexBuffer` can't be used to flatten the point data. + +### Cluster partitioning + +When working with clustered geometry, it can be beneficial to organize clusters into larger groups (partitions) for more efficient processing or workload distribution. This library provides an algorithm to partition clusters into groups of similar size while prioritizing locality: + +```c++ +const size_t partition_size = 24; + +std::vector cluster_partitions(cluster_count); +size_t partition_count = meshopt_partitionClusters(&cluster_partitions[0], &cluster_indices[0], total_index_count, + &cluster_index_counts[0], cluster_count, &vertices[0].x, vertex_count, sizeof(Vertex), partition_size); +``` + +The algorithm assigns each cluster to a partition, aiming for a target partition size while prioritizing topological locality (sharing vertices) and spatial locality. The resulting partitions can be used for more efficient batched processing of clusters, or for hierarchical simplification schemes similar to Nanite. + +Two clusters are considered topologically adjacent if they reference the same indices. In some cases, it can be helpful to process the indices using `meshopt_generateShadowIndexBuffer` (or remap them manually using the remap table generated by `meshopt_generatePositionRemap`), which allows clusters to be considered adjacent even when boundary vertices have different indices due to attribute discontinuities. + +If vertex positions are specified (not `NULL`), spatial locality will influence priority of merging clusters; otherwise, the algorithm will rely solely on topological connections and will not merge disconnected clusters into the same partition, which may result in smaller partitions for some inputs. + +After partitioning, each element in the destination array contains the partition ID (ranging from 0 to the returned partition count minus 1) for the corresponding cluster. Note that the partitions may be both smaller and larger than the target size; given a target size, the maximum partition size returned currently is `target + target / 3`. + +### Cluster position quantization + +When working with clustered geometry, it may be possible to use cluster-relative quantization for vertex positions. In particular, a common representation involves quantizing positions to lie on an integer grid with an exponent shared between clusters, to avoid quantizing shared vertices differently between clusters, and storing each position as an offset from the cluster anchor. A variant of this representation called [Compressed1 position encoding](https://microsoft.github.io/DirectX-Specs/d3d/Raytracing2.html#compressed1-position-encoding) is supported by DXR2. This library provides a helper function, `meshopt_computePositionExponent`, that can be used to compute the exponent, as well as an example (see `encodeMeshletsDXR` in `demo/main.cpp`) demonstrating the full pipeline. + +First, compute the shared exponent to guarantee that any vertex in every cluster fits in a 24-bit signed integer, and additionally can be encoded as a K-bit unsigned delta from the per-cluster anchor (in the example below, `K=16` which matches DXR2 limits): + +```c++ +const int min_exp = -13; // start with an acceptable level of precision; 2^-13 ~= 0.1mm in metric units +const int max_bits = 16; // maximum number of offset bits per axis + +int exponent = min_exp; + +for (size_t i = 0; i < meshlets.size(); ++i) +{ + AABB meshlet_aabb = ...; // compute meshlet AABB based on vertex positions + int cexp = meshopt_computePositionExponent(meshlet_aabb.min, meshlet_aabb.max, min_exp, max_bits); + exponent = std::max(exponent, cexp); +} + +float scale = ldexpf(1.f, exponent); +``` + +After this, each cluster can be encoded independently; when encoding each position, positions should be converted to the integer grid, and the anchor should be established: + +```c++ +int positions[256][3]; +int anchor[3] = {INT_MAX, INT_MAX, INT_MAX}; + +for (size_t j = 0; j < meshlet.vertex_count; ++j) +{ + unsigned int v = meshlet_vertices[meshlet.vertex_offset + j]; + const float* p = &vertices[v].px; + + for (int k = 0; k < 3; ++k) + { + positions[j][k] = int(roundf(p[k] / scale)); + anchor[k] = std::min(anchor[k], positions[j][k]); + } +} +``` + +The exponent selection guarantees that as a result, for each cluster, we get 24-bit `anchor` components, and for each vertex `position[k] - anchor[k]` fits into a 16-bit unsigned integer. The positions *could* then be encoded using a fixed-width 16-bit encoding, with 6 bytes per vertex; however, because many clusters will require fewer bits, it's more efficient to determine the bit count per axis per cluster (by computing the number of bits per axis that is sufficient to fit `position[k] - anchor[k]`), and encoding the resulting deltas from the anchor into a bitstream. When using DXR2, the resulting position bits can be given directly to the cluster BVH builder by using `D3D12_VERTEX_FORMAT_COMPRESSED1` format. + +The same data could be directly decoded from a mesh shader in hybrid rendering pipelines, since each cluster uses a consistent number of bits per axis; this would require implementing unaligned bitstream read access in the shader. Alternatively, deltas could be stored as 16-bit integers alongside other vertex data (taking as much space as `snorm16`/`half` positions with significantly increased precision for larger meshes). + +> In the examples above, `max_bits` is set to 16 to match DXR2 limits. If DXR2 compatibility is not required, it may sometimes be necessary to use more bits to accommodate larger clusters. This is particularly relevant when using hierarchical cluster LOD, as all clusters at all levels of detail must share the exponent to eliminate gaps between adjacent clusters at different resolutions; Nanite uses up to 21 bits per axis for this reason. + +This scheme works well together with meshlet compression described below, which can be used to encode topology (triangles) efficiently. + +## Mesh compression + +In case storage size or transmission bandwidth is of importance, you might want to additionally compress vertex and index data. While several mesh compression libraries, like Google Draco, are available, they typically are designed to maximize the compression ratio at the cost of disturbing the vertex/index order (which makes the meshes inefficient to render on GPU) or decompression performance. They also frequently don't support custom game-ready quantized vertex formats and thus require to re-quantize the data after loading it, introducing extra quantization errors and making decoding slower. + +Alternatively you can use general purpose compression libraries like zstd or Oodle to compress vertex/index data - however these compressors aren't designed to exploit redundancies in vertex/index data and as such compression rates can be unsatisfactory. + +To that end, this library provides algorithms to "encode" vertex and index data. The result of the encoding is generally significantly smaller than initial data, and remains compressible with general purpose compressors - so you can either store encoded data directly (for modest compression ratios and maximum decoding performance), or further compress it with LZ4/zstd/Oodle to maximize compression ratio. + +> Note: this compression scheme is available as a glTF extension [EXT_meshopt_compression](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/EXT_meshopt_compression/README.md) as well as [KHR_meshopt_compression](https://github.com/KhronosGroup/glTF/pull/2517). + +### Vertex compression + +This library provides a lossless algorithm to encode/decode vertex data. To encode vertices, you need to allocate a target buffer (using the worst case bound) and call the encoding function: + +```c++ +std::vector vbuf(meshopt_encodeVertexBufferBound(vertex_count, sizeof(Vertex))); +vbuf.resize(meshopt_encodeVertexBuffer(&vbuf[0], vbuf.size(), vertices, vertex_count, sizeof(Vertex))); +``` + +To decode the data at runtime, call the decoding function: + +```c++ +int res = meshopt_decodeVertexBuffer(vertices, vertex_count, sizeof(Vertex), &vbuf[0], vbuf.size()); +assert(res == 0); +``` + +Note that vertex encoding assumes that vertex buffer was optimized for vertex fetch, and that vertices are quantized. Feeding unoptimized data into the encoder may result in poor compression ratios. The codec is lossless by itself - the only lossy step is quantization/reordering or filters that you may apply before encoding. Additionally, if the vertex data contains padding bytes, they should be zero-initialized to ensure that the encoder does not need to store uninitialized data. + +Decoder is heavily optimized and can directly target write-combined memory; you can expect it to run at 3-6 GB/s on modern desktop CPUs. Compression ratio depends on the data; vertex data compression ratio is typically around 2-4x (compared to already quantized and optimally packed data). General purpose lossless compressors can further improve the compression ratio at some cost to decoding performance. If decoding always targets regular cached memory, compiling the library with `MESHOPTIMIZER_VERTEXCODEC_ZEROCOPY` define can yield additional ~10% decoding performance. + +The vertex codec tries to take advantage of the inherent locality of sequential vertices and identify bit patterns that repeat in consecutive vertices. Typically, vertex cache + vertex fetch provides a reasonably local vertex traversal order; without an index buffer, it is recommended to sort vertices spatially (via `meshopt_spatialSortRemap`) to improve the compression ratio. + +It is crucial to correctly specify the stride when encoding vertex data; however, for compression ratio it does not matter whether the vertices are interleaved or deinterleaved, as the codecs perform full byte deinterleaving internally. The stride of each stream must be a multiple of 4 bytes. + +For optimal compression results, the values should be quantized to small integers. It can be valuable to use bit counts that are not multiples of 8. For example, instead of using 16 bits to represent texture coordinates, use 12-bit integers and divide by 4095 in the shader. Alternatively, using half-precision floats can often achieve good results. +For single-precision floating-point data, it's recommended to use `meshopt_quantizeFloat` to remove entropy from the lower bits of the mantissa; for best results, consider using 15 bits or 7 bits for extreme compression. +For normal or tangent vectors, using octahedral encoding is recommended over three components as it reduces redundancy; similarly, consider using 10-12 bits per component instead of 16. + +When data is bit packed, specifying compression level 3 (via `meshopt_encodeVertexBufferLevel`) can improve the compression further by redistributing bits between components. + +### Index compression + +This library also provides algorithms to encode/decode index data. To encode triangle indices, you need to allocate a target buffer (using the worst case bound) and call the encoding function: + +```c++ +std::vector ibuf(meshopt_encodeIndexBufferBound(index_count, vertex_count)); +ibuf.resize(meshopt_encodeIndexBuffer(&ibuf[0], ibuf.size(), indices, index_count)); +``` + +To decode the data at runtime, call the decoding function: + +```c++ +int res = meshopt_decodeIndexBuffer(indices, index_count, &ibuf[0], ibuf.size()); +assert(res == 0); +``` + +Note that index encoding assumes that the index buffer was optimized for vertex cache and vertex fetch. Feeding unoptimized data into the encoder will result in poor compression ratios. Codec preserves the order of triangles, however it can rotate each triangle to improve compression ratio (which means the provoking vertex may change). + +Decoder is heavily optimized and can directly target write-combined memory; you can expect it to run at 3-6 GB/s on modern desktop CPUs. + +The index codec targets 1 byte per triangle as a best case (6x smaller than raw 16-bit index data); on real-world meshes, it's typical to achieve 1-1.2 bytes per triangle. To reach this, the index data needs to be optimized for vertex cache and vertex fetch. Optimizations that do not disrupt triangle locality (such as overdraw) are safe to use in between. +To reduce the data size further, it's possible to use `meshopt_optimizeVertexCacheStrip` instead of `meshopt_optimizeVertexCache` when optimizing for vertex cache. This trades off some efficiency in vertex transform for smaller index (and sometimes vertex) data. + +When referenced vertex indices are not sequential, the index codec will use around 2 bytes per index. This can happen when the referenced vertices are a sparse subset of the vertex buffer, such as when encoding LODs. General-purpose compression can be especially helpful in this case. + +Index buffer codec only supports triangle list topology; when encoding triangle strips or line lists, use `meshopt_encodeIndexSequence`/`meshopt_decodeIndexSequence` instead. This codec typically encodes indices into ~1 byte per index, but compressing the results further with a general purpose compressor can improve the results to 1-3 bits per index. + + +### Meshlet compression + +When using mesh shading or clustered raytracing, meshlet vertex reference and triangle data can be compressed similarly to index data. This library provides a dedicated codec that exploits locality inherent in meshlet data. Unlike vertex and index buffer codecs that work on entire buffers, the meshlet codec encodes each meshlet independently; this allows applications to have more flexibility in structuring the runtime storage and adjust the decoded data during decoding. This also means that in some applications, additional data describing the meshlet (vertex/triangle count, encoded size) will need to be encoded into the meshlet stream, if it isn't already available during decoding. + +To encode a meshlet, you need to allocate a target buffer (using the worst case bound) and call the encoding function with the vertex index references and micro-index buffer, as produced by `meshopt_buildMeshlets`: + +```c++ +std::vector mbuf(meshopt_encodeMeshletBound(max_vertices, max_triangles)); + +for (const meshopt_Meshlet& m : meshlets) +{ + size_t msize = meshopt_encodeMeshlet(&mbuf[0], mbuf.size(), + &meshlet_vertices[m.vertex_offset], m.vertex_count, &meshlet_triangles[m.triangle_offset], m.triangle_count); + + // write m.vertex_count, m.triangle_count, msize and mbuf[0..msize-1] to the output stream +} +``` + +To decode the data at runtime, call the decoding function: + +```c++ +uint16_t* vertices = ...; +uint8_t* triangles = ...; + +// automatically deduces `vertex_size=2` and `triangle_size=3` based on pointer types +int res = meshopt_decodeMeshlet(vertices, m.vertex_count, triangles, m.triangle_count, stream, encoded_size); +assert(res == 0); +``` + +Vertex index references can be decoded as either 16-bit or 32-bit integers; triangle data can be decoded as 3 bytes per triangle (matching `meshopt_buildMeshlets` output format) or as a 32-bit integer per triangle (with indices packed as `a | (b << 8) | (c << 16)` and top byte unused). Output buffers must have available space aligned to 4 bytes; for example, decoding a 3-triangle stream using 3 bytes per triangle needs to be able to write 12 bytes to the output triangles array. + +When using the C++ API, `meshopt_decodeMeshlet` will automatically deduce the element sizes based on the types of vertex and triangle pointers; when using the C API, the sizes need to be specified explicitly. + +Decoder is heavily optimized and can directly target write-combined memory; you can expect it to run at 7-10 GB/s on modern desktop CPUs. + +> Applications that do most of the streaming decompression on the GPU can also decode meshlet data on the GPU if CPU decoding is inconvenient; an example [meshletdec.slang](./demo/meshletdec.slang) shader is provided for 32-bit output format, and can be easily adapted to other formats, including custom ones. + +Note that meshlet encoding assumes that the meshlet data was optimized; meshlets should be processed using `meshopt_optimizeMeshletLevel` with level 1 or higher (3 recommended for improved compression) before encoding. Additionally, vertex references should have a high degree of reference locality; this can be achieved by building meshlets from meshes optimized for vertex cache/fetch, or linearizing the vertex reference data and reordering the vertex buffer using `meshopt_optimizeVertexFetch`. Feeding unoptimized data into the encoder will result in poor compression ratios. Codec preserves the order of triangles, however it can rotate each triangle to improve compression ratio (which means the provoking vertex may change). + +Meshlets without vertex references are supported; passing `NULL` vertices and `0` vertex count during encoding and decoding will produce encoded meshlets with just triangle data. Note that parameters supplied during decoding must match those used during encoding; if a meshlet was encoded with vertex references, it must be decoded with the same number of vertex references. + +The meshlet codec targets 5-7 bits per triangle for triangle data; when vertex references are encoded, the encoded size strongly depends on how linear the references are, but it's typical to see 9-12 bits per triangle in aggregate. To reduce the compressed size further, it's possible to compress the resulting encoded data with a general purpose compressor, which usually achieves 5-8 bits/triangle in aggregate; note that in this case general purpose compressors should be applied to a stream with many encoded meshlets at once to amortize their overhead. + +### Point cloud compression + +The vertex encoding algorithms can be used to compress arbitrary streams of attribute data; one other use case besides triangle meshes is point cloud data. Typically point clouds come with position, color and possibly other attributes but don't have an implied point order. + +To compress point clouds efficiently, it's recommended to first preprocess the points by sorting them using the spatial sort algorithm: + +```c++ +std::vector remap(point_count); +meshopt_spatialSortRemap(&remap[0], positions, point_count, sizeof(vec3)); + +// for each attribute stream +meshopt_remapVertexBuffer(positions, positions, point_count, sizeof(vec3), &remap[0]); +``` + +After this the resulting arrays should be quantized (e.g. using 16-bit fixed point numbers for positions and 8-bit color components), and the result can be compressed using `meshopt_encodeVertexBuffer` as described in the previous section. To decompress, `meshopt_decodeVertexBuffer` will recover the quantized data that can be used directly or converted back to original floating-point data. The compression ratio depends on the nature of source data, for colored points it's typical to get 35-40 bits per point. + +### Vertex filters + +To further leverage the inherent structure of some vertex data, it's possible to use filters that encode and decode the data in a lossy manner. This is similar to quantization but can be used without having to change the shader code. After decoding, the filter transformation needs to be reversed. For native game engine pipelines, it is usually more optimal to carefully prequantize and pretransform the vertex data, but sometimes (for example when serializing data in glTF format) this is not a practical option and filters are more convenient. This library provides four filters: + +- Octahedral filter (`meshopt_encodeFilterOct`/`meshopt_decodeFilterOct`) encodes quantized (snorm) normal or tangent vectors using octahedral encoding. Any number of bits between 2 and 16 can be used with 4 bytes or 8 bytes per vector. +- Quaternion filter (`meshopt_encodeFilterQuat`/`meshopt_decodeFilterQuat`) encodes quantized (snorm) quaternion vectors; this can be used to encode rotations or tangent frames. Any number of bits between 4 and 16 can be used with 8 bytes per vector. +- Exponential filter (`meshopt_encodeFilterExp`/`meshopt_decodeFilterExp`) encodes single-precision floating-point vectors; this can be used to encode arbitrary floating-point data more efficiently. In addition to an arbitrary bit count (<= 24), the filter takes a "mode" parameter that allows specifying how the exponent sharing is performed to trade off compression ratio and quality: + - `meshopt_EncodeExpSeparate` does not share exponents and results in the largest output + - `meshopt_EncodeExpSharedVector` shares exponents between different components of the same vector + - `meshopt_EncodeExpSharedComponent` shares exponents between the same component in different vectors + - `meshopt_EncodeExpClamped` does not share exponents but clamps the exponent range to reduce exponent entropy +- Color filter (`meshopt_encodeFilterColor`/`meshopt_decodeFilterColor`) encodes quantized (unorm) RGBA colors using YCoCg encoding. Any number of bits between 2 and 16 can be used with 4 bytes or 8 bytes per vector. + +Note that all filters are lossy and require the data to be deinterleaved with one attribute per stream; this facilitates efficient SIMD implementation of filter decoders, which decodes at 5-10 GB/s on modern desktop CPUs, allowing the overall decompression speed to be closer to that of the raw vertex codec. + +### Versioning and compatibility + +The following guarantees on data compatibility are provided for point releases (*no* guarantees are given for development branch): + +- Data encoded with older versions of the library can always be decoded with newer versions; +- Data encoded with newer versions of the library can be decoded with older versions, provided that encoding versions are set correctly; if binary stability of encoded data is important, use `meshopt_encodeVertexVersion` and `meshopt_encodeIndexVersion` to 'pin' the data versions (or `version` argument of `meshopt_encodeVertexBufferLevel`). + +By default, vertex data is encoded for format version 1 (compatible with meshoptimizer v0.23+), and index data is encoded for format version 1 (compatible with meshoptimizer v0.14+). When decoding the data, the decoder will automatically detect the version from the data header. + +Meshlet data uses a frameless format without an embedded version header, so it's compatible across all library versions (starting with meshoptimizer v1.1). Any future change to meshlet encoding would be exposed as new functions; to adopt future improvements without breaking existing data, applications could record a format version externally, once per entire meshlet stream or file. + +## Simplification + +All algorithms presented so far don't affect visual appearance at all, with the exception of quantization that has minimal controlled impact. However, fundamentally the most effective way to reduce the rendering or transmission cost of a mesh is to reduce the number of triangles in the mesh. + +### Basic simplification + +This library provides a simplification algorithm, `meshopt_simplify`, that reduces the number of triangles in the mesh. Given a vertex and an index buffer, it generates a second index buffer that uses existing vertices in the vertex buffer. This index buffer can be used directly for rendering with the original vertex buffer (preferably after vertex cache optimization using `meshopt_optimizeVertexCache`), or a new compact vertex/index buffer can be generated using `meshopt_optimizeVertexFetch` that uses the optimal number and order of vertices. + +```c++ +float threshold = 0.2f; +size_t target_index_count = size_t(index_count * threshold); +float target_error = 1e-2f; + +std::vector lod(index_count); +float lod_error = 0.f; +lod.resize(meshopt_simplify(&lod[0], indices, index_count, &vertices[0].x, vertex_count, sizeof(Vertex), + target_index_count, target_error, /* options= */ 0, &lod_error)); +``` + +Target error is an approximate measure of the deviation from the original mesh using distance normalized to `[0..1]` range (e.g. `1e-2f` means that simplifier will try to maintain the error to be below 1% of the mesh extents). Note that the simplifier attempts to produce the requested number of indices at minimal error, but because of topological restrictions and error limit it is not guaranteed to reach the target index count and can stop earlier. + +To disable the error limit, `target_error` can be set to `FLT_MAX`. This makes it more likely that the simplifier will reach the target index count, but it may produce a mesh that looks significantly different from the original, so using the resulting error to control viewing distance would be required. Conversely, setting `target_index_count` to 0 will simplify the input mesh as much as possible within the specified error limit; this can be useful for generating LODs that should look good at a given viewing distance. + +The algorithm follows the topology of the original mesh in an attempt to preserve attribute seams, borders and overall appearance. For meshes with inconsistent topology or many seams, such as faceted meshes, it can result in simplifier getting "stuck" and not being able to simplify the mesh fully. Therefore it's critical that identical vertices are "welded" together, that is, the input vertex buffer does not contain duplicates. Additionally, it may be worthwhile to weld the vertices without taking into account vertex attributes that aren't critical and can be rebuilt later, or use "permissive" mode described below. + +Alternatively, the library provides another simplification algorithm, `meshopt_simplifySloppy`, which doesn't follow the topology of the original mesh. This means that it doesn't preserve attribute seams or borders, but it can collapse internal details that are too small to matter because it can merge mesh features that are topologically disjoint but spatially close. In general, this algorithm produces meshes with worse geometric quality and poor attribute quality compared to `meshopt_simplify`. + +The algorithm can also return the resulting normalized deviation that can be used to choose the correct level of detail based on screen size or solid angle; the error can be converted to object space by multiplying by the scaling factor returned by `meshopt_simplifyScale`. For example, given a mesh with a precomputed LOD and a prescaled error, the screen-space normalized error can be computed and used for LOD selection: + +```c++ +// lod_factor can be 1 or can be adjusted for more or less aggressive LOD selection +float d = max(0, distance(camera_position, mesh_center) - mesh_radius); +float e = d * (tan(camera_fovy / 2) * 2 / screen_height); // 1px in mesh space +bool lod_ok = e * lod_factor >= lod_error; +``` + +When a sequence of LOD meshes is generated that all use the original vertex buffer, care must be taken to order vertices optimally to not penalize mobile GPU architectures that are only capable of transforming a sequential vertex buffer range. It's recommended in this case to first optimize each LOD for vertex cache, then assemble all LODs in one large index buffer starting from the coarsest LOD (the one with fewest triangles), and call `meshopt_optimizeVertexFetch` on the final large index buffer. This will make sure that coarser LODs require a smaller vertex range and are efficient with respect to vertex fetch and transform. + +### Attribute-aware simplification + +While `meshopt_simplify` is aware of attribute discontinuities by default (and infers them through the supplied index buffer) and tries to preserve them, it can be useful to provide information about attribute values. This allows the simplifier to take attribute error into account which can improve shading (by using vertex normals), texture deformation (by using texture coordinates), and may be necessary to preserve vertex colors when textures are not used in the first place. This can be done by using a variant of the simplification function that takes attribute values and weight factors, `meshopt_simplifyWithAttributes`: + +```c++ +const float nrm_weight = 0.5f; +const float attr_weights[3] = {nrm_weight, nrm_weight, nrm_weight}; + +std::vector lod(index_count); +float lod_error = 0.f; +lod.resize(meshopt_simplifyWithAttributes(&lod[0], indices, index_count, &vertices[0].x, vertex_count, sizeof(Vertex), + &vertices[0].nx, sizeof(Vertex), attr_weights, 3, /* vertex_lock= */ NULL, + target_index_count, target_error, /* options= */ 0, &lod_error)); +``` + +The attributes are passed as a separate buffer (in the example above it's a subset of the same vertex buffer) and should be stored as consecutive floats; attribute weights are used to control the importance of each attribute in the simplification process. For normalized attributes like normals and vertex colors, a weight around 1.0 is usually appropriate; internally, a change of `1/weight` in attribute value over a distance `d` is approximately equivalent to a change of `d` in position. Using higher weights may be appropriate to preserve attribute quality at the cost of position quality. If the attribute has a different scale (e.g. unnormalized vertex colors in [0..255] range), the weight should be divided by the scaling factor (1/255 in this example). + +Including texture coordinates in the attribute set is optional, as simplification generally preserves texture quality reasonably well by default; if included, a weight of around 10-100 is usually appropriate depending on the UV density. It's also possible to compute the weight automatically by setting it to the reciprocal average density of UVs, which can be computed as `1/sqrt(average UV area)` = `1/sqrt(sum(abs(uv area)) / triangle count)` over all triangles in the mesh, possibly scaled by a constant factor if necessary. + +Both the target error and the resulting error combine positional error and attribute error, so the error can be used to control the LOD while taking attribute quality into account, assuming carefully chosen weights. When the error is used for LOD selection, enabling `meshopt_SimplifyErrorClamped` is recommended to avoid large attribute errors producing overly conservative LOD selection. + +### Permissive simplification + +By default, `meshopt_simplify` preserves attribute discontinuities inferred from the supplied index buffer. For meshes with many seams, the simplifier can get "stuck" and fail to fully simplify the mesh, as it cannot collapse vertices across attribute seams. This is especially problematic for meshes with faceted normals (flat shading), as the simplifier may not be able to reduce the triangle count at all. The `meshopt_SimplifyPermissive` option relaxes these restrictions, allowing the simplifier to collapse vertices across attribute discontinuities when the resulting error is acceptable: + +```c++ +std::vector lod(index_count); +float lod_error = 0.f; +lod.resize(meshopt_simplifyWithAttributes(&lod[0], indices, index_count, &vertices[0].x, vertex_count, sizeof(Vertex), + &vertices[0].nx, sizeof(Vertex), attr_weights, 3, /* vertex_lock= */ NULL, + target_index_count, target_error, /* options= */ meshopt_SimplifyPermissive, &lod_error)); +``` + +To maintain appearance, it's highly recommended to use this option together with attribute-aware simplification, as shown above, as it allows the simplifier to maintain attribute quality. In this mode, it is often desirable to selectively preserve certain attribute seams, such as UV seams or sharp creases. This can be achieved by using the `vertex_lock` array with flag `meshopt_SimplifyVertex_Protect` set for individual vertices to protect specific discontinuities. To fill this array, use `meshopt_generatePositionRemap` to create a mapping table for vertices with identical positions, and then compare each vertex to the remapped vertex to determine which attributes are different: + +```c++ +std::vector remap(vertices.size()); +meshopt_generatePositionRemap(&remap[0], &vertices[0].px, vertices.size(), sizeof(Vertex)); + +std::vector locks(vertices.size()); +for (size_t i = 0; i < vertices.size(); ++i) { + unsigned int r = remap[i]; + + if (r != i && (vertices[r].tx != vertices[i].tx || vertices[r].ty != vertices[i].ty)) + locks[i] |= meshopt_SimplifyVertex_Protect; // protect UV seams +} +``` + +This approach provides fine-grained control over which discontinuities to preserve. The permissive mode combined with selective locking provides a balance between simplification quality and attribute preservation, and usually results in higher quality LODs for the same target triangle count (and dramatically higher quality compared to `meshopt_simplifySloppy`). + +### Simplification with vertex update + +All simplification functions described so far reuse the original vertex buffer and only produce a new index buffer. This means that the resulting mesh will have the same vertex positions and attributes as the original mesh; this is optimal for minimizing the memory consumption and for highly detailed meshes often provides good quality. However, for more aggressive simplification to retain visual quality, it may be necessary to adjust vertex data for optimal appearance. This can be done by using a variant of the simplification function that updates vertex positions and attributes, `meshopt_simplifyWithUpdate`: + +```c++ +float result_error = 0.f; +indices.resize(meshopt_simplifyWithUpdate(&indices[0], indices.size(), &vertices[0].px, vertices.size(), sizeof(Vertex), + &vertices[0].nx, sizeof(Vertex), attr_weights, 3, /* vertex_lock= */ NULL, + target_index_count, target_error, /* options= */ 0, &result_error)); +``` + +Unlike `meshopt_simplify`/`meshopt_simplifyWithAttributes`, this function updates the index buffer as well as vertex positions and attributes in place. The resulting indices still refer to the original vertex buffer; any attributes that are not passed to the simplifier can be left unchanged. However, since the original contents of `vertices` is no longer valid for rendering the original mesh, a new compact vertex/index buffer should be generated using `meshopt_optimizeVertexFetch` (after optimizing the index data with `meshopt_optimizeVertexCache`). If the original data was important, it should be copied before calling this function. + +Since the vertex positions are updated, this may require updating some attributes that could previously be left as-is when using the original vertex buffer. Notably, texture coordinates need to be updated to avoid texture distortion; thus it's highly recommended to include texture coordinates in the attribute data passed to the simplifier. For attributes to be updated, the corresponding attribute weight must not be zero; for texture coordinates, a weight of 1.0 is usually sufficient in this case (although a higher or mesh dependent weight could be used with this function or other functions to reduce UV stretching). + +Attributes that have specific constraints like normals and colors should be renormalized or clamped after the function returns new data. Attributes like bone indices/weights don't have to be updated for reasonable results (but regularization via `meshopt_SimplifyRegularize` may still be helpful to maintain deformation quality). If bone weights *are* provided as attributes, they will need to be clamped and renormalized after the update to ensure they continue to add up to 1. + +Using unique vertex data for each LOD in a chain can improve visual quality, but it comes at a cost of ~doubling vertex memory used (if each LOD is using half the triangles of the previous LOD). To reduce the memory footprint, it is possible to use shared vertices with `meshopt_simplifyWithAttributes` for the first one or two LODs in the chain, and only switch to `meshopt_simplifyWithUpdate` for the remainder. In that case, similarly to the use of `meshopt_simplify` described earlier, care must be taken to optimally arrange the vertices in the original vertex buffer. + +### Advanced simplification + +`meshopt_simplify*` functions expose additional options and parameters that can be used to control the simplification process in more detail. + +For basic customization, a number of options can be passed via `options` bitmask that adjust the behavior of the simplifier: + +- `meshopt_SimplifyLockBorder` restricts the simplifier from collapsing edges that are on the border of the mesh. This can be useful for simplifying mesh subsets independently, so that the LODs can be combined without introducing cracks. +- `meshopt_SimplifyErrorAbsolute` changes the error metric from relative to absolute both for the input error limit as well as for the resulting error. This can be used instead of `meshopt_simplifyScale`. +- `meshopt_SimplifySparse` improves simplification performance assuming input indices are a sparse subset of the mesh. This can be useful when simplifying small mesh subsets independently, and is intended to be used for meshlet simplification. For consistency, it is recommended to use absolute errors when sparse simplification is desired, as this flag changes the meaning of the relative errors. +- `meshopt_SimplifyPrune` allows the simplifier to remove isolated components regardless of the topological restrictions inside the component. This is generally recommended for full-mesh simplification as it can improve quality and reduce triangle count; note that with this option, triangles connected to locked vertices may be removed as part of their component. +- `meshopt_SimplifyRegularize` produces more regular triangle sizes and shapes during simplification, at some cost to geometric quality. This can improve geometric quality under deformation such as skinning. `meshopt_SimplifyRegularizeLight` can be used instead of this flag to use a smaller regularization factor, reducing the impact on geometric quality. +- `meshopt_SimplifyPermissive` allows collapses across attribute discontinuities, except for vertices that are tagged with `meshopt_SimplifyVertex_Protect` via `vertex_lock`. +- `meshopt_SimplifyPreserveFolds` tries to preserve fold lines between opposite-facing triangles at a small performance cost. +- `meshopt_SimplifyErrorClamped` clamps attribute error to match position error scale and avoid extreme error values in areas with high attribute variance. This setting is recommended when using attribute aware simplification if error values are used for LOD selection or generation. + +When using `meshopt_simplifyWithAttributes`, it is also possible to lock certain vertices by providing a `vertex_lock` array that contains a value for each vertex in the mesh, with `meshopt_SimplifyVertex_Lock` set for vertices that should not be collapsed. This can be useful to preserve certain vertices, such as the boundary of the mesh, with more control than `meshopt_SimplifyLockBorder` option provides. When using `meshopt_simplifyWithUpdate`, locking vertices (whether via `vertex_lock` or `meshopt_SimplifyLockBorder`) will also prevent the simplifier from updating their positions and attributes; this can be useful together with `meshopt_SimplifySparse` for meshlet simplification, as meshlets at one level of hierarchy can be simplified together without excessive data copying. + +Locking vertices restricts simplification and makes it more likely that the simplifier gets stuck before reaching the index target; if some areas of the mesh are more important than others but should still be eligible for simplification, `vertex_lock` array can be used to mark specific vertices as high priority using `meshopt_SimplifyVertex_Priority` bit, which makes it more likely that the vertex will be preserved during simplification. + +In addition to the `meshopt_SimplifyPrune` flag, you can explicitly prune isolated components by calling the `meshopt_simplifyPrune` function. This can be done before regular simplification or as the only step, which is useful for scenarios like isosurface cleanup. Similar to other simplification functions, the `target_error` argument controls the cutoff of component radius and is specified in relative units (e.g., `1e-2f` will remove components under 1%). If an absolute cutoff is desired, divide the parameter by the factor returned by `meshopt_simplifyScale`. + +Simplification currently assumes that the input mesh is using the same material for all triangles. If the mesh uses multiple materials, it is possible to split the mesh into subsets based on the material and simplify each subset independently, using `meshopt_SimplifyLockBorder` or `vertex_lock` to preserve material boundaries; however, this limits the collapses and may reduce the resulting quality. An alternative approach is to encode information about the material into the vertex buffer, ensuring that all three vertices referencing the same triangle have the same material ID; this may require duplicating vertices on the boundary between materials. After this, simplification can be performed as usual, and after simplification per-triangle material information can be computed from the vertex material IDs. There is no need to inform the simplifier of the value of the material ID: the implicit boundaries created by duplicating vertices with conflicting material IDs will be preserved automatically (unless permissive simplification is used, in which case material boundaries should be protected via `vertex_lock`). If the source mesh is already split into subsets with non-overlapping vertex indices, and permissive simplification is not used, it should be sufficient to concatenate the subsets into a single vertex/index buffer and simplify the entire mesh at once; the result can be split back into subsets after simplification. + +When generating a LOD chain, you can either re-simplify each LOD from the original mesh or use the previous LOD as the starting point for the next level. The latter approach is more efficient and produces smoother visual transitions between LOD levels while preserving mesh attributes better. With this method, resulting error values from previous levels should be accumulated for LOD selection. Additionally, consider using `meshopt_SimplifySparse` to improve performance when generating deep LOD chains. + +### Point cloud simplification + +In addition to triangle mesh simplification, this library provides a function to simplify point clouds. The algorithm reduces the point cloud to a specified number of points while preserving the overall appearance, and can optionally take per-point colors into account: + +```c++ +const float color_weight = 1; +std::vector indices(target_count); +indices.resize(meshopt_simplifyPoints(&indices[0], &points[0].x, points.size(), sizeof(Point), + &points[0].r, sizeof(Point), color_weight, target_count)); +``` + +The resulting indices can be used to render the simplified point cloud; to reduce the memory footprint, the point cloud can be reindexed to create an array of points from the indices. + +## Efficiency analyzers + +While the only way to get precise performance data is to measure performance on the target GPU, it can be valuable to measure the impact of these optimization in a GPU-independent manner. To this end, the library provides analyzers for all three major optimization routines. For each optimization there is a corresponding analyze function, like `meshopt_analyzeOverdraw`, that returns a struct with statistics. + +`meshopt_analyzeVertexCache` returns vertex cache statistics. The common metric to use is ACMR - average cache miss ratio, which is the ratio of the total number of vertex invocations to the triangle count. The worst-case ACMR is 3 (GPU has to process 3 vertices for each triangle); on regular grids the optimal ACMR approaches 0.5. On real meshes it usually is in [0.5..1.5] range depending on the amount of vertex splits. One other useful metric is ATVR - average transformed vertex ratio - which represents the ratio of vertex shader invocations to the total vertices, and has the best case of 1.0 regardless of mesh topology (each vertex is transformed once). + +`meshopt_analyzeVertexFetch` returns vertex fetch statistics. The main metric it uses is overfetch - the ratio between the number of bytes read from the vertex buffer to the total number of bytes in the vertex buffer. Assuming non-redundant vertex buffers, the best case is 1.0 - each byte is fetched once. + +`meshopt_analyzeOverdraw` returns overdraw statistics. The main metric it uses is overdraw - the ratio between the number of pixel shader invocations to the total number of covered pixels, as measured from several different orthographic cameras. The best case for overdraw is 1.0 - each pixel is shaded once. + +`meshopt_analyzeCoverage` returns coverage statistics: the ratio of covered pixels to the viewport extent from each cardinal axis. This is not an efficiency measure per se, but it can be used to measure silhouette change after simplification as well as more precise distance based culling, where the amount of view dependent coverage can be estimated by computing a dot product between the view direction and the coverage vector. + +Note that all analyzers use approximate models for the relevant GPU units, so the numbers you will get as the result are only a rough approximation of the actual performance. + +## Deinterleaved geometry + +All of the examples above assume that geometry is represented as a single vertex buffer and a single index buffer. This requires storing all vertex attributes - position, normal, texture coordinate, skinning weights etc. - in a single contiguous struct. However, in some cases using multiple vertex streams may be preferable. In particular, if some passes require only positional data - such as depth pre-pass or shadow map - then it may be beneficial to split it from the rest of the vertex attributes to make sure the bandwidth use during these passes is optimal. On some mobile GPUs a position-only attribute stream also improves efficiency of tiling algorithms. + +Most of the functions in this library either only need the index buffer (such as vertex cache optimization) or only need positional information (such as overdraw optimization). However, several tasks require knowledge about all vertex attributes. + +For indexing, `meshopt_generateVertexRemap` assumes that there's just one vertex stream; when multiple vertex streams are used, it's necessary to use `meshopt_generateVertexRemapMulti` as follows: + +```c++ +meshopt_Stream streams[] = { + {&unindexed_pos[0], sizeof(float) * 3, sizeof(float) * 3}, + {&unindexed_nrm[0], sizeof(float) * 3, sizeof(float) * 3}, + {&unindexed_uv[0], sizeof(float) * 2, sizeof(float) * 2}, +}; + +std::vector remap(index_count); +size_t vertex_count = meshopt_generateVertexRemapMulti(&remap[0], NULL, index_count, index_count, streams, sizeof(streams) / sizeof(streams[0])); +``` + +After this `meshopt_remapVertexBuffer` needs to be called once for each vertex stream to produce the correctly reindexed stream. For shadow indexing, similarly `meshopt_generateShadowIndexBufferMulti` is available as a replacement. + +Instead of calling `meshopt_optimizeVertexFetch` for reordering vertices in a single vertex buffer for efficiency, calling `meshopt_optimizeVertexFetchRemap` and then calling `meshopt_remapVertexBuffer` for each stream again is recommended. + +Finally, when compressing vertex data, `meshopt_encodeVertexBuffer` should be used on each vertex stream separately - this allows the encoder to best utilize correlation between attribute values for different vertices. + +## Specialized processing + +In addition to the core optimization techniques, the library provides several specialized algorithms for specific rendering techniques and pipeline optimizations that require a particular configuration of vertex and index data. + +### Triangle strip conversion + +On most hardware, indexed triangle lists are the most efficient way to drive the GPU. However, in some cases triangle strips might prove beneficial: + +- On some older GPUs, triangle strips may be a bit more efficient to render +- On extremely memory constrained systems, index buffers for triangle strips could save a bit of memory + +This library provides an algorithm for converting a vertex cache optimized triangle list to a triangle strip: + +```c++ +std::vector strip(meshopt_stripifyBound(index_count)); +unsigned int restart_index = ~0u; +size_t strip_size = meshopt_stripify(&strip[0], indices, index_count, vertex_count, restart_index); +``` + +Typically you should expect triangle strips to have ~50-60% of indices compared to triangle lists (~1.5-1.8 indices per triangle) and have ~5% worse ACMR. +Note that triangle strips can be stitched with or without restart index support. Using restart indices can result in ~10% smaller index buffers, but on some GPUs restart indices may result in decreased performance. + +To reduce the triangle strip size further, it's recommended to use `meshopt_optimizeVertexCacheStrip` instead of `meshopt_optimizeVertexCache` when optimizing for vertex cache. This trades off some efficiency in vertex transform for smaller index buffers. + +### Geometry shader adjacency + +For algorithms that use geometry shaders and require adjacency information, this library can generate an index buffer with adjacency data: + +```c++ +std::vector adjacency(indices.size() * 2); +meshopt_generateAdjacencyIndexBuffer(&adjacency[0], &indices[0], indices.size(), &vertices[0].x, vertices.size(), sizeof(Vertex)); +``` + +This creates an index buffer suitable for rendering with triangle-with-adjacency topology, providing 3 extra vertices per triangle that represent vertices opposite to each triangle's edge. This data can be used to compute silhouettes and perform other types of local geometric processing in geometry shaders. To render the mesh with adjacency data, the index buffer should be used with `D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ`/`VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY`/`GL_TRIANGLES_ADJACENCY` topology. + +Note that the use of geometry shaders may have a performance impact on some GPUs; in some cases alternative implementation strategies may be more efficient. + +### Tessellation with displacement mapping + +For hardware tessellation with crack-free displacement mapping, this library can generate a special index buffer that supports PN-AEN tessellation: + +```c++ +std::vector tess(indices.size() * 4); +meshopt_generateTessellationIndexBuffer(&tess[0], &indices[0], indices.size(), &vertices[0].x, vertices.size(), sizeof(Vertex)); +``` + +This generates a 12-vertex patch for each input triangle with the following layout: + +- 0, 1, 2: original triangle vertices +- 3, 4: opposing edge for edge 0, 1 +- 5, 6: opposing edge for edge 1, 2 +- 7, 8: opposing edge for edge 2, 0 +- 9, 10, 11: dominant vertices for corners 0, 1, 2 + +This allows the use of hardware tessellation to implement PN-AEN and/or displacement mapping without cracks along UV seams or normal discontinuities. To render the mesh, the index buffer should be used with `D3D_PRIMITIVE_TOPOLOGY_12_CONTROL_POINT_PATCHLIST`/`VK_PRIMITIVE_TOPOLOGY_PATCH_LIST` (`patchControlPoints=12`) topology. For more details please refer to the following papers: [Crack-Free Point-Normal Triangles using Adjacent Edge Normals](https://developer.download.nvidia.com/whitepapers/2010/PN-AEN-Triangles-Whitepaper.pdf), [Tessellation on Any Budget](https://www.nvidia.com/content/pdf/gdc2011/john_mcdonald.pdf) and [My Tessellation Has Cracks!](https://developer.download.nvidia.com/assets/gamedev/files/gdc12/GDC12_DUDASH_MyTessellationHasCracks.pdf). + +### Visibility buffers + +To render geometry into visibility buffers, access to primitive index in fragment shader is required. While it is possible to use `SV_PrimitiveID`/`gl_PrimitiveID` in the fragment shader, this can result in suboptimal performance on some GPUs (notably, AMD RDNA1 and all NVidia GPUs), and may not be supported on mobile or console hardware. Using mesh shaders to generate primitive IDs is efficient but requires hardware support that is not universally available. To work around these limitations, this library provides a way to generate a special index buffer that uses provoking vertex to encode primitive IDs: + +```c++ +std::vector provoke(indices.size()); +std::vector reorder(vertices.size() + indices.size() / 3); +reorder.resize(meshopt_generateProvokingIndexBuffer(&provoke[0], &reorder[0], &indices[0], indices.size(), vertices.size())); +``` + +This generates a special index buffer along with a reorder table that satisfies two constraints: + +- `provoke[3 * tri] == tri` +- `reorder[provoke[x]]` refers to the original triangle vertices + +To render the mesh with provoking vertex data, the application should use `provoke` as an index buffer and a vertex shader that passes vertex index (`SV_VertexID`/`gl_VertexIndex`) via a `flat`/`nointerpolation` attribute to the fragment shader as a primitive index, and loads vertex data manually by computing the real vertex index based on `reorder` table (`reorder[gl_VertexIndex]`). For more details please refer to [Variable Rate Shading with Visibility Buffer Rendering](https://advances.realtimerendering.com/s2024/content/Hable/Advances_SIGGRAPH_2024_VisibilityVRS-SIGGRAPH_Advances_2024.pptx); naturally, this technique does not require VRS. + +> Note: This assumes the provoking vertex is the first vertex of a triangle, which is true for all graphics APIs except OpenGL/WebGL. For OpenGL/WebGL, you may need to rotate each triangle (abc -> bca) in the resulting index buffer, or use the `glProvokingVertex` function (OpenGL 3.2+) or `WEBGL_provoking_vertex` extension (WebGL2) to change the provoking vertex convention. For WebGL2, this is highly recommended to avoid a variety of emulation slowdowns that happen by default if `flat` attributes are used, such as an implicit use of geometry shaders. + +Because the order of indices in the resulting index buffer must be preserved exactly for the technique to work, all optimizations that reorder indices (such as vertex cache optimization) must be applied before generating the provoking index buffer. Additionally, if index compression is used, `meshopt_encodeIndexSequence` should be used instead of `meshopt_encodeIndexBuffer` to ensure that the triangles are not rotated during encoding. + +### Opacity micromaps + +When using hardware raytracing with alpha-tested transparency, tracing the ray requires invoking any-hit shaders for each intersected surface; this can be inefficient as it requires extra communication and synchronization between raytracing hardware and shader units. Opacity micromaps (OMMs) can significantly accelerate the tests by providing opacity masks for each triangle; this requires subdividing each triangle using a uniform grid (4^N microtriangles for subdivision level N), with each microtriangle storing 1 bit (for 2-state micromaps) or 2 bits (for 4-state micromaps) of opacity data. To minimize the memory overhead, the maps can be reused between triangles using a per-triangle OMM index buffer. This library provides algorithms to generate OMM data for a given mesh from an alpha texture; the resulting data can be used directly in Vulkan via [VK_KHR_opacity_micromap](https://docs.vulkan.org/spec/latest/chapters/raytraversal.html#ray-opacity-micromap) or in DirectX via [DXR1.2](https://github.com/microsoft/DirectX-Specs/blob/master/d3d/Raytracing.md#opacity-micromaps). + +Generating opacity micromaps happens in three stages: measure (and layout), rasterize and compact. Compaction is optional but recommended, and can be performed on each mesh individually or on all meshes in the same model/scene. + +First, call `meshopt_opacityMapMeasure` to compute a subdivision level for each triangle based on its texel footprint; this also computes initial per-triangle OMM indices as it's common for triangles in the source mesh to refer to the same UVs: + +```c++ +const int states = 4; // 2-state or 4-state OMMs (used after measure) +const int max_level = 6; // max subdivision level +const float target_edge = 3.0f; // target 3x3px area for each microtriangle + +std::vector levels(indices.size() / 3); +std::vector sources(indices.size() / 3); +std::vector omm_indices(indices.size() / 3); +size_t omm_count = meshopt_opacityMapMeasure(&levels[0], &sources[0], &omm_indices[0], &indices[0], indices.size(), + &vertices[0].u, vertices.size(), sizeof(Vertex), texture_width, texture_height, max_level, target_edge); +``` + +Each OMM entry requires separate storage which can be determined based on subdivision level and format (2-state or 4-state), and can be computed via `meshopt_opacityMapEntrySize`: + +```c++ +std::vector offsets(omm_count); +size_t data_size = 0; + +for (size_t i = 0; i < omm_count; ++i) +{ + offsets[i] = unsigned(data_size); + data_size += meshopt_opacityMapEntrySize(levels[i], states); +} +``` + +Second, call `meshopt_opacityMapRasterize` for each triangle to compute the opacity state per microtriangle. This can be done sequentially or in parallel; it can use the original texture resolution or a smaller mip level to balance rasterization cost vs quality. When generating 4-state micromaps, using mip 0 is recommended to produce maximally conservative output so that enabling opacity micromaps does not noticeably change the raytraced output. For 2-state micromaps, or if the original textures are much higher resolution than the micromap subdivision, smaller mips (e.g. 1 or 2) can also work well. + +```c++ +for (size_t i = 0; i < omm_count; ++i) +{ + unsigned int tri = sources[i]; + const float* uv0 = &vertices[indices[tri * 3 + 0]].u; + const float* uv1 = &vertices[indices[tri * 3 + 1]].u; + const float* uv2 = &vertices[indices[tri * 3 + 2]].u; + + // texture addressing below assumes RGBA texture input without padding; +3 points to A + meshopt_opacityMapRasterize(&data[offsets[i]], levels[i], states, uv0, uv1, uv2, + texture.data() + 3, 4, texture_width * 4, texture_width, texture_height); +} +``` + +> Note: Opacity micromap data is sensitive to triangle corner order, which index or meshlet compression can change. If compression is used, index data should be decompressed from the compressed representation before rasterization, or order should be normalized before rasterization as well as after decompression (e.g. by rotating each triangle so that `a < b, c`). Since per-triangle OMM indices use the original triangle order, it's recommended to perform OMM processing after the index order has been finalized. + +After rasterization, the OMM data *can* be used as is; however, it's typical to see redundant entries that either can be reused between different triangles, or that have consistent states for all micro-triangles, which can be represented using "special" indices (-4..-1) per triangle. Thus it's recommended to compact the data - if it's already laid out sequentially similarly to the example above, then just calling `meshopt_opacityMapCompact` and trimming the output arrays is sufficient for optimal output: + +```c++ +omm_count = meshopt_opacityMapCompact(&data[0], data_size, &levels[0], &offsets[0], omm_count, &omm_indices[0], indices.size() / 3, states); +data_size = (omm_count == 0) ? 0 : offsets[omm_count - 1] + meshopt_opacityMapEntrySize(levels[omm_count - 1], states); +``` + +After compaction, `levels` and `offsets` (`omm_count` entries) and `data` (`data_size` bytes) can be serialized and later passed to the raytracing runtime to build the opacity micromap structures. Often, compacting OMM data across multiple meshes can produce smaller results; in that case, all resulting OMM indices will point to a single OMM array object. + +Additionally, note that while the code above works with 32-bit OMM indices, after compaction it's typical to see each mesh refer to a small section of OMM array data which can be represented using 16-bit (or, sometimes, 8-bit indices). The index data can be narrowed to a shorter type in these cases; note that when using 16-bit OMM index data, due to special indices, the index values should be in range `[0..65531]` (and `[0..251]` for 8-bit indices, assuming 4-state OMMs are used). + +When using 4-state OMMs, rasterization code produces both unknown-transparent and unknown-opaque states based on microtriangle coverage; this enables the use of forced 2-state flag during traversal for specific effects where micromap data is sufficient for reasonable quality; this is recommended for performance as this results in no any-hit invocations. + +### Tangent spaces + +Meshes that use tangent space normal maps often require per-vertex tangent vectors in addition to normals. These could be exported alongside mesh data, but this library also provides an algorithm similar to MikkTSpace that can generate them from positions, normals and texture coordinates: + +```c++ +std::vector tangents(indices.size()); +meshopt_generateTangents(&tangents[0].x, &indices[0], indices.size(), + &vertices[0].px, vertices.size(), sizeof(Vertex), &vertices[0].nx, sizeof(Vertex), &vertices[0].tx, sizeof(Vertex)); +``` + +For each triangle *corner* this writes a normalized tangent vector (xyz) and an orientation sign (+-1); the bitangent can be reconstructed in the shader as `cross(normal, tangent.xyz) * tangent.w`. Note that some coordinate space conventions that flip V direction in the texture space require negating orientation sign. The input can be indexed, as in the example above, or not (`indices=NULL`); this does not affect the output tangents. + +Because tangents are computed per corner, applying them to mesh vertices requires de-indexing the mesh and generating a new index/vertex buffer afterwards (see `Indexing` section earlier), or using indexed data and copying tangents to existing vertex data while duplicating vertices with different tangents (see `tangents` example in `demo/main.cpp`). With the indexed input, if it contains UV mirroring, vertices along the mirror edge may have different tangent spaces on different sides of the edge and need to be split - copying tangents to existing vertex data without splitting will not produce correct results. + +If splitting is required, it can be efficiently done with an extra pass and an index list per vertex: + +```c++ +// seed each vertex with one of its corner tangents; the loop below fixes any mismatches +for (size_t i = 0; i < indices.size(); ++i) + vertices[indices[i]].tangent = tangents[i]; + +std::vector splits(vertices.size(), ~0u); + +for (size_t i = 0; i < indices.size(); ++i) +{ + // walk the chain of split copies looking for a vertex whose tangent matches + unsigned int v = indices[i]; + while (v != ~0u && vertices[v].tangent != tangents[i]) + v = splits[v]; + + // no match in chain: append a new split copy with the target tangent and chain it + if (v == ~0u) + { + v = unsigned(vertices.size()); + vertices.push_back(vertices[indices[i]]); + vertices[v].tangent = tangents[i]; + splits.push_back(splits[indices[i]]); + splits[indices[i]] = v; + } + + indices[i] = v; +} +``` + +The algorithm uses a MikkTSpace-like construction but by default, uses a modified weighting scheme that significantly improves tangent quality around beveled regions in the mesh. If the normal maps are baked from higher resolution geometry using MikkTSpace weighting, it's possible to produce MikkTSpace-compatible tangents by passing `meshopt_TangentCompatible` option as an extra argument to the function. + +While it is expected that the normals come from the authored mesh, this library also provides an algorithm to generate normals from positions alone: + +```c++ +const float crease = 1.f; // 1 rad ~= 60 deg +std::vector normals(indices.size()); +meshopt_generateNormals(&normals[0].x, &indices[0], indices.size(), &vertices[0].px, vertices.size(), sizeof(Vertex), crease); +``` + +This function classifies edges as soft/hard based on the specified crease angle (in radians; 30/45/60 degrees are commonly used values) and computes a normal for every corner by averaging normals of incident faces connected by soft edges. Additionally, the optional `smoothing` parameter can be used to perform additional iterative smoothing of the resulting normals, with larger values resulting in smoother normals (recommended range `[0..5]`, defaults to `0`). + +Similarly to `meshopt_generateTangents`, this function computes per-corner normals (3 floats for each of 3 corners of each triangle). Applying them to mesh vertices requires de-indexing the mesh, or copying normals to existing vertices while duplicating vertices with different normals. + +## Memory management + +Many algorithms allocate temporary memory to store intermediate results or accelerate processing. The amount of memory allocated is a function of various input parameters such as vertex count and index count. By default memory is allocated using `operator new` and `operator delete`; if these operators are overloaded by the application, the overloads will be used instead. Alternatively it's possible to specify custom allocation/deallocation functions using `meshopt_setAllocator`, e.g. + +```c++ +meshopt_setAllocator(malloc, free); +``` + +> Note that the library expects the allocation function to either throw in case of out-of-memory (in which case the exception will propagate to the caller) or abort, so technically the use of `malloc` above isn't safe. If you want to handle out-of-memory errors without using C++ exceptions, you can use `setjmp`/`longjmp` instead. + +When building meshoptimizer as a shared library, allocations from the templated index wrappers provided in the header (used when index data is not `unsigned int`) will only be redirected to these callbacks if the library is built with `MESHOPTIMIZER_ALLOC_EXPORT` defined. + +When building meshoptimizer with MSVC, if a non-default calling convention is used such as `__vectorcall` (via `/Gv`), `MESHOPTIMIZER_ALLOC_CALLCONV` should be defined to match the calling convention of `operator new`/`delete`. + +Vertex, index and meshlet decoders (`meshopt_decodeVertexBuffer`, `meshopt_decodeIndexBuffer`, `meshopt_decodeIndexSequence`, `meshopt_decodeMeshlet`, `meshopt_decodeMeshletRaw`) do not allocate memory and work completely within the buffer space provided via arguments. + +All functions have bounded stack usage that does not exceed 32 KB for any algorithms. + +## Experimental APIs + +Several algorithms provided by this library are marked as "experimental"; this status is reflected in the comments as well as the annotation `MESHOPTIMIZER_EXPERIMENTAL` for each function. + +APIs that are not experimental (annotated with `MESHOPTIMIZER_API`) are considered stable, which means that library updates will not break compatibility: existing calls should compile (API compatibility), existing binaries should link (ABI compatibility), and existing behavior should not change significantly (for example, floating point parameters will have similar behavior). This does not mean that the output of the algorithms will be identical: future versions may improve the algorithms and produce different results. + +APIs that *are* experimental may have their interface change, both in ways that will cause existing calls to not compile, and in ways that may compile but have significantly different behavior (e.g., changes in parameter order, meaning, valid ranges). Experimental APIs may also, in rare cases, be removed from future library versions. It is recommended to carefully read release notes when updating the library if experimental APIs are in use. Some experimental APIs may also lack documentation in this README. + +Applications may configure the library to change the attributes of experimental APIs, for example defining `MESHOPTIMIZER_EXPERIMENTAL` as `__attribute__((deprecated))` will emit compiler warnings when experimental APIs are used. When building a shared library with CMake, `MESHOPT_STABLE_EXPORTS` option can be set to only export stable APIs; this produces an ABI-stable shared library that can be updated without recompiling the application code. + +Currently, the following APIs are experimental: + +- `meshopt_SimplifyPreserveFolds` and `meshopt_SimplifyErrorClamped` flags for `meshopt_simplify*` functions +- `meshopt_generateNormals` function +- `meshopt_remesh` function and `meshopt_Remesh*` flags + +## License + +This library is available to anybody free of charge, under the terms of [MIT License](LICENSE.md). + +To honor the license agreement, please include attribution into the user-facing product documentation and/or credits, for example using this or similar text: + +> Uses meshoptimizer. Copyright (c) 2016-2026, Arseny Kapoulkine diff --git a/Lib/meshoptimizer/demo/ansi.c b/Lib/meshoptimizer/demo/ansi.c new file mode 100644 index 0000000..9e4f347 --- /dev/null +++ b/Lib/meshoptimizer/demo/ansi.c @@ -0,0 +1,2 @@ +/* This file makes sure the library can be used by C89 code */ +#include "../src/meshoptimizer.h" diff --git a/Lib/meshoptimizer/demo/clusterlod.h b/Lib/meshoptimizer/demo/clusterlod.h new file mode 100644 index 0000000..fd83f71 --- /dev/null +++ b/Lib/meshoptimizer/demo/clusterlod.h @@ -0,0 +1,962 @@ +/** + * clusterlod - a small "library"/example built on top of meshoptimizer to generate cluster LOD hierarchies + * This is intended to either be used as is, or as a reference for implementing similar functionality in your engine. + * + * To use this code, you need to have one source file which includes meshoptimizer.h and defines CLUSTERLOD_IMPLEMENTATION + * before including this file. Other source files in your project can just include this file and use the provided functions. + * + * Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) + * This code is distributed under the MIT License. See notice at the end of this file. + */ +#pragma once + +#include + +struct clodConfig +{ + // configuration of each cluster; maps to meshopt_buildMeshlets* parameters + size_t max_vertices; + size_t min_triangles; + size_t max_triangles; + + // partitioning setup; maps to meshopt_partitionClusters parameters (plus optional partition sorting) + // note: partition size is the target size, not maximum; actual partitions may be up to 1/3 larger (e.g. target 24 results in maximum 32) + bool partition_spatial; + bool partition_sort; + size_t partition_size; + + // clusterization setup; maps to meshopt_buildMeshletsSpatial / meshopt_buildMeshletsFlex + bool cluster_spatial; + float cluster_fill_weight; + float cluster_split_factor; + + // every level aims to reduce the number of triangles by ratio, and considers clusters that don't reach the threshold stuck + float simplify_ratio; + float simplify_threshold; + + // to compute the error of simplified clusters, we use the formula that combines previous accumulated error as follows: + // max(previous_error * simplify_error_merge_previous, current_error) + current_error * simplify_error_merge_additive + float simplify_error_merge_previous; + float simplify_error_merge_additive; + + // amplify the error of clusters that go through sloppy simplification to account for appearance degradation + float simplify_error_factor_sloppy; + + // limit error by edge length, aiming to remove subpixel triangles even if the attribute error is high + float simplify_error_edge_limit; + + // clamp attribute error to match position error scale and avoid overly conservative LOD selection + bool simplify_error_clamped; + + // use permissive simplification instead of regular simplification (make sure to use attribute_protect_mask if this is set!) + bool simplify_permissive; + + // use permissive or sloppy simplification but only if regular simplification gets stuck + bool simplify_fallback_permissive; + bool simplify_fallback_sloppy; + + // use regularization during simplification to make triangle density more uniform, at some cost to overall triangle count; recommended for deformable objects + bool simplify_regularize; + + // try to preserve fold lines between opposite-facing triangles during simplification, at a small performance cost + bool simplify_preserve_folds; + + // dilate open borders for each cluster to try to mitigate area loss; most useful for foliage, should not be enabled everywhere + // note: this will mutate vertex_positions in place; the caller should copy positions for each cluster in the output callback as they will be overwritten by simplified clusters + bool simplify_dilate_borders; + + // should clodCluster::bounds be computed based on the geometry of each cluster + bool optimize_bounds; + + // should clodCluster::indices be optimized for locality; helps with rasterization performance and ray tracing performance in fast-build modes + bool optimize_clusters; + int optimize_clusters_level; +}; + +struct clodMesh +{ + // input triangle indices + const unsigned int* indices; + size_t index_count; + + // total vertex count + size_t vertex_count; + + // input vertex positions; must be 3 floats per vertex + const float* vertex_positions; + size_t vertex_positions_stride; + + // input vertex attributes; used for attribute-aware simplification and permissive simplification + const float* vertex_attributes; + size_t vertex_attributes_stride; + + // input vertex locks; allows to preserve additional seams (when not using attribute_protect_mask) or lock vertices via meshopt_SimplifyVertex_* flags + const unsigned char* vertex_lock; + + // attribute weights for attribute-aware simplification; maps to meshopt_simplifyWithAttributes parameters + const float* attribute_weights; + size_t attribute_count; + + // attribute mask to flag attribute discontinuities for permissive simplification; mask (1< +void clodBuild(clodConfig config, clodMesh mesh, Output output) +{ + struct Call + { + static int output(void* output_context, clodGroup group, const clodCluster* clusters, size_t cluster_count) + { + return (*static_cast(output_context))(group, clusters, cluster_count); + } + }; + + clodBuild(config, mesh, &output, &Call::output); +} +#endif + +#ifdef CLUSTERLOD_IMPLEMENTATION +// For reference, see the original Nanite paper: +// Brian Karis. Nanite: A Deep Dive. 2021 +#include +#include +#include + +#include +#include + +namespace clod +{ + +struct Cluster +{ + size_t vertices; + + // offset into indices vector in the processing loop + size_t index_offset; + size_t index_count; + + int refined; + + clodBounds bounds; +}; + +static clodBounds boundsCompute(const clodMesh& mesh, const unsigned int* indices, size_t index_count, float error) +{ + meshopt_Bounds bounds = meshopt_computeClusterBounds(indices, index_count, mesh.vertex_positions, mesh.vertex_count, mesh.vertex_positions_stride); + + return {{bounds.center[0], bounds.center[1], bounds.center[2]}, bounds.radius, error}; +} + +static clodBounds boundsMerge(const clodBounds* bounds, size_t count, size_t stride) +{ + meshopt_Bounds merged = meshopt_computeSphereBounds(&bounds[0].center[0], count, stride, &bounds[0].radius, stride); + + // merged bounds error must be conservative wrt cluster errors + float error = 0.f; + for (size_t j = 0; j < count; ++j) + error = std::max(error, reinterpret_cast(reinterpret_cast(bounds) + j * stride)->error); + + return {{merged.center[0], merged.center[1], merged.center[2]}, merged.radius, error}; +} + +static void clusterize(std::vector& clusters, std::vector& cluster_indices, const clodConfig& config, const clodMesh& mesh, const unsigned int* indices, size_t index_count, int refined = -1) +{ + size_t max_meshlets = meshopt_buildMeshletsBound(index_count, config.max_vertices, config.min_triangles); + size_t buffer_size = max_meshlets * sizeof(meshopt_Meshlet) + index_count * (sizeof(unsigned int) + sizeof(unsigned char)); + + // use stack buffer for small mesh subsets (should be enough for group clusterization; initial clusterization uses heap) + alignas(meshopt_Meshlet) char buffer_stack[32768]; + std::vector buffer_heap; + char* buffer = (buffer_size <= sizeof(buffer_stack)) ? buffer_stack : (buffer_heap.resize(buffer_size), buffer_heap.data()); + + meshopt_Meshlet* meshlets = reinterpret_cast(buffer); + unsigned int* meshlet_vertices = reinterpret_cast(meshlets + max_meshlets); + unsigned char* meshlet_triangles = reinterpret_cast(meshlet_vertices + index_count); + + size_t meshlet_count; + + if (config.cluster_spatial) + meshlet_count = meshopt_buildMeshletsSpatial(meshlets, meshlet_vertices, meshlet_triangles, indices, index_count, + mesh.vertex_positions, mesh.vertex_count, mesh.vertex_positions_stride, + config.max_vertices, config.min_triangles, config.max_triangles, config.cluster_fill_weight); + else + meshlet_count = meshopt_buildMeshletsFlex(meshlets, meshlet_vertices, meshlet_triangles, indices, index_count, + mesh.vertex_positions, mesh.vertex_count, mesh.vertex_positions_stride, + config.max_vertices, config.min_triangles, config.max_triangles, 0.f, config.cluster_split_factor); + + size_t cluster_offset = clusters.size(); + clusters.resize(cluster_offset + meshlet_count); + + size_t index_offset = cluster_indices.size(); + cluster_indices.resize(index_offset + index_count); + + for (size_t i = 0; i < meshlet_count; ++i) + { + const meshopt_Meshlet& meshlet = meshlets[i]; + Cluster& cluster = clusters[cluster_offset + i]; + + if (config.optimize_clusters) + meshopt_optimizeMeshletLevel(&meshlet_vertices[meshlet.vertex_offset], meshlet.vertex_count, &meshlet_triangles[meshlet.triangle_offset], meshlet.triangle_count, config.optimize_clusters_level); + + cluster.vertices = meshlet.vertex_count; + + // note: we discard meshlet-local indices; they can be recovered by the caller using clodLocalIndices + cluster.index_offset = index_offset; + cluster.index_count = meshlet.triangle_count * 3; + for (size_t j = 0; j < meshlet.triangle_count * 3; ++j) + cluster_indices[index_offset + j] = meshlet_vertices[meshlet.vertex_offset + meshlet_triangles[meshlet.triangle_offset + j]]; + index_offset += meshlet.triangle_count * 3; + + cluster.refined = refined; + } +} + +static std::vector partition(std::vector& clusters, const clodConfig& config, const clodMesh& mesh, const std::vector& indices, const std::vector& remap) +{ + if (clusters.size() <= config.partition_size) + return {0, unsigned(clusters.size())}; + + // copy remapped cluster index data into a flat array for partitioning + std::vector cluster_counts(clusters.size()); + std::vector cluster_indices; + cluster_indices.reserve(indices.size()); + + for (size_t i = 0; i < clusters.size(); ++i) + { + const Cluster& cluster = clusters[i]; + + cluster_counts[i] = unsigned(cluster.index_count); + + for (size_t j = 0; j < cluster.index_count; ++j) + cluster_indices.push_back(remap[indices[cluster.index_offset + j]]); + } + + // partition clusters into groups; the output is a partition id per cluster + std::vector cluster_part(clusters.size()); + size_t partition_count = meshopt_partitionClusters(&cluster_part[0], &cluster_indices[0], cluster_indices.size(), &cluster_counts[0], cluster_counts.size(), + config.partition_spatial ? mesh.vertex_positions : NULL, remap.size(), mesh.vertex_positions_stride, config.partition_size); + + if (config.partition_sort) + { + // compute partition points for sorting; any representative point will do, we use last cluster center for simplicity + std::vector partition_point(partition_count * 3); + for (size_t i = 0; i < clusters.size(); ++i) + memcpy(&partition_point[cluster_part[i] * 3], clusters[i].bounds.center, sizeof(float) * 3); + + // sort partitions spatially; the output is a remap table from old index (partition id) to new index + std::vector partition_remap(partition_count); + meshopt_spatialSortRemap(partition_remap.data(), partition_point.data(), partition_count, sizeof(float) * 3); + + for (size_t i = 0; i < clusters.size(); ++i) + cluster_part[i] = partition_remap[cluster_part[i]]; + } + + // count clusters in each partition and compute offsets via prefix sum + std::vector partition_offsets(partition_count + 1); + for (size_t i = 0; i < clusters.size(); ++i) + partition_offsets[cluster_part[i] + 1]++; + + for (size_t i = 1; i <= partition_count; ++i) + partition_offsets[i] += partition_offsets[i - 1]; + + // distribute clusters into contiguous partitions + std::vector write = partition_offsets; + std::vector result(clusters.size()); + for (size_t i = 0; i < clusters.size(); ++i) + result[write[cluster_part[i]]++] = clusters[i]; + + clusters.swap(result); + return partition_offsets; +} + +static void lockBoundary(std::vector& locks, const Cluster* clusters, const unsigned int* group_offsets, size_t group_count, const std::vector& cluster_indices, const std::vector& remap, const unsigned char* vertex_lock) +{ + // for each remapped vertex, use bit 7 as temporary storage to indicate that the vertex has been used by a different group previously + for (size_t i = 0; i < locks.size(); ++i) + locks[i] &= ~((1 << 0) | (1 << 7)); + + for (size_t i = 0; i < group_count; ++i) + { + const Cluster* group = clusters + group_offsets[i]; + size_t group_size = group_offsets[i + 1] - group_offsets[i]; + + // mark all remapped vertices as locked if seen by a prior group + for (size_t j = 0; j < group_size; ++j) + { + const Cluster& cluster = group[j]; + + for (size_t k = 0; k < cluster.index_count; ++k) + { + unsigned int v = cluster_indices[cluster.index_offset + k]; + unsigned int r = remap[v]; + + locks[r] |= locks[r] >> 7; + } + } + + // mark all remapped vertices as seen + for (size_t j = 0; j < group_size; ++j) + { + const Cluster& cluster = group[j]; + + for (size_t k = 0; k < cluster.index_count; ++k) + { + unsigned int v = cluster_indices[cluster.index_offset + k]; + unsigned int r = remap[v]; + + locks[r] |= 1 << 7; + } + } + } + + for (size_t i = 0; i < locks.size(); ++i) + { + unsigned int r = remap[i]; + + // consistently lock all vertices with the same position; keep protect bit if set + locks[i] = (locks[r] & 1) | (locks[i] & meshopt_SimplifyVertex_Protect); + + if (vertex_lock) + locks[i] |= vertex_lock[i]; + } +} + +struct SloppyVertex +{ + float x, y, z; + unsigned int id; +}; + +static void simplifyFallback(std::vector& lod, const clodMesh& mesh, const std::vector& indices, const std::vector& locks, size_t target_count, float* error) +{ + std::vector subset(indices.size()); + std::vector subset_locks(indices.size()); + + lod.resize(indices.size()); + + size_t positions_stride = mesh.vertex_positions_stride / sizeof(float); + + // deindex the mesh subset to avoid calling simplifySloppy on the entire vertex buffer (which is prohibitively expensive without sparsity) + for (size_t i = 0; i < indices.size(); ++i) + { + unsigned int v = indices[i]; + assert(v < mesh.vertex_count); + + subset[i].x = mesh.vertex_positions[v * positions_stride + 0]; + subset[i].y = mesh.vertex_positions[v * positions_stride + 1]; + subset[i].z = mesh.vertex_positions[v * positions_stride + 2]; + subset[i].id = v; + + subset_locks[i] = locks[v]; + lod[i] = unsigned(i); + } + + lod.resize(meshopt_simplifySloppy(&lod[0], &lod[0], lod.size(), &subset[0].x, subset.size(), sizeof(SloppyVertex), subset_locks.data(), target_count, FLT_MAX, error)); + + // convert error to absolute + *error *= meshopt_simplifyScale(&subset[0].x, subset.size(), sizeof(SloppyVertex)); + + // restore original vertex indices + for (size_t i = 0; i < lod.size(); ++i) + lod[i] = subset[lod[i]].id; +} + +static void simplify(std::vector& lod, const clodConfig& config, const clodMesh& mesh, const std::vector& indices, const std::vector& locks, size_t target_count, float* error) +{ + assert(target_count <= indices.size()); + lod.resize(indices.size()); + + unsigned int options = meshopt_SimplifySparse | meshopt_SimplifyErrorAbsolute; + + if (config.simplify_error_clamped) + options |= meshopt_SimplifyErrorClamped; + if (config.simplify_permissive) + options |= meshopt_SimplifyPermissive; + if (config.simplify_regularize) + options |= meshopt_SimplifyRegularize; + if (config.simplify_preserve_folds) + options |= meshopt_SimplifyPreserveFolds; + + lod.resize(meshopt_simplifyWithAttributes(&lod[0], &indices[0], indices.size(), + mesh.vertex_positions, mesh.vertex_count, mesh.vertex_positions_stride, + mesh.vertex_attributes, mesh.vertex_attributes_stride, mesh.attribute_weights, mesh.attribute_count, + &locks[0], target_count, FLT_MAX, options, error)); + + if (lod.size() > target_count && config.simplify_fallback_permissive && !config.simplify_permissive) + lod.resize(meshopt_simplifyWithAttributes(&lod[0], &indices[0], indices.size(), + mesh.vertex_positions, mesh.vertex_count, mesh.vertex_positions_stride, + mesh.vertex_attributes, mesh.vertex_attributes_stride, mesh.attribute_weights, mesh.attribute_count, + &locks[0], target_count, FLT_MAX, options | meshopt_SimplifyPermissive, error)); + + // while it's possible to call simplifySloppy directly, it doesn't support sparsity or absolute error, so we need to do some extra work + if (lod.size() > target_count && config.simplify_fallback_sloppy) + { + simplifyFallback(lod, mesh, indices, locks, target_count, error); + *error *= config.simplify_error_factor_sloppy; // scale error up to account for appearance degradation + } + + // replace degenerate outputs with the first triangle; this ensures we do not produce empty clusters that break DAG invariants + if (lod.empty() && !indices.empty()) + lod.assign(indices.begin(), indices.begin() + 3); + + // optionally limit error by edge length, aiming to remove subpixel triangles even if the attribute error is high + if (config.simplify_error_edge_limit > 0) + { + float max_edge_sq = 0; + + for (size_t i = 0; i < indices.size(); i += 3) + { + unsigned int a = indices[i + 0], b = indices[i + 1], c = indices[i + 2]; + assert(a < mesh.vertex_count && b < mesh.vertex_count && c < mesh.vertex_count); + + const float* va = &mesh.vertex_positions[a * (mesh.vertex_positions_stride / sizeof(float))]; + const float* vb = &mesh.vertex_positions[b * (mesh.vertex_positions_stride / sizeof(float))]; + const float* vc = &mesh.vertex_positions[c * (mesh.vertex_positions_stride / sizeof(float))]; + + // compute squared edge lengths + float eab = (va[0] - vb[0]) * (va[0] - vb[0]) + (va[1] - vb[1]) * (va[1] - vb[1]) + (va[2] - vb[2]) * (va[2] - vb[2]); + float eac = (va[0] - vc[0]) * (va[0] - vc[0]) + (va[1] - vc[1]) * (va[1] - vc[1]) + (va[2] - vc[2]) * (va[2] - vc[2]); + float ebc = (vb[0] - vc[0]) * (vb[0] - vc[0]) + (vb[1] - vc[1]) * (vb[1] - vc[1]) + (vb[2] - vc[2]) * (vb[2] - vc[2]); + + float emax = std::max(std::max(eab, eac), ebc); + float emin = std::min(std::min(eab, eac), ebc); + + // we prefer using min edge length to reduce the number of triangles <1px thick, but need some stopgap for thin and long triangles like wires + max_edge_sq = std::max(max_edge_sq, std::max(emin, emax / 4)); + } + + // adjust the error to limit it for dense clusters based on edge lengths + *error = std::min(*error, sqrtf(max_edge_sq) * config.simplify_error_edge_limit); + } +} + +static int outputGroup(const clodConfig& config, const clodMesh& mesh, const Cluster* group, size_t group_size, const std::vector& cluster_indices, const clodBounds& simplified, int depth, void* output_context, clodOutput output_callback) +{ + // use stack buffer for small groups; should pretty much never trigger heap allocations + clodCluster clusters_stack[32]; + std::vector clusters_heap; + clodCluster* group_clusters = (group_size <= sizeof(clusters_stack) / sizeof(clusters_stack[0])) ? clusters_stack : (clusters_heap.resize(group_size), clusters_heap.data()); + + for (size_t i = 0; i < group_size; ++i) + { + const Cluster& cluster = group[i]; + clodCluster& result = group_clusters[i]; + + result.refined = cluster.refined; + result.bounds = (config.optimize_bounds && cluster.refined != -1) ? boundsCompute(mesh, cluster_indices.data() + cluster.index_offset, cluster.index_count, cluster.bounds.error) : cluster.bounds; + result.indices = cluster_indices.data() + cluster.index_offset; + result.index_count = cluster.index_count; + result.vertex_count = cluster.vertices; + } + + return output_callback ? output_callback(output_context, {depth, simplified}, group_clusters, group_size) : -1; +} + +static unsigned long long* edgeLookup(std::vector& table, unsigned long long key) +{ + size_t mask = table.size() - 1; + size_t h = size_t((key * 0x9E3779B97F4A7C15ull) >> 32) & mask; + + while (table[h] != ~0ull && table[h] != key) + h = (h + 1) & mask; + + return &table[h]; +} + +static float boundaryArea(const clodMesh& mesh, const std::vector& indices, const std::vector& locks, const std::vector& remap, std::vector& table) +{ + memset(table.data(), -1, sizeof(unsigned long long) * table.size()); + + for (size_t i = 0; i < indices.size(); i += 3) + for (int e = 0; e < 3; ++e) + { + unsigned int a = remap[indices[i + e]], b = remap[indices[i + (e == 2 ? 0 : e + 1)]]; + unsigned long long id = (unsigned long long)a << 32 | b; + *edgeLookup(table, id) = id; + } + + float area = 0.f; + + for (size_t i = 0; i < indices.size(); i += 3) + { + bool border = false; + + for (int e = 0; e < 3; ++e) + { + unsigned int a = remap[indices[i + e]], b = remap[indices[i + (e == 2 ? 0 : e + 1)]]; + unsigned long long id = (unsigned long long)b << 32 | a; + + // we only track border triangles - at least one edge should be unlocked and open (the reverse edge does not exist) + border |= (locks[a] & locks[b] & 1) == 0 && *edgeLookup(table, id) == ~0ull; + } + + if (!border) + continue; + + const float* va = &mesh.vertex_positions[indices[i + 0] * (mesh.vertex_positions_stride / sizeof(float))]; + const float* vb = &mesh.vertex_positions[indices[i + 1] * (mesh.vertex_positions_stride / sizeof(float))]; + const float* vc = &mesh.vertex_positions[indices[i + 2] * (mesh.vertex_positions_stride / sizeof(float))]; + + float ab[3] = {vb[0] - va[0], vb[1] - va[1], vb[2] - va[2]}; + float ac[3] = {vc[0] - va[0], vc[1] - va[1], vc[2] - va[2]}; + float nt[3] = {ab[1] * ac[2] - ab[2] * ac[1], ab[2] * ac[0] - ab[0] * ac[2], ab[0] * ac[1] - ab[1] * ac[0]}; + + area += sqrtf(nt[0] * nt[0] + nt[1] * nt[1] + nt[2] * nt[2]) * 0.5f; + } + + return area; +} + +static void dilateBorders(const clodMesh& mesh, const std::vector& merged, const std::vector& simplified, const std::vector& locks, const std::vector& remap, clodBounds& bounds, std::vector& dilate_offsets, std::vector& edge_table) +{ + // hash table is sized to be able to track all edges + size_t table_size = 16; + while (table_size < merged.size() + merged.size() / 2) + table_size *= 2; + + edge_table.resize(table_size); + + float old_area = boundaryArea(mesh, merged, locks, remap, edge_table); + if (old_area == 0.f) + return; + + // limit expansion to cases of moderate area shrinkage; it may be unsafe to dilate otherwise + float new_area = boundaryArea(mesh, simplified, locks, remap, edge_table); + if (new_area >= old_area || new_area < old_area / 4) + return; + + float perimeter = 0.f; + + for (size_t i = 0; i < simplified.size(); i += 3) + for (int e = 0; e < 3; ++e) + { + unsigned int a = remap[simplified[i + e]], b = remap[simplified[i + (e == 2 ? 0 : e + 1)]]; + unsigned long long id = (unsigned long long)b << 32 | a; + + // the border check must match the one in boundaryArea (we also rely on table being filled by boundaryArea to avoid extra work) + if ((locks[a] & locks[b] & 1) != 0 || *edgeLookup(edge_table, id) != ~0ull) + continue; + + const float* va = &mesh.vertex_positions[simplified[i + e] * (mesh.vertex_positions_stride / sizeof(float))]; + const float* vb = &mesh.vertex_positions[simplified[i + (e == 2 ? 0 : e + 1)] * (mesh.vertex_positions_stride / sizeof(float))]; + const float* vc = &mesh.vertex_positions[simplified[i + (e + 2) % 3] * (mesh.vertex_positions_stride / sizeof(float))]; + + float ev[3] = {vb[0] - va[0], vb[1] - va[1], vb[2] - va[2]}; + float el = sqrtf(ev[0] * ev[0] + ev[1] * ev[1] + ev[2] * ev[2]); + + // compute edge normal by projecting C onto AB + float cv[3] = {va[0] - vc[0], va[1] - vc[1], va[2] - vc[2]}; + float cp = (cv[0] * ev[0] + cv[1] * ev[1] + cv[2] * ev[2]) / (el > 0.f ? el * el : 1.f); + float nv[3] = {cv[0] - ev[0] * cp, cv[1] - ev[1] * cp, cv[2] - ev[2] * cp}; + float nl = sqrtf(nv[0] * nv[0] + nv[1] * nv[1] + nv[2] * nv[2]); + + // accumulate edge normals weighted by edge length; we will renormalize by perimeter before applying + float ns = nl > 0.f ? el / nl : 0; + + if ((locks[a] & 1) == 0) + dilate_offsets[a * 4 + 0] += nv[0] * ns, dilate_offsets[a * 4 + 1] += nv[1] * ns, dilate_offsets[a * 4 + 2] += nv[2] * ns, dilate_offsets[a * 4 + 3] += el; + if ((locks[b] & 1) == 0) + dilate_offsets[b * 4 + 0] += nv[0] * ns, dilate_offsets[b * 4 + 1] += nv[1] * ns, dilate_offsets[b * 4 + 2] += nv[2] * ns, dilate_offsets[b * 4 + 3] += el; + + perimeter += el; + } + + float distance = perimeter > 0.f ? (old_area - new_area) / perimeter : 0.f; + + for (size_t i = 0; i < simplified.size(); ++i) + { + unsigned int r = remap[simplified[i]]; + if (locks[r] & 1) + continue; + + float* vi = const_cast(&mesh.vertex_positions[simplified[i] * (mesh.vertex_positions_stride / sizeof(float))]); + float* vr = const_cast(&mesh.vertex_positions[r * (mesh.vertex_positions_stride / sizeof(float))]); + float* n = &dilate_offsets[r * 4]; + + if (n[3] > 0.f) + { + // we average the normals and then divide by length twice: once to normalize, and once to extend the offset into a miter + float nx = n[0] / n[3], ny = n[1] / n[3], nz = n[2] / n[3]; + float nn = nx * nx + ny * ny + nz * nz; + float ns = distance / (nn > 0.15f ? nn : 0.15f); + + vr[0] += nx * ns, vr[1] += ny * ns, vr[2] += nz * ns; + n[0] = n[1] = n[2] = n[3] = 0.f; + + // expand group bounds to accommodate the offset vertex + float dx = vr[0] - bounds.center[0], dy = vr[1] - bounds.center[1], dz = vr[2] - bounds.center[2]; + bounds.radius = std::max(bounds.radius, sqrtf(dx * dx + dy * dy + dz * dz)); + } + + // copy dilated positions back to all referencing wedges from the canonical copy updated above; no-op for vertices that weren't dilated + vi[0] = vr[0], vi[1] = vr[1], vi[2] = vr[2]; + } +} + +static clodNode mergeNodes(const clodNode* nodes, size_t offset, size_t count) +{ + clodNode result = {}; + result.bounds = boundsMerge(&nodes[offset].bounds, count, sizeof(clodNode)); + result.group = -1; + result.child_offset = unsigned(offset); + result.child_count = unsigned(count); + return result; +} + +} // namespace clod + +clodConfig clodDefaultConfig(size_t max_triangles) +{ + assert(max_triangles >= 4 && max_triangles <= 256); + + clodConfig config = {}; + config.max_vertices = max_triangles; + config.min_triangles = max_triangles / 3; + config.max_triangles = max_triangles; + + config.partition_spatial = true; + config.partition_size = 16; + + config.cluster_spatial = false; + config.cluster_split_factor = 2.0f; + + config.optimize_clusters = true; + config.optimize_clusters_level = 1; + + config.simplify_ratio = 0.5f; + config.simplify_threshold = 0.85f; + config.simplify_error_merge_previous = 1.0f; + config.simplify_error_factor_sloppy = 2.0f; + config.simplify_error_clamped = true; + config.simplify_permissive = true; + config.simplify_fallback_permissive = false; // note: by default we run in permissive mode, but it's also possible to disable that and use it only as a fallback + config.simplify_fallback_sloppy = true; + + return config; +} + +clodConfig clodDefaultConfigRT(size_t max_triangles) +{ + clodConfig config = clodDefaultConfig(max_triangles); + + // for ray tracing, we may want smaller clusters when that improves BVH quality further; for maximum ray tracing performance this could be reduced even further + config.min_triangles = max_triangles / 4; + + // by default, we use larger max_vertices for RT; the vertex count is not important for ray tracing performance, and this helps improve cluster utilization + config.max_vertices = std::min(size_t(256), max_triangles * 2); + + config.cluster_spatial = true; + config.cluster_fill_weight = 0.5f; + + return config; +} + +void clodBuild(clodConfig config, clodMesh mesh, void* output_context, clodOutput output_callback) +{ + using namespace clod; + + assert(mesh.vertex_attributes_stride % sizeof(float) == 0); + assert(mesh.attribute_count * sizeof(float) <= mesh.vertex_attributes_stride); + assert(mesh.attribute_protect_mask < (1u << (mesh.vertex_attributes_stride / sizeof(float)))); + + std::vector locks(mesh.vertex_count); + + // for cluster connectivity, we need a position-only remap that maps vertices with the same position to the same index + std::vector remap(mesh.vertex_count); + meshopt_generatePositionRemap(&remap[0], mesh.vertex_positions, mesh.vertex_count, mesh.vertex_positions_stride); + + // set up protect bits on UV seams for permissive mode + if (mesh.attribute_protect_mask) + { + size_t max_attributes = mesh.vertex_attributes_stride / sizeof(float); + + for (size_t i = 0; i < mesh.vertex_count; ++i) + { + unsigned int r = remap[i]; // canonical vertex with the same position + + for (size_t j = 0; j < max_attributes; ++j) + if (r != i && (mesh.attribute_protect_mask & (1u << j)) && mesh.vertex_attributes[i * max_attributes + j] != mesh.vertex_attributes[r * max_attributes + j]) + locks[i] |= meshopt_SimplifyVertex_Protect; + } + } + + std::vector dilate_offsets(config.simplify_dilate_borders ? mesh.vertex_count * 4 : 0); + std::vector dilate_table; + + // initial clusterization splits the original mesh + std::vector cluster_indices; + std::vector clusters; + clusterize(clusters, cluster_indices, config, mesh, mesh.indices, mesh.index_count); + + // compute initial precise bounds; subsequent bounds will be using group-merged bounds + for (Cluster& cluster : clusters) + cluster.bounds = boundsCompute(mesh, cluster_indices.data() + cluster.index_offset, cluster.index_count, 0.f); + + std::vector pending; + std::vector pending_indices; + pending.reserve(clusters.size()); + pending_indices.reserve(size_t(cluster_indices.size() * config.simplify_threshold)); + + std::vector merged, simplified; + merged.reserve((config.partition_size + config.partition_size / 3) * config.max_triangles * 3); + + int depth = 0; + + // merge and simplify clusters until we can't merge anymore + while (clusters.size() > 1) + { + std::vector group_offsets = partition(clusters, config, mesh, cluster_indices, remap); + + pending.clear(); + pending_indices.clear(); + + // mark boundaries between groups with a lock bit to avoid gaps in simplified result + lockBoundary(locks, clusters.data(), group_offsets.data(), group_offsets.size() - 1, cluster_indices, remap, mesh.vertex_lock); + + // every group needs to be simplified now + for (size_t i = 0; i + 1 < group_offsets.size(); ++i) + { + const Cluster* group = clusters.data() + group_offsets[i]; + size_t group_size = group_offsets[i + 1] - group_offsets[i]; + + merged.clear(); + for (size_t j = 0; j < group_size; ++j) + merged.insert(merged.end(), cluster_indices.begin() + group[j].index_offset, cluster_indices.begin() + group[j].index_offset + group[j].index_count); + + size_t target_size = size_t((merged.size() / 3) * config.simplify_ratio) * 3; + + // enforce bounds and error monotonicity + // note: it is incorrect to use the precise bounds of the merged or simplified mesh, because this may violate monotonicity + clodBounds bounds = boundsMerge(&group[0].bounds, group_size, sizeof(Cluster)); + + float error = 0.f; + simplify(simplified, config, mesh, merged, locks, target_size, &error); + if (simplified.size() > merged.size() * config.simplify_threshold) + { + bounds.error = FLT_MAX; // terminal group, won't simplify further + outputGroup(config, mesh, group, group_size, cluster_indices, bounds, depth, output_context, output_callback); + continue; // simplification is stuck; abandon the merge + } + + // enforce error monotonicity (with an optional hierarchical factor to separate transitions more) + bounds.error = std::max(bounds.error * config.simplify_error_merge_previous, error) + error * config.simplify_error_merge_additive; + + // output the new group with all clusters; the resulting id will be recorded in new clusters as clodCluster::refined + int refined = outputGroup(config, mesh, group, group_size, cluster_indices, bounds, depth, output_context, output_callback); + + // now that we've output the group with the original clusters, we need to dilate simplified clusters if requested + if (config.simplify_dilate_borders) + dilateBorders(mesh, merged, simplified, locks, remap, bounds, dilate_offsets, dilate_table); + + // enqueue new clusters for further processing + size_t cluster_offset = pending.size(); + clusterize(pending, pending_indices, config, mesh, simplified.data(), simplified.size(), refined); + + // update cluster group bounds to the group-merged bounds; this ensures that we compute the group bounds for whatever group this cluster will be part of conservatively + for (size_t j = cluster_offset; j < pending.size(); ++j) + pending[j].bounds = bounds; + } + + clusters.swap(pending); + cluster_indices.swap(pending_indices); + depth++; + } + + if (clusters.size()) + { + assert(clusters.size() == 1); + const Cluster& cluster = clusters[0]; + + clodBounds bounds = cluster.bounds; + bounds.error = FLT_MAX; // terminal group, won't simplify further + + outputGroup(config, mesh, clusters.data(), clusters.size(), cluster_indices, bounds, depth, output_context, output_callback); + } +} + +size_t clodLocalIndices(unsigned int* vertices, unsigned char* triangles, const unsigned int* indices, size_t index_count) +{ + return meshopt_extractMeshletIndices(vertices, triangles, indices, index_count); +} + +size_t clodBuildHierarchyBound(size_t group_count, size_t node_width, size_t level_count) +{ + // count nodes for each tree depth; we pad by level_count at each iteration to account for unknown level distribution in the forest + size_t total = level_count; + for (size_t frontier = group_count; frontier > 1; frontier = (frontier + node_width - 1) / node_width) + total += frontier + level_count; + + return total; +} + +size_t clodBuildHierarchy(clodNode* nodes, const clodGroup* groups, size_t group_count, size_t node_width, size_t level_count) +{ + using namespace clod; + + // reserve space for per-level roots + size_t offset = level_count; + + std::vector row(group_count); + std::vector order(group_count); + + for (size_t level = 0; level < level_count; ++level) + { + // start each tree hierarchy from the groups of that level as leaves + row.clear(); + for (size_t i = 0; i < group_count; ++i) + if (groups[i].depth == int(level)) + { + clodNode node = {}; + node.bounds = groups[i].simplified; + node.group = int(i); + row.push_back(node); + } + + // build the tree going up one level at a time, using spatial grouping of centers as a proxy for locality + while (row.size() > 1) + { + size_t count = row.size(); + meshopt_spatialClusterPoints(order.data(), row[0].bounds.center, count, sizeof(clodNode), node_width); + + for (size_t i = 0; i < count; ++i) + nodes[offset + i] = row[order[i]]; + + row.clear(); + for (size_t i = 0; i < count; i += node_width) + { + // spatialClusterPoints guarantees that each cluster except for last one is full + size_t children = std::min(node_width, count - i); + row.push_back(mergeNodes(nodes, offset + i, children)); + } + + offset += count; + } + + // the root of the current level goes into the fixed section at the beginning + assert(row.size() == 1); + nodes[level] = row[0]; + } + + assert(offset <= clodBuildHierarchyBound(group_count, node_width, level_count)); + return offset; +} +#endif + +/** + * Copyright (c) 2016-2026 Arseny Kapoulkine + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ diff --git a/Lib/meshoptimizer/demo/index.html b/Lib/meshoptimizer/demo/index.html new file mode 100644 index 0000000..883fb93 --- /dev/null +++ b/Lib/meshoptimizer/demo/index.html @@ -0,0 +1,148 @@ + + + + meshoptimizer - demo + + + + + + + + + + + + + diff --git a/Lib/meshoptimizer/demo/main.cpp b/Lib/meshoptimizer/demo/main.cpp new file mode 100644 index 0000000..fc7ea5e --- /dev/null +++ b/Lib/meshoptimizer/demo/main.cpp @@ -0,0 +1,1978 @@ +#include "../src/meshoptimizer.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "../extern/fast_obj.h" + +#define SDEFL_IMPLEMENTATION +#include "../extern/sdefl.h" + +// This file uses assert() to verify algorithm correctness +#undef NDEBUG +#include + +#if defined(__linux__) +double timestamp() +{ + timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return double(ts.tv_sec) + 1e-9 * double(ts.tv_nsec); +} +#elif defined(_WIN32) +struct LARGE_INTEGER +{ + __int64 QuadPart; +}; +extern "C" __declspec(dllimport) int __stdcall QueryPerformanceCounter(LARGE_INTEGER* lpPerformanceCount); +extern "C" __declspec(dllimport) int __stdcall QueryPerformanceFrequency(LARGE_INTEGER* lpFrequency); + +double timestamp() +{ + LARGE_INTEGER freq, counter; + QueryPerformanceFrequency(&freq); + QueryPerformanceCounter(&counter); + return double(counter.QuadPart) / double(freq.QuadPart); +} +#else +double timestamp() +{ + return double(clock()) / double(CLOCKS_PER_SEC); +} +#endif + +struct Vertex +{ + float px, py, pz; + float nx, ny, nz; + float tx, ty; +}; + +struct Mesh +{ + std::vector vertices; + std::vector indices; +}; + +Mesh parseObj(const char* path, double& reindex) +{ + fastObjMesh* obj = fast_obj_read(path); + if (!obj) + { + printf("Error loading %s: file not found\n", path); + return Mesh(); + } + + size_t total_indices = 0; + + for (unsigned int i = 0; i < obj->face_count; ++i) + if (obj->face_vertices[i] > 2) + total_indices += 3 * (obj->face_vertices[i] - 2); + + std::vector vertices(total_indices); + + size_t vertex_offset = 0; + size_t index_offset = 0; + + for (unsigned int i = 0; i < obj->face_count; ++i) + { + if (obj->face_vertices[i] <= 2) + continue; + + for (unsigned int j = 0; j < obj->face_vertices[i]; ++j) + { + fastObjIndex gi = obj->indices[index_offset + j]; + + Vertex v = + { + obj->positions[gi.p * 3 + 0], + obj->positions[gi.p * 3 + 1], + obj->positions[gi.p * 3 + 2], + obj->normals[gi.n * 3 + 0], + obj->normals[gi.n * 3 + 1], + obj->normals[gi.n * 3 + 2], + obj->texcoords[gi.t * 2 + 0], + obj->texcoords[gi.t * 2 + 1], + }; + + // triangulate polygon on the fly; offset-3 is always the first polygon vertex + if (j >= 3) + { + vertices[vertex_offset + 0] = vertices[vertex_offset - 3]; + vertices[vertex_offset + 1] = vertices[vertex_offset - 1]; + vertex_offset += 2; + } + + vertices[vertex_offset] = v; + vertex_offset++; + } + + index_offset += obj->face_vertices[i]; + } + + fast_obj_destroy(obj); + + reindex = timestamp(); + + Mesh result; + + // empty mesh + if (total_indices == 0) + return result; + + std::vector remap(total_indices); + + size_t total_vertices = meshopt_generateVertexRemap(&remap[0], NULL, total_indices, &vertices[0], total_indices, sizeof(Vertex)); + + result.indices.resize(total_indices); + meshopt_remapIndexBuffer(&result.indices[0], NULL, total_indices, &remap[0]); + + result.vertices.resize(total_vertices); + meshopt_remapVertexBuffer(&result.vertices[0], &vertices[0], total_indices, sizeof(Vertex), &remap[0]); + + return result; +} + +void dumpObj(const std::vector& vertices, const std::vector& indices, bool recomputeNormals = false) +{ + std::vector normals; + + if (recomputeNormals) + { + normals.resize(vertices.size() * 3); + + for (size_t i = 0; i < indices.size(); i += 3) + { + unsigned int a = indices[i], b = indices[i + 1], c = indices[i + 2]; + + const Vertex& va = vertices[a]; + const Vertex& vb = vertices[b]; + const Vertex& vc = vertices[c]; + + float nx = (vb.py - va.py) * (vc.pz - va.pz) - (vb.pz - va.pz) * (vc.py - va.py); + float ny = (vb.pz - va.pz) * (vc.px - va.px) - (vb.px - va.px) * (vc.pz - va.pz); + float nz = (vb.px - va.px) * (vc.py - va.py) - (vb.py - va.py) * (vc.px - va.px); + + for (int k = 0; k < 3; ++k) + { + unsigned int index = indices[i + k]; + + normals[index * 3 + 0] += nx; + normals[index * 3 + 1] += ny; + normals[index * 3 + 2] += nz; + } + } + } + + for (size_t i = 0; i < vertices.size(); ++i) + { + const Vertex& v = vertices[i]; + + float nx = v.nx, ny = v.ny, nz = v.nz; + + if (recomputeNormals) + { + nx = normals[i * 3 + 0]; + ny = normals[i * 3 + 1]; + nz = normals[i * 3 + 2]; + + float l = sqrtf(nx * nx + ny * ny + nz * nz); + float s = l == 0.f ? 0.f : 1.f / l; + + nx *= s; + ny *= s; + nz *= s; + } + + fprintf(stderr, "v %f %f %f\n", v.px, v.py, v.pz); + fprintf(stderr, "vn %f %f %f\n", nx, ny, nz); + } + + for (size_t i = 0; i < indices.size(); i += 3) + { + unsigned int a = indices[i], b = indices[i + 1], c = indices[i + 2]; + + fprintf(stderr, "f %d//%d %d//%d %d//%d\n", a + 1, a + 1, b + 1, b + 1, c + 1, c + 1); + } +} + +void dumpObj(const char* section, const std::vector& indices) +{ + fprintf(stderr, "o %s\n", section); + + for (size_t j = 0; j < indices.size(); j += 3) + { + unsigned int a = indices[j], b = indices[j + 1], c = indices[j + 2]; + + fprintf(stderr, "f %d//%d %d//%d %d//%d\n", a + 1, a + 1, b + 1, b + 1, c + 1, c + 1); + } +} + +struct PackedVertex +{ + unsigned short px, py, pz; + unsigned short pw; // padding to 4b boundary + signed char nx, ny, nz, nw; + unsigned short tx, ty; +}; + +void packMesh(std::vector& pv, const std::vector& vertices) +{ + for (size_t i = 0; i < vertices.size(); ++i) + { + const Vertex& vi = vertices[i]; + PackedVertex& pvi = pv[i]; + + pvi.px = meshopt_quantizeHalf(vi.px); + pvi.py = meshopt_quantizeHalf(vi.py); + pvi.pz = meshopt_quantizeHalf(vi.pz); + pvi.pw = 0; + + pvi.nx = char(meshopt_quantizeSnorm(vi.nx, 8)); + pvi.ny = char(meshopt_quantizeSnorm(vi.ny, 8)); + pvi.nz = char(meshopt_quantizeSnorm(vi.nz, 8)); + pvi.nw = 0; + + pvi.tx = meshopt_quantizeHalf(vi.tx); + pvi.ty = meshopt_quantizeHalf(vi.ty); + } +} + +struct PackedVertexOct +{ + unsigned short px, py, pz; + signed char nu, nv; // octahedron encoded normal, aliases .pw + unsigned short tx, ty; +}; + +void packMesh(std::vector& pv, const std::vector& vertices) +{ + for (size_t i = 0; i < vertices.size(); ++i) + { + const Vertex& vi = vertices[i]; + PackedVertexOct& pvi = pv[i]; + + pvi.px = meshopt_quantizeHalf(vi.px); + pvi.py = meshopt_quantizeHalf(vi.py); + pvi.pz = meshopt_quantizeHalf(vi.pz); + + float nsum = fabsf(vi.nx) + fabsf(vi.ny) + fabsf(vi.nz); + float nx = vi.nx / nsum; + float ny = vi.ny / nsum; + float nz = vi.nz; + + float nu = nz >= 0 ? nx : (1 - fabsf(ny)) * (nx >= 0 ? 1 : -1); + float nv = nz >= 0 ? ny : (1 - fabsf(nx)) * (ny >= 0 ? 1 : -1); + + pvi.nu = char(meshopt_quantizeSnorm(nu, 8)); + pvi.nv = char(meshopt_quantizeSnorm(nv, 8)); + + pvi.tx = meshopt_quantizeHalf(vi.tx); + pvi.ty = meshopt_quantizeHalf(vi.ty); + } +} + +void simplify(const Mesh& mesh, float threshold = 0.2f, unsigned int options = 0) +{ + Mesh lod; + + double start = timestamp(); + + size_t target_index_count = size_t(mesh.indices.size() * threshold); + float target_error = 1e-2f; + float result_error = 0; + + lod.indices.resize(mesh.indices.size()); // note: simplify needs space for index_count elements in the destination array, not target_index_count + lod.indices.resize(meshopt_simplify(&lod.indices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), target_index_count, target_error, options, &result_error)); + + lod.vertices.resize(lod.indices.size() < mesh.vertices.size() ? lod.indices.size() : mesh.vertices.size()); // note: this is just to reduce the cost of resize() + lod.vertices.resize(meshopt_optimizeVertexFetch(&lod.vertices[0], &lod.indices[0], lod.indices.size(), &mesh.vertices[0], mesh.vertices.size(), sizeof(Vertex))); + + double end = timestamp(); + + printf("%-9s: %d triangles => %d triangles (%.2f%% deviation) in %.2f msec\n", + "Simplify", + int(mesh.indices.size() / 3), int(lod.indices.size() / 3), + result_error * 100, + (end - start) * 1000); +} + +void simplifyAttr(const Mesh& mesh, float threshold = 0.2f, unsigned int options = 0) +{ + Mesh lod; + + double start = timestamp(); + + size_t target_index_count = size_t(mesh.indices.size() * threshold); + float target_error = 1e-2f; + float result_error = 0; + + const float nrm_weight = 0.5f; + const float attr_weights[3] = {nrm_weight, nrm_weight, nrm_weight}; + + lod.indices.resize(mesh.indices.size()); // note: simplify needs space for index_count elements in the destination array, not target_index_count + lod.indices.resize(meshopt_simplifyWithAttributes(&lod.indices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), &mesh.vertices[0].nx, sizeof(Vertex), attr_weights, 3, NULL, target_index_count, target_error, options, &result_error)); + + lod.vertices.resize(lod.indices.size() < mesh.vertices.size() ? lod.indices.size() : mesh.vertices.size()); // note: this is just to reduce the cost of resize() + lod.vertices.resize(meshopt_optimizeVertexFetch(&lod.vertices[0], &lod.indices[0], lod.indices.size(), &mesh.vertices[0], mesh.vertices.size(), sizeof(Vertex))); + + double end = timestamp(); + + printf("%-9s: %d triangles => %d triangles (%.2f%% deviation) in %.2f msec\n", + "SimplifyAttr", + int(mesh.indices.size() / 3), int(lod.indices.size() / 3), + result_error * 100, + (end - start) * 1000); +} + +void simplifyUpdate(const Mesh& mesh, float threshold = 0.2f, unsigned int options = 0) +{ + Mesh lod; + + double start = timestamp(); + + size_t target_index_count = size_t(mesh.indices.size() * threshold); + float target_error = 1e-2f; + float result_error = 0; + + const float nrm_weight = 0.5f; + const float attr_weights[3] = {nrm_weight, nrm_weight, nrm_weight}; + + lod = mesh; // start from the original mesh + lod.indices.resize(meshopt_simplifyWithUpdate(&lod.indices[0], mesh.indices.size(), &lod.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), &lod.vertices[0].nx, sizeof(Vertex), attr_weights, 3, NULL, target_index_count, target_error, options, &result_error)); + + lod.vertices.resize(meshopt_optimizeVertexFetch(&lod.vertices[0], &lod.indices[0], lod.indices.size(), &lod.vertices[0], lod.vertices.size(), sizeof(Vertex))); + + for (size_t i = 0; i < lod.vertices.size(); ++i) + { + // update normals + Vertex& v = lod.vertices[i]; + float nl = sqrtf(v.nx * v.nx + v.ny * v.ny + v.nz * v.nz); + if (nl > 0) + { + v.nx /= nl; + v.ny /= nl; + v.nz /= nl; + } + } + + double end = timestamp(); + + printf("%-9s: %d triangles => %d triangles (%.2f%% deviation) in %.2f msec\n", + "SimplifyUpdt", + int(mesh.indices.size() / 3), int(lod.indices.size() / 3), + result_error * 100, + (end - start) * 1000); +} + +void simplifySloppy(const Mesh& mesh, float threshold = 0.2f) +{ + Mesh lod; + + double start = timestamp(); + + size_t target_index_count = size_t(mesh.indices.size() * threshold); + float target_error = 1e-1f; + float result_error = 0; + + lod.indices.resize(mesh.indices.size()); // note: simplify needs space for index_count elements in the destination array, not target_index_count + lod.indices.resize(meshopt_simplifySloppy(&lod.indices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), target_index_count, target_error, &result_error)); + + lod.vertices.resize(lod.indices.size() < mesh.vertices.size() ? lod.indices.size() : mesh.vertices.size()); // note: this is just to reduce the cost of resize() + lod.vertices.resize(meshopt_optimizeVertexFetch(&lod.vertices[0], &lod.indices[0], lod.indices.size(), &mesh.vertices[0], mesh.vertices.size(), sizeof(Vertex))); + + double end = timestamp(); + + printf("%-9s: %d triangles => %d triangles (%.2f%% deviation) in %.2f msec\n", + "SimplifyS", + int(mesh.indices.size() / 3), int(lod.indices.size() / 3), + result_error * 100, + (end - start) * 1000); +} + +void simplifyPoints(const Mesh& mesh, float threshold = 0.2f) +{ + double start = timestamp(); + + size_t target_vertex_count = size_t(mesh.vertices.size() * threshold); + if (target_vertex_count == 0) + return; + + std::vector indices(target_vertex_count); + indices.resize(meshopt_simplifyPoints(&indices[0], &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), NULL, 0, 0, target_vertex_count)); + + double end = timestamp(); + + printf("%-9s: %d points => %d points in %.2f msec\n", + "SimplifyP", + int(mesh.vertices.size()), int(indices.size()), (end - start) * 1000); +} + +void simplifyComplete(const Mesh& mesh) +{ + static const size_t lod_count = 5; + + double start = timestamp(); + + // generate 4 LOD levels (1-4), with each subsequent LOD using 70% triangles + // note that each LOD uses the same (shared) vertex buffer + std::vector lods[lod_count]; + + lods[0] = mesh.indices; + + for (size_t i = 1; i < lod_count; ++i) + { + std::vector& lod = lods[i]; + + float threshold = powf(0.7f, float(i)); + size_t target_index_count = size_t(mesh.indices.size() * threshold) / 3 * 3; + float target_error = 1e-2f; + + // we can simplify all the way from base level or from the last result + // simplifying from the base level sometimes produces better results, but simplifying from last level is faster + const std::vector& source = lods[i - 1]; + + if (source.size() < target_index_count) + target_index_count = source.size(); + + lod.resize(source.size()); + lod.resize(meshopt_simplify(&lod[0], &source[0], source.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), target_index_count, target_error)); + } + + double middle = timestamp(); + + // optimize each individual LOD for vertex cache & overdraw + for (size_t i = 0; i < lod_count; ++i) + { + std::vector& lod = lods[i]; + + meshopt_optimizeVertexCache(&lod[0], &lod[0], lod.size(), mesh.vertices.size()); + meshopt_optimizeOverdraw(&lod[0], &lod[0], lod.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), 1.0f); + } + + // concatenate all LODs into one IB + // note: the order of concatenation is important - since we optimize the entire IB for vertex fetch, + // putting coarse LODs first makes sure that the vertex range referenced by them is as small as possible + // some GPUs process the entire range referenced by the index buffer region so doing this optimizes the vertex transform + // cost for coarse LODs + // this order also produces much better vertex fetch cache coherency for coarse LODs (since they're essentially optimized first) + // somewhat surprisingly, the vertex fetch cache coherency for fine LODs doesn't seem to suffer that much. + size_t lod_index_offsets[lod_count] = {}; + size_t lod_index_counts[lod_count] = {}; + size_t total_index_count = 0; + + for (int i = lod_count - 1; i >= 0; --i) + { + lod_index_offsets[i] = total_index_count; + lod_index_counts[i] = lods[i].size(); + + total_index_count += lods[i].size(); + } + + std::vector indices(total_index_count); + + for (size_t i = 0; i < lod_count; ++i) + { + memcpy(&indices[lod_index_offsets[i]], &lods[i][0], lods[i].size() * sizeof(lods[i][0])); + } + + std::vector vertices = mesh.vertices; + + // vertex fetch optimization should go last as it depends on the final index order + // note that the order of LODs above affects vertex fetch results + meshopt_optimizeVertexFetch(&vertices[0], &indices[0], indices.size(), &vertices[0], vertices.size(), sizeof(Vertex)); + + double end = timestamp(); + + printf("%-9s: %d triangles => %d LOD levels down to %d triangles in %.2f msec, optimized in %.2f msec\n", + "SimplifyC", + int(lod_index_counts[0]) / 3, int(lod_count), int(lod_index_counts[lod_count - 1]) / 3, + (middle - start) * 1000, (end - middle) * 1000); + + // for using LOD data at runtime, in addition to vertices and indices you have to save lod_index_offsets/lod_index_counts. + + { + meshopt_VertexCacheStatistics vcs0 = meshopt_analyzeVertexCache(&indices[lod_index_offsets[0]], lod_index_counts[0], vertices.size(), 16, 0, 0); + meshopt_VertexFetchStatistics vfs0 = meshopt_analyzeVertexFetch(&indices[lod_index_offsets[0]], lod_index_counts[0], vertices.size(), sizeof(Vertex)); + meshopt_VertexCacheStatistics vcsN = meshopt_analyzeVertexCache(&indices[lod_index_offsets[lod_count - 1]], lod_index_counts[lod_count - 1], vertices.size(), 16, 0, 0); + meshopt_VertexFetchStatistics vfsN = meshopt_analyzeVertexFetch(&indices[lod_index_offsets[lod_count - 1]], lod_index_counts[lod_count - 1], vertices.size(), sizeof(Vertex)); + + typedef PackedVertexOct PV; + + std::vector pv(vertices.size()); + packMesh(pv, vertices); + + std::vector vbuf(meshopt_encodeVertexBufferBound(vertices.size(), sizeof(PV))); + vbuf.resize(meshopt_encodeVertexBuffer(&vbuf[0], vbuf.size(), &pv[0], vertices.size(), sizeof(PV))); + + std::vector ibuf(meshopt_encodeIndexBufferBound(indices.size(), vertices.size())); + ibuf.resize(meshopt_encodeIndexBuffer(&ibuf[0], ibuf.size(), &indices[0], indices.size())); + + printf("%-9s ACMR %f...%f Overfetch %f..%f Codec VB %.1f bits/vertex IB %.1f bits/triangle\n", + "", + vcs0.acmr, vcsN.acmr, vfs0.overfetch, vfsN.overfetch, + double(vbuf.size()) / double(vertices.size()) * 8, + double(ibuf.size()) / double(indices.size() / 3) * 8); + } +} + +void simplifyClusters(const Mesh& mesh, float threshold = 0.2f) +{ + const size_t max_vertices = 64; + const size_t max_triangles = 64; + const size_t target_group_size = 8; + + double start = timestamp(); + + // build clusters (meshlets) out of the mesh + size_t max_meshlets = meshopt_buildMeshletsBound(mesh.indices.size(), max_vertices, max_triangles); + std::vector meshlets(max_meshlets); + std::vector meshlet_vertices(mesh.indices.size()); + std::vector meshlet_triangles(mesh.indices.size()); + + meshlets.resize(meshopt_buildMeshlets(&meshlets[0], &meshlet_vertices[0], &meshlet_triangles[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), max_vertices, max_triangles, 0.f)); + + double middle = timestamp(); + + // generate position remap; we'll use that to partition clusters using position-only adjacency + std::vector remap(mesh.vertices.size()); + meshopt_generatePositionRemap(&remap[0], &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex)); + + // partition clusters in groups; each group will be simplified separately and the boundaries between groups will be preserved + std::vector cluster_indices; + cluster_indices.reserve(mesh.indices.size()); + std::vector cluster_sizes(meshlets.size()); + + for (size_t i = 0; i < meshlets.size(); ++i) + { + const meshopt_Meshlet& m = meshlets[i]; + + for (size_t j = 0; j < m.triangle_count * 3; ++j) + { + unsigned int v = meshlet_vertices[m.vertex_offset + meshlet_triangles[m.triangle_offset + j]]; + + // use the first vertex with equivalent position so that cluster adjacency ignores attribute seams + cluster_indices.push_back(remap[v]); + } + + cluster_sizes[i] = m.triangle_count * 3; + } + + std::vector partition(meshlets.size()); + size_t partition_count = meshopt_partitionClusters(&partition[0], &cluster_indices[0], cluster_indices.size(), &cluster_sizes[0], cluster_sizes.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), target_group_size); + + // convert partitions to linked lists to make it easier to iterate over (vectors of vectors would work too) + std::vector partnext(meshlets.size(), -1); + std::vector partlast(partition_count, -1); + + for (size_t i = 0; i < meshlets.size(); ++i) + { + unsigned int part = partition[i]; + + if (partlast[part] >= 0) + partnext[partlast[part]] = int(i); + + partlast[part] = int(i); + partnext[i] = -1; + } + + double parttime = timestamp(); + + float scale = meshopt_simplifyScale(&mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex)); + + std::vector lod; + lod.reserve(mesh.indices.size()); + + float error = 0.f; + + for (size_t i = 0; i < meshlets.size(); ++i) + { + if (partlast[partition[i]] < 0) + continue; // part of a group that was already processed + + // mark group as processed + partlast[partition[i]] = -1; + + size_t group_offset = lod.size(); + + for (int j = int(i); j >= 0; j = partnext[j]) + { + const meshopt_Meshlet& m = meshlets[j]; + + for (size_t k = 0; k < m.triangle_count * 3; ++k) + lod.push_back(meshlet_vertices[m.vertex_offset + meshlet_triangles[m.triangle_offset + k]]); + } + + size_t group_triangles = (lod.size() - group_offset) / 3; + + // simplify the group, preserving the border vertices + // note: this technically also locks the exterior border; a full mesh analysis (see clusterlod.h / lockBoundary) would work better for some meshes + unsigned int options = meshopt_SimplifyLockBorder | meshopt_SimplifySparse | meshopt_SimplifyErrorAbsolute; + + float group_target_error = 1e-2f * scale; + size_t group_target = size_t(float(group_triangles) * threshold) * 3; + float group_error = 0.f; + size_t group_size = meshopt_simplify(&lod[group_offset], &lod[group_offset], group_triangles * 3, &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), group_target, group_target_error, options, &group_error); + + error = group_error > error ? group_error : error; + + // simplified group is available in lod[group_offset..group_offset + group_size] + lod.resize(group_offset + group_size); + } + + double end = timestamp(); + + printf("%-9s: %d triangles => %d triangles (%.2f%% deviation) in %.2f msec, clusterized in %.2f msec, partitioned in %.2f msec (%d clusters in %d groups, %.1f avg)\n", + "SimplifyG", + int(mesh.indices.size() / 3), int(lod.size() / 3), + error / scale * 100, + (end - parttime) * 1000, (middle - start) * 1000, (parttime - middle) * 1000, + int(meshlets.size()), int(partition_count), double(meshlets.size()) / double(partition_count)); +} + +void optimize(const Mesh& mesh, bool fifo = false) +{ + Mesh copy = mesh; + // note: we assume that the mesh is already optimally indexed (via parseObj); if that is not the case, you'd need to reindex first + + double start = timestamp(); + + // vertex cache optimization should go first as it provides starting order for overdraw + // note: fifo optimization is not recommended as a default, since it produces worse results, but it's faster to run so it can be useful for procedural meshes + if (fifo) + meshopt_optimizeVertexCacheFifo(©.indices[0], ©.indices[0], copy.indices.size(), copy.vertices.size(), /* cache_size= */ 16); + else + meshopt_optimizeVertexCache(©.indices[0], ©.indices[0], copy.indices.size(), copy.vertices.size()); + + // reorder indices for overdraw, balancing overdraw and vertex cache efficiency + const float kThreshold = 1.01f; // allow up to 1% worse ACMR to get more reordering opportunities for overdraw + meshopt_optimizeOverdraw(©.indices[0], ©.indices[0], copy.indices.size(), ©.vertices[0].px, copy.vertices.size(), sizeof(Vertex), kThreshold); + + // vertex fetch optimization should go last as it depends on the final index order + meshopt_optimizeVertexFetch(©.vertices[0], ©.indices[0], copy.indices.size(), ©.vertices[0], copy.vertices.size(), sizeof(Vertex)); + + double end = timestamp(); + + meshopt_VertexCacheStatistics vcs = meshopt_analyzeVertexCache(©.indices[0], copy.indices.size(), copy.vertices.size(), 16, 0, 0); + meshopt_VertexFetchStatistics vfs = meshopt_analyzeVertexFetch(©.indices[0], copy.indices.size(), copy.vertices.size(), sizeof(Vertex)); + meshopt_OverdrawStatistics os = meshopt_analyzeOverdraw(©.indices[0], copy.indices.size(), ©.vertices[0].px, copy.vertices.size(), sizeof(Vertex)); + + meshopt_VertexCacheStatistics vcs_nv = meshopt_analyzeVertexCache(©.indices[0], copy.indices.size(), copy.vertices.size(), 32, 32, 32); + meshopt_VertexCacheStatistics vcs_amd = meshopt_analyzeVertexCache(©.indices[0], copy.indices.size(), copy.vertices.size(), 14, 64, 128); + meshopt_VertexCacheStatistics vcs_intel = meshopt_analyzeVertexCache(©.indices[0], copy.indices.size(), copy.vertices.size(), 128, 0, 0); + + printf("Optimize%s: ACMR %f ATVR %f (NV %f AMD %f Intel %f) overfetch %f overdraw %f in %.2f msec\n", + fifo ? "F" : " ", + vcs.acmr, vcs.atvr, vcs_nv.atvr, vcs_amd.atvr, vcs_intel.atvr, vfs.overfetch, os.overdraw, (end - start) * 1000); +} + +template +size_t compress(const std::vector& data, int level = SDEFL_LVL_DEF) +{ + std::vector cbuf(sdefl_bound(int(data.size() * sizeof(T)))); + sdefl s = {}; + return sdeflate(&s, &cbuf[0], reinterpret_cast(&data[0]), int(data.size() * sizeof(T)), level); +} + +void encodeIndex(const std::vector& indices, size_t vertex_count, char desc) +{ + // allocate result outside of the timing loop to exclude memset() from decode timing + std::vector result(indices.size()); + + double start = timestamp(); + + std::vector buffer(meshopt_encodeIndexBufferBound(indices.size(), vertex_count)); + buffer.resize(meshopt_encodeIndexBuffer(&buffer[0], buffer.size(), &indices[0], indices.size())); + + double middle = timestamp(); + + int res = meshopt_decodeIndexBuffer(&result[0], indices.size(), &buffer[0], buffer.size()); + assert(res == 0); + (void)res; + + double end = timestamp(); + + size_t csize = compress(buffer); + + for (size_t i = 0; i < indices.size(); i += 3) + { + assert( + (result[i + 0] == indices[i + 0] && result[i + 1] == indices[i + 1] && result[i + 2] == indices[i + 2]) || + (result[i + 1] == indices[i + 0] && result[i + 2] == indices[i + 1] && result[i + 0] == indices[i + 2]) || + (result[i + 2] == indices[i + 0] && result[i + 0] == indices[i + 1] && result[i + 1] == indices[i + 2])); + } + + printf("IdxCodec%c: %.1f bits/triangle (post-deflate %.1f bits/triangle); encode %.2f msec (%.3f GB/s), decode %.2f msec (%.2f GB/s)\n", + desc, + double(buffer.size() * 8) / double(indices.size() / 3), + double(csize * 8) / double(indices.size() / 3), + (middle - start) * 1000, + (double(result.size() * 4) / 1e9) / (middle - start), + (end - middle) * 1000, + (double(result.size() * 4) / 1e9) / (end - middle)); +} + +void encodeIndex(const Mesh& mesh, char desc) +{ + encodeIndex(mesh.indices, mesh.vertices.size(), desc); +} + +void encodeIndexSequence(const std::vector& data, size_t vertex_count, char desc) +{ + // allocate result outside of the timing loop to exclude memset() from decode timing + std::vector result(data.size()); + + double start = timestamp(); + + std::vector buffer(meshopt_encodeIndexSequenceBound(data.size(), vertex_count)); + buffer.resize(meshopt_encodeIndexSequence(&buffer[0], buffer.size(), &data[0], data.size())); + + double middle = timestamp(); + + int res = meshopt_decodeIndexSequence(&result[0], data.size(), &buffer[0], buffer.size()); + assert(res == 0); + (void)res; + + double end = timestamp(); + + size_t csize = compress(buffer); + + assert(memcmp(&data[0], &result[0], data.size() * sizeof(unsigned int)) == 0); + + printf("IdxCodec%c: %.1f bits/index (post-deflate %.1f bits/index); encode %.2f msec (%.3f GB/s), decode %.2f msec (%.2f GB/s)\n", + desc, + double(buffer.size() * 8) / double(data.size()), + double(csize * 8) / double(data.size()), + (middle - start) * 1000, + (double(result.size() * 4) / 1e9) / (middle - start), + (end - middle) * 1000, + (double(result.size() * 4) / 1e9) / (end - middle)); +} + +template +static void validateDecodeMeshlet(const unsigned char* data, size_t size, const unsigned int* vertices, size_t vertex_count, const unsigned char* triangles, size_t triangle_count) +{ + V rv[256]; + T rt[sizeof(T) == 1 ? 256 * 3 : 256]; + + int rc = meshopt_decodeMeshlet(rv, vertex_count, rt, triangle_count, data, size); + assert(rc == 0); + + for (size_t j = 0; j < vertex_count; ++j) + assert(rv[j] == V(vertices[j])); + + for (size_t j = 0; j < triangle_count; ++j) + { + unsigned int a = triangles[j * 3 + 0]; + unsigned int b = triangles[j * 3 + 1]; + unsigned int c = triangles[j * 3 + 2]; + + unsigned int tri = sizeof(T) == 1 ? rt[j * 3] | (rt[j * 3 + 1] << 8) | (rt[j * 3 + 2] << 16) : rt[j]; + + unsigned int abc = (a << 0) | (b << 8) | (c << 16); + unsigned int bca = (b << 0) | (c << 8) | (a << 16); + unsigned int cba = (c << 0) | (a << 8) | (b << 16); + + assert(tri == abc || tri == bca || tri == cba); + } +} + +void encodeMeshlets(const Mesh& mesh, size_t max_vertices, size_t max_triangles, bool reorder = true, int level = 3) +{ + size_t max_meshlets = meshopt_buildMeshletsBound(mesh.indices.size(), max_vertices, max_triangles); + std::vector meshlets(max_meshlets); + std::vector meshlet_vertices(mesh.indices.size()); + std::vector meshlet_triangles(mesh.indices.size()); + + meshlets.resize(meshopt_buildMeshlets(&meshlets[0], &meshlet_vertices[0], &meshlet_triangles[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), max_vertices, max_triangles, 0.f)); + + if (meshlets.size()) + { + const meshopt_Meshlet& last = meshlets.back(); + + // this is an example of how to trim the vertex/triangle arrays when copying data out to GPU storage + meshlet_vertices.resize(last.vertex_offset + last.vertex_count); + meshlet_triangles.resize(last.triangle_offset + last.triangle_count * 3); + } + + std::vector cbuf(meshopt_encodeMeshletBound(max_vertices, max_triangles)); + + // optimize each meshlet for locality; this is important for performance, and critical for good compression + for (size_t i = 0; i < meshlets.size(); ++i) + meshopt_optimizeMeshletLevel(&meshlet_vertices[meshlets[i].vertex_offset], meshlets[i].vertex_count, &meshlet_triangles[meshlets[i].triangle_offset], meshlets[i].triangle_count, level); + + // optimize the order of vertex references within each meshlet and globally; this is valuable for access locality and critical for compression of vertex references + // note that this reorders the vertex buffer too, so if a traditional index buffer is required it would need to be reconstructed from the meshlet data for optimal locality + std::vector vertices = mesh.vertices; + if (reorder) + meshopt_optimizeVertexFetch(&vertices[0], &meshlet_vertices[0], meshlet_vertices.size(), &mesh.vertices[0], mesh.vertices.size(), sizeof(Vertex)); + + size_t mbst = 0; + + std::vector packed; + + for (size_t i = 0; i < meshlets.size(); ++i) + { + const meshopt_Meshlet& meshlet = meshlets[i]; + + size_t mbs = meshopt_encodeMeshlet(&cbuf[0], cbuf.size(), &meshlet_vertices[meshlet.vertex_offset], meshlet.vertex_count, &meshlet_triangles[meshlet.triangle_offset], meshlet.triangle_count); + assert(mbs > 0); + + // 24-bit header: 7 bit (vertex_count-1), 7 bit (triangle_count-1), 10 bit size + // fits up to 128v/128t meshlet with 1024 bytes of encoded data; meshopt_encodeMeshletBound(128,128) < 1000 + assert(size_t(meshlet.vertex_count - 1) < 128 && size_t(meshlet.triangle_count - 1) < 128 && mbs < 1024); + unsigned int header = ((meshlet.vertex_count - 1) & 0x7f) | (((meshlet.triangle_count - 1) & 0x7f) << 7) | ((unsigned(mbs) & 0x3ff) << 14); + packed.push_back((unsigned char)(header & 0xff)); + packed.push_back((unsigned char)((header >> 8) & 0xff)); + packed.push_back((unsigned char)((header >> 16) & 0xff)); + packed.insert(packed.end(), &cbuf[0], &cbuf[mbs]); + + validateDecodeMeshlet(&cbuf[0], mbs, &meshlet_vertices[meshlet.vertex_offset], meshlet.vertex_count, &meshlet_triangles[meshlet.triangle_offset], meshlet.triangle_count); + validateDecodeMeshlet(&cbuf[0], mbs, &meshlet_vertices[meshlet.vertex_offset], meshlet.vertex_count, &meshlet_triangles[meshlet.triangle_offset], meshlet.triangle_count); + validateDecodeMeshlet(&cbuf[0], mbs, &meshlet_vertices[meshlet.vertex_offset], meshlet.vertex_count, &meshlet_triangles[meshlet.triangle_offset], meshlet.triangle_count); + validateDecodeMeshlet(&cbuf[0], mbs, &meshlet_vertices[meshlet.vertex_offset], meshlet.vertex_count, &meshlet_triangles[meshlet.triangle_offset], meshlet.triangle_count); + + mbst += mbs; + } + + size_t mbc = compress(packed); + + printf("MeshletCodec (%d/%d): %d meshlets, %d bytes/meshlet; %d bytes, %.1f bits/triangle\n", + int(max_vertices), int(max_triangles), + int(meshlets.size()), + int(mbst / meshlets.size()), + int(mbst), double(mbst * 8) / double(mesh.indices.size() / 3)); + printf("MeshletCodec (%d/%d, packed): %d bytes/meshlet, %.1f bits/triangle; post-deflate: %d bytes/meshlet, %.1f bits/triangle)\n", + int(max_vertices), int(max_triangles), + int(packed.size() / meshlets.size()), double(packed.size() * 8) / double(mesh.indices.size() / 3), + int(mbc / meshlets.size()), double(mbc * 8) / double(mesh.indices.size() / 3)); + +#if !TRACE + double mbtime = 0; + + for (int i = 0; i < 10; ++i) + { + unsigned int rv[256]; + unsigned int rt[256]; + double t0 = timestamp(); + unsigned char* p = &packed[0]; + for (size_t j = 0; j < meshlets.size(); ++j) + { + unsigned int header = p[0] | (p[1] << 8) | (p[2] << 16); + size_t vertex_count = (header & 0x7f) + 1; + size_t triangle_count = ((header >> 7) & 0x7f) + 1; + size_t size = (header >> 14) & 0x3ff; + meshopt_decodeMeshletRaw(rv, vertex_count, rt, triangle_count, p + 3, size); + p += 3 + size; + } + double t1 = timestamp(); + + mbtime = (mbtime == 0 || t1 - t0 < mbtime) ? (t1 - t0) : mbtime; + } + + printf("MeshletCodec (%d/%d, packed): decode time %.3f msec, %.3fB tri/sec, %.1f ns/meshlet\n", + int(max_vertices), int(max_triangles), + mbtime * 1000, double(mesh.indices.size() / 3) / 1e9 / mbtime, mbtime * 1e9 / double(meshlets.size())); +#endif +} + +static void writeBits(std::vector& data, unsigned int& acc, int& acc_bits, unsigned int value, int bits) +{ + assert(value < (1u << bits)); + assert(acc_bits + bits <= 32); + + acc |= value << acc_bits; + acc_bits += bits; + + while (acc_bits >= 8) + { + data.push_back((unsigned char)(acc & 0xff)); + acc >>= 8; + acc_bits -= 8; + } +} + +void encodeMeshletsDXR(const Mesh& mesh, size_t max_triangles, int min_exp, int level = 3) +{ + // split mesh into clusters, optimized for RT + size_t min_triangles = max_triangles / 4; + size_t max_vertices = 256; + float fill_weight = 0.25f; + size_t max_meshlets = meshopt_buildMeshletsBound(mesh.indices.size(), max_vertices, min_triangles); + + std::vector shadow_indices(mesh.indices.size()); + meshopt_generateShadowIndexBuffer(&shadow_indices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(float) * 3, sizeof(Vertex)); + + std::vector meshlets(max_meshlets); + std::vector meshlet_vertices(mesh.indices.size()); + std::vector meshlet_triangles(mesh.indices.size()); + meshlets.resize(meshopt_buildMeshletsSpatial(&meshlets[0], &meshlet_vertices[0], &meshlet_triangles[0], &shadow_indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), max_vertices, min_triangles, max_triangles, fill_weight)); + + if (meshlets.size()) + { + const meshopt_Meshlet& last = meshlets.back(); + + // this is an example of how to trim the vertex/triangle arrays when copying data out to GPU storage + meshlet_vertices.resize(last.vertex_offset + last.vertex_count); + meshlet_triangles.resize(last.triangle_offset + last.triangle_count * 3); + } + + // optimize each meshlet for locality; this is critical for good compression + for (size_t i = 0; i < meshlets.size(); ++i) + meshopt_optimizeMeshletLevel(&meshlet_vertices[meshlets[i].vertex_offset], meshlets[i].vertex_count, &meshlet_triangles[meshlets[i].triangle_offset], meshlets[i].triangle_count, level); + + // compute shared exponent per mesh: + // - must be at most min_exp which should be set to maximum required precision (e.g. -10 for 1mm accuracy given metric units) + // - must accommodate each cluster to fit into Compressed1 DXR encoding (16-bit offsets from 24-bit anchors) + // note, for DGF parity min_exp should be set based on mesh AABB; we do not do this as we do not consider this rigorous. + int exponent = min_exp; + + for (const meshopt_Meshlet& meshlet : meshlets) + { + float minv[3] = {FLT_MAX, FLT_MAX, FLT_MAX}, maxv[3] = {-FLT_MAX, -FLT_MAX, -FLT_MAX}; + + for (size_t j = 0; j < meshlet.vertex_count; ++j) + { + unsigned int v = meshlet_vertices[meshlet.vertex_offset + j]; + const float* p = &mesh.vertices[v].px; + + for (int k = 0; k < 3; ++k) + { + minv[k] = fminf(minv[k], p[k]); + maxv[k] = fmaxf(maxv[k], p[k]); + } + } + + int cexp = meshopt_computePositionExponent(minv, maxv, min_exp, /* max_bits= */ 16); + exponent = (exponent > cexp) ? exponent : cexp; + } + + std::vector packed; + size_t stats_headers = 0, stats_positions = 0, stats_topology = 0; + + for (size_t i = 0; i < meshlets.size(); ++i) + { + const meshopt_Meshlet& meshlet = meshlets[i]; + + // for each cluster, use meshopt codec for meshlet topology; will need to decode using meshopt_decodeMeshlet at runtime + unsigned char topology[1024]; + assert(meshopt_encodeMeshletBound(0, max_triangles) <= sizeof(topology)); + + size_t tops = meshopt_encodeMeshlet(topology, sizeof(topology), NULL, 0, &meshlet_triangles[meshlet.triangle_offset], meshlet.triangle_count); + assert(tops > 0); + + // for each cluster, positions are encoded using 24-bit anchor and up-to-16-bit offset per axis + float scale = ldexpf(1.f, exponent); + + int positions[256][3]; + int anchor[3] = {INT_MAX, INT_MAX, INT_MAX}; + + for (size_t j = 0; j < meshlet.vertex_count; ++j) + { + unsigned int v = meshlet_vertices[meshlet.vertex_offset + j]; + const float* p = &mesh.vertices[v].px; + + for (int k = 0; k < 3; ++k) + { + positions[j][k] = int(roundf(p[k] / scale)); + anchor[k] = anchor[k] < positions[j][k] ? anchor[k] : positions[j][k]; + } + } + + // compute optimal per-cluster per-axis bit count + int bits[3] = {1, 1, 1}; + + for (size_t j = 0; j < meshlet.vertex_count; ++j) + for (int k = 0; k < 3; ++k) + { + int v = positions[j][k] - anchor[k]; + assert(v >= 0 && v < (1 << 16)); + + while (v >= (1 << bits[k])) + bits[k]++; + } + + // prepare cluster header: 1 word with cluster sizes, and 3 words for DXR-compatible cluster position header + // see https://microsoft.github.io/DirectX-Specs/d3d/Raytracing2.html#compressed1-position-encoding + unsigned int header[4] = { + (meshlet.vertex_count - 1) | ((meshlet.triangle_count - 1) << 8) | (unsigned(tops) << 16), + (unsigned(anchor[0]) << 8) | (exponent + 127), + (unsigned(anchor[1]) << 8) | (unsigned(bits[1] - 1) << 4) | unsigned(bits[0] - 1), + (unsigned(anchor[2]) << 8) | unsigned(bits[2] - 1), + }; + + // write cluster header, cluster positions, and cluster topology + // note: we write the data in a way that is easiest to consume for the decoding; however, for best post-deflate compression consider reordering it + // (by grouping headers, positions, and topology into three separate streams) + // maximum post-deflate compression can be achieved by interleaving the cluster position bits per axis (all X => all Y => all Z), but that requires extra decoding work + // this layout can be decoded using `meshopt_decodeMeshlet` + `memcpy` when the consumer is DXR2-compatible. + for (unsigned int word : header) + for (int k = 0; k < 4; ++k) + packed.push_back((unsigned char)((word >> (k * 8)) & 0xff)); + + unsigned int acc = 0; + int acc_bits = 0; + + for (size_t j = 0; j < meshlet.vertex_count; ++j) + for (int k = 0; k < 3; ++k) + writeBits(packed, acc, acc_bits, positions[j][k] - anchor[k], bits[k]); + + if (acc_bits) + packed.push_back((unsigned char)(acc & 0xff)); + + packed.insert(packed.end(), topology, topology + tops); + + stats_headers += 4 * 4; + stats_positions += (meshlet.vertex_count * (bits[0] + bits[1] + bits[2]) + 7) / 8; + stats_topology += tops; + } + + size_t mbc = compress(packed); + size_t tris = mesh.indices.size() / 3; + + printf("MeshletCodecDXR (%d@%d): %d meshlets, %d bytes; %.1f bytes/triangle (%.1f headers, %.1f positions, %.1f topology); post-deflate: %.1f bytes/triangle\n", + int(max_triangles), exponent, + int(meshlets.size()), int(packed.size()), + double(packed.size()) / double(tris), double(stats_headers) / double(tris), double(stats_positions) / double(tris), double(stats_topology) / double(tris), + double(mbc) / double(tris)); + +#if !TRACE + double mbtime = 0; + size_t mbsize = 0; + + for (int i = 0; i < 10; ++i) + { + unsigned char clp[12 + 256 * 6]; // up to 48 bits per vertex + unsigned char clt[256 * 3 + 4]; // +4 bytes for padding + + double t0 = timestamp(); + unsigned char* p = &packed[0]; + for (size_t j = 0; j < meshlets.size(); ++j) + { + // decode cluster header + size_t vertex_count = p[0] + 1; + size_t triangle_count = p[1] + 1; + size_t topology_size = p[2] | (p[3] << 8); + + // decode DXR header to establish position size + size_t position_bitsx = (p[8] & 15) + 1; + size_t position_bitsy = ((p[8] >> 4) & 15) + 1; + size_t position_bitsz = (p[12] & 15) + 1; + size_t position_size = (vertex_count * (position_bitsx + position_bitsy + position_bitsz) + 7) / 8; + + // memcpy DXR Compressed1 portion as is + void* clpopt = topology_size ? clp : clt; // note: hack to ensure memcpy is not elided for benchmarking purposes; topology_size is always >0 so this always copies into clp + memcpy(clpopt, p + 4, 12 + position_size); + + // decode topology as 3 bytes per triangle + int rc = meshopt_decodeMeshlet(static_cast(NULL), 0, clt, triangle_count, p + 16 + position_size, topology_size); + assert(rc == 0); + (void)rc; + + p += 16 + position_size + topology_size; + if (i == 0) // stats for decode throughput + mbsize += position_size + triangle_count * 3; + } + double t1 = timestamp(); + + mbtime = (mbtime == 0 || t1 - t0 < mbtime) ? (t1 - t0) : mbtime; + } + + printf("MeshletCodecDXR (%d): decode time %.3f msec, %.3fB tri/sec, %.3f GB/sec\n", + int(max_triangles), + mbtime * 1000, double(tris) / 1e9 / mbtime, double(mbsize) / mbtime * 1e-9); +#endif +} + +template +void packVertex(const Mesh& mesh, const char* pvn) +{ + std::vector pv(mesh.vertices.size()); + packMesh(pv, mesh.vertices); + + size_t csize = compress(pv); + + printf("VtxPack%s : %.1f bits/vertex (post-deflate %.1f bits/vertex)\n", pvn, + double(pv.size() * sizeof(PV) * 8) / double(mesh.vertices.size()), + double(csize * 8) / double(mesh.vertices.size())); +} + +template +void encodeVertex(const Mesh& mesh, const char* pvn, int level = 2) +{ + std::vector pv(mesh.vertices.size()); + packMesh(pv, mesh.vertices); + + // allocate result outside of the timing loop to exclude memset() from decode timing + std::vector result(mesh.vertices.size()); + + double start = timestamp(); + + std::vector vbuf(meshopt_encodeVertexBufferBound(mesh.vertices.size(), sizeof(PV))); + vbuf.resize(meshopt_encodeVertexBufferLevel(&vbuf[0], vbuf.size(), &pv[0], mesh.vertices.size(), sizeof(PV), level, -1)); + + double middle = timestamp(); + + int res = meshopt_decodeVertexBuffer(&result[0], mesh.vertices.size(), sizeof(PV), &vbuf[0], vbuf.size()); + assert(res == 0); + (void)res; + + double end = timestamp(); + + assert(memcmp(&pv[0], &result[0], pv.size() * sizeof(PV)) == 0); + + size_t csize = compress(vbuf); + + printf("VtxCodec%1s: %.1f bits/vertex (post-deflate %.1f bits/vertex); encode %.2f msec (%.3f GB/s), decode %.2f msec (%.2f GB/s)\n", pvn, + double(vbuf.size() * 8) / double(mesh.vertices.size()), + double(csize * 8) / double(mesh.vertices.size()), + (middle - start) * 1000, + (double(result.size() * sizeof(PV)) / 1e9) / (middle - start), + (end - middle) * 1000, + (double(result.size() * sizeof(PV)) / 1e9) / (end - middle)); +} + +void stripify(const Mesh& mesh, bool use_restart, char desc) +{ + unsigned int restart_index = use_restart ? ~0u : 0; + + // note: input mesh is assumed to be optimized for vertex cache and vertex fetch + double start = timestamp(); + std::vector strip(meshopt_stripifyBound(mesh.indices.size())); + strip.resize(meshopt_stripify(&strip[0], &mesh.indices[0], mesh.indices.size(), mesh.vertices.size(), restart_index)); + double end = timestamp(); + + size_t restarts = 0; + for (size_t i = 0; i < strip.size(); ++i) + restarts += use_restart && strip[i] == restart_index; + + Mesh copy = mesh; + copy.indices.resize(meshopt_unstripify(©.indices[0], &strip[0], strip.size(), restart_index)); + assert(copy.indices.size() <= meshopt_unstripifyBound(strip.size())); + + meshopt_VertexCacheStatistics vcs = meshopt_analyzeVertexCache(©.indices[0], mesh.indices.size(), mesh.vertices.size(), 16, 0, 0); + meshopt_VertexCacheStatistics vcs_nv = meshopt_analyzeVertexCache(©.indices[0], mesh.indices.size(), mesh.vertices.size(), 32, 32, 32); + meshopt_VertexCacheStatistics vcs_amd = meshopt_analyzeVertexCache(©.indices[0], mesh.indices.size(), mesh.vertices.size(), 14, 64, 128); + meshopt_VertexCacheStatistics vcs_intel = meshopt_analyzeVertexCache(©.indices[0], mesh.indices.size(), mesh.vertices.size(), 128, 0, 0); + + printf("Stripify%c: ACMR %f ATVR %f (NV %f AMD %f Intel %f); %.1f run avg, %d strip indices (%.1f%%) in %.2f msec\n", + desc, + vcs.acmr, vcs.atvr, vcs_nv.atvr, vcs_amd.atvr, vcs_intel.atvr, + use_restart ? double(strip.size() - restarts) / double(restarts + 1) : 0, + int(strip.size()), double(strip.size()) / double(mesh.indices.size()) * 100, + (end - start) * 1000); +} + +void shadow(const Mesh& mesh) +{ + // note: input mesh is assumed to be optimized for vertex cache and vertex fetch + + double start = timestamp(); + // this index buffer can be used for position-only rendering using the same vertex data that the original index buffer uses + std::vector shadow_indices(mesh.indices.size()); + meshopt_generateShadowIndexBuffer(&shadow_indices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0], mesh.vertices.size(), sizeof(float) * 3, sizeof(Vertex)); + double end = timestamp(); + + // while you can't optimize the vertex data after shadow IB was constructed, you can and should optimize the shadow IB for vertex cache + // this is valuable even if the original indices array was optimized for vertex cache! + meshopt_optimizeVertexCache(&shadow_indices[0], &shadow_indices[0], shadow_indices.size(), mesh.vertices.size()); + + meshopt_VertexCacheStatistics vcs = meshopt_analyzeVertexCache(&mesh.indices[0], mesh.indices.size(), mesh.vertices.size(), 16, 0, 0); + meshopt_VertexCacheStatistics vcss = meshopt_analyzeVertexCache(&shadow_indices[0], shadow_indices.size(), mesh.vertices.size(), 16, 0, 0); + + std::vector shadow_flags(mesh.vertices.size()); + size_t shadow_vertices = 0; + + for (size_t i = 0; i < shadow_indices.size(); ++i) + { + unsigned int index = shadow_indices[i]; + shadow_vertices += 1 - shadow_flags[index]; + shadow_flags[index] = 1; + } + + printf("ShadowIB : ACMR %f (%.2fx improvement); %d shadow vertices (%.2fx improvement) in %.2f msec\n", + vcss.acmr, double(vcs.vertices_transformed) / double(vcss.vertices_transformed), + int(shadow_vertices), double(mesh.vertices.size()) / double(shadow_vertices), + (end - start) * 1000); +} + +static int follow(int* parents, int index) +{ + while (index != parents[index]) + { + int parent = parents[index]; + parents[index] = parents[parent]; + index = parent; + } + + return index; +} + +void meshlets(const Mesh& mesh, bool scan = false, bool uniform = false, bool flex = false, bool spatial = false, bool dump = false) +{ + // NVidia recommends 64/126; we also test uniform configuration with 64/64 which is better for earlier AMD GPUs + const size_t max_vertices = 64; + const size_t max_triangles = uniform ? 64 : 126; + const size_t min_triangles = spatial ? 16 : (uniform ? 24 : 32); // only used in flex/spatial modes + + // note: should be set to 0 unless cone culling is used at runtime! + const float cone_weight = 0.25f; + const float split_factor = flex ? 2.0f : 0.0f; + + // note: input mesh is assumed to be optimized for vertex cache and vertex fetch + double start = timestamp(); + size_t max_meshlets = meshopt_buildMeshletsBound(mesh.indices.size(), max_vertices, min_triangles); + std::vector meshlets(max_meshlets); + std::vector meshlet_vertices(mesh.indices.size()); + std::vector meshlet_triangles(mesh.indices.size()); + + if (scan) + meshlets.resize(meshopt_buildMeshletsScan(&meshlets[0], &meshlet_vertices[0], &meshlet_triangles[0], &mesh.indices[0], mesh.indices.size(), mesh.vertices.size(), max_vertices, max_triangles)); + else if (flex) + meshlets.resize(meshopt_buildMeshletsFlex(&meshlets[0], &meshlet_vertices[0], &meshlet_triangles[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), max_vertices, min_triangles, max_triangles, cone_weight, split_factor)); + else if (spatial) + meshlets.resize(meshopt_buildMeshletsSpatial(&meshlets[0], &meshlet_vertices[0], &meshlet_triangles[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), max_vertices, min_triangles, max_triangles, 0.f)); + else // note: equivalent to the call of buildMeshletsFlex() with split_factor = 0 and min_triangles = max_triangles + meshlets.resize(meshopt_buildMeshlets(&meshlets[0], &meshlet_vertices[0], &meshlet_triangles[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), max_vertices, max_triangles, cone_weight)); + + if (!dump) + for (size_t i = 0; i < meshlets.size(); ++i) + meshopt_optimizeMeshlet(&meshlet_vertices[meshlets[i].vertex_offset], &meshlet_triangles[meshlets[i].triangle_offset], meshlets[i].triangle_count, meshlets[i].vertex_count); + + if (meshlets.size()) + { + const meshopt_Meshlet& last = meshlets.back(); + + // this is an example of how to trim the vertex/triangle arrays when copying data out to GPU storage + meshlet_vertices.resize(last.vertex_offset + last.vertex_count); + meshlet_triangles.resize(last.triangle_offset + last.triangle_count * 3); + } + + double end = timestamp(); + + if (dump) + dumpObj(mesh.vertices, std::vector()); + + double avg_vertices = 0; + double avg_triangles = 0; + double avg_boundary = 0; + double avg_connected = 0; + size_t not_full = 0; + + std::vector boundary(mesh.vertices.size()); + + for (size_t i = 0; i < meshlets.size(); ++i) + { + const meshopt_Meshlet& m = meshlets[i]; + + for (unsigned int j = 0; j < m.vertex_count; ++j) + boundary[meshlet_vertices[m.vertex_offset + j]]++; + } + + std::vector cluster; + + for (size_t i = 0; i < meshlets.size(); ++i) + { + const meshopt_Meshlet& m = meshlets[i]; + + if (dump) + { + cluster.clear(); + for (unsigned int j = 0; j < m.triangle_count * 3; ++j) + cluster.push_back(meshlet_vertices[m.vertex_offset + meshlet_triangles[m.triangle_offset + j]]); + + char cname[32]; + snprintf(cname, sizeof(cname), "ml_%d", int(i)); + dumpObj(cname, cluster); + } + + avg_vertices += m.vertex_count; + avg_triangles += m.triangle_count; + not_full += uniform ? m.triangle_count < max_triangles : m.vertex_count < max_vertices; + + for (unsigned int j = 0; j < m.vertex_count; ++j) + if (boundary[meshlet_vertices[m.vertex_offset + j]] > 1) + avg_boundary += 1; + + // union-find vertices to check if the meshlet is connected + int parents[256]; + for (unsigned int j = 0; j < m.vertex_count; ++j) + parents[j] = int(j); + + for (unsigned int j = 0; j < m.triangle_count * 3; ++j) + { + int v0 = meshlet_triangles[m.triangle_offset + j]; + int v1 = meshlet_triangles[m.triangle_offset + j + (j % 3 == 2 ? -2 : 1)]; + + v0 = follow(parents, v0); + v1 = follow(parents, v1); + + parents[v0] = v1; + } + + int roots = 0; + for (unsigned int j = 0; j < m.vertex_count; ++j) + roots += follow(parents, j) == int(j); + + assert(roots != 0); + avg_connected += roots; + } + + avg_vertices /= double(meshlets.size()); + avg_triangles /= double(meshlets.size()); + avg_boundary /= double(meshlets.size()); + avg_connected /= double(meshlets.size()); + + printf("Meshlets%c: %d meshlets (avg vertices %.1f, avg triangles %.1f, avg boundary %.1f, avg connected %.2f, not full %d) in %.2f msec\n", + scan ? 'S' : (flex ? 'F' : (spatial ? 'X' : (uniform ? 'U' : ' '))), + int(meshlets.size()), avg_vertices, avg_triangles, avg_boundary, avg_connected, int(not_full), (end - start) * 1000); + + float camera[3] = {100, 100, 100}; + + size_t rejected = 0; + size_t accepted = 0; + double radius_mean = 0; + double cone_mean = 0; + + std::vector radii(meshlets.size()); + std::vector cones(meshlets.size()); + + double startc = timestamp(); + for (size_t i = 0; i < meshlets.size(); ++i) + { + const meshopt_Meshlet& m = meshlets[i]; + + meshopt_Bounds bounds = meshopt_computeMeshletBounds(&meshlet_vertices[m.vertex_offset], &meshlet_triangles[m.triangle_offset], m.triangle_count, &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex)); + + radii[i] = bounds.radius; + cones[i] = 90.f - acosf(bounds.cone_cutoff) * (180.f / 3.1415926f); + + radius_mean += radii[i]; + cone_mean += cones[i]; + + // trivial accept: we can't ever backface cull this meshlet + accepted += (bounds.cone_cutoff >= 1); + + // perspective projection: dot(normalize(cone_apex - camera_position), cone_axis) > cone_cutoff + // alternative formulation for perspective projection that doesn't use apex (and uses cluster bounding sphere instead): + // dot(normalize(center - camera_position), cone_axis) > cone_cutoff + radius / length(center - camera_position) + float cview[3] = {bounds.center[0] - camera[0], bounds.center[1] - camera[1], bounds.center[2] - camera[2]}; + float cviewlength = sqrtf(cview[0] * cview[0] + cview[1] * cview[1] + cview[2] * cview[2]); + + rejected += cview[0] * bounds.cone_axis[0] + cview[1] * bounds.cone_axis[1] + cview[2] * bounds.cone_axis[2] >= bounds.cone_cutoff * cviewlength + bounds.radius; + } + double endc = timestamp(); + + radius_mean /= double(meshlets.size()); + cone_mean /= double(meshlets.size()); + + double radius_variance = 0; + + for (size_t i = 0; i < meshlets.size(); ++i) + radius_variance += (radii[i] - radius_mean) * (radii[i] - radius_mean); + + radius_variance /= double(meshlets.size() - 1); + + double radius_stddev = sqrt(radius_variance); + + size_t meshlets_std = 0; + + for (size_t i = 0; i < meshlets.size(); ++i) + meshlets_std += radii[i] < radius_mean + radius_stddev; + + printf("Bounds : radius mean %f stddev %f; %.1f%% meshlets under 1σ; cone angle %.1f°; cone reject %.1f%% trivial accept %.1f%% in %.2f msec\n", + radius_mean, radius_stddev, + double(meshlets_std) / double(meshlets.size()) * 100, + cone_mean, double(rejected) / double(meshlets.size()) * 100, double(accepted) / double(meshlets.size()) * 100, + (endc - startc) * 1000); +} + +void spatialSort(const Mesh& mesh) +{ + typedef PackedVertexOct PV; + + std::vector pv(mesh.vertices.size()); + packMesh(pv, mesh.vertices); + + double start = timestamp(); + + std::vector remap(mesh.vertices.size()); + meshopt_spatialSortRemap(&remap[0], &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex)); + + double end = timestamp(); + + meshopt_remapVertexBuffer(&pv[0], &pv[0], mesh.vertices.size(), sizeof(PV), &remap[0]); + + std::vector vbuf(meshopt_encodeVertexBufferBound(mesh.vertices.size(), sizeof(PV))); + vbuf.resize(meshopt_encodeVertexBuffer(&vbuf[0], vbuf.size(), &pv[0], mesh.vertices.size(), sizeof(PV))); + + size_t csize = compress(vbuf); + + printf("Spatial : %.1f bits/vertex (post-deflate %.1f bits/vertex); sort %.2f msec\n", + double(vbuf.size() * 8) / double(mesh.vertices.size()), + double(csize * 8) / double(mesh.vertices.size()), + (end - start) * 1000); +} + +void spatialSortTriangles(const Mesh& mesh) +{ + typedef PackedVertexOct PV; + + Mesh copy = mesh; + + double start = timestamp(); + + meshopt_spatialSortTriangles(©.indices[0], ©.indices[0], mesh.indices.size(), ©.vertices[0].px, copy.vertices.size(), sizeof(Vertex)); + + double end = timestamp(); + + meshopt_optimizeVertexCache(©.indices[0], ©.indices[0], copy.indices.size(), copy.vertices.size()); + meshopt_optimizeVertexFetch(©.vertices[0], ©.indices[0], copy.indices.size(), ©.vertices[0], copy.vertices.size(), sizeof(Vertex)); + + std::vector pv(mesh.vertices.size()); + packMesh(pv, copy.vertices); + + std::vector vbuf(meshopt_encodeVertexBufferBound(mesh.vertices.size(), sizeof(PV))); + vbuf.resize(meshopt_encodeVertexBuffer(&vbuf[0], vbuf.size(), &pv[0], mesh.vertices.size(), sizeof(PV))); + + std::vector ibuf(meshopt_encodeIndexBufferBound(mesh.indices.size(), mesh.vertices.size())); + ibuf.resize(meshopt_encodeIndexBuffer(&ibuf[0], ibuf.size(), ©.indices[0], mesh.indices.size())); + + size_t csizev = compress(vbuf); + size_t csizei = compress(ibuf); + + printf("SpatialT : %.1f bits/vertex (post-deflate %.1f bits/vertex); %.1f bits/triangle (post-deflate %.1f bits/triangle); sort %.2f msec\n", + double(vbuf.size() * 8) / double(mesh.vertices.size()), + double(csizev * 8) / double(mesh.vertices.size()), + double(ibuf.size() * 8) / double(mesh.indices.size() / 3), + double(csizei * 8) / double(mesh.indices.size() / 3), + (end - start) * 1000); +} + +void spatialClusterPoints(const Mesh& mesh, size_t cluster_size) +{ + typedef PackedVertexOct PV; + + double start = timestamp(); + + std::vector index(mesh.vertices.size()); + meshopt_spatialClusterPoints(&index[0], &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), cluster_size); + + double end = timestamp(); + + std::vector pv(mesh.vertices.size()); + packMesh(pv, mesh.vertices); + + std::vector pvo(mesh.vertices.size()); + for (size_t i = 0; i < index.size(); ++i) + pvo[i] = pv[index[i]]; + + std::vector vbuf(meshopt_encodeVertexBufferBound(mesh.vertices.size(), sizeof(PV))); + vbuf.resize(meshopt_encodeVertexBuffer(&vbuf[0], vbuf.size(), &pvo[0], mesh.vertices.size(), sizeof(PV))); + + size_t csize = compress(vbuf); + + printf("SpatialCP: %.1f bits/vertex (post-deflate %.1f bits/vertex); sort %.2f msec\n", + double(vbuf.size() * 8) / double(mesh.vertices.size()), + double(csize * 8) / double(mesh.vertices.size()), + (end - start) * 1000); +} + +void filterTriangles(const Mesh& mesh) +{ + double start = timestamp(); + + std::vector newindices(mesh.indices.size()); + newindices.resize(meshopt_filterIndexBuffer(&newindices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0], mesh.vertices.size(), sizeof(float) * 3, sizeof(Vertex))); + + double end = timestamp(); + + printf("FilterIB : %d triangles -> %d (%.2f%% redundant) in %.2f msec\n", + int(mesh.indices.size() / 3), int(newindices.size() / 3), + double((mesh.indices.size() - newindices.size()) / 3) / double(mesh.indices.size() / 3) * 100.0, + (end - start) * 1000); +} + +void tessellationAdjacency(const Mesh& mesh) +{ + double start = timestamp(); + + // 12 indices per input triangle + std::vector tessib(mesh.indices.size() * 4); + meshopt_generateTessellationIndexBuffer(&tessib[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex)); + + double middle = timestamp(); + + // 6 indices per input triangle + std::vector adjib(mesh.indices.size() * 2); + meshopt_generateAdjacencyIndexBuffer(&adjib[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex)); + + double end = timestamp(); + + printf("Tesselltn: %d patches in %.2f msec\n", int(mesh.indices.size() / 3), (middle - start) * 1000); + printf("Adjacency: %d patches in %.2f msec\n", int(mesh.indices.size() / 3), (end - middle) * 1000); +} + +void provoking(const Mesh& mesh) +{ + double start = timestamp(); + + // worst case number of vertices: vertex count + triangle count + std::vector pib(mesh.indices.size()); + std::vector reorder(mesh.vertices.size() + mesh.indices.size() / 3); + + size_t pcount = meshopt_generateProvokingIndexBuffer(&pib[0], &reorder[0], &mesh.indices[0], mesh.indices.size(), mesh.vertices.size()); + reorder.resize(pcount); + + double end = timestamp(); + + // validate invariant: pib[i] == i/3 for provoking vertices + for (size_t i = 0; i < mesh.indices.size(); i += 3) + assert(pib[i] == i / 3); + + // validate invariant: reorder[pib[x]] == ib[x] modulo triangle rotation + // note: this is technically not promised by the interface (it may reorder triangles!), it just happens to hold right now + for (size_t i = 0; i < mesh.indices.size(); i += 3) + { + unsigned int a = mesh.indices[i + 0], b = mesh.indices[i + 1], c = mesh.indices[i + 2]; + unsigned int ra = reorder[pib[i + 0]], rb = reorder[pib[i + 1]], rc = reorder[pib[i + 2]]; + + assert((a == ra && b == rb && c == rc) || (a == rb && b == rc && c == ra) || (a == rc && b == ra && c == rb)); + } + + // best case number of vertices: max(vertex count, triangle count), assuming non-redundant indexing (all vertices are used) + // note: this is a lower bound, and it's not theoretically possible on some meshes; + // for example, a union of a flat shaded cube (12t 24v) and a smooth shaded icosahedron (20t 12v) will have 36 vertices and 32 triangles + // however, the best case for that union is 44 vertices (24 cube vertices + 20 icosahedron vertices due to provoking invariant) + size_t bestv = mesh.vertices.size() > mesh.indices.size() / 3 ? mesh.vertices.size() : mesh.indices.size() / 3; + + printf("Provoking: %d triangles / %d vertices (+%.1f%% extra) in %.2f msec\n", + int(mesh.indices.size() / 3), int(pcount), double(pcount) / double(bestv) * 100.0 - 100.0, (end - start) * 1000); +} + +static int reindexCompare(void* context, unsigned int lhs, unsigned int rhs) +{ + const Vertex* vertices = static_cast(context); + const Vertex& lv = vertices[lhs]; + const Vertex& rv = vertices[rhs]; + + float ln = lv.nx * lv.nx + lv.ny * lv.ny + lv.nz * lv.nz; + float rn = rv.nx * rv.nx + rv.ny * rv.ny + rv.nz * rv.nz; + + // 1/1024px UV tolerance, 3 degree normal tolerance + return fabsf(lv.tx - rv.tx) < 1e-3f && + fabsf(lv.ty - rv.ty) < 1e-3f && + (lv.nx * rv.nx + lv.ny * rv.ny + lv.nz * rv.nz >= 0.9986f * sqrtf(ln * rn)); +} + +void reindexFuzzy(const Mesh& mesh) +{ + std::vector pv(mesh.vertices.size()); + packMesh(pv, mesh.vertices); + + std::vector remap(mesh.vertices.size()); + + double start = timestamp(); + + size_t up = meshopt_generateVertexRemap(&remap[0], &mesh.indices[0], mesh.indices.size(), &pv[0], mesh.vertices.size(), sizeof(PackedVertex)); + + double middle = timestamp(); + + size_t uf = meshopt_generateVertexRemapCustom(&remap[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), reindexCompare, const_cast(&mesh.vertices[0])); + + double end = timestamp(); + + printf("ReindexQ : %d vertices => %d unique vertices in %.2f msec\n", + int(mesh.vertices.size()), int(up), (middle - start) * 1000); + printf("ReindexF : %d vertices => %d unique vertices in %.2f msec\n", + int(mesh.vertices.size()), int(uf), (end - middle) * 1000); +} + +void coverage(const Mesh& mesh) +{ + double start = timestamp(); + + meshopt_CoverageStatistics cs = meshopt_analyzeCoverage(&mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex)); + + double end = timestamp(); + + printf("Coverage : X %.1f%% Y %.1f%% Z %.1f%% in %.2f msec\n", + cs.coverage[0] * 100, cs.coverage[1] * 100, cs.coverage[2] * 100, (end - start) * 1000); +} + +struct VertexTangent +{ + float px, py, pz; + float nx, ny, nz; + float tu, tv; + float tx, ty, tz, tw; +}; + +void tangents(const Mesh& mesh) +{ + double start = timestamp(); + + // note: tangent generation produces a tangent vector and orientation (+-1) per *corner* (3 per triangle), even for indexed inputs + // you *could* then copy the tangents to the existing vertices, but that will not work correctly in case the input has UV mirroring + // a simple way to handle this is to work on unindexed data (indices==NULL), generate tangents, and re-index + // we show a more involved way that splits vertices with divergent tangents on the fly. + std::vector tangents(mesh.indices.size() * 4); + + meshopt_generateTangents(&tangents[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), &mesh.vertices[0].nx, sizeof(Vertex), &mesh.vertices[0].tx, sizeof(Vertex)); + + std::vector newvertices(mesh.vertices.size()); + std::vector newindices = mesh.indices; + + // copy positions/normals/UVs (Vertex has no tangent field, so we use VertexTangent) + for (size_t i = 0; i < mesh.vertices.size(); ++i) + { + VertexTangent& vt = newvertices[i]; + const Vertex& v = mesh.vertices[i]; + + vt.px = v.px, vt.py = v.py, vt.pz = v.pz; + vt.nx = v.nx, vt.ny = v.ny, vt.nz = v.nz; + vt.tu = v.tx, vt.tv = v.ty; + } + + // seed each vertex with one of its corner tangents; the loop below fixes any mismatches + for (size_t i = 0; i < mesh.indices.size(); ++i) + { + VertexTangent& vt = newvertices[mesh.indices[i]]; + const float* t = &tangents[i * 4]; + vt.tx = t[0], vt.ty = t[1], vt.tz = t[2], vt.tw = t[3]; + } + + std::vector splits(mesh.vertices.size(), ~0u); + + for (size_t i = 0; i < mesh.indices.size(); ++i) + { + const float* t = &tangents[i * 4]; + unsigned int v = mesh.indices[i]; + + // walk the chain of split copies looking for a vertex whose tangent matches + while (v != ~0u && (newvertices[v].tx != t[0] || newvertices[v].ty != t[1] || newvertices[v].tz != t[2] || newvertices[v].tw != t[3])) + v = splits[v]; + + // no match in chain: append a new split copy with the target tangent and chain it + if (v == ~0u) + { + v = unsigned(newvertices.size()); + newvertices.push_back(newvertices[mesh.indices[i]]); + newvertices[v].tx = t[0], newvertices[v].ty = t[1], newvertices[v].tz = t[2], newvertices[v].tw = t[3]; + splits.push_back(splits[mesh.indices[i]]); + splits[mesh.indices[i]] = v; + } + + newindices[i] = v; + } + + double end = timestamp(); + + printf("Tangents : %d corners, %d splits in %.2f msec\n", int(mesh.indices.size()), int(newvertices.size() - mesh.vertices.size()), (end - start) * 1000); +} + +void normals(const Mesh& mesh, float smoothing) +{ + // note: normal generation produces a normal vector per *corner* (3 per triangle), even for indexed inputs + // you could split the existing vertices on the fly (see tangents()), here we demonstrate an alternative approach with full reindexing + std::vector normals(mesh.indices.size() * 3); + + double start = timestamp(); + + const float crease = 1.f; // 1 rad ~= 60 deg + meshopt_generateNormals(&normals[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), crease, smoothing); + + double middle = timestamp(); + + std::vector unindexed(mesh.indices.size()); + + for (size_t i = 0; i < mesh.indices.size(); ++i) + { + unindexed[i] = mesh.vertices[mesh.indices[i]]; + unindexed[i].nx = normals[i * 3 + 0]; + unindexed[i].ny = normals[i * 3 + 1]; + unindexed[i].nz = normals[i * 3 + 2]; + } + + std::vector remap(mesh.indices.size()); + size_t vertex_count = meshopt_generateVertexRemap(&remap[0], NULL, mesh.indices.size(), &unindexed[0], mesh.indices.size(), sizeof(Vertex)); + + std::vector newindices(mesh.indices.size()); + meshopt_remapIndexBuffer(&newindices[0], NULL, mesh.indices.size(), &remap[0]); + + std::vector newvertices(vertex_count); + meshopt_remapVertexBuffer(&newvertices[0], &unindexed[0], mesh.indices.size(), sizeof(Vertex), &remap[0]); + + double end = timestamp(); + + printf("Normals %d: %d corners, %d vertices in %.2f msec; reindexed in %.2f msec\n", + int(ceilf(smoothing)), int(mesh.indices.size()), int(vertex_count), (middle - start) * 1000, (end - middle) * 1000); +} + +void remesh(const Mesh& mesh, int resolution, unsigned int options = 0) +{ + double start = timestamp(); + size_t capacity = meshopt_remesh(NULL, 0, &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), resolution, options); + double middle = timestamp(); + std::vector triangles(capacity * 3 * 3); + size_t count = meshopt_remesh(&triangles[0], capacity, &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), resolution, options); + double end = timestamp(); + + assert(count <= capacity); + + printf("Remesh%3d: %d triangles => %d triangles in %.2f msec (count %.2f msec, compute %.2f msec)\n", resolution, + int(mesh.indices.size() / 3), int(count), (end - start) * 1000, (middle - start) * 1000, (end - middle) * 1000); +} + +void nanite(const std::vector& vertices, const std::vector& indices); // nanite.cpp + +bool loadMesh(Mesh& mesh, const char* path) +{ + double start = timestamp(); + double middle; + mesh = parseObj(path, middle); + double end = timestamp(); + + if (mesh.vertices.empty()) + { + printf("Mesh %s is empty, skipping\n", path); + return false; + } + + printf("# %s: %d vertices, %d triangles; read in %.2f msec; indexed in %.2f msec\n", path, int(mesh.vertices.size()), int(mesh.indices.size() / 3), (middle - start) * 1000, (end - middle) * 1000); + return true; +} + +void processDeinterleaved(const char* path) +{ + // Most algorithms in the library work out of the box with deinterleaved geometry, but some require slightly special treatment; + // this code runs a simplified version of complete opt. pipeline using deinterleaved geo. There's no compression performed but you + // can trivially run it by quantizing all elements and running meshopt_encodeVertexBuffer once for each vertex stream. + fastObjMesh* obj = fast_obj_read(path); + if (!obj) + { + printf("Error loading %s: file not found\n", path); + return; + } + + size_t total_indices = 0; + + for (unsigned int i = 0; i < obj->face_count; ++i) + total_indices += 3 * (obj->face_vertices[i] - 2); + + std::vector unindexed_pos(total_indices * 3); + std::vector unindexed_nrm(total_indices * 3); + std::vector unindexed_uv(total_indices * 2); + + size_t vertex_offset = 0; + size_t index_offset = 0; + + for (unsigned int i = 0; i < obj->face_count; ++i) + { + if (obj->face_vertices[i] <= 2) + continue; + + for (unsigned int j = 0; j < obj->face_vertices[i]; ++j) + { + fastObjIndex gi = obj->indices[index_offset + j]; + + // triangulate polygon on the fly; offset-3 is always the first polygon vertex + if (j >= 3) + { + memcpy(&unindexed_pos[(vertex_offset + 0) * 3], &unindexed_pos[(vertex_offset - 3) * 3], 3 * sizeof(float)); + memcpy(&unindexed_nrm[(vertex_offset + 0) * 3], &unindexed_nrm[(vertex_offset - 3) * 3], 3 * sizeof(float)); + memcpy(&unindexed_uv[(vertex_offset + 0) * 2], &unindexed_uv[(vertex_offset - 3) * 2], 2 * sizeof(float)); + memcpy(&unindexed_pos[(vertex_offset + 1) * 3], &unindexed_pos[(vertex_offset - 1) * 3], 3 * sizeof(float)); + memcpy(&unindexed_nrm[(vertex_offset + 1) * 3], &unindexed_nrm[(vertex_offset - 1) * 3], 3 * sizeof(float)); + memcpy(&unindexed_uv[(vertex_offset + 1) * 2], &unindexed_uv[(vertex_offset - 1) * 2], 2 * sizeof(float)); + vertex_offset += 2; + } + + memcpy(&unindexed_pos[vertex_offset * 3], &obj->positions[gi.p * 3], 3 * sizeof(float)); + memcpy(&unindexed_nrm[vertex_offset * 3], &obj->normals[gi.n * 3], 3 * sizeof(float)); + memcpy(&unindexed_uv[vertex_offset * 2], &obj->texcoords[gi.t * 2], 2 * sizeof(float)); + vertex_offset++; + } + + index_offset += obj->face_vertices[i]; + } + + fast_obj_destroy(obj); + + double start = timestamp(); + + meshopt_Stream streams[] = { + {&unindexed_pos[0], sizeof(float) * 3, sizeof(float) * 3}, + {&unindexed_nrm[0], sizeof(float) * 3, sizeof(float) * 3}, + {&unindexed_uv[0], sizeof(float) * 2, sizeof(float) * 2}, + }; + + std::vector remap(total_indices); + + size_t total_vertices = meshopt_generateVertexRemapMulti(&remap[0], NULL, total_indices, total_indices, streams, sizeof(streams) / sizeof(streams[0])); + + std::vector indices(total_indices); + meshopt_remapIndexBuffer(&indices[0], NULL, total_indices, &remap[0]); + + std::vector pos(total_vertices * 3); + meshopt_remapVertexBuffer(&pos[0], &unindexed_pos[0], total_indices, sizeof(float) * 3, &remap[0]); + + std::vector nrm(total_vertices * 3); + meshopt_remapVertexBuffer(&nrm[0], &unindexed_nrm[0], total_indices, sizeof(float) * 3, &remap[0]); + + std::vector uv(total_vertices * 2); + meshopt_remapVertexBuffer(&uv[0], &unindexed_uv[0], total_indices, sizeof(float) * 2, &remap[0]); + + double reindex = timestamp(); + + meshopt_optimizeVertexCache(&indices[0], &indices[0], total_indices, total_vertices); + + meshopt_optimizeVertexFetchRemap(&remap[0], &indices[0], total_indices, total_vertices); + meshopt_remapVertexBuffer(&pos[0], &pos[0], total_vertices, sizeof(float) * 3, &remap[0]); + meshopt_remapVertexBuffer(&nrm[0], &nrm[0], total_vertices, sizeof(float) * 3, &remap[0]); + meshopt_remapVertexBuffer(&uv[0], &uv[0], total_vertices, sizeof(float) * 2, &remap[0]); + + double optimize = timestamp(); + + // note: since shadow index buffer is computed based on regular vertex/index buffer, the stream points at the indexed data - not unindexed_pos + meshopt_Stream shadow_stream = {&pos[0], sizeof(float) * 3, sizeof(float) * 3}; + + std::vector shadow_indices(total_indices); + meshopt_generateShadowIndexBufferMulti(&shadow_indices[0], &indices[0], total_indices, total_vertices, &shadow_stream, 1); + + meshopt_optimizeVertexCache(&shadow_indices[0], &shadow_indices[0], total_indices, total_vertices); + + double shadow = timestamp(); + + printf("Deintrlvd: %d vertices, reindexed in %.2f msec, optimized in %.2f msec, generated & optimized shadow indices in %.2f msec\n", + int(total_vertices), (reindex - start) * 1000, (optimize - reindex) * 1000, (shadow - optimize) * 1000); +} + +void process(const char* path) +{ + Mesh mesh; + if (!loadMesh(mesh, path)) + return; + + optimize(mesh); + optimize(mesh, /* fifo= */ true); + + Mesh copy = mesh; + meshopt_optimizeVertexCache(©.indices[0], ©.indices[0], copy.indices.size(), copy.vertices.size()); + meshopt_optimizeVertexFetch(©.vertices[0], ©.indices[0], copy.indices.size(), ©.vertices[0], copy.vertices.size(), sizeof(Vertex)); + + Mesh copystrip = mesh; + meshopt_optimizeVertexCacheStrip(©strip.indices[0], ©strip.indices[0], copystrip.indices.size(), copystrip.vertices.size()); + meshopt_optimizeVertexFetch(©strip.vertices[0], ©strip.indices[0], copystrip.indices.size(), ©strip.vertices[0], copystrip.vertices.size(), sizeof(Vertex)); + + stripify(copy, false, ' '); + stripify(copy, /* use_restart= */ true, 'R'); + stripify(copystrip, /* use_restart= */ true, 'S'); + + meshlets(copy, /* scan= */ true); + meshlets(copy, /* scan= */ false); + meshlets(copy, /* scan= */ false, /* uniform= */ true); + meshlets(copy, /* scan= */ false, /* uniform= */ false, /* flex= */ true); + meshlets(copy, /* scan= */ false, /* uniform= */ true, /* flex= */ false, /* spatial= */ true); + + shadow(copy); + filterTriangles(copy); + tessellationAdjacency(copy); + provoking(copy); + + encodeIndex(copy, ' '); + encodeIndex(copystrip, 'S'); + + std::vector strip(meshopt_stripifyBound(copystrip.indices.size())); + strip.resize(meshopt_stripify(&strip[0], ©strip.indices[0], copystrip.indices.size(), copystrip.vertices.size(), 0)); + + encodeIndexSequence(strip, copystrip.vertices.size(), 'D'); + + packVertex(copy, ""); + encodeVertex(copy, ""); + encodeVertex(copy, "O"); + + encodeMeshlets(mesh, 64, 96); + encodeMeshletsDXR(mesh, 128, /* min_exp= */ -14); + + simplify(mesh); + simplify(mesh, 0.1f, meshopt_SimplifyPrune); + simplifyAttr(mesh); + simplifyAttr(mesh, 0.1f, meshopt_SimplifyPermissive); + simplifyUpdate(mesh); + simplifyUpdate(mesh, 0.1f, meshopt_SimplifyPermissive); + simplifySloppy(mesh); + simplifyComplete(mesh); + simplifyPoints(mesh); + simplifyClusters(mesh); + + spatialSort(mesh); + spatialSortTriangles(mesh); + spatialClusterPoints(mesh, 64); + + reindexFuzzy(mesh); + coverage(mesh); + + tangents(mesh); + normals(mesh, 1.5f); + + remesh(mesh, 128); + remesh(mesh, 128, meshopt_RemeshSolve); + + if (path) + processDeinterleaved(path); +} + +void processDev(const char* path) +{ + Mesh mesh; + if (!loadMesh(mesh, path)) + return; + + remesh(mesh, 160, meshopt_RemeshSolve); +} + +void processNanite(const char* path) +{ + Mesh mesh; + if (!loadMesh(mesh, path)) + return; + + nanite(mesh.vertices, mesh.indices); +} + +int main(int argc, char** argv) +{ + void runTests(); + + if (argc == 1) + { + runTests(); + } + else + { + if (strcmp(argv[1], "-d") == 0) + { + for (int i = 2; i < argc; ++i) + processDev(argv[i]); + } + else if (strcmp(argv[1], "-n") == 0) + { + for (int i = 2; i < argc; ++i) + processNanite(argv[i]); + } + else + { + for (int i = 1; i < argc; ++i) + process(argv[i]); + + runTests(); + } + } +} diff --git a/Lib/meshoptimizer/demo/meshletdec.slang b/Lib/meshoptimizer/demo/meshletdec.slang new file mode 100644 index 0000000..8242617 --- /dev/null +++ b/Lib/meshoptimizer/demo/meshletdec.slang @@ -0,0 +1,209 @@ +/** + * meshletdec.slang - an example GPU decoder for meshlet data encoded using meshopt_encodeMeshlet + * This is intended to be used as a starting point for applications that want to decode meshlet data on the GPU. + * + * The shader exposes an entrypoint, decodeMeshlets, that decodes a set of meshlets; each meshlet is decoded independently, + * and the output vertex/triangle data is written as uint32 per element (triangle data is written as 0xccbbaa). + * This matches the output format for meshopt_decodeMeshlet with vertex_size=4 triangle_size=4. If alternative formats are + * needed, the code should be changed to output them; note that for triangle data, it may make sense to output data to shared + * memory to be able to use larger aligned 32-bit writes to global memory after that. + * + * Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) + * This code is distributed under the MIT License. See notice at the end of this file. + */ + +struct MeshletDesc +{ + uint stream_offset; + uint output_offset; + uint16_t encoded_size; + uint8_t vertex_count; + uint8_t triangle_count; +}; + +[[vk::binding(0)]] +StructuredBuffer gStream : register(t0); +[[vk::binding(1)]] +StructuredBuffer gMeshlets : register(t1); +[[vk::binding(2)]] +RWStructuredBuffer gOutput : register(u2); +[[vk::binding(3)]] +cbuffer MeshletConfigCB : register(b3) { uint gMeshletCount; } + +uint decodeVertices(uint out_vertices, uint ctrl, uint data, uint bound, uint vertex_count) +{ + uint last = ~0u; + + for (uint i = 0; i < vertex_count; i += 4) + { + if (data > bound) + return ~0u; + + uint code4 = uint(gStream[ctrl + i / 4]); + + for (int k = 0; k < 4; ++k) + { + int code = ((code4 >> k) & 1) | ((code4 >> (k + 3)) & 2); + int length = code4 == 0xff ? 4 : code; + + // branchlessly read up to 4 bytes + uint mask = (length == 4) ? ~0u : (1 << (8 * length)) - 1; + uint v = (uint(gStream[data + 0]) | (uint(gStream[data + 1]) << 8) | (uint(gStream[data + 2]) << 16) | (uint(gStream[data + 3]) << 24)) & mask; + + // unzigzag + 1 + uint d = (v >> 1) ^ -int(v & 1); + uint r = last + d + 1; + + if (i + k < vertex_count) + gOutput[out_vertices + i + k] = r; + + data += length; + last = r; + } + } + + return data; +} + +uint decodeTriangle(uint code, uint extra0, uint extra1, uint extra2, inout uint fifo0, inout uint fifo1, inout uint fifo2, inout uint next, inout uint extra) +{ + // reuse: 0-1 extra vertices + uint fifo = code < 4 ? fifo0 : (code < 8 ? fifo1 : fifo2); + uint edge = fifo >> ((code << 3) & 16); // shift by 16 if bit 1 is set (odd edge for each triangle) + uint c_reuse = (code & 1) == 1 ? extra0 : next; + + // restart: 0-3 extra vertices + uint extran = code & 3; + uint a = extran > 0 ? extra0 : next; + uint b = extran > 1 ? extra1 : next + (1 - extran); + uint c = extran > 2 ? extra2 : next + (2 - extran); + + // select between reuse and restart and repack triangle into edge format (0xcbac) + a = code >= 12 ? a : (edge >> 8) & 0xff; + b = code >= 12 ? b : edge & 0xff; + c = code >= 12 ? c : c_reuse; + + uint tri = c | (a << 8) | (b << 16) | (c << 24); + + // advance next/extra; reuse codes use 1 lsb for extra count, restart codes use 2 lsbs + uint extrab = code < 12 ? 1 : 3; + next += extrab - (code & extrab); + extra += code & extrab; + + // rotate fifo + fifo2 = fifo1; + fifo1 = fifo0; + fifo0 = tri; + + // output triangle is stored without extra edge vertex (0xcbac => 0xcba) + return tri >> 8; +} + +uint decodeTriangles(uint out_triangles, uint codes, uint extra, uint bound, uint triangle_count) +{ + uint next = 0; + uint fifo0 = 0, fifo1 = 0, fifo2 = 0; // two edge fifo entries in one uint: 0xcbac + + for (uint i = 0; i < triangle_count; i += 2) + { + if (extra > bound) + return ~0u; + + uint codeg = uint(gStream[codes + i / 2]); + + // first triangle + uint extra0 = uint(gStream[extra + 0]); + uint extra1 = uint(gStream[extra + 1]); + uint extra2 = uint(gStream[extra + 2]); + uint tri = decodeTriangle(codeg & 15, extra0, extra1, extra2, fifo0, fifo1, fifo2, next, extra); + + gOutput[out_triangles + i] = tri; + + // second triangle, if any + extra0 = uint(gStream[extra + 0]); + extra1 = uint(gStream[extra + 1]); + extra2 = uint(gStream[extra + 2]); + tri = decodeTriangle(codeg >> 4, extra0, extra1, extra2, fifo0, fifo1, fifo2, next, extra); + + if (i + 1 < triangle_count) + gOutput[out_triangles + i + 1] = tri; + } + + return extra; +} + +int decodeMeshlet(uint out_vertices, uint vertex_count, uint out_triangles, uint triangle_count, uint buffer, uint buffer_size) +{ + uint codes_size = (triangle_count + 1) / 2; + uint ctrl_size = (vertex_count + 3) / 4; + uint gap_size = (codes_size + ctrl_size < 16) ? 16 - (codes_size + ctrl_size) : 0; + + if (buffer_size < codes_size + ctrl_size + gap_size) + return -2; + + uint end = buffer + buffer_size; + uint codes = end - codes_size; + uint ctrl = codes - ctrl_size; + uint data = buffer; + + // gap ensures we have at least 16 bytes available after bound; this allows decoder to over-read safely + uint bound = ctrl - gap_size; + + data = decodeVertices(out_vertices, ctrl, data, bound, vertex_count); + if (data == ~0u) + return -2; + + data = decodeTriangles(out_triangles, codes, data, bound, triangle_count); + if (data == ~0u) + return -2; + + return (data == bound) ? 0 : -3; +} + +[shader("compute")] +[numthreads(32, 1, 1)] +void decodeMeshlets(uint3 dispatch_thread_id: SV_DispatchThreadID) +{ + uint meshlet_count = gMeshletCount; + + uint tid = dispatch_thread_id.x; + if (tid >= meshlet_count) + return; + + MeshletDesc desc = gMeshlets[tid]; + uint out_vertices = desc.output_offset; + uint out_triangles = desc.output_offset + uint(desc.vertex_count); + + int rc = decodeMeshlet(out_vertices, uint(desc.vertex_count), out_triangles, uint(desc.triangle_count), desc.stream_offset, uint(desc.encoded_size)); + + // if decoding failed, we write 0xff.. to the first word of the output data + // this can be adjusted arbitrarily; for example, a separate buffer with a single status for the entire stream could be used + // note that decoding fails only if the input data is corrupt; so this may not be required at all depending on the requirements + if (rc < 0) + gOutput[desc.output_offset] = ~0u; +} + +/** + * Copyright (c) 2016-2026 Arseny Kapoulkine + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ diff --git a/Lib/meshoptimizer/demo/nanite.cpp b/Lib/meshoptimizer/demo/nanite.cpp new file mode 100644 index 0000000..ed8469e --- /dev/null +++ b/Lib/meshoptimizer/demo/nanite.cpp @@ -0,0 +1,523 @@ +// For reference, see the original Nanite paper: +// Brian Karis. Nanite: A Deep Dive. 2021 +#include "../src/meshoptimizer.h" + +#include +#include +#include +#include + +#include +#include + +#define CLUSTERLOD_IMPLEMENTATION +#include "clusterlod.h" + +struct Vertex +{ + float px, py, pz; + float nx, ny, nz; + float tx, ty; +}; + +// computes approximate (perspective) projection error of a cluster in screen space (0..1; multiply by screen height to get pixels) +// camera_proj is projection[1][1], or cot(fovy/2); camera_znear is *positive* near plane distance +// for DAG cut to be valid, boundsError must be monotonic: it must return a larger error for parent cluster +// for simplicity, we ignore perspective distortion and use rotationally invariant projection size estimation +static float boundsError(const clodBounds& bounds, float camera_x, float camera_y, float camera_z, float camera_proj, float camera_znear) +{ + float dx = bounds.center[0] - camera_x, dy = bounds.center[1] - camera_y, dz = bounds.center[2] - camera_z; + float d = sqrtf(dx * dx + dy * dy + dz * dz) - bounds.radius; + return bounds.error / (d > camera_znear ? d : camera_znear) * (camera_proj * 0.5f); +} + +static int measureComponents(std::vector& parents, const std::vector& indices, const std::vector& remap); +static size_t measureBoundary(std::vector& used, const std::vector >& clusters, const std::vector& remap); +static float sahOverhead(const std::vector >& clusters, const std::vector& vertices); + +void dumpObj(const std::vector& vertices, const std::vector& indices, bool recomputeNormals = false); +void dumpObj(const char* section, const std::vector& indices); + +// simulate BVH based LOD cut and return triangle count +static size_t countTriangles(const std::vector& nodes, unsigned int index, const std::vector& groups, const std::vector >& group_clusters, const std::vector >& clusters, float cx, float cy, float cz, float proj, float znear, float threshold) +{ + const clodNode& node = nodes[index]; + + // prune the subtree once it's too detailed + if (boundsError(node.bounds, cx, cy, cz, proj, znear) <= threshold) + return 0; + + size_t tris = 0; + + if (node.group < 0) + { + // internal node: recurse into children + for (unsigned int i = 0; i < node.child_count; ++i) + tris += countTriangles(nodes, node.child_offset + i, groups, group_clusters, clusters, cx, cy, cz, proj, znear, threshold); + } + else + { + // leaf node: keep each cluster that is the least detailed one passing the threshold + std::pair range = group_clusters[node.group]; + for (unsigned int i = range.first; i < range.first + range.second; ++i) + if (clusters[i].first < 0 || boundsError(groups[clusters[i].first].simplified, cx, cy, cz, proj, znear) <= threshold) + tris += clusters[i].second; + } + + return tris; +} + +void nanite(const std::vector& vertices, const std::vector& indices) +{ +#ifdef _MSC_VER + static const char* dump = NULL; // tired of C4996 + static const char* clrt = NULL; +#else + static const char* dump = getenv("DUMP"); + static const char* clrt = getenv("CLRT"); +#endif + + clodConfig config = clodDefaultConfig(/*max_triangles=*/128); + + if (clrt) + { + config = clodDefaultConfigRT(config.max_triangles); + config.cluster_fill_weight = float(atof(clrt)); + } + + const float attribute_weights[3] = {0.5f, 0.5f, 0.5f}; + + clodMesh mesh = {}; + mesh.indices = indices.data(); + mesh.index_count = indices.size(); + mesh.vertex_count = vertices.size(); + mesh.vertex_positions = &vertices[0].px; + mesh.vertex_positions_stride = sizeof(Vertex); + mesh.vertex_attributes = &vertices[0].nx; + mesh.vertex_attributes_stride = sizeof(Vertex); + mesh.attribute_weights = attribute_weights; + mesh.attribute_count = sizeof(attribute_weights) / sizeof(attribute_weights[0]); + mesh.attribute_protect_mask = (1 << 3) | (1 << 4); // protect UV seams, maps to Vertex::tx/ty + + struct Stats + { + size_t groups; + size_t clusters; + size_t triangles; + size_t vertices; + + size_t full_clusters; + size_t singleton_groups; + + size_t stuck_clusters; + size_t stuck_triangles; + + double radius; + + std::vector > indices; // for detailed connectivity analysis + }; + + std::vector stats; + std::vector groups; + + std::vector > group_clusters; // per group: (offset, count) into cluster_data + std::vector > cluster_data; // per cluster: (clodCluster::refined, triangle count) + + std::vector > cut; + int cut_level = dump ? atoi(dump) : -2; + + // for testing purposes, we can compute a DAG cut from a given viewpoint and dump it as an OBJ + float maxx = 0.f, maxy = 0.f, maxz = 0.f; + for (size_t i = 0; i < vertices.size(); ++i) + { + maxx = std::max(maxx, vertices[i].px * 2); + maxy = std::max(maxy, vertices[i].py * 2); + maxz = std::max(maxz, vertices[i].pz * 2); + } + + float threshold = 2e-3f; // 2 pixels at 1080p + float fovy = 60.f; + float znear = 1e-2f; + float proj = 1.f / tanf(fovy * 3.1415926f / 180.f * 0.5f); + + clodBuild(config, mesh, [&](clodGroup group, const clodCluster* clusters, size_t cluster_count) -> int { // clang-format! + if (stats.size() <= size_t(group.depth)) + stats.push_back({}); + + Stats& level = stats[group.depth]; + + level.groups++; + level.clusters += cluster_count; + if (group.simplified.error == FLT_MAX) + level.stuck_clusters += cluster_count; + level.singleton_groups += cluster_count == 1; + + group_clusters.push_back(std::make_pair(unsigned(cluster_data.size()), unsigned(cluster_count))); + + for (size_t i = 0; i < cluster_count; ++i) + { + const clodCluster& cluster = clusters[i]; + + cluster_data.push_back(std::make_pair(cluster.refined, unsigned(cluster.index_count / 3))); + + level.triangles += cluster.index_count / 3; + if (group.simplified.error == FLT_MAX) + level.stuck_triangles += cluster.index_count / 3; + level.vertices += cluster.vertex_count; + + level.full_clusters += (cluster.index_count == config.max_triangles * 3); + level.radius += cluster.bounds.radius; + + level.indices.push_back(std::vector(cluster.indices, cluster.indices + cluster.index_count)); + + // when requesting DAG cut at a given level, we need to render all terminal clusters at lower depth as well + if (cut_level >= 0 && (group.depth == cut_level || (group.depth < cut_level && group.simplified.error == FLT_MAX))) + cut.push_back(std::vector(cluster.indices, cluster.indices + cluster.index_count)); + + // when requesting DAG cut from a viewpoint, we need to check if each cluster is the least detailed cluster that passes the error threshold + if (cut_level == -1 && (cluster.refined < 0 || boundsError(groups[cluster.refined].simplified, maxx, maxy, maxz, proj, znear) <= threshold) && boundsError(group.simplified, maxx, maxy, maxz, proj, znear) > threshold) + cut.push_back(std::vector(cluster.indices, cluster.indices + cluster.index_count)); + } + + level.indices.push_back(std::vector()); // mark end of group for measureBoundary + + groups.push_back(group); + return int(groups.size() - 1); + }); + + // for cluster connectivity analysis and boundary statistics, we need a position-only remap that maps vertices with the same position to the same index + std::vector remap(vertices.size()); + meshopt_generatePositionRemap(&remap[0], &vertices[0].px, vertices.size(), sizeof(Vertex)); + + std::vector used(vertices.size()); + + size_t lowest_clusters = 0; + size_t lowest_triangles = 0; + + for (size_t i = 0; i < stats.size(); ++i) + { + Stats& level = stats[i]; + + lowest_clusters += level.stuck_clusters; + lowest_triangles += level.stuck_triangles; + + size_t connected = 0; + for (const auto& cluster : level.indices) + connected += measureComponents(used, cluster, remap); + + size_t boundary = measureBoundary(used, level.indices, remap); + float saho = clrt ? sahOverhead(level.indices, vertices) : 0.f; + + double inv_clusters = 1.0 / double(level.clusters); + + printf("lod %d: %d clusters (%.1f%% full, %.1f tri/cl, %.1f vtx/cl, %.2f connected, %.1f boundary, %.1f partition, %d singletons, %.3f sah overhead, %f radius), %d triangles", + int(i), int(level.clusters), + double(level.full_clusters) * inv_clusters * 100, double(level.triangles) * inv_clusters, double(level.vertices) * inv_clusters, + double(connected) * inv_clusters, double(boundary) * inv_clusters, + double(level.clusters) / double(level.groups), int(level.singleton_groups), + saho, level.radius * inv_clusters, + int(level.triangles)); + if (level.stuck_clusters && level.clusters > 1) + printf("; stuck %d clusters (%d triangles)", int(level.stuck_clusters), int(level.stuck_triangles)); + printf("\n"); + } + + printf("lowest lod: %d clusters, %d triangles\n", int(lowest_clusters), int(lowest_triangles)); + + // build a spatial hierarchy over the groups and use it to compute the cut size from a given viewpoint + { + size_t node_width = 8; + size_t levels = groups.back().depth + 1; // groups are generated in order of increasing depth + + std::vector nodes(clodBuildHierarchyBound(groups.size(), node_width, levels)); + nodes.resize(clodBuildHierarchy(nodes.data(), groups.data(), groups.size(), node_width, levels)); + + size_t bvh_tris = 0; + for (size_t i = 0; i < levels; ++i) // the hierarchy is a forest with one root per level + bvh_tris += countTriangles(nodes, unsigned(i), groups, group_clusters, cluster_data, maxx, maxy, maxz, proj, znear, threshold); + printf("bvh cut (error %.3f): %d triangles\n", threshold, int(bvh_tris)); + } + + if (cut_level >= -1) + { + size_t cut_tris = 0; + for (auto& cluster : cut) + cut_tris += cluster.size() / 3; + + if (cut_level >= 0) + printf("cut (level %d): %d triangles\n", cut_level, int(cut_tris)); + else + printf("cut (error %.3f): %d triangles\n", threshold, int(cut_tris)); + + dumpObj(vertices, std::vector()); + + for (auto& cluster : cut) + dumpObj("cluster", cluster); + } +} + +// What follows is code that is helpful for collecting metrics, visualizing cuts, etc. +// This code is not used in the actual clustering implementation and can be ignored. +static int follow(std::vector& parents, int index) +{ + while (index != parents[index]) + { + int parent = parents[index]; + parents[index] = parents[parent]; + index = parent; + } + + return index; +} + +static int measureComponents(std::vector& parents, const std::vector& indices, const std::vector& remap) +{ + assert(parents.size() == remap.size()); + + for (size_t i = 0; i < indices.size(); ++i) + { + unsigned int v = remap[indices[i]]; + parents[v] = v; + } + + for (size_t i = 0; i < indices.size(); ++i) + { + int v0 = remap[indices[i]]; + int v1 = remap[indices[i + (i % 3 == 2 ? -2 : 1)]]; + + v0 = follow(parents, v0); + v1 = follow(parents, v1); + + parents[v0] = v1; + } + + for (size_t i = 0; i < indices.size(); ++i) + { + unsigned int v = remap[indices[i]]; + parents[v] = follow(parents, v); + } + + int roots = 0; + for (size_t i = 0; i < indices.size(); ++i) + { + unsigned int v = remap[indices[i]]; + roots += parents[v] == int(v); + parents[v] = -1; // make sure we only count each root once + } + + return roots; +} + +static size_t measureBoundary(std::vector& used, const std::vector >& clusters, const std::vector& remap) +{ + for (size_t i = 0; i < used.size(); ++i) + used[i] = -1; + + // mark vertices that are used by multiple groups with -2 + int group = 0; + + for (size_t i = 0; i < clusters.size(); ++i) + { + group += clusters[i].empty(); + + for (size_t j = 0; j < clusters[i].size(); ++j) + { + unsigned int v = remap[clusters[i][j]]; + + used[v] = (used[v] == -1 || used[v] == group) ? group : -2; + } + } + + size_t result = 0; + + for (size_t i = 0; i < clusters.size(); ++i) + { + // count vertices that are used by multiple groups and change marks to -1 + for (size_t j = 0; j < clusters[i].size(); ++j) + { + unsigned int v = remap[clusters[i][j]]; + + result += (used[v] == -2); + used[v] = (used[v] == -2) ? -1 : used[v]; + } + + // change marks back from -1 to -2 for the next pass + for (size_t j = 0; j < clusters[i].size(); ++j) + { + unsigned int v = remap[clusters[i][j]]; + + used[v] = (used[v] == -1) ? -2 : used[v]; + } + } + + return int(result); +} + +struct Box +{ + float min[3]; + float max[3]; +}; + +static const Box kDummyBox = {{FLT_MAX, FLT_MAX, FLT_MAX}, {-FLT_MAX, -FLT_MAX, -FLT_MAX}}; + +static void mergeBox(Box& box, const Box& other) +{ + for (int k = 0; k < 3; ++k) + { + box.min[k] = other.min[k] < box.min[k] ? other.min[k] : box.min[k]; + box.max[k] = other.max[k] > box.max[k] ? other.max[k] : box.max[k]; + } +} + +inline float surface(const Box& box) +{ + float sx = box.max[0] - box.min[0], sy = box.max[1] - box.min[1], sz = box.max[2] - box.min[2]; + return sx * sy + sx * sz + sy * sz; +} + +static float sahCost(const Box* boxes, unsigned int* order, unsigned int* temp, size_t count) +{ + Box total = boxes[order[0]]; + for (size_t i = 1; i < count; ++i) + mergeBox(total, boxes[order[i]]); + + int best_axis = -1; + int best_bin = -1; + float best_cost = FLT_MAX; + + const int kBins = 15; + + for (int axis = 0; axis < 3; ++axis) + { + Box bins[kBins]; + unsigned int counts[kBins] = {}; + + float extent = total.max[axis] - total.min[axis]; + if (extent <= 0.f) + continue; + + for (int i = 0; i < kBins; ++i) + bins[i] = kDummyBox; + + for (size_t i = 0; i < count; ++i) + { + unsigned int index = order[i]; + float p = (boxes[index].min[axis] + boxes[index].max[axis]) * 0.5f; + int bin = int((p - total.min[axis]) / extent * (kBins - 1) + 0.5f); + assert(bin >= 0 && bin < kBins); + + mergeBox(bins[bin], boxes[index]); + counts[bin]++; + } + + Box laccum = kDummyBox, raccum = kDummyBox; + size_t lcount = 0, rcount = 0; + float costs[kBins] = {}; + + for (int i = 0; i < kBins - 1; ++i) + { + mergeBox(laccum, bins[i]); + mergeBox(raccum, bins[kBins - 1 - i]); + + lcount += counts[i]; + costs[i] += lcount ? surface(laccum) * lcount : 0.f; + rcount += counts[kBins - 1 - i]; + costs[kBins - 2 - i] += rcount ? surface(raccum) * rcount : 0.f; + } + + for (int i = 0; i < kBins - 1; ++i) + if (costs[i] < best_cost) + { + best_cost = costs[i]; + best_bin = i; + best_axis = axis; + } + } + + if (best_axis == -1) + return surface(total) * float(count); + + float best_extent = total.max[best_axis] - total.min[best_axis]; + + size_t offset0 = 0, offset1 = count; + + for (size_t i = 0; i < count; ++i) + { + unsigned int index = order[i]; + float p = (boxes[index].min[best_axis] + boxes[index].max[best_axis]) * 0.5f; + int bin = int((p - total.min[best_axis]) / best_extent * (kBins - 1) + 0.5f); + assert(bin >= 0 && bin < kBins); + + if (bin <= best_bin) + temp[offset0++] = index; + else + temp[--offset1] = index; + } + + assert(offset0 == offset1); + + if (offset0 == 0 || offset0 == count) + return surface(total) * float(count); + + return surface(total) + sahCost(boxes, temp, order, offset0) + sahCost(boxes, temp + offset0, order + offset0, count - offset0); +} + +static float sahCost(const Box* boxes, size_t count) +{ + if (count == 0) + return 0.f; + + std::vector order(count); + for (size_t i = 0; i < count; ++i) + order[i] = unsigned(i); + + std::vector temp(count); + return sahCost(boxes, &order[0], &temp[0], count); +} + +static float sahOverhead(const std::vector >& clusters, const std::vector& vertices) +{ + std::vector all_tris, cluster_tris, cluster_boxes; + + float sahc = 0.f; + + for (size_t i = 0; i < clusters.size(); ++i) + { + if (clusters[i].empty()) + continue; + + cluster_tris.clear(); + + Box cluster_box = kDummyBox; + + for (size_t k = 0; k < clusters[i].size(); k += 3) + { + Box box = kDummyBox; + + for (int v = 0; v < 3; ++v) + { + const Vertex& vertex = vertices[clusters[i][k + v]]; + + Box p = {{vertex.px, vertex.py, vertex.pz}, {vertex.px, vertex.py, vertex.pz}}; + mergeBox(box, p); + } + + mergeBox(cluster_box, box); + + all_tris.push_back(box); + cluster_tris.push_back(box); + } + + cluster_boxes.push_back(cluster_box); + + sahc += sahCost(&cluster_tris[0], cluster_tris.size()); + sahc -= surface(cluster_box); // box will be accounted for in tlas + } + + sahc += sahCost(&cluster_boxes[0], cluster_boxes.size()); + + float saht = sahCost(all_tris.data(), all_tris.size()); + + return sahc / saht; +} diff --git a/Lib/meshoptimizer/demo/pirate.glb b/Lib/meshoptimizer/demo/pirate.glb new file mode 100644 index 0000000000000000000000000000000000000000..261f086367f766ec821ffc499dbd62f62353966a GIT binary patch literal 24696 zcmb@t1$5TBl$ z)UGLNDJv^&Y|;ee!RIl}rHQ3=<;@kE0JEpj)2GAr-xGbje2reNn$p(h(z>R~`Z`#w zT@#g^kzZTdR8im1oL^jD+t66rWP4|ORaRJ2Q&d=7tqEvuY$@&N=os#*DQs#gZMMA} z1d!shx$O@OAnrn#&E7E~GCODlLJn)g;8Gy(gPL zvZb)Dxw5UW*;bkBe{BvV{jYYXm5ywwZ2W&+(Vw7YRhEwa3%K_nY648I?=eg+D|^qQ z@z3Z#Aoch8Gbz2fv9hF86X55nX)bImw*lhmG?Qnt;EE{1=)gldqTW zf6%P0w-MKbWn`qqg=c0&r9<3`>s#tzGt7Q|zObWg9sj>G1qI^+7atfinY_IqCpPi< zz!;R0|DPGNaq<_D4|KhK%>MsD_J8B8tg@!L)FxNS5gB2zQITn3iT@8c_Vn`cg8ckb zJ3Ng>9|-(ES@it>-ghQnPrv^mGu}Sl|3hS;jQqtS?7Xm~n8bhfp2^$jY5Ysiii@El z)_+j#_nm2z*#AvpY-(FyS6bJcF{S|uhPTNKx%r1cKu+Jk&}2nLctYfAE1`Pl8C~H8 z)vC6z6~;`SMn99Q$;Z>r)78`8%k>scf_9?R$gr zx0L=#0YYQzq<^Twe^U4tWACy0o3r$Yu*9%5jm<*U7D8dEEUfucw;PHYhmXz3+{r10+kI~!DH`gLU#-g1n%%5c=DQO4gb1M#=qccDmQ~+ra5p z!-S5rp6oxp3qNfBZt=PwZvI~8SaI9S`gq7ql`zJ2k6WARsY5hkimyW6SnzJ2B9{`%W1D@Y43DclO?H|Fo& zwAy#eibuxdqbpaoOkz3Qx*UDv!6jl^c>Q2{b=21{V_uY{W@arS5EU?60KlmhmBmfI zI+BS4(BI1q6rvcuHvU8ehGV#fZ`7$UW3Qge1*k@4yQ@?tKu!sZBCE}Qqta1CjhVxO zaF`b(1ZYH|23YeiTfrbFNmNXn0ejzrdwL)!aK^O2py^3N5|paMBvozF5HKw~v3ht= zcp?}GgDB5<4{Oqp&)&nDkv^ot(+#}Vg`Ngr{({S2NVjX=jb9QMFSQHyn(w#W`UWsl z7ACZ2jZ-au#I!yX@VSm0ik78vj5eoH^M^82uEq@Z@SJ>_Cjg%Lg+&=g%xV?UUW~t0 zbN1SIw(H#4&u*>!E!X#4x6EMk20E~$$>@WZnGL}Nl&c!U5G3?#^Glya!|R z<{j(l7;LGzFnwQ2^{tCd=Y(&_-zF|lm%e%Gzofd=HGJCL;H`C^a62yTSC=z90#k}v z^;ji{aOSR3O|AfAzXmU3cFtLJNdQJdK{GbhUEVq`K6XRJ)`Ngm4j#iEc@ndYy#UN_ z`}|HbiCGi7fbU3NvefOfw!V-VE3Vh&@Q+UU`RdPoGx`E@1tk|5>qGa6hsFt>o(2nZ z7vU$`m{49YACu8Ys0%D#&-rGXO?uNR8E@}@G;2iK#BFu?<1Rg- zKYDWS?1PV5hsb*yO6!YaI}@Wwvn-Ex^geph$8hBZ-zRO=l|EITwPr%g=ycRZ~O!Q+5{JZNQr6Hy=*#Z`w1qeyp`sQ=!cI zEaZjiF}_K@BBDCKp(UkSqvSDv8zf zWoXvzi^E4o;zAMxGh)?_aE(r7<^q#8Dke(s*r@eNIV2Sz~!3K3D`XvtyY8MHne+vk57{AbQ4=YB|iCs!h3D~{q>`l*5`Zw zn0@oqx7U74cw=}Wcw>AfU_BO`m1;Tc5DP!ffYL`Wo3@VGlIU>x3%zV%*Ga{Jw#1B^ zt$lYg6mMpR+}qTb9yew>4ZZVXdEj%|Pe-5pT0H)h&y5zYaZouid~)rHXXl5W|K{`` zyNwS{RLt_M^y-%VVlXC;oxEhGZ|cQ4x($I9qqAnPPRa%NZ?c>jAz!PmfCutZ!5%{g zDnB31Yk%Cby4r8(zTmyeQ{b6uAjBbUe94<;OqyiG0$dmzq0g?`&t{4F7 z$drOKE}(&(WD*kDs4gun6+CtrXTo@Vgo_9;+)JP?EDpy70z|`0HJOZ1$}MPd6I1}r zvhIa97jn6n$rSk~*surQqkVtjQ|G4A%(iDj~bdzjJU; z#!f}!xChU+SZehz#3K@FWR)UP)y)V@IQH3z>1Ul2r_MYIc%U_OURIcUH~09OHHw$P`wmf2TQ20FOSHo9nvy&%$An7)ipzZ3x2Bn9(k>JZ27ui zNNZ5%owj`qDP^>@0|B2YZXzeOpB+3G`P^YX_R_xJkCzfy@%yN{&hGxxeNQU{;b!}D z*jx3+P{YvfYVQ`jy=?Rs_58V&nVGM<`-?C*MnPdDEQqrXK48WuWFUU=n*S|TF{F>DNhYA~&{8Q{2-8)$;SKQXV+K*>jCCQlf{ zV8yDxO-k2W>#yHPr%ctDSqBa;ubRyJ!hEz?vtj6jAd6OUTR6;rOep0*)s<5kyB!bh z8X6mK>4hU+okU}wX^ltlPX@EsEo^oy`Q!Snw_dj$H8+PW<~aW(G8rE``s!=+)7kSG z!yTTQ+B|;D1kP_>?*?(;D(Lg{GzYC(Jd=0&+wabE_MbnMK+10@>j@|!;6Cdf?eiHOWuIrZ1 zd0f#f*eQHcPM`gI*W3wtu}xFf&OKLNEIiFWQ{gesXR0+eZwlcz2Yd{!aeRfJ{=9PJ z1R!Y0%-XQw)T-RWV(279hPjZE)HwdFWAhe>KW0+tRGvVf0vH!lP5fMgV@813<2Wz@ z9WSMzm832$A&hWzv{(=muY}+m$$X;3;uVsTVk9gD@FKhm3V<;MUZ|xUTLx1QV@k@6 z7uL*2WcOFvH$^9UJpPL#0EfzyJFRZ2o0~OPYH(fBVSC`o@%i-nQ+>zaE|deS+-=9zk*O%!lg#ek=+~PVszd^-NCr z`^rQz<{m}``eK`h(9jJIlgZ$i%65mGKo0>HQ~^Xq!Jy9>h>f@tGj zSVRPlS)nwZ*f)PheeX{*zF+rWd;w&T|3metGo8Ih*H6EDw)xB3HOcp0`;}?tCtQ6U z@Jt-$GP*{(E#&8^oxGjft6%EdLHQ)*N}f|BsKOiACCNcX=Hx5jAS>(j-@ZW5zkGq% z^PD&5A8$B#Ahu%7`~~vkwLgs{+dP3O`(9iPo_b7>Tl@X;fnf29FRx}Vx2$XUW&hmS z!uw%EpALEU8hQn%zC1p5+Q@=s2V;Xst@TJxZ_l%Ogwzq6J0I)tO!U{7G2g6}|1*_x{w8QSA={!d@owH=EGntuKF( zXJ}`Vb2#7Zm@_ptylmydTequMmaNKt?7D7X+dRdAE3^YG@238AT-*6k^6x394YX}D zRviAAvP3_JIk$HXZAN2TQ|s(y*dE4Mey&4!RQ#Br^`@D5anZl0I^MhO>@Xy*<=O7c zfG_U|hI>bS5kzT-9s4LS@`#HnQ!|g0OBlDuFqYTyTCLN5_${e#BX=V!q&6-s)XjpQ z)$dIf*PQHnyQ}ql=8?R$c;GpYZSKI$?S~)TJq|pONIb+F+q}PZI%2Bx&8L8u6DZ{TlW4q`*qWs^!r0LcHL$lZ@PVId>B9X+12M8 z>QCRwPeGagFVV zh8*=;ks5x(k;f#q{nWY$y zKy8>V-*~?0(F4!qYqehuJsNc>?rPOvM!aojv}ns>HT8afzRO(KBk)l0EK$XY=>JITIkOo`2SdWIE;}$`Nfg9CNz= zQ^=wE;8Xf<9Y6UcYuCBHyPhte*0`T^n;iP7SGdbIADy;E*|oYa``?LQ-@AC_2l~vS z5A=}_)VI(1i|WL?OKZ*yjk8fmZ@bG5xcimm2>GnX<;w}jf}j2Rz^%kFwt!#zNdarf zCQ>*p^Nv5sq89U7M;=L9x`15OcqHoJFYOT1%o{XXH%=vWWF+K+FAIoofV|0X0`K zRXa>j%4cNP#P1w^A?s-F1}x}|>%xSUp51pM7RUav)O$fnyW`Z;?9)q#D%Mm?NNL8t z=g!>IKi9H=JZ?pt)0okVq8FD2f8&&)EpylZ zG_*7Om!X|0l`*M4)a1i)|In0Jb;r5QZn6%&tX9}OEe?;d%Ihs;*dH~ugqDw za#?pAY5DdJ)%mTPn;bgF5w*f%JQ_IbR$sFH7| zx0IbUt^IE7=%+1VPO^XnHu(eu06sXi3_lDXFw_E<62dJIADcM^7=qJak}BKg6)=Ij z1ddbPINlGP4l2YF1{y#m_8}C&8v7n~yqP z;hJ{a?>D#}a6pX%@^Sl#m514%sy8@~R}aj1mgOFJ^Yy3af#Y!}Kv8`rqwH}#z#KTV zLQHQt>7LO3-9NN??AHaR8DkInLTa_`)?XIm*u6@XZVPYbU*#^O@t{bw`#F z^~I!YV}h@cLekY0lmzkX^AlXeLLnLO+c#y$G<~)3UWz@zk-5}9?0d8C*9H5XqwDIJ z4#{Dhi5)UNKv7I6WD!zR`I!}qvgQw8X0{hNqV~pOJoYp-HGE8}3?c5=z%0<|Ccxna zx>#`OMFr96fuP!=xJ?96i>^ERux-i)*aR7hA_Nu6C8?ikgBNfe0m#*%@acW9-mqHH zQ$@cYuYb8I|6x5Afq)2<73?;OpF5&2{>$yxO6B~O~c>1S>MbgpXiMa zpn2yN=X+)6c$w#ygEMjG%K2ZbNAqjw(Kj=bI!Ib5beojNM>2 zqvOEEKDyod^Iwf)M=)G?Y5`N|PGm4A)*;n#0H^81-L|i zHxxe!4Onm$d;n^IpuiEP0R-Lfk+nNumoyLfGU3P`Oh!T{xI=*{f>FEdp*5B z-5H-3KjGbK5?NG#n{l|}WZ=^_=T%1|=I7>LMAxq2ZEZF0o9l*zX6xtA@EJXG$hV7n zf_nz0d|uF1xqs~UgHs3QWrU~t2Rr5X#+7c4?F-Q9oU_>BK|V&K#4A$Kx{AA^A}Bo5 zSXOayW9e$<#RA`NYRvm*svSX;Tl+|_g3YFee596)hxf1PX$*-h?_p0t7}L^a_~QEg z73UAtOmN5^VHn>s$@j{I$B9n8_t)*2mmwhpEDnW&qYT1SI?VmuEteHH4Av)tsFu-v zFP?w-MWn~azcz#r1VLjWoFw3+)vBemks?={JT5EX(!d&OO1L8ri3A{wtP-enHiL-T zu6}hY1P@w62wHYG2m=KW$8cycZMGE#tuROkoyuKKGa|e}=qaqnYQ*1WIkV*0b zjUju_k6Sjnc8n%tUcYDFr}Vr1KCTf_1`4--SkL@*=7!kVJGW21-T&sw)fW^c% zLs5^+Bf%S@LkfGmMlHxpn#&0-*5;ccLWIMKFrT`y_G$4W3J!1CbD?XiXIxWGb#C*l zRaIw>&I+5Hl{K2e6F4Sm1C@Y6r3z#s0SJ*cnud8hm`8paS}$~%8X~XfB(T*K_@3^_ z4br(9S!S)rWJ6P-i9>hxOE6_dWoB}VR1gn=h0t_oz=!_6$o^gk4@yvpOjYcAsDKJZ zKseeDPz!+g6~sc7hA_g&UjYi^AsI8gbrx^=?qr%SsdK}HFTsiB)ow_A__7sOBG+9& zB@wH^+qwI`eK%+K)r~8ku6_3Htf`w;JxjPcb4q>Fmnm=KeisTE3(ytUmMjqU^!)He z+OeD2RlV_gqXzY0v6(U%pDLkn>3ktG%9rCCm1{@l+ea1%Miv)RX@&?VAat1Gbltbp z-{sn_lQ%YgGkD{~L$?>#!K;_ge|mG}tuZ7ZAZ4ZH)7(uJK?016cvp8VuZ{E=NR5f| zcbKjBam_kvP73iWYp$uQAv=3iqV+KmXlnKJcpk8i1{I~DWI15!J-(koqz5bMejGO` z=b_c=L)~~XUn1rxlx{X8K_5^NWGZSYf-4@~7K??divCmDY*6*i;nAc8pa9}F0AW)> z$%OxZfrvl^twFKzetwBgzB9&@4$<}nij$k7TbHcrxqqrUcho3-^5>(dGsk-bW(zz5 z`Fz(UD{f73(=%O{gidi|NX?z5BR-!`UACj=U~(0O9zeLqWUs3?7Hb)1A0Em>a5fbP z4bB1~h0pL1dDxNY5&<0b9K`Wey);($ranMr3JQ`&T|Ybi#6>^e9J{S6hNbhP3X%k5 zam%RT-gz9!ka3?rS@7=HQjaJDm&KOoC3XzU7sqQa%&KsAS;Y#?oBcyx`;@mFGg;)M zRkJK!h*qjp%jH2ngi7QV%5=AfMsi3dsb*DHTI`RZD?`q;?K)wPRhpl$R|3Fx~NofGM#2=`?9 zdl&>pqqC1fXGilaE*#e~+ySL33fdfJRQ_}lu;oe`+`o{BSUINVTgctJ$LDn)z52rX4!Qf_uCXF32-9j+4nm`f1Pbaz`+?{Pu^HH;LGbNe>Dk8^X?Fc&n6;bDCR|59fgj$}YGVNtRXejz*(*8lA3 zwX>D|`|h1UFaL7st7n53Z(n)!S&qS%tQb|b|H0Fj;4=Nx4cZlUzlcILdT~r94%skq zm>FMZAV;Ryr#xOYdLW{8itMXLn)~ z9b9k$iHS1V9C^n_N5?mn*Vn|BbnZWJI*6!*BjQH3c|p`KJI1Qpd%j+_r_r_O&U_cq z*VA4o7rr}lZ&znQ-=%x^zQ6bNxqWkP_S~r2)`U*_Uie(J=jqX|ooCjL9x6_&n!IJx zyq^{&j<&m(x4I{5YHq^B&+gngcu&CCGOsnsZj!h*;oGTYzl=)UKZQ^4 z;)>d?cJZzsyw@U?&{yIjs^g6*U7gcAB{lo3Kke_IkaSEMb7k)4LlcLnM^2d=c);DtcZHetdm;#EGp<6}KIK zL%;iVe{l7%**!=13_G!-@Jhky(LLke+5HexeJb{fxGaR>_NC%M&gQR(#e$s7lZy&8 zbbwuOpy2$_>aL!`!qf$~Cg~(}4(XG}kDu>gDjfaJZCpJk=)8xoam#aDDE<>kbeW)6 zjfV}ORGNbYbX#yBK~*>eDH7F$BLX;5uhFG~l1X>TI5ifTnPIb_1h9~8-cfI)#RlZT z_apDo&%Cy`^;V3fCkY@5o+q^Kr(nTP|I^``yQ-GNb*Xv@y@C zFV)ofQ9})$Ja@GH)85~H-2xXlW$_LvRgrs#l@*5Z(YP;veR5;5x|FObX}k9G<-1ea zDU|+A%|Cy4Q{A)s!ta_4F@z)<;j+MKWOpVJ4vYRF38?NGFedvk)<3*1~ zMlNU;Ax0lJl&PY`6f$;gde`^vD{pr_PwgLrWi)K!hBhq^w|exC9n3G~4Q$A5u$jlq zZUl;1CF3aQ2ISaWEy-m;^Cr^gXxy^HMv!X49LSs*TXXk%muFq=S8NnQ78eYu6urUAMXBi}94S-iBFfgfY0|YW0$D_DfwZMC_`Z=<`#^l#9pDT|mB({N(ev zB4%^l>wyC|f4}khq1`{frES}@fA2=-vJJG;#BUdpF0kKlb}n7Ac=o6G7TITeqqfI& z1YF#{ww4#<=jYHVUEjT6#kgBjn;QdkAJ2836mur;2>$f#p(jec)+wIFoWPzqghI&) z(NPQ&r0EGjE^!S>6Z4~l+60035jvQw2w0?Kn?uPf zD2VIsh6N?>T}T$`6-vej?a?!$wV&N*vn7s?)iUwT$GqRZy8Xr1l2cC(+@AkQVIo`s z<6Ngbi+a2E{)Q7hYYwjYbPh4){%XbpgH2*-3WmX0CS+U91eSuUl$d znrKeb{bE;fAHd3itC`zT|rp}t0q55{{vZd)x0!KQHoA0wQEctT& z{hoWfgU{JbPyFaw^rE`19+B?Mk2clJZCw2kzEOQ<%;cpaD$nP;iq?TuYd5Xvo-%24 zwabEr#_r7V4z;6{T{HX~X$>B&)9t*65%K|2q6RHhmx$`v8->|L77^Rio=cK zCa$8l2ihmDHaV-*s%eJCiieBbR*D_$v#kkUA69{#v(7e z9Veo)+}B)H<3|oFuBaT$cQw0lXqcNq;h|>~jqoy*q_g9U3EqWt4TfV=p`+oAiDTg+VoY?QbwetJV_Lff*JQO)yNljz1 z;$pUyXRm-R?~FE})8&Z8UC5x})*!N%c z%M}ynu$if83|877O(VxoXl$M^`O>AW+rIf`Tg$_NfrpP~%oy@}TH5c=Q&XF7-q^qY z`|F>**%efM>(>p(&Q^IuWv*X@+TegU7W^S~+wf7rMY80ZJdBK`*8Ch{J3g_1^D)`> z8(u&%K^v(n@Z{++ue3;uMMW5W!ue!%s% zT7O|`1OsseLX<`oQ8)X-Yc2aDJ^F+i^(&qkkttz4$fR$ zlvGo=qXR0?;Zi5(Aq&sDCn=|g?RS})JLuwCQFZc=^SpC@=uGFbGTO<p{a({AKEub{$=w#UdeL0yvA;(e-YtjIt zpK6TJ2z=nSyQeWM9FGPd9WR5u;Z;xo{8+yMoJ7XGYyrCZhmF8UB9R7+z>JYVN}m}6 z2-A-CXlpAE;%v2YzAW(?qHrg{^c)+Ak2zNZ|DdUDZ6(zUN2Es?X*4RNt`RC9cfFjFlNU#4FVh~i?dJ`Q04 zzuYf7IbCYqr8&3q;-+nLi1R*^%TBpzG=tw<`eW7Q(G#~lI-Bug^;#Y`(KTY&$z7_p z+j7M%|C_6S_+k9Goj<9=c1`GacAwzHX|1J?PRqo1_#ZyCc@CO zk*O3C4-GF+0q^wiXf@oB`%9vs8!^mb%TTx*$_rQbYPc?QY!%vq4eo2z$nxrmlUu9? z*JQ4+y}-#EoEX^qw)1J_vq^);Hx$>H?&V!5cfVTIJ1asUsz3{O=(|=ERg-E{Wz zJNngQVh)Fl88KwS5y|;HFnsqfBSu}=y?gxee7Ezz9w~q8aP^0~9jnK_xwLRUq_=;e zx~KQ^ig)@e%S=NIy=yp?Q?6dwc<#tu!#_1btBy(nr zSd48eUl+9LH}mJnuWrZWQ!lwc9=^$&^Tm!!5|(|Tg`WHJvC(%Z%^A^57a#%%#0YgAU2m#`A3R5~(++Cv6d@xl~<5RnBs zTs3rZ0T9BIX<6CL=F(-T0C1bTHCUKNB{(wvFpjUfpgM-*@pl&v3&WOWIPa=o*v% z(KGF%z6%|T=U2IX`BP-u_Sr-C=FiUSS`S#VE`QftTXvJ%j&Zi^zkJ>@ zXv>Z~xc`kUdjYa+&beuyo8y8!tAiraS}#nzd}wxARZdo?qq>FbKxnA#W+_glYHMjR zcTB`1$zyUxErpJu7BRI+#pR(ID)ogrOvfq0(l9kDNRc1vlj1m)n)=E1dltv9pZ`dB9dpO-GXTucZJUQ+~j~>r~x($WD8cLeb}rOeE7uo zp{KDgk~Pwbug2!hp5pU@GmyA){S4gg%UH&Q!!-@>4p38no*+KhHt_wRrs~K9M)a31 z=!~_IaO49Meomzxc-OlDNwKM>TCLRyNclQ`M3_@pL{tgj@;u^Ag;^P10)gjTXjXA6 za(eC$Pt$)a?8*4S@1*!ofrLrfZ_kWdIYu`5-Z!$mabLxG_62-0xPIT_(}<9{>h|#8 z#(Bkm-8ans(r=Si*r=XbvUkIooe_%=jmFCu5fu>?#?x?cEGkO2aB|)WD5Vx7f~eKS zCBe2i8q|7($7DHPk@_&+-VCQUtKQU^x=xVCj;jsAuIkWl8$k$W?<-)NtD z&j_MR&M_=W0e?7DZyLRBz(!T~dT7?Z#(iZ-W(IpPd;bR;3mfr>2#ZG!)m@7nd|N@YAV7`n0*xLo&NSEt}R% zn(HG(+U#XOA&7c+YEEYVmtsONs@NPF%DF#q)%M+^FcGN5rLp znq>0Cz$Lg}XKh8!BB@Rh!x2)`hJ6!m?}D;v^?7Zv7@QL|mA1u`fxVwfrv+?vh8l6gz6!Nnh~3IKuy?T4S(B9 zj6~NF@L=vNVC~Av%+(kaE*c}hG=LIS7n@#MFg!OsJ0@*-S!G#iRe2pgTw~!m7};>K zUJqniC5^>_`;;6KLoIVtvU4{#)$2qkL6-wZA&H?fXqd9xeCj0umB!z>i=`cg*VDqb zFqeCk<&_N0Ei5RGTJ`kJ;JH>NT__SraFOQsr;iRFdGhp;Lg&oW=uZxw8|0DMgh*w7 z7?4%o5dcVW08geKfg17x)6O>&Han-{_56+f#^Lz448YUb= zF`R66nfnax#_|SgAI*3owQ`QQWws*TIoDXx`6K>tA79$D>;4Vr%AO zYkWuXme2Zl)cByFj(I}ou1sIXQhT*3b!KT4N+r{f=(2%C6@&T+ww!Su-a_l~^1(NQ zi3eSCu?i|B_fY%u%y)ER9MT+QG9>>O`R~>_4Gp`Lmr4=2xL`43teXcyt&h%Ahc(2F zDQlLs^=6J?D&>Gz*PB{}ve=aRX#;-%z!9qC0xpSN2?7@_<&qpYC}2YI5B?zj`QGP^ zOlDN4@BVkM$?!vkV!f8><;(N*NiNB3ZVF;eBFKV_aCqc`LYLU*y3!0oqztGEAz?ON zLaJ;#*YFQ`{z2*<92~?+<*A-tF#bNgv%WjjFr&-*>P`HTmc(&w%_16=%o|3;2Za{4 zZ~?}bmGYf}%O_H(6s40#k(oyl4;1()NKz-cmxl17R{5~uY7*v)PGX2fm333?QjQ1p zGdl9;(&+M`Q##NugXn63q_VA}?c8W`g?W0&8hSrEIcUxLO>-ttdlqj@oMp9s{PE63 z`&za?>GiJ^a`QNGG+*x^OVKDEzyc~;X*U*J_$N_EWf<4f=Y^F&yL`xW#+CB+THA5!QMFfVop}s*#&Wh!By4 zI=k>iLIf_G-s5fCxMh`XFY0};VL{!n0s+gFGuO|T&);ZFTkPiNmCN|P{2k-F=)1)M zr(C-WdYjQzMZKf8tZ(T;4mT_a8ohk@F!IoeU3n1?eIJj*CMj=KTKv(Tmh~vUW>hb! zV8f{3`tyDlC(fR;1_b$ToIRe>w`fzs%pH%fqV)BLQQ7Jh(99v57AO{M@@Vr|+`n_j zHFW9^L2J5^Ue)r^6Z1&f{oTVNANf5*X>^X0f+Cb7Oru1r5|Tw2ODN^=IRQ+S-asJ} zI)#UUk&2Aq>2(6WRLZ3xJbSThI7pI_M9?i`5gYjkry^V6>L?=PT(O4eVg#Rtd^Lw z4x9Ort>ZuSeBbefd+7Zq@hy`fHH^lYy?(1Srl+UvB1U?Gto&~FlpK7craODXpLKV& zjnx5Odc20k5zs@p{^?QSX*h)?XWLOnV3kl$NjQUoOC)ejp?vrO`jZ>EnR0<%!PH>M zF=1v$R%d~;y@#9i_Xk6c)&FkIo8s;wPY`Euy<#~mx;>r~$MaV4T>qAZ*M|;KjdZ<_ zn?cDk0JZ}F1_y>P#14LjARz2juC6@zg$QnMTkaDp)>~pn370RIiQsp0AXXw4X$bF};fNG*)tW_QHiu5q z>FqQ@W=G6-AMcEitO<>DBOJ&QA2bhovGG)dp;k~Oa#{jGVf#_3Zc>Rtjwo_5Sjx`8|iQFpm$HzM!Vt|%43q0N(Mp_@RV$gBNOA& zMO1|AVc>^v8W-4*b;DOfLlF`KrE@R=E|vN)#K4|Tw&Tc|dXbb*!%&)lCdV*JBHi98 zkQ`uAN>uS0C6I;%M*Es9X*!9Dpre@7)8I5VMK!lW4(!;lfC^}081@8H#l?_iHo^uheWdL$|{m>=WiL@UDQi1h==Ry9kgElJM@`p@&7IUp?mMp10}^ zI!h|s#3A8SA$_OQc8l$UD&k(CUeAO(BL3&R7_R2%4 zc_-wllpLy5B8qNg#% z45dAvNB@3uHO6D-Rt}3o4mj`!)!_`*r=b1OG8)gVi z3ZabfW>Rs5tG&fR3X}?Gv?L;x!7=zSGsqb+EVZMV5Pa$v;i;6%+^3Sn(MmwnW5}0F+B4U2Bk)(<#0J1 zE`yQoNfUzj&UViPLE4Nz-c8={nBS#{^4u4uw z8(Qn{WqoQLr84h4-x{CEYj}5g5;|+pbZCS%w0Qn3D~C@I#zHllp#V}xdu+%GvX+u(CSeo-2WR=Q zToMb(29HSTi{#&@=fAQ}?a^@v*CBAT1o{ukegYh&3zS?Ai$|wws29#FM1U$*urW8G z)CEU4T3i_&nGmyzu4B(f)YN?ANVM@-HUGFSpb5=m?jlj&wVvbVqb@z~d)c3Z7u zIee+Y%(sn)W}8ZXXPn~}ev{g0bXI9yoHXuw70^4#lq!3@L2I~Ah)^|8#+QkeCSX97 zN|FQFqY$~q*CEmXSV*%RRnHEhY2Xwca? zy7I+*1zVsesz<7_iyS@UqEpDQ=@d1G6{tv-gbSEJjmn-=yJ&(zuiClS( z1pLHI&4J&dA?g@D)ENPj&_*l$8El@H9S<19j1N}e*VnJ16aD=8qeUD$g`WpMKx7zv zWPbEW<$Xx*=*WXB@f~s6FQ3W<27y#82U-+<5Qy;D5<3A70x4=T>cWu2T}Nk)fU^-i zPPRV!^l8{5g7e7%(L7=$f?*uq1_pVh0N=3}Q29${v&p1B5kL26w$4*26dH|!aEo@7kZ4@s8_ktFi5)w{d@r6Dh3jKLK~{1x21TGk z954Y*p$ewb>H2#+gkEcT-M9h~v54U!zMp`MkxAZ?L^Gi)63Xla^}`Y(WEMou3U*{u z6asDai$hk1(4Jrmm{M8@D@cz5^46W6YhK=ey@to(9et9lk24G6u&zI!Kk}-n(1H5@U zZytlCqsmc77MxNts1&$3K(|+5cDMxNLd!}}nRJCv<;vr7OzyUBQ`>QMOvokA)B5V= zn}P)eBA#1$tb|R|j$rplWOMii)-)0~b=2frc1+1wcLSLe$_`*^`Ff#DBZAX7YAz@( z62>~^x)}oEf@v;lD1U5M^M`)7>42D~6sYQ?8TrGNTxaJwP2MVQE~u9esxYQfqs3!P zQXr#?njOh}CzF@Kn@Tsd|%|@TVfd{G28Pr?iv3g)VJqTSJ;8T8yBet&MLF zRt-3j@KLp8X0$y0J%&+ zQUV_mgU7*JSl)Zm#x&235^*p^8KScD*L)Ny9nmX6&=v`8bnt5auH9N+=D5KPXx zon-D*oMPXQt5YM%w4q`O18O1P6~3o5_OhH1x;?$UqasCwDu~AEby-yC8;$9x$v~)x zM$kF7FMHjn;ngNL9Fngao*5Wi$H!=7E|c#S{y`kT1BgZJ$5v~ZUSQ1%@Gj<-k3jM% zJQOF(==qp8>f>RwT9a3H8MKcK?uHSBWH7K5Mcm9D7OlI|oWN_H{7^esqo*^o1T&UV)m*5*0JU*0zOH;k0f~SPixzbb* z_&|1=GRUo3VDfUP>@au^V zL3QY5mLY&llIi8~3_lv3iPCwfGCxe3%&sX+$6SD(A=T5dY+8-bXJM*TSv9v+kPr9L6&TuEqg|I3S!GH z$$(p%6-7FgBxA35DkH(mO#(lY{3}=yPY2FGb_9sXo;G#&venb4wTG%RRy^2+_M{e zvU+M$#h;X5Quur|N#~5%lPcKGv@}Wq17xN$#`6sZBc@G?sb}U>Q|V5ua%8)wynN@Te=svCRz^&YQz{>s?az#V>lHCr6+SMSrm;GTB4Dtt8&_jHzgSzvM1E|h(_^v7P^ z(#h0BWhQ(A(TZ1&K8Lt^m^ZJrj|_XO>~E#{4p8my*IQ}6D}dc+g;Ma!*xT3@oa!DT z9BT^iT@wn~?mqJiG<95PTa&KgQ^hOcd=SmrT-XH6+*(dJXTVpIHpSr32-kCBSfI-e zd!0yiC|XFwXxC$j6&BOPwli^zEA7avn!IgnW@sV{cZH*eZI>)($6;eDV>{1H+d=L^Kev6aIp6n}yE4lgdU|D$e!|o75tsSMr%c)I=;c44nH1T!UN|4MAV+N*g zdNgM2ZrEPb+@)XHCrSz=1DP6xB3Y_v4(QX}Er7C}4{iUr6%y;QQ8#Ux<$`dt z3OfK-mhoR-hD*Y1n+>KGsvt*gz?GvWI0X;z&!oq+mUeJm1Qt}{Nq?H0>mMxyW#)9$ zIoMOOh_Ib0z;FQbcJDD2Xsr2v*U|c9f+nD4NS))4~`Gp!jp9me zo&*Bd>LV2-qOm>>gr1s(FPV%P3fOsIk<^Yi3vo%N6dC0$Je3q6{!<9JY(O57&}3~D zz%8Hi`B;X77^+cxREF4gmO-PMuKU8`KcK)oLMP#`NXkK6m8zgxU`52pUE&onnOJag zqHt9Ulx^+lh#<(+X_{@B6R>+aHKeLub+vj$J;>7Z#89P3)FG?`9{aHsMbZ+(wry|I zkQe)PRvYO!rYn;9t6olcsl^N( z5NJ512jE^ZEtkZ+0zEko@5Zyof;&Z-I4}Ug3`W5Iy^uu6?J^3fYyB-O!HScjH5N&$ zIJ_WXTHzDZ^DZtI+wKXvu+Yeik6d2byl5^Hc&A0bAP*~{N(tiN%V0vQ!_>{d5jJ$Y zI#BKH&97Na9E_`e<*GU-pS8%K;Dl7{VG61~E*FRs zR7D8Gz?8~RylGj~;4NV#S*Ny7_Thu>FKl;Ib}idXr&1=7}OWK zsjz13f|1Z!3|b?tXe-?qP?s@k*>VVpWUNLnwF>~1CMN4ix)M#wiew->CJ*5IX{Te7 zav`S@$g^w|&$&BTba6sR43fG98Cg@oceXS$Db%IN5>V6?)RAHZK|Re5iVn%Zlp~bi z(^}+rd!n{bx+=~z0=h~@7MCWvk75x4E|17G-6HLZe0wd|Qk^SegpI9|_gxsNf{1&m zo_PToJ6=EyEe@fHfrkPME>rOrr|0t>6gXd})S{9Q zCFl!#8xmM!1(LHdQDw}fa(*g96|>wHO^9oh%`Fjz;aFg>nc3wCZ9_6{k!n!p17o2o zl&vrnxa6G1G-j!3&B}$cOZ?G>CGs#ctu3a2{}88`~bNkpfg2Y$%BZJRG45<%0|mQ$JvUkHdAXk zqqz|7Uu)K6TFOMn?MkyW?KBalsLz|>VJ{srO*>C1^D>(crl_NwDZFx`UTZ>0?^m*l z$lUVA8Z^bVV9dfAI)`(cPYZ9xA*m+A?UGY&=j`D&2OSSdpp#$4Q zc|O_ArXMY|V%7xcpDu+NM3)|gp{gi!6(>TeW#z0U3di?w3#=rgR*_p>-VpwYZ;P5W zq?E|)2tlt~Z2MbiEIQ9-g_q@dMio{{#8j3_1|^+B5OSJ6Nd<4Ev???Vl_POU=akh8 z+Buag0bJ_9_nX^gEO3CMMl303sz6K5;*vvwMO8y_E!X&R?+q|d4%09rV_c-rlL4uz z+>nHfn6T$sY+Ia$x3JeX5heM>qY*bH@3j8w> z4r2^qTE&$u+0a;M3yE|Cj9Yrww8)awtIW68=!PGYh2(+*XH7DPwfqJF@l(LSEm@^5 z23#qi0a1dQlrc1&gJpCbaTcNoX2M2PP7JRayP!sBq)1a!$ul)(ib>LT%ybx7{Dae! z4z$O9rcog)gyX6b1*2;TH2VLsl`|F>Ni{{5 z*@5<&`#Xw>>Z(*5ZyLmM#8#gU5qE1j)k5)xQMc z{b9d%t~@gw=D~+2=_jvw-us8Q^o{U*vt2&cJP|(94Eh(EYv=j7hah;@Pn-Ys{vJ;L z9e%a3zI@01-sRKj)6skH^8-E};BWO_FIfAE_h2J@zIo}<;MN-jgy5gvd7mFR-2S!y zqTgu0@#FUd-}E2!ANHRJ<4uSBn2+s#=Rf8D#1A{S_a5^f_1)uzX7|~+yL5)p--;fJ zei;3LN02|!dix*a-zLwrX7Bu5a#tdc&nM3$XTFy_oP_gVH2t%0eao9yRvxvOp|Li< z$NN`w*{`C + + + meshoptimizer - demo + + + + + + + + + +
+ + + + + + + + diff --git a/Lib/meshoptimizer/demo/repaint.js b/Lib/meshoptimizer/demo/repaint.js new file mode 100644 index 0000000..0eb7aa4 --- /dev/null +++ b/Lib/meshoptimizer/demo/repaint.js @@ -0,0 +1,593 @@ +// GPU color/texture transfer for remeshing demo +// +// Remeshed atlas is rasterized in UV space, the triangle edges are expanded to cover extra gutter space. +// The fragment shader traces rays against the original mesh (falling back to closest point on miss). +// The results are shaded using a simplified version of the original material, and written to the output texture. +// Texels that were not rasterized are infilled afterwards using the mips of the resulting texture. +// When painting vertex colors, we directly rasterize points for target vertex/triangles and trace rays/eval materials as above. +import * as THREE from 'three'; +import { MeshBVH, BVHShaderGLSL, MeshBVHUniformStruct, FloatVertexAttributeTexture, UIntVertexAttributeTexture } from 'three-mesh-bvh'; + +function expandGeometry(geometry, size, gutter) { + var position = geometry.attributes.position; + var normal = geometry.attributes.normal; + var tangent = geometry.attributes.tangent; + var uv = geometry.attributes.uv; + var indices = geometry.index.array; + + var count = indices.length; + var positions = new Float32Array(count * 3); + var normals = new Float32Array(count * 3); + var tangents = tangent ? new Float32Array(count * 4) : null; + var uvs = new Float32Array(count * 2); + + for (var i = 0; i < count; i += 3) { + var a = indices[i], + b = indices[i + 1], + c = indices[i + 2]; + var x0 = uv.getX(a) * size, + y0 = uv.getY(a) * size; + var x1 = uv.getX(b) * size, + y1 = uv.getY(b) * size; + var x2 = uv.getX(c) * size, + y2 = uv.getY(c) * size; + + // shift corners away from the opposite edge center + var d0 = Math.hypot(x0 * 2 - x1 - x2, y0 * 2 - y1 - y2); + var d1 = Math.hypot(x1 * 2 - x0 - x2, y1 * 2 - y0 - y2); + var d2 = Math.hypot(x2 * 2 - x0 - x1, y2 * 2 - y0 - y1); + var e0 = d0 == 0 ? 0 : gutter / d0; + var e1 = d1 == 0 ? 0 : gutter / d1; + var e2 = d2 == 0 ? 0 : gutter / d2; + + for (var j = 0; j < 3; ++j) { + // expand each triangle in barycentric space to accommodate extra gutter width + var ej = j == 0 ? e0 : j == 1 ? e1 : e2; + var w0 = j == 0 ? 1 + ej * 2 : -ej; + var w1 = j == 1 ? 1 + ej * 2 : -ej; + var w2 = j == 2 ? 1 + ej * 2 : -ej; + + // interpolate all attributes; we will use UVs to rasterize and positions/normals to trace against the original mesh + var v = i + j; + positions[v * 3 + 0] = position.getX(a) * w0 + position.getX(b) * w1 + position.getX(c) * w2; + positions[v * 3 + 1] = position.getY(a) * w0 + position.getY(b) * w1 + position.getY(c) * w2; + positions[v * 3 + 2] = position.getZ(a) * w0 + position.getZ(b) * w1 + position.getZ(c) * w2; + + normals[v * 3 + 0] = normal.getX(a) * w0 + normal.getX(b) * w1 + normal.getX(c) * w2; + normals[v * 3 + 1] = normal.getY(a) * w0 + normal.getY(b) * w1 + normal.getY(c) * w2; + normals[v * 3 + 2] = normal.getZ(a) * w0 + normal.getZ(b) * w1 + normal.getZ(c) * w2; + + if (tangent) { + // tangents are only used when normal maps are baked + tangents[v * 4 + 0] = tangent.getX(a) * w0 + tangent.getX(b) * w1 + tangent.getX(c) * w2; + tangents[v * 4 + 1] = tangent.getY(a) * w0 + tangent.getY(b) * w1 + tangent.getY(c) * w2; + tangents[v * 4 + 2] = tangent.getZ(a) * w0 + tangent.getZ(b) * w1 + tangent.getZ(c) * w2; + tangents[v * 4 + 3] = tangent.getW(a) * w0 + tangent.getW(b) * w1 + tangent.getW(c) * w2; + } + + uvs[v * 2 + 0] = (x0 * w0 + x1 * w1 + x2 * w2) / size; + uvs[v * 2 + 1] = (y0 * w0 + y1 * w1 + y2 * w2) / size; + } + } + + var result = new THREE.BufferGeometry(); + result.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + result.setAttribute('normal', new THREE.BufferAttribute(normals, 3)); + result.setAttribute('uv', new THREE.BufferAttribute(uvs, 2)); + if (tangent) result.setAttribute('tangent', new THREE.BufferAttribute(tangents, 4)); + return result; +} + +function sampleGeometry(geometry, size) { + var position = geometry.attributes.position; + var normal = geometry.attributes.normal; + var indices = geometry.index.array; + + var vertices = position.count; + var triangles = indices.length / 3; + var samples = vertices + triangles; // one sample per vertex plus one sample per triangle centroid + + var positions = new Float32Array(samples * 3); + var normals = new Float32Array(samples * 3); + var uvs = new Float32Array(samples * 2); + + for (var i = 0; i < vertices; ++i) { + for (var k = 0; k < 3; ++k) { + positions[i * 3 + k] = position.getComponent(i, k); + normals[i * 3 + k] = normal.getComponent(i, k); + } + } + + for (var i = 0; i < triangles; ++i) { + var a = indices[i * 3 + 0], + b = indices[i * 3 + 1], + c = indices[i * 3 + 2]; + + // normals will be normalized in the shader + for (var k = 0; k < 3; ++k) { + positions[(vertices + i) * 3 + k] = (position.getComponent(a, k) + position.getComponent(b, k) + position.getComponent(c, k)) / 3; + normals[(vertices + i) * 3 + k] = normal.getComponent(a, k) + normal.getComponent(b, k) + normal.getComponent(c, k); + } + } + + for (var i = 0; i < samples; ++i) { + uvs[i * 2 + 0] = ((i % size) + 0.5) / size; + uvs[i * 2 + 1] = (Math.floor(i / size) + 0.5) / size; + } + + var result = new THREE.BufferGeometry(); + result.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + result.setAttribute('normal', new THREE.BufferAttribute(normals, 3)); + result.setAttribute('uv', new THREE.BufferAttribute(uvs, 2)); + return result; +} + +function extractColors(data, colors, indices, facets) { + var vertices = colors.length / 4; + var triangles = indices.length / 3; + + for (var i = 0; i < vertices; ++i) { + if (data[i * 4 + 3] == 0) continue; + + for (var k = 0; k < 3; ++k) colors[i * 4 + k] = Math.pow(data[i * 4 + k] / 255, 2.2); + colors[i * 4 + 3] = 1; + } + + for (var i = 0; i < triangles; ++i) { + var offset = (vertices + i) * 4; + if (data[offset + 3] == 0) continue; + + for (var j = 0; j < 3; ++j) { + var v = indices[i * 3 + j]; + + for (var k = 0; k < 3; ++k) colors[v * 4 + k] += Math.pow(data[offset + k] / 255, 2.2); + colors[v * 4 + 3]++; + } + } + + for (var i = 0; i < colors.length; i += 4) { + var weight = colors[i + 3]; + if (weight == 0) continue; + + for (var k = 0; k < 3; ++k) colors[i + k] /= weight; + colors[i + 3] = 1; + } + + if (facets) { + for (var i = 0; i < triangles; ++i) { + var a = indices[i * 3 + 0], + b = indices[i * 3 + 1], + c = indices[i * 3 + 2]; + + for (var k = 0; k < 3; ++k) { + var avg = (colors[a * 4 + k] + colors[b * 4 + k] + colors[c * 4 + k]) / 3; + colors[a * 4 + k] = colors[b * 4 + k] = colors[c * 4 + k] = avg; + } + } + } +} + +var MAP_SIZE = 1024; +var MAP_BUDGET = 1 << 30; +var MAP_SLOTS = ['map', 'aoMap', 'emissiveMap', 'roughnessMap', 'metalnessMap', 'normalMap']; + +function createTextureArray(materials) { + var textures = []; + var layers = new Map(); + + for (var i = 0; i < materials.length; ++i) { + for (var j = 0; j < MAP_SLOTS.length; ++j) { + var texture = materials[i][MAP_SLOTS[j]]; + + // deduplicate images by source UUID to avoid wasting layers for reused images + if (texture && texture.image && !layers.has(texture.source)) { + layers.set(texture.source, textures.length); + textures.push(texture); + } + } + } + + var depth = Math.max(textures.length, 1); + var size = MAP_SIZE; + + // prevent accidental catastrophic memory use by downscaling texture array until it fits + while (size > 4 && depth * size * size * 4 > MAP_BUDGET) size /= 2; + + // all images need to be resized to the same size because we need all of them in one texture array + var canvas = document.createElement('canvas'); + canvas.width = canvas.height = size; + var context = canvas.getContext('2d', { willReadFrequently: true }); + + var data = new Uint8Array(size * size * 4 * depth); + + for (var i = 0; i < textures.length; ++i) { + context.clearRect(0, 0, size, size); + context.drawImage(textures[i].image, 0, 0, size, size); + data.set(context.getImageData(0, 0, size, size).data, i * size * size * 4); + } + + var result = new THREE.DataArrayTexture(data, size, size, depth); + result.wrapS = THREE.RepeatWrapping; + result.wrapT = THREE.RepeatWrapping; + result.minFilter = THREE.LinearFilter; + result.magFilter = THREE.LinearFilter; + result.needsUpdate = true; + + return { array: result, layers: layers }; +} + +function createMaterialTexture(materials, layers) { + var stride = 24; + var count = Math.max(materials.length, 1); + var data = new Float32Array(count * stride); + + function getLayer(material, slot) { + var texture = material[slot]; + return texture && layers.has(texture.source) ? layers.get(texture.source) : -1; + } + + for (var i = 0; i < materials.length; ++i) { + var material = materials[i]; + var map = material.map; + + // simplify UV transform by only taking repeat/offset from main texture + data[i * stride + 0] = map ? map.repeat.x : 1; + data[i * stride + 1] = map ? map.repeat.y : 1; + data[i * stride + 2] = map ? map.offset.x : 0; + data[i * stride + 3] = map ? map.offset.y : 0; + + // layer indices + for (var j = 0; j < MAP_SLOTS.length; ++j) { + data[i * stride + 4 + j] = getLayer(material, MAP_SLOTS[j]); + } + + // material parameters + data[i * stride + 12] = material.color ? material.color.r : 1; + data[i * stride + 13] = material.color ? material.color.g : 1; + data[i * stride + 14] = material.color ? material.color.b : 1; + data[i * stride + 15] = material.aoMapIntensity !== undefined ? material.aoMapIntensity : 1; + + data[i * stride + 16] = material.emissive ? material.emissive.r * material.emissiveIntensity : 0; + data[i * stride + 17] = material.emissive ? material.emissive.g * material.emissiveIntensity : 0; + data[i * stride + 18] = material.emissive ? material.emissive.b * material.emissiveIntensity : 0; + + data[i * stride + 20] = material.metalness !== undefined ? material.metalness : 0; + data[i * stride + 21] = material.roughness !== undefined ? material.roughness : 1; + data[i * stride + 22] = material.normalScale ? material.normalScale.x : 1; + } + + var texture = new THREE.DataTexture(data, stride / 4, count, THREE.RGBAFormat, THREE.FloatType); + texture.needsUpdate = true; + return texture; +} + +var bakeShader = new THREE.ShaderMaterial({ + glslVersion: THREE.GLSL3, + uniforms: { + bvh: { value: null }, + attrNormal: { value: null }, + attrUv: { value: null }, + attrColor: { value: null }, + attrTangent: { value: null }, + attrMaterial: { value: null }, + materials: { value: null }, + textures: { value: null }, + thickness: { value: 0 }, + bakeNormals: { value: 0 }, + bakeMaterial: { value: 0 }, + }, + vertexShader: ` +attribute vec4 tangent; + +varying vec3 vPosition; +varying vec3 vNormal; +varying vec4 vTangent; + +void main() { + vPosition = position; + vNormal = normal; + vTangent = tangent; + + gl_Position = vec4(uv * 2.0 - 1.0, 0.0, 1.0); + gl_PointSize = 1.0; // for vertex colors +} +`, + fragmentShader: ` +precision highp isampler2D; +precision highp usampler2D; +precision highp sampler2DArray; + +${BVHShaderGLSL.common_functions} +${BVHShaderGLSL.bvh_struct_definitions} +${BVHShaderGLSL.bvh_ray_functions} +${BVHShaderGLSL.bvh_distance_functions} + +uniform BVH bvh; +uniform sampler2D attrNormal; +uniform sampler2D attrUv; +uniform sampler2D attrColor; +uniform sampler2D attrTangent; +uniform usampler2D attrMaterial; +uniform sampler2D materials; +uniform sampler2DArray textures; +uniform float thickness; +uniform float bakeNormals; +uniform float bakeMaterial; + +vec3 fromsrgb(vec3 c) { + return pow(c, vec3(2.2)); +} + +vec3 tosrgb(vec3 c) { + return pow(c, vec3(1.0 / 2.2)); +} + +varying vec3 vPosition; +varying vec3 vNormal; +varying vec4 vTangent; + +layout(location = 0) out vec4 outColor; +layout(location = 1) out vec4 outNormal; +layout(location = 2) out vec4 outMaterial; + +void main() { + vec3 normal = normalize(vNormal); + + uvec4 face = uvec4(0u); + vec3 faceNormal = vec3(0.0), bary = vec3(0.0), closest = vec3(0.0); + float side = 1.0, dist = 0.0; + float maxClosest = thickness * 10.0; + + if (bvhIntersectFirstHit(bvh, vPosition + normal * thickness, -normal, face, faceNormal, bary, side, dist) && dist < thickness * 2.0) { + // ray hit + } else if (bvhClosestPointToPoint(bvh, vPosition, maxClosest, face, faceNormal, bary, side, closest) < maxClosest) { + // closest point found; note, bary is ordered incorrectly in three-mesh-bvh for closest point so we need to swizzle it + bary = bary.zxy; + } else { + discard; + } + + vec2 uv = textureSampleBarycoord(attrUv, bary, face.xyz).xy; + int mati = int(uTexelFetch1D(attrMaterial, face.x).r); + + vec4 md0 = texelFetch(materials, ivec2(0, mati), 0); + vec4 md1 = texelFetch(materials, ivec2(1, mati), 0); + vec4 md2 = texelFetch(materials, ivec2(2, mati), 0); + vec4 md3 = texelFetch(materials, ivec2(3, mati), 0); + vec4 md4 = texelFetch(materials, ivec2(4, mati), 0); + vec4 md5 = texelFetch(materials, ivec2(5, mati), 0); + + // md0: uv transform, md1.xyzw + md2.xy: layer indices in MAP_SLOTS order, + // md3: base color + ao intensity, md4.xyz: emissive color, md5.xyz: metalness + roughness + normal scale + vec2 point = uv * md0.xy + md0.zw; + + vec3 color = md3.rgb; + if (md1.x >= 0.0) color *= fromsrgb(texture(textures, vec3(point, md1.x)).rgb); + + // vertex colors are linear, and are white when the source mesh or material doesn't use them + color *= textureSampleBarycoord(attrColor, bary, face.xyz).rgb; + + if (md1.y >= 0.0) { + float occlusion = texture(textures, vec3(point, md1.y)).r; + color *= 1.0 + md3.w * (occlusion - 1.0); + } + + float roughness = md5.y; + float metalness = md5.x; + if (md1.w >= 0.0) roughness *= texture(textures, vec3(point, md1.w)).g; + if (md2.x >= 0.0) metalness *= texture(textures, vec3(point, md2.x)).b; + + // approximate metalness/roughness impact when material is not baked + if (bakeMaterial < 0.5) color *= 1.0 - metalness * (1.0 - roughness * roughness * roughness); + + vec3 emissive = md4.rgb; + if (md1.z >= 0.0) emissive *= fromsrgb(texture(textures, vec3(point, md1.z)).rgb); + color += emissive; + + outColor = vec4(tosrgb(color), 1.0); + outNormal = vec4(0.0); + outMaterial = vec4(0.0, roughness, metalness, 1.0); + + if (bakeNormals > 0.5) { + vec3 snormal = textureSampleBarycoord(attrNormal, bary, face.xyz).xyz; + + if (md2.y >= 0.0) { + vec4 stangent = textureSampleBarycoord(attrTangent, bary, face.xyz); + vec3 bitangent = cross(snormal, stangent.xyz) * (stangent.w < 0.0 ? -1.0 : 1.0); + vec3 nmap = texture(textures, vec3(point, md2.y)).xyz * 2.0 - 1.0; + nmap.xy *= md5.z; + + // replace source (smooth) normal with perturbed normal + snormal = stangent.xyz * nmap.x + bitangent * nmap.y + snormal * nmap.z; + } + + vec3 axisN = normal; + vec3 axisT = vTangent.xyz - axisN * dot(axisN, vTangent.xyz); + vec3 axisB = cross(axisN, axisT) * (vTangent.w < 0.0 ? -1.0 : 1.0); + + vec3 tbn = vec3(dot(snormal, axisT), dot(snormal, axisB), dot(snormal, axisN)); + outNormal = vec4(normalize(tbn) * 0.5 + 0.5, 1.0); + } +} +`, + depthTest: false, + depthWrite: false, + side: THREE.DoubleSide, +}); + +var infillShader = new THREE.ShaderMaterial({ + glslVersion: THREE.GLSL3, + uniforms: { + map: { value: null }, + }, + vertexShader: ` +void main() { + gl_Position = vec4(vec2(gl_VertexID & 1, gl_VertexID >> 1) * 4.0 - 1.0, 0.0, 1.0); +} +`, + fragmentShader: ` +uniform sampler2D map; + +layout(location = 0) out vec4 result; + +void main() { + ivec2 p = ivec2(gl_FragCoord.xy); + vec2 uv = (vec2(p) + 0.5) / vec2(textureSize(map, 0)); + + // use first texel with non-zero alpha as fallback + vec4 c = texelFetch(map, p, 0); + for (int l = 1; l <= 10 && c.a < 1e-4; ++l) c = textureLod(map, uv, float(l)); + + // filtering results in premultiplied value that we need to unpremultiply + result = vec4(c.rgb / max(c.a, 1e-4), 1.0); +} +`, + depthTest: false, + depthWrite: false, +}); + +function bindCache(cache, thickness, bakeNormals, bakeMaterial) { + bakeShader.uniforms.bvh.value = cache.bvh; + bakeShader.uniforms.attrNormal.value = cache.attributes.normal; + bakeShader.uniforms.attrUv.value = cache.attributes.uv; + bakeShader.uniforms.attrColor.value = cache.attributes.color; + bakeShader.uniforms.attrTangent.value = cache.attributes.tangent; + bakeShader.uniforms.attrMaterial.value = cache.attributes.material; + bakeShader.uniforms.materials.value = cache.materials; + bakeShader.uniforms.textures.value = cache.textures; + bakeShader.uniforms.thickness.value = thickness; + bakeShader.uniforms.bakeNormals.value = bakeNormals ? 1 : 0; + bakeShader.uniforms.bakeMaterial.value = bakeMaterial ? 1 : 0; +} + +function renderSamples(renderer, object, size, count, infill) { + var target = new THREE.WebGLRenderTarget(size, size, { + count: count, + minFilter: THREE.LinearMipmapNearestFilter, // we use linear filtering with explicit lod for infill + depthBuffer: false, + generateMipmaps: infill, + }); + + var infilled = []; + for (var i = 0; infill && i < count; ++i) { + infilled.push(new THREE.WebGLRenderTarget(size, size, { depthBuffer: false })); + } + + renderer.setClearColor(0x000000, 0); + renderer.setRenderTarget(target); + renderer.render(object, new THREE.OrthographicCamera()); + + if (infill) { + var infillQuad = new THREE.Mesh(new THREE.BufferGeometry(), infillShader); + infillQuad.geometry.setDrawRange(0, 3); + infillQuad.frustumCulled = false; + + for (var i = 0; i < count; ++i) { + infillShader.uniforms.map.value = target.textures[i]; + + renderer.setRenderTarget(infilled[i]); + renderer.render(infillQuad, new THREE.OrthographicCamera()); + } + } + + renderer.setRenderTarget(null); + + var data = []; + for (var i = 0; i < count; ++i) { + data.push(new Uint8Array(size * size * 4)); + renderer.readRenderTargetPixels(infill ? infilled[i] : target, 0, 0, size, size, data[i], undefined, infill ? 0 : i); + } + + target.dispose(); + for (var i = 0; i < infilled.length; ++i) infilled[i].dispose(); + + return data; +} + +// data comes from render texture, but we need it as DataTexture to be compatible with canvas/GLB export +function createTexture(data, size, srgb) { + var texture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat); + if (srgb) texture.colorSpace = THREE.SRGBColorSpace; + texture.magFilter = THREE.LinearFilter; + texture.minFilter = THREE.LinearFilter; + texture.needsUpdate = true; + return texture; +} + +export function buildCache(geometry, materials) { + var bvh = new MeshBVH(geometry, { indirect: true }); + + var bvhgpu = new MeshBVHUniformStruct(); + var attrgpu = { + normal: new FloatVertexAttributeTexture(), + uv: new FloatVertexAttributeTexture(), + color: new FloatVertexAttributeTexture(), + tangent: new FloatVertexAttributeTexture(), + material: new UIntVertexAttributeTexture(), + }; + + bvhgpu.updateFrom(bvh); + attrgpu.normal.updateFrom(geometry.attributes.normal); + attrgpu.uv.updateFrom(geometry.attributes.uv); + attrgpu.color.updateFrom(geometry.attributes.color); + attrgpu.tangent.updateFrom(geometry.attributes.tangent); + attrgpu.material.updateFrom(geometry.attributes.material); + + var textures = createTextureArray(materials); + var matgpu = createMaterialTexture(materials, textures.layers); + + return { + bvh: bvhgpu, + attributes: attrgpu, + materials: matgpu, + textures: textures.array, + + dispose: function () { + this.bvh.dispose(); + this.materials.dispose(); + this.textures.dispose(); + + for (var k in this.attributes) this.attributes[k].dispose(); + }, + }; +} + +export function transferTexture(renderer, geometry, cache, thickness, size, gutter, infill, bakeNormals, bakeMaterial) { + var expanded = expandGeometry(geometry, size, gutter); + var mesh = new THREE.Mesh(expanded, bakeShader); + mesh.frustumCulled = false; + + bindCache(cache, thickness, bakeNormals, bakeMaterial); + + var targets = bakeMaterial ? 3 : bakeNormals ? 2 : 1; + var data = renderSamples(renderer, mesh, size, targets, infill); + + expanded.dispose(); + + return { + map: createTexture(data[0], size, true), + normalMap: bakeNormals ? createTexture(data[1], size, false) : null, + materialMap: bakeMaterial ? createTexture(data[2], size, false) : null, + }; +} + +export function transferColors(renderer, geometry, cache, thickness, facets) { + var vertices = geometry.attributes.position.count; + var triangles = geometry.index.array.length / 3; + var size = Math.ceil(Math.sqrt(vertices + triangles)); + + var samples = sampleGeometry(geometry, size); + var points = new THREE.Points(samples, bakeShader); + points.frustumCulled = false; + + bindCache(cache, thickness, false, false); + + var data = renderSamples(renderer, points, size, 1)[0]; + + samples.dispose(); + + var colors = new Float32Array(vertices * 4); + extractColors(data, colors, geometry.index.array, facets); + + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4)); +} diff --git a/Lib/meshoptimizer/demo/simplify.html b/Lib/meshoptimizer/demo/simplify.html new file mode 100644 index 0000000..acb4108 --- /dev/null +++ b/Lib/meshoptimizer/demo/simplify.html @@ -0,0 +1,1102 @@ + + + + meshoptimizer - demo + + + + + + + + +
+ + + + + diff --git a/Lib/meshoptimizer/demo/tests.cpp b/Lib/meshoptimizer/demo/tests.cpp new file mode 100644 index 0000000..9be331a --- /dev/null +++ b/Lib/meshoptimizer/demo/tests.cpp @@ -0,0 +1,3708 @@ +#include "../src/meshoptimizer.h" + +#include +#include +#include +#include + +#include + +// This file uses assert() to verify algorithm correctness +#undef NDEBUG +#include + +struct PV +{ + unsigned short px, py, pz; + unsigned char nu, nv; // octahedron encoded normal, aliases .pw + unsigned short tx, ty; +}; + +// note: 4 6 5 triangle here is a combo-breaker: +// we encode it without rotating, a=next, c=next - this means we do *not* bump next to 6 +// which means that the next triangle can't be encoded via next sequencing! +static const unsigned int kIndexBuffer[] = {0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9}; + +static const unsigned char kIndexDataV0[] = { + 0xe0, 0xf0, 0x10, 0xfe, 0xff, 0xf0, 0x0c, 0xff, 0x02, 0x02, 0x02, 0x00, 0x76, 0x87, 0x56, 0x67, + 0x78, 0xa9, 0x86, 0x65, 0x89, 0x68, 0x98, 0x01, 0x69, 0x00, 0x00 // +}; + +// note: this exercises two features of v1 format, restarts (0 1 2) and last +static const unsigned int kIndexBufferTricky[] = {0, 1, 2, 2, 1, 3, 0, 1, 2, 2, 1, 5, 2, 1, 4}; + +static const unsigned char kIndexDataV1[] = { + 0xe1, 0xf0, 0x10, 0xfe, 0x1f, 0x3d, 0x00, 0x0a, 0x00, 0x76, 0x87, 0x56, 0x67, 0x78, 0xa9, 0x86, + 0x65, 0x89, 0x68, 0x98, 0x01, 0x69, 0x00, 0x00 // +}; + +static const unsigned int kIndexSequence[] = {0, 1, 51, 2, 49, 1000}; + +static const unsigned char kIndexSequenceV1[] = { + 0xd1, 0x00, 0x04, 0xcd, 0x01, 0x04, 0x07, 0x98, 0x1f, 0x00, 0x00, 0x00, 0x00 // +}; + +static const PV kVertexBuffer[] = { + {0, 0, 0, 0, 0, 0, 0}, + {300, 0, 0, 0, 0, 500, 0}, + {0, 300, 0, 0, 0, 0, 500}, + {300, 300, 0, 0, 0, 500, 500}, +}; + +static const unsigned char kVertexDataV0[] = { + 0xa0, 0x01, 0x3f, 0x00, 0x00, 0x00, 0x58, 0x57, 0x58, 0x01, 0x26, 0x00, 0x00, 0x00, 0x01, 0x0c, + 0x00, 0x00, 0x00, 0x58, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x3f, 0x00, + 0x00, 0x00, 0x17, 0x18, 0x17, 0x01, 0x26, 0x00, 0x00, 0x00, 0x01, 0x0c, 0x00, 0x00, 0x00, 0x17, + 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00 // +}; + +static const unsigned char kVertexDataV1[] = { + 0xa1, 0xee, 0xaa, 0xee, 0x00, 0x4b, 0x4b, 0x4b, 0x00, 0x00, 0x4b, 0x00, 0x00, 0x7d, 0x7d, 0x7d, + 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x00, 0x62 // +}; + +// This binary blob is a valid v1 encoding of vertex buffer but it used a custom version of +// the encoder that exercised all features of the format; because of this it is much larger +// and will never be produced by the encoder itself. +static const unsigned char kVertexDataV1Custom[] = { + 0xa1, 0xd4, 0x94, 0xd4, 0x01, 0x0e, 0x00, 0x58, 0x57, 0x58, 0x02, 0x02, 0x12, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0e, 0x00, 0x7d, 0x7d, 0x7d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x62 // +}; + +static void decodeIndexV0() +{ + const size_t index_count = sizeof(kIndexBuffer) / sizeof(kIndexBuffer[0]); + + std::vector buffer(kIndexDataV0, kIndexDataV0 + sizeof(kIndexDataV0)); + + unsigned int decoded[index_count]; + assert(meshopt_decodeIndexBuffer(decoded, index_count, &buffer[0], buffer.size()) == 0); + assert(memcmp(decoded, kIndexBuffer, sizeof(kIndexBuffer)) == 0); +} + +static void decodeIndexV1() +{ + const size_t index_count = sizeof(kIndexBufferTricky) / sizeof(kIndexBufferTricky[0]); + + std::vector buffer(kIndexDataV1, kIndexDataV1 + sizeof(kIndexDataV1)); + + unsigned int decoded[index_count]; + assert(meshopt_decodeIndexBuffer(decoded, index_count, &buffer[0], buffer.size()) == 0); + assert(memcmp(decoded, kIndexBufferTricky, sizeof(kIndexBufferTricky)) == 0); +} + +static void decodeIndexV1More() +{ + const unsigned char input[] = { + 0xe1, 0xf0, 0x10, 0xfe, 0xff, 0xf0, 0x0c, 0xff, 0x02, 0x02, 0x02, 0x00, 0x76, 0x87, 0x56, 0x67, + 0x78, 0xa9, 0x86, 0x65, 0x89, 0x68, 0x98, 0x01, 0x69, 0x00, 0x00 // + }; + + const unsigned int ib[] = {0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9}; + const size_t index_count = sizeof(ib) / sizeof(ib[0]); + + std::vector buffer(input, input + sizeof(input)); + + unsigned int decoded[index_count]; + assert(meshopt_decodeIndexBuffer(decoded, index_count, 4, &buffer[0], buffer.size()) == 0); + assert(memcmp(decoded, ib, sizeof(ib)) == 0); +} + +static void decodeIndexV1ThreeEdges() +{ + const unsigned char input[] = { + 0xe1, 0xf0, 0x20, 0x30, 0x40, 0x00, 0x76, 0x87, 0x56, 0x67, 0x78, 0xa9, 0x86, 0x65, 0x89, 0x68, + 0x98, 0x01, 0x69, 0x00, 0x00 // + }; + + const unsigned int ib[] = {0, 1, 2, 1, 0, 3, 2, 1, 4, 0, 2, 5}; + const size_t index_count = sizeof(ib) / sizeof(ib[0]); + + std::vector buffer(input, input + sizeof(input)); + + unsigned int decoded[index_count]; + assert(meshopt_decodeIndexBuffer(decoded, index_count, 4, &buffer[0], buffer.size()) == 0); + assert(memcmp(decoded, ib, sizeof(ib)) == 0); +} + +static void decodeIndex16() +{ + const size_t index_count = sizeof(kIndexBuffer) / sizeof(kIndexBuffer[0]); + const size_t vertex_count = 10; + + std::vector buffer(meshopt_encodeIndexBufferBound(index_count, vertex_count)); + buffer.resize(meshopt_encodeIndexBuffer(&buffer[0], buffer.size(), kIndexBuffer, index_count)); + + unsigned short decoded[index_count]; + assert(meshopt_decodeIndexBuffer(decoded, index_count, &buffer[0], buffer.size()) == 0); + + for (size_t i = 0; i < index_count; ++i) + assert(decoded[i] == kIndexBuffer[i]); +} + +static void encodeIndexMemorySafe() +{ + const size_t index_count = sizeof(kIndexBuffer) / sizeof(kIndexBuffer[0]); + const size_t vertex_count = 10; + + std::vector buffer(meshopt_encodeIndexBufferBound(index_count, vertex_count)); + buffer.resize(meshopt_encodeIndexBuffer(&buffer[0], buffer.size(), kIndexBuffer, index_count)); + + // check that encode is memory-safe; note that we reallocate the buffer for each try to make sure ASAN can verify buffer access + for (size_t i = 0; i <= buffer.size(); ++i) + { + std::vector shortbuffer(i); + size_t result = meshopt_encodeIndexBuffer(i == 0 ? NULL : &shortbuffer[0], i, kIndexBuffer, index_count); + + if (i == buffer.size()) + assert(result == buffer.size()); + else + assert(result == 0); + } +} + +static void decodeIndexMemorySafe() +{ + const size_t index_count = sizeof(kIndexBuffer) / sizeof(kIndexBuffer[0]); + const size_t vertex_count = 10; + + std::vector buffer(meshopt_encodeIndexBufferBound(index_count, vertex_count)); + buffer.resize(meshopt_encodeIndexBuffer(&buffer[0], buffer.size(), kIndexBuffer, index_count)); + + // check that decode is memory-safe; note that we reallocate the buffer for each try to make sure ASAN can verify buffer access + unsigned int decoded[index_count]; + + for (size_t i = 0; i <= buffer.size(); ++i) + { + std::vector shortbuffer(buffer.begin(), buffer.begin() + i); + int result = meshopt_decodeIndexBuffer(decoded, index_count, i == 0 ? NULL : &shortbuffer[0], i); + + if (i == buffer.size()) + assert(result == 0); + else + assert(result < 0); + } +} + +static void decodeIndexRejectExtraBytes() +{ + const size_t index_count = sizeof(kIndexBuffer) / sizeof(kIndexBuffer[0]); + const size_t vertex_count = 10; + + std::vector buffer(meshopt_encodeIndexBufferBound(index_count, vertex_count)); + buffer.resize(meshopt_encodeIndexBuffer(&buffer[0], buffer.size(), kIndexBuffer, index_count)); + + // check that decoder doesn't accept extra bytes after a valid stream + std::vector largebuffer(buffer); + largebuffer.push_back(0); + + unsigned int decoded[index_count]; + assert(meshopt_decodeIndexBuffer(decoded, index_count, &largebuffer[0], largebuffer.size()) < 0); +} + +static void decodeIndexRejectMalformedHeaders() +{ + const size_t index_count = sizeof(kIndexBuffer) / sizeof(kIndexBuffer[0]); + const size_t vertex_count = 10; + + std::vector buffer(meshopt_encodeIndexBufferBound(index_count, vertex_count)); + buffer.resize(meshopt_encodeIndexBuffer(&buffer[0], buffer.size(), kIndexBuffer, index_count)); + + // check that decoder doesn't accept malformed headers + std::vector brokenbuffer(buffer); + brokenbuffer[0] = 0; + + unsigned int decoded[index_count]; + assert(meshopt_decodeIndexBuffer(decoded, index_count, &brokenbuffer[0], brokenbuffer.size()) < 0); +} + +static void decodeIndexRejectInvalidVersion() +{ + const size_t index_count = sizeof(kIndexBuffer) / sizeof(kIndexBuffer[0]); + const size_t vertex_count = 10; + + std::vector buffer(meshopt_encodeIndexBufferBound(index_count, vertex_count)); + buffer.resize(meshopt_encodeIndexBuffer(&buffer[0], buffer.size(), kIndexBuffer, index_count)); + + // check that decoder doesn't accept invalid version + std::vector brokenbuffer(buffer); + brokenbuffer[0] |= 0x0f; + + unsigned int decoded[index_count]; + assert(meshopt_decodeIndexBuffer(decoded, index_count, &brokenbuffer[0], brokenbuffer.size()) < 0); +} + +static void decodeIndexMalformedVByte() +{ + const unsigned char input[] = { + 0xe1, 0x20, 0x20, 0x20, 0xff, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0xff, 0xff, 0xff, 0xff, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20 // + }; + + unsigned int decoded[66]; + assert(meshopt_decodeIndexBuffer(decoded, 66, input, sizeof(input)) < 0); +} + +static void roundtripIndexTricky() +{ + const size_t index_count = sizeof(kIndexBufferTricky) / sizeof(kIndexBufferTricky[0]); + const size_t vertex_count = 6; + + std::vector buffer(meshopt_encodeIndexBufferBound(index_count, vertex_count)); + buffer.resize(meshopt_encodeIndexBuffer(&buffer[0], buffer.size(), kIndexBufferTricky, index_count)); + + unsigned int decoded[index_count]; + assert(meshopt_decodeIndexBuffer(decoded, index_count, &buffer[0], buffer.size()) == 0); + assert(memcmp(decoded, kIndexBufferTricky, sizeof(kIndexBufferTricky)) == 0); +} + +static void encodeIndexEmpty() +{ + std::vector buffer(meshopt_encodeIndexBufferBound(0, 0)); + buffer.resize(meshopt_encodeIndexBuffer(&buffer[0], buffer.size(), NULL, 0)); + + assert(meshopt_decodeIndexBuffer(static_cast(NULL), 0, &buffer[0], buffer.size()) == 0); +} + +static void decodeIndexSequence() +{ + const size_t index_count = sizeof(kIndexSequence) / sizeof(kIndexSequence[0]); + + std::vector buffer(kIndexSequenceV1, kIndexSequenceV1 + sizeof(kIndexSequenceV1)); + + unsigned int decoded[index_count]; + assert(meshopt_decodeIndexSequence(decoded, index_count, &buffer[0], buffer.size()) == 0); + assert(memcmp(decoded, kIndexSequence, sizeof(kIndexSequence)) == 0); +} + +static void decodeIndexSequence16() +{ + const size_t index_count = sizeof(kIndexSequence) / sizeof(kIndexSequence[0]); + const size_t vertex_count = 1001; + + std::vector buffer(meshopt_encodeIndexSequenceBound(index_count, vertex_count)); + buffer.resize(meshopt_encodeIndexSequence(&buffer[0], buffer.size(), kIndexSequence, index_count)); + + unsigned short decoded[index_count]; + assert(meshopt_decodeIndexSequence(decoded, index_count, &buffer[0], buffer.size()) == 0); + + for (size_t i = 0; i < index_count; ++i) + assert(decoded[i] == kIndexSequence[i]); +} + +static void encodeIndexSequenceMemorySafe() +{ + const size_t index_count = sizeof(kIndexSequence) / sizeof(kIndexSequence[0]); + const size_t vertex_count = 1001; + + std::vector buffer(meshopt_encodeIndexSequenceBound(index_count, vertex_count)); + buffer.resize(meshopt_encodeIndexSequence(&buffer[0], buffer.size(), kIndexSequence, index_count)); + + // check that encode is memory-safe; note that we reallocate the buffer for each try to make sure ASAN can verify buffer access + for (size_t i = 0; i <= buffer.size(); ++i) + { + std::vector shortbuffer(i); + size_t result = meshopt_encodeIndexSequence(i == 0 ? NULL : &shortbuffer[0], i, kIndexSequence, index_count); + + if (i == buffer.size()) + assert(result == buffer.size()); + else + assert(result == 0); + } +} + +static void decodeIndexSequenceMemorySafe() +{ + const size_t index_count = sizeof(kIndexSequence) / sizeof(kIndexSequence[0]); + const size_t vertex_count = 1001; + + std::vector buffer(meshopt_encodeIndexSequenceBound(index_count, vertex_count)); + buffer.resize(meshopt_encodeIndexSequence(&buffer[0], buffer.size(), kIndexSequence, index_count)); + + // check that decode is memory-safe; note that we reallocate the buffer for each try to make sure ASAN can verify buffer access + unsigned int decoded[index_count]; + + for (size_t i = 0; i <= buffer.size(); ++i) + { + std::vector shortbuffer(buffer.begin(), buffer.begin() + i); + int result = meshopt_decodeIndexSequence(decoded, index_count, i == 0 ? NULL : &shortbuffer[0], i); + + if (i == buffer.size()) + assert(result == 0); + else + assert(result < 0); + } +} + +static void decodeIndexSequenceRejectExtraBytes() +{ + const size_t index_count = sizeof(kIndexSequence) / sizeof(kIndexSequence[0]); + const size_t vertex_count = 1001; + + std::vector buffer(meshopt_encodeIndexSequenceBound(index_count, vertex_count)); + buffer.resize(meshopt_encodeIndexSequence(&buffer[0], buffer.size(), kIndexSequence, index_count)); + + // check that decoder doesn't accept extra bytes after a valid stream + std::vector largebuffer(buffer); + largebuffer.push_back(0); + + unsigned int decoded[index_count]; + assert(meshopt_decodeIndexSequence(decoded, index_count, &largebuffer[0], largebuffer.size()) < 0); +} + +static void decodeIndexSequenceRejectMalformedHeaders() +{ + const size_t index_count = sizeof(kIndexSequence) / sizeof(kIndexSequence[0]); + const size_t vertex_count = 1001; + + std::vector buffer(meshopt_encodeIndexSequenceBound(index_count, vertex_count)); + buffer.resize(meshopt_encodeIndexSequence(&buffer[0], buffer.size(), kIndexSequence, index_count)); + + // check that decoder doesn't accept malformed headers + std::vector brokenbuffer(buffer); + brokenbuffer[0] = 0; + + unsigned int decoded[index_count]; + assert(meshopt_decodeIndexSequence(decoded, index_count, &brokenbuffer[0], brokenbuffer.size()) < 0); +} + +static void decodeIndexSequenceRejectInvalidVersion() +{ + const size_t index_count = sizeof(kIndexSequence) / sizeof(kIndexSequence[0]); + const size_t vertex_count = 1001; + + std::vector buffer(meshopt_encodeIndexSequenceBound(index_count, vertex_count)); + buffer.resize(meshopt_encodeIndexSequence(&buffer[0], buffer.size(), kIndexSequence, index_count)); + + // check that decoder doesn't accept invalid version + std::vector brokenbuffer(buffer); + brokenbuffer[0] |= 0x0f; + + unsigned int decoded[index_count]; + assert(meshopt_decodeIndexSequence(decoded, index_count, &brokenbuffer[0], brokenbuffer.size()) < 0); +} + +static void encodeIndexSequenceEmpty() +{ + std::vector buffer(meshopt_encodeIndexSequenceBound(0, 0)); + buffer.resize(meshopt_encodeIndexSequence(&buffer[0], buffer.size(), NULL, 0)); + + assert(meshopt_decodeIndexSequence(static_cast(NULL), 0, &buffer[0], buffer.size()) == 0); +} + +static void decodeVertexV0() +{ + const size_t vertex_count = sizeof(kVertexBuffer) / sizeof(kVertexBuffer[0]); + + std::vector buffer(kVertexDataV0, kVertexDataV0 + sizeof(kVertexDataV0)); + + PV decoded[vertex_count]; + assert(meshopt_decodeVertexBuffer(decoded, vertex_count, sizeof(PV), &buffer[0], buffer.size()) == 0); + assert(memcmp(decoded, kVertexBuffer, sizeof(kVertexBuffer)) == 0); +} + +static void decodeVertexV0More() +{ + const unsigned char expected[] = { + 0, 0, 0, 0, 0, 1, 2, 8, 0, 2, 4, 16, 0, 3, 6, 24, + 0, 4, 8, 32, 0, 5, 10, 40, 0, 6, 12, 48, 0, 7, 14, 56, + 0, 8, 16, 64, 0, 9, 18, 72, 0, 10, 20, 80, 0, 11, 22, 88, + 0, 12, 24, 96, 0, 13, 26, 104, 0, 14, 28, 112, 0, 15, 30, 120 // + }; + + const unsigned char input[] = { + 0xa0, 0x00, 0x01, 0x2a, 0xaa, 0xaa, 0xaa, 0x02, 0x04, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, + 0x03, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00 // + }; + + unsigned char decoded[sizeof(expected)]; + assert(meshopt_decodeVertexBuffer(decoded, 16, 4, input, sizeof(input)) == 0); + assert(memcmp(decoded, expected, sizeof(expected)) == 0); +} + +static void decodeVertexV0Mode2() +{ + const unsigned char expected[] = { + 0, 0, 0, 0, 4, 5, 6, 7, 8, 10, 12, 14, 12, 15, 18, 21, + 16, 20, 24, 28, 20, 25, 30, 35, 24, 30, 36, 42, 28, 35, 42, 49, + 32, 40, 48, 56, 36, 45, 54, 63, 40, 50, 60, 70, 44, 55, 66, 77, + 48, 60, 72, 84, 52, 65, 78, 91, 56, 70, 84, 98, 60, 75, 90, 105 // + }; + + const unsigned char input[] = { + 0xa0, 0x02, 0x08, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x02, 0x0a, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0x02, 0x0c, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x02, 0x0e, 0xee, 0xee, + 0xee, 0xee, 0xee, 0xee, 0xee, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00 // + }; + + unsigned char decoded[sizeof(expected)]; + assert(meshopt_decodeVertexBuffer(decoded, 16, 4, input, sizeof(input)) == 0); + assert(memcmp(decoded, expected, sizeof(expected)) == 0); +} + +static void decodeVertexV1() +{ + const size_t vertex_count = sizeof(kVertexBuffer) / sizeof(kVertexBuffer[0]); + + std::vector buffer(kVertexDataV1, kVertexDataV1 + sizeof(kVertexDataV1)); + + PV decoded[vertex_count]; + assert(meshopt_decodeVertexBuffer(decoded, vertex_count, sizeof(PV), &buffer[0], buffer.size()) == 0); + assert(memcmp(decoded, kVertexBuffer, sizeof(kVertexBuffer)) == 0); +} + +static void decodeVertexV1Custom() +{ + const size_t vertex_count = sizeof(kVertexBuffer) / sizeof(kVertexBuffer[0]); + + std::vector buffer(kVertexDataV1Custom, kVertexDataV1Custom + sizeof(kVertexDataV1Custom)); + + PV decoded[vertex_count]; + assert(meshopt_decodeVertexBuffer(decoded, vertex_count, sizeof(PV), &buffer[0], buffer.size()) == 0); + assert(memcmp(decoded, kVertexBuffer, sizeof(kVertexBuffer)) == 0); +} + +static void decodeVertexV1Deltas() +{ + const unsigned short expected[] = { + 248, 248, 240, 240, 249, 250, 243, 244, 250, 252, 246, 248, 251, 254, 249, 252, + 252, 256, 252, 256, 253, 258, 255, 260, 254, 260, 258, 264, 255, 262, 261, 268, + 256, 264, 264, 272, 257, 262, 267, 268, 258, 260, 270, 264, 259, 258, 273, 260, + 260, 256, 276, 256, 261, 254, 279, 252, 262, 252, 282, 248, 263, 250, 285, 244 // + }; + + const unsigned char input[] = { + 0xa1, 0x99, 0x99, 0x01, 0x2a, 0xaa, 0xaa, 0xaa, 0x02, 0x04, 0x44, 0x44, 0x44, 0x43, 0x33, 0x33, + 0x33, 0x02, 0x06, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x02, 0x08, 0x88, 0x88, 0x88, 0x87, + 0x77, 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xf8, 0x00, 0xf8, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0x01, 0x01 // + }; + + unsigned short decoded[sizeof(expected) / sizeof(expected[0])]; + assert(meshopt_decodeVertexBuffer(decoded, 16, 8, input, sizeof(input)) == 0); + assert(memcmp(decoded, expected, sizeof(expected)) == 0); +} + +static void encodeVertexMemorySafe() +{ + const size_t vertex_count = sizeof(kVertexBuffer) / sizeof(kVertexBuffer[0]); + + std::vector buffer(meshopt_encodeVertexBufferBound(vertex_count, sizeof(PV))); + buffer.resize(meshopt_encodeVertexBuffer(&buffer[0], buffer.size(), kVertexBuffer, vertex_count, sizeof(PV))); + + // check that encode is memory-safe; note that we reallocate the buffer for each try to make sure ASAN can verify buffer access + for (size_t i = 0; i <= buffer.size(); ++i) + { + std::vector shortbuffer(i); + size_t result = meshopt_encodeVertexBuffer(i == 0 ? NULL : &shortbuffer[0], i, kVertexBuffer, vertex_count, sizeof(PV)); + + if (i == buffer.size()) + assert(result == buffer.size()); + else + assert(result == 0); + } +} + +static void decodeVertexMemorySafe() +{ + const size_t vertex_count = sizeof(kVertexBuffer) / sizeof(kVertexBuffer[0]); + + std::vector buffer(meshopt_encodeVertexBufferBound(vertex_count, sizeof(PV))); + buffer.resize(meshopt_encodeVertexBuffer(&buffer[0], buffer.size(), kVertexBuffer, vertex_count, sizeof(PV))); + + // check that decode is memory-safe; note that we reallocate the buffer for each try to make sure ASAN can verify buffer access + PV decoded[vertex_count]; + + for (size_t i = 0; i <= buffer.size(); ++i) + { + std::vector shortbuffer(buffer.begin(), buffer.begin() + i); + int result = meshopt_decodeVertexBuffer(decoded, vertex_count, sizeof(PV), i == 0 ? NULL : &shortbuffer[0], i); + (void)result; + + if (i == buffer.size()) + assert(result == 0); + else + assert(result < 0); + } +} + +static void decodeVertexRejectExtraBytes() +{ + const size_t vertex_count = sizeof(kVertexBuffer) / sizeof(kVertexBuffer[0]); + + std::vector buffer(meshopt_encodeVertexBufferBound(vertex_count, sizeof(PV))); + buffer.resize(meshopt_encodeVertexBuffer(&buffer[0], buffer.size(), kVertexBuffer, vertex_count, sizeof(PV))); + + // check that decoder doesn't accept extra bytes after a valid stream + std::vector largebuffer(buffer); + largebuffer.push_back(0); + + PV decoded[vertex_count]; + assert(meshopt_decodeVertexBuffer(decoded, vertex_count, sizeof(PV), &largebuffer[0], largebuffer.size()) < 0); +} + +static void decodeVertexRejectMalformedHeaders() +{ + const size_t vertex_count = sizeof(kVertexBuffer) / sizeof(kVertexBuffer[0]); + + std::vector buffer(meshopt_encodeVertexBufferBound(vertex_count, sizeof(PV))); + buffer.resize(meshopt_encodeVertexBuffer(&buffer[0], buffer.size(), kVertexBuffer, vertex_count, sizeof(PV))); + + // check that decoder doesn't accept malformed headers + std::vector brokenbuffer(buffer); + brokenbuffer[0] = 0; + + PV decoded[vertex_count]; + assert(meshopt_decodeVertexBuffer(decoded, vertex_count, sizeof(PV), &brokenbuffer[0], brokenbuffer.size()) < 0); +} + +static void decodeVertexBitGroups() +{ + unsigned char data[16 * 4]; + + // this tests 0/2/4/8 bit groups in one stream + for (size_t i = 0; i < 16; ++i) + { + data[i * 4 + 0] = 0; + data[i * 4 + 1] = (unsigned char)(i * 1); + data[i * 4 + 2] = (unsigned char)(i * 2); + data[i * 4 + 3] = (unsigned char)(i * 8); + } + + std::vector buffer(meshopt_encodeVertexBufferBound(16, 4)); + buffer.resize(meshopt_encodeVertexBuffer(&buffer[0], buffer.size(), data, 16, 4)); + + unsigned char decoded[16 * 4]; + assert(meshopt_decodeVertexBuffer(decoded, 16, 4, &buffer[0], buffer.size()) == 0); + assert(memcmp(decoded, data, sizeof(data)) == 0); +} + +static void decodeVertexBitGroupSentinels() +{ + unsigned char data[16 * 4]; + + // this tests 0/2/4/8 bit groups and sentinels in one stream + for (size_t i = 0; i < 16; ++i) + { + if (i == 7 || i == 13) + { + data[i * 4 + 0] = 42; + data[i * 4 + 1] = 42; + data[i * 4 + 2] = 42; + data[i * 4 + 3] = 42; + } + else + { + data[i * 4 + 0] = 0; + data[i * 4 + 1] = (unsigned char)(i * 1); + data[i * 4 + 2] = (unsigned char)(i * 2); + data[i * 4 + 3] = (unsigned char)(i * 8); + } + } + + std::vector buffer(meshopt_encodeVertexBufferBound(16, 4)); + buffer.resize(meshopt_encodeVertexBuffer(&buffer[0], buffer.size(), data, 16, 4)); + + unsigned char decoded[16 * 4]; + assert(meshopt_decodeVertexBuffer(decoded, 16, 4, &buffer[0], buffer.size()) == 0); + assert(memcmp(decoded, data, sizeof(data)) == 0); +} + +static void decodeVertexBitGroupSentinelCount() +{ + const unsigned char expected[13 * 4] = { + 0xff, 0, 0, 0, 0xfe, 0, 0, 0, 0xfd, 0, 0, 0, 0xfd, 0, 0, 0, + 0xfd, 0, 0, 0, 0xfc, 0, 0, 0, 0xfb, 0, 0, 0, 0xfb, 0, 0, 0, + 0xfa, 0, 0, 0, 0xfa, 0, 0, 0, 0xf9, 0, 0, 0, 0xf9, 0, 0, 0, + 0xf8, 0, 0, 0 // + }; + + // encodes several 2-bit sentinels including lane 12; lane 3 is clear so bit 30 does not alias bit 0 during counting + const unsigned char input[] = { + 0xa1, 0xa9, + 0x01, 0xfc, 0x3c, 0xcc, 0xcc, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 // + }; + + unsigned char decoded[sizeof(expected)]; + assert(meshopt_decodeVertexBuffer(decoded, 13, 4, input, sizeof(input)) == 0); + assert(memcmp(decoded, expected, sizeof(expected)) == 0); +} + +static void decodeVertexDeltas() +{ + unsigned short data[16 * 4]; + + // this forces wider deltas by using values that cross byte boundary + for (size_t i = 0; i < 16; ++i) + { + data[i * 4 + 0] = (unsigned short)(0xf8 + i * 1); + data[i * 4 + 1] = (unsigned short)(0xf8 + (i < 8 ? i : 16 - i) * 2); + data[i * 4 + 2] = (unsigned short)(0xf0 + i * 3); + data[i * 4 + 3] = (unsigned short)(0xf0 + (i < 8 ? i : 16 - i) * 4); + } + + std::vector buffer(meshopt_encodeVertexBufferBound(16, 8)); + buffer.resize(meshopt_encodeVertexBufferLevel(&buffer[0], buffer.size(), data, 16, 8, 2, -1)); + + unsigned short decoded[16 * 4]; + assert(meshopt_decodeVertexBuffer(decoded, 16, 8, &buffer[0], buffer.size()) == 0); + assert(memcmp(decoded, data, sizeof(data)) == 0); +} + +static void decodeVertexBitXor() +{ + unsigned int data[16 * 4]; + + // this forces xors by using bit values at an offset + for (size_t i = 0; i < 16; ++i) + { + data[i * 4 + 0] = unsigned(i << 0); + data[i * 4 + 1] = unsigned(i << 2); + data[i * 4 + 2] = unsigned(i << 15); + data[i * 4 + 3] = unsigned(i << 28); + } + + std::vector buffer(meshopt_encodeVertexBufferBound(16, 16)); + buffer.resize(meshopt_encodeVertexBufferLevel(&buffer[0], buffer.size(), data, 16, 16, 3, -1)); + + unsigned int decoded[16 * 4]; + assert(meshopt_decodeVertexBuffer(decoded, 16, 16, &buffer[0], buffer.size()) == 0); + assert(memcmp(decoded, data, sizeof(data)) == 0); +} + +static void decodeVertexLarge() +{ + unsigned char data[128 * 4]; + + // this tests 0/2/4/8 bit groups in one stream + for (size_t i = 0; i < 128; ++i) + { + data[i * 4 + 0] = 0; + data[i * 4 + 1] = (unsigned char)(i * 1); + data[i * 4 + 2] = (unsigned char)(i * 2); + data[i * 4 + 3] = (unsigned char)(i * 8); + } + + std::vector buffer(meshopt_encodeVertexBufferBound(128, 4)); + buffer.resize(meshopt_encodeVertexBuffer(&buffer[0], buffer.size(), data, 128, 4)); + + unsigned char decoded[128 * 4]; + assert(meshopt_decodeVertexBuffer(decoded, 128, 4, &buffer[0], buffer.size()) == 0); + assert(memcmp(decoded, data, sizeof(data)) == 0); +} + +static void decodeVertexSmall() +{ + unsigned char data[13 * 4]; + + // this tests 0/2/4/8 bit groups in one stream + for (size_t i = 0; i < 13; ++i) + { + data[i * 4 + 0] = 0; + data[i * 4 + 1] = (unsigned char)(i * 1); + data[i * 4 + 2] = (unsigned char)(i * 2); + data[i * 4 + 3] = (unsigned char)(i * 8); + } + + std::vector buffer(meshopt_encodeVertexBufferBound(13, 4)); + buffer.resize(meshopt_encodeVertexBuffer(&buffer[0], buffer.size(), data, 13, 4)); + + unsigned char decoded[13 * 4]; + assert(meshopt_decodeVertexBuffer(decoded, 13, 4, &buffer[0], buffer.size()) == 0); + assert(memcmp(decoded, data, sizeof(data)) == 0); +} + +static void encodeVertexEmpty() +{ + std::vector buffer(meshopt_encodeVertexBufferBound(0, 16)); + buffer.resize(meshopt_encodeVertexBuffer(&buffer[0], buffer.size(), NULL, 0, 16)); + + assert(meshopt_decodeVertexBuffer(NULL, 0, 16, &buffer[0], buffer.size()) == 0); +} + +static void decodeVersion() +{ + assert(meshopt_decodeVertexVersion(reinterpret_cast("\xa0"), 1) == 0); + assert(meshopt_decodeVertexVersion(reinterpret_cast("\xa1"), 1) == 1); + assert(meshopt_decodeVertexVersion(reinterpret_cast("\xa1hello"), 6) == 1); + + assert(meshopt_decodeVertexVersion(NULL, 0) == -1); + assert(meshopt_decodeVertexVersion(reinterpret_cast("\xa7"), 1) == -1); + assert(meshopt_decodeVertexVersion(reinterpret_cast("\xb1"), 1) == -1); + + assert(meshopt_decodeIndexVersion(reinterpret_cast("\xe0"), 1) == 0); + assert(meshopt_decodeIndexVersion(reinterpret_cast("\xd1"), 1) == 1); + assert(meshopt_decodeIndexVersion(reinterpret_cast("\xe1hello"), 6) == 1); + + assert(meshopt_decodeIndexVersion(NULL, 0) == -1); + assert(meshopt_decodeIndexVersion(reinterpret_cast("\xa7"), 1) == -1); + assert(meshopt_decodeIndexVersion(reinterpret_cast("\xa1"), 1) == -1); +} + +static void decodeFilterOct8() +{ + const unsigned char data[4 * 4] = { + 0, 1, 127, 0, + 0, 187, 127, 1, + 255, 1, 127, 0, + 14, 130, 127, 1 // + }; + + const unsigned char expected[4 * 4] = { + 0, 1, 127, 0, + 0, 159, 82, 1, + 255, 1, 127, 0, + 1, 130, 241, 1 // + }; + + // Aligned by 4 + unsigned char full[4 * 4]; + memcpy(full, data, sizeof(full)); + meshopt_decodeFilterOct(full, 4, 4); + assert(memcmp(full, expected, sizeof(full)) == 0); + + // Tail processing for unaligned data + unsigned char tail[3 * 4]; + memcpy(tail, data, sizeof(tail)); + meshopt_decodeFilterOct(tail, 3, 4); + assert(memcmp(tail, expected, sizeof(tail)) == 0); +} + +static void decodeFilterOct12() +{ + const unsigned short data[4 * 4] = { + 0, 1, 2047, 0, + 0, 1870, 2047, 1, + 2017, 1, 2047, 0, + 14, 1300, 2047, 1 // + }; + + const unsigned short expected[4 * 4] = { + 0, 16, 32767, 0, + 0, 32621, 3088, 1, + 32764, 16, 471, 0, + 307, 28541, 16093, 1 // + }; + + // Aligned by 4 + unsigned short full[4 * 4]; + memcpy(full, data, sizeof(full)); + meshopt_decodeFilterOct(full, 4, 8); + assert(memcmp(full, expected, sizeof(full)) == 0); + + // Tail processing for unaligned data + unsigned short tail[3 * 4]; + memcpy(tail, data, sizeof(tail)); + meshopt_decodeFilterOct(tail, 3, 8); + assert(memcmp(tail, expected, sizeof(tail)) == 0); +} + +static void decodeFilterQuat12() +{ + const unsigned short data[4 * 4] = { + 0, 1, 0, 0x7fc, + 0, 1870, 0, 0x7fd, + 2017, 1, 0, 0x7fe, + 14, 1300, 0, 0x7ff // + }; + + const unsigned short expected[4 * 4] = { + 32767, 0, 11, 0, + 0, 25013, 0, 21166, + 11, 0, 23504, 22830, + 158, 14715, 0, 29277 // + }; + + // Aligned by 4 + unsigned short full[4 * 4]; + memcpy(full, data, sizeof(full)); + meshopt_decodeFilterQuat(full, 4, 8); + assert(memcmp(full, expected, sizeof(full)) == 0); + + // Tail processing for unaligned data + unsigned short tail[3 * 4]; + memcpy(tail, data, sizeof(tail)); + meshopt_decodeFilterQuat(tail, 3, 8); + assert(memcmp(tail, expected, sizeof(tail)) == 0); +} + +static void decodeFilterExp() +{ + const unsigned int data[4] = { + 0, + 0xff000003, + 0x02fffff7, + 0xfe7fffff, + }; + + const unsigned int expected[4] = { + 0, + 0x3fc00000, + 0xc2100000, + 0x49fffffe, + }; + + // Aligned by 4 + unsigned int full[4]; + memcpy(full, data, sizeof(full)); + meshopt_decodeFilterExp(full, 4, 4); + assert(memcmp(full, expected, sizeof(full)) == 0); + + // Tail processing for unaligned data + unsigned int tail[3]; + memcpy(tail, data, sizeof(tail)); + meshopt_decodeFilterExp(tail, 3, 4); + assert(memcmp(tail, expected, sizeof(tail)) == 0); +} + +static void encodeFilterOct8() +{ + const float data[4 * 4] = { + 1, 0, 0, 0, + 0, -1, 0, 0, + 0.7071068f, 0, 0.707168f, 1, + -0.7071068f, 0, -0.707168f, 1 // + }; + + const unsigned char expected[4 * 4] = { + 0x7f, 0, 0x7f, 0, + 0, 0x81, 0x7f, 0, + 0x3f, 0, 0x7f, 0x7f, + 0x81, 0x40, 0x7f, 0x7f // + }; + + unsigned char encoded[4 * 4]; + meshopt_encodeFilterOct(encoded, 4, 4, 8, data); + + assert(memcmp(encoded, expected, sizeof(expected)) == 0); + + signed char decoded[4 * 4]; + memcpy(decoded, encoded, sizeof(decoded)); + meshopt_decodeFilterOct(decoded, 4, 4); + + for (size_t i = 0; i < 4 * 4; ++i) + assert(fabsf(decoded[i] / 127.f - data[i]) < 1e-2f); +} + +static void encodeFilterOct12() +{ + const float data[4 * 4] = { + 1, 0, 0, 0, + 0, -1, 0, 0, + 0.7071068f, 0, 0.707168f, 1, + -0.7071068f, 0, -0.707168f, 1 // + }; + + const unsigned short expected[4 * 4] = { + 0x7ff, 0, 0x7ff, 0, + 0x0, 0xf801, 0x7ff, 0, + 0x3ff, 0, 0x7ff, 0x7fff, + 0xf801, 0x400, 0x7ff, 0x7fff // + }; + + unsigned short encoded[4 * 4]; + meshopt_encodeFilterOct(encoded, 4, 8, 12, data); + + assert(memcmp(encoded, expected, sizeof(expected)) == 0); + + short decoded[4 * 4]; + memcpy(decoded, encoded, sizeof(decoded)); + meshopt_decodeFilterOct(decoded, 4, 8); + + for (size_t i = 0; i < 4 * 4; ++i) + assert(fabsf(decoded[i] / 32767.f - data[i]) < 1e-3f); +} + +static void encodeFilterQuat12() +{ + const float data[4 * 4] = { + 1, 0, 0, 0, + 0, -1, 0, 0, + 0.7071068f, 0, 0, 0.707168f, + -0.7071068f, 0, 0, -0.707168f // + }; + + const unsigned short expected[4 * 4] = { + 0, 0, 0, 0x7fc, + 0, 0, 0, 0x7fd, + 0x7ff, 0, 0, 0x7ff, + 0x7ff, 0, 0, 0x7ff // + }; + + unsigned short encoded[4 * 4]; + meshopt_encodeFilterQuat(encoded, 4, 8, 12, data); + + assert(memcmp(encoded, expected, sizeof(expected)) == 0); + + short decoded[4 * 4]; + memcpy(decoded, encoded, sizeof(decoded)); + meshopt_decodeFilterQuat(decoded, 4, 8); + + for (size_t i = 0; i < 4; ++i) + { + float dx = decoded[i * 4 + 0] / 32767.f; + float dy = decoded[i * 4 + 1] / 32767.f; + float dz = decoded[i * 4 + 2] / 32767.f; + float dw = decoded[i * 4 + 3] / 32767.f; + + float dp = + data[i * 4 + 0] * dx + + data[i * 4 + 1] * dy + + data[i * 4 + 2] * dz + + data[i * 4 + 3] * dw; + + assert(fabsf(fabsf(dp) - 1.f) < 1e-4f); + } +} + +static void encodeFilterExp() +{ + const float data[4] = { + 1, + -23.4f, + -0.1f, + 11.0f, + }; + + // separate exponents: each component gets its own value + const unsigned int expected1[4] = { + 0xf3002000, + 0xf7ffd133, + 0xefffcccd, + 0xf6002c00, + }; + + // shared exponents (vector): all components of each vector get the same value + const unsigned int expected2[4] = { + 0xf7000200, + 0xf7ffd133, + 0xf6ffff9a, + 0xf6002c00, + }; + + // shared exponents (component): each component gets the same value across all vectors + const unsigned int expected3[4] = { + 0xf3002000, + 0xf7ffd133, + 0xf3fffccd, + 0xf7001600, + }; + + unsigned int encoded1[4]; + meshopt_encodeFilterExp(encoded1, 2, 8, 15, data, meshopt_EncodeExpSeparate); + + unsigned int encoded2[4]; + meshopt_encodeFilterExp(encoded2, 2, 8, 15, data, meshopt_EncodeExpSharedVector); + + unsigned int encoded3[4]; + meshopt_encodeFilterExp(encoded3, 2, 8, 15, data, meshopt_EncodeExpSharedComponent); + + assert(memcmp(encoded1, expected1, sizeof(expected1)) == 0); + assert(memcmp(encoded2, expected2, sizeof(expected2)) == 0); + assert(memcmp(encoded3, expected3, sizeof(expected3)) == 0); + + float decoded1[4]; + memcpy(decoded1, encoded1, sizeof(decoded1)); + meshopt_decodeFilterExp(decoded1, 2, 8); + + float decoded2[4]; + memcpy(decoded2, encoded2, sizeof(decoded2)); + meshopt_decodeFilterExp(decoded2, 2, 8); + + float decoded3[4]; + memcpy(decoded3, encoded3, sizeof(decoded3)); + meshopt_decodeFilterExp(decoded3, 2, 8); + + for (size_t i = 0; i < 4; ++i) + { + assert(fabsf(decoded1[i] - data[i]) < 1e-3f); + assert(fabsf(decoded2[i] - data[i]) < 1e-3f); + assert(fabsf(decoded3[i] - data[i]) < 1e-3f); + } +} + +static void encodeFilterExpZero() +{ + const float data[4] = { + 0.f, + -0.f, + 1.1754944e-38f, + -1.1754944e-38f, + }; + const unsigned int expected[4] = { + 0xf2000000, + 0xf2000000, + 0x8e000000, + 0x8e000000, + }; + + unsigned int encoded[4]; + meshopt_encodeFilterExp(encoded, 4, 4, 15, data, meshopt_EncodeExpSeparate); + + assert(memcmp(encoded, expected, sizeof(expected)) == 0); + + float decoded[4]; + memcpy(decoded, encoded, sizeof(decoded)); + meshopt_decodeFilterExp(&decoded, 4, 4); + + for (size_t i = 0; i < 4; ++i) + assert(decoded[i] == 0); +} + +static void encodeFilterExpOverflow() +{ + // at bits=24, values with an all-ones mantissa round up to 2^23 which must be clamped + const float data[4] = { + 0.99999994f, // largest float below 1.0 + -0.99999994f, + 255.99998f, // largest float below 256.0 + 1.f, + }; + const unsigned int expected[4] = { + 0xe97fffff, + 0xe9800000, // -2^23 is representable as is + 0xf17fffff, + 0xea400000, + }; + + unsigned int encoded[4]; + meshopt_encodeFilterExp(encoded, 4, 4, 24, data, meshopt_EncodeExpSeparate); + + assert(memcmp(encoded, expected, sizeof(expected)) == 0); + + float decoded[4]; + memcpy(decoded, encoded, sizeof(decoded)); + meshopt_decodeFilterExp(decoded, 4, 4); + + assert(decoded[0] == 0.99999988f); + assert(decoded[1] == -1.f); + assert(decoded[2] == 255.99997f); + assert(decoded[3] == 1.f); +} + +static void encodeFilterExpZeroShared() +{ + const float data[4] = { + 0.f, + 0.1f, + -0.025f, + -0.f, + }; + + // shared exponents (vector) + const unsigned int expected1[4] = { + 0xef000000, + 0xef003333, + 0xedffcccd, + 0xed000000, + }; + + // shared exponents (component) + const unsigned int expected2[4] = { + 0xed000000, + 0xef003333, + 0xedffcccd, + 0xef000000, + }; + + unsigned int encoded1[4]; + meshopt_encodeFilterExp(encoded1, 2, 8, 15, data, meshopt_EncodeExpSharedVector); + + unsigned int encoded2[4]; + meshopt_encodeFilterExp(encoded2, 2, 8, 15, data, meshopt_EncodeExpSharedComponent); + + assert(memcmp(encoded1, expected1, sizeof(expected1)) == 0); + assert(memcmp(encoded2, expected2, sizeof(expected2)) == 0); + + float decoded1[4]; + memcpy(decoded1, encoded1, sizeof(decoded1)); + meshopt_decodeFilterExp(decoded1, 2, 8); + + float decoded2[4]; + memcpy(decoded2, encoded2, sizeof(decoded2)); + meshopt_decodeFilterExp(decoded2, 2, 8); + + for (size_t i = 0; i < 4; ++i) + { + assert(fabsf(decoded1[i] - data[i]) < 1e-5f); + assert(fabsf(decoded2[i] - data[i]) < 1e-5f); + } + + // zeroes should be preserved exactly + assert(decoded1[0] == 0 && decoded1[3] == 0); + assert(decoded2[0] == 0 && decoded2[3] == 0); +} + +static void encodeFilterExpAlias() +{ + const float data[4] = { + 1, + -23.4f, + -0.1f, + 11.0f, + }; + + // separate exponents: each component gets its own value + const unsigned int expected1[4] = { + 0xf3002000, + 0xf7ffd133, + 0xefffcccd, + 0xf6002c00, + }; + + // shared exponents (vector): all components of each vector get the same value + const unsigned int expected2[4] = { + 0xf7000200, + 0xf7ffd133, + 0xf6ffff9a, + 0xf6002c00, + }; + + // shared exponents (component): each component gets the same value across all vectors + const unsigned int expected3[4] = { + 0xf3002000, + 0xf7ffd133, + 0xf3fffccd, + 0xf7001600, + }; + + unsigned int encoded1[4]; + memcpy(encoded1, data, sizeof(data)); + meshopt_encodeFilterExp(encoded1, 2, 8, 15, reinterpret_cast(encoded1), meshopt_EncodeExpSeparate); + + unsigned int encoded2[4]; + memcpy(encoded2, data, sizeof(data)); + meshopt_encodeFilterExp(encoded2, 2, 8, 15, reinterpret_cast(encoded2), meshopt_EncodeExpSharedVector); + + unsigned int encoded3[4]; + memcpy(encoded3, data, sizeof(data)); + meshopt_encodeFilterExp(encoded3, 2, 8, 15, reinterpret_cast(encoded3), meshopt_EncodeExpSharedComponent); + + assert(memcmp(encoded1, expected1, sizeof(expected1)) == 0); + assert(memcmp(encoded2, expected2, sizeof(expected2)) == 0); + assert(memcmp(encoded3, expected3, sizeof(expected3)) == 0); +} + +static void encodeFilterExpClamp() +{ + const float data[4] = { + 1, + -23.4f, + -0.1f, + 11.0f, + }; + + // separate exponents: each component gets its own value + // note: third value is exponent clamped + const unsigned int expected[4] = { + 0xf3002000, + 0xf7ffd133, + 0xf2fff99a, + 0xf6002c00, + }; + + unsigned int encoded[4]; + meshopt_encodeFilterExp(encoded, 2, 8, 15, data, meshopt_EncodeExpClamped); + + assert(memcmp(encoded, expected, sizeof(expected)) == 0); + + float decoded[4]; + memcpy(decoded, encoded, sizeof(decoded)); + meshopt_decodeFilterExp(decoded, 2, 8); + + for (size_t i = 0; i < 4; ++i) + assert(fabsf(decoded[i] - data[i]) < 1e-3f); +} + +static void encodeFilterColor8() +{ + const float data[4 * 4] = { + 1.0f, 0.0f, 0.0f, 1.0f, + 0.0f, 1.0f, 0.0f, 0.5f, + 0.0f, 0.0f, 1.0f, 0.25f, + 0.4f, 0.4f, 0.4f, 0.75f // + }; + + const unsigned char expected[4 * 4] = { + 0x40, 0x7f, 0xc1, 0xff, + 0x7f, 0x00, 0x7f, 0xc0, + 0x40, 0x81, 0xc0, 0xa0, + 0x66, 0x00, 0x00, 0xdf // + }; + + unsigned char encoded[4 * 4]; + meshopt_encodeFilterColor(encoded, 4, 4, 8, data); + + assert(memcmp(encoded, expected, sizeof(expected)) == 0); + + unsigned char decoded[4 * 4]; + memcpy(decoded, encoded, sizeof(decoded)); + meshopt_decodeFilterColor(decoded, 4, 4); + + for (size_t i = 0; i < 4 * 4; ++i) + assert(fabsf(decoded[i] / 255.f - data[i]) < 1e-2f); + + // ensure grayscale is preserved + assert(decoded[12] == decoded[13] && decoded[12] == decoded[14]); +} + +static void encodeFilterColor12() +{ + const float data[4 * 4] = { + 1.0f, 0.0f, 0.0f, 1.0f, + 0.0f, 1.0f, 0.0f, 0.5f, + 0.0f, 0.0f, 1.0f, 0.25f, + 0.4f, 0.4f, 0.4f, 0.75f // + }; + + const unsigned short expected[4 * 4] = { + 0x0400, 0x07ff, 0xfc01, 0x0fff, + 0x07ff, 0x0000, 0x07ff, 0x0c00, + 0x0400, 0xf801, 0xfc00, 0x0a00, + 0x0666, 0x0000, 0x0000, 0x0dff // + }; + + unsigned short encoded[4 * 4]; + meshopt_encodeFilterColor(encoded, 4, 8, 12, data); + + assert(memcmp(encoded, expected, sizeof(expected)) == 0); + + unsigned short decoded[4 * 4]; + memcpy(decoded, encoded, sizeof(decoded)); + meshopt_decodeFilterColor(decoded, 4, 8); + + for (size_t i = 0; i < 4 * 4; ++i) + assert(fabsf(decoded[i] / 65535.f - data[i]) < 1e-3f); + + // ensure grayscale is preserved + assert(decoded[12] == decoded[13] && decoded[12] == decoded[14]); +} + +static void clusterBoundsDegenerate() +{ + const float vbd[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; + const unsigned int ibd[] = {0, 0, 0}; + const unsigned int ib1[] = {0, 1, 2}; + + // all of the bounds below are degenerate as they use 0 triangles, one topology-degenerate triangle and one position-degenerate triangle respectively + meshopt_Bounds bounds0 = meshopt_computeClusterBounds(NULL, 0, NULL, 0, 12); + meshopt_Bounds boundsd = meshopt_computeClusterBounds(ibd, 3, vbd, 3, 12); + meshopt_Bounds bounds1 = meshopt_computeClusterBounds(ib1, 3, vbd, 3, 12); + + assert(bounds0.center[0] == 0 && bounds0.center[1] == 0 && bounds0.center[2] == 0 && bounds0.radius == 0); + assert(boundsd.center[0] == 0 && boundsd.center[1] == 0 && boundsd.center[2] == 0 && boundsd.radius == 0); + assert(bounds1.center[0] == 0 && bounds1.center[1] == 0 && bounds1.center[2] == 0 && bounds1.radius == 0); + + const float vb1[] = {1, 0, 0, 0, 1, 0, 0, 0, 1}; + const unsigned int ib2[] = {0, 1, 2, 0, 2, 1}; + + // these bounds have a degenerate cone since the cluster has two triangles with opposite normals + meshopt_Bounds bounds2 = meshopt_computeClusterBounds(ib2, 6, vb1, 3, 12); + + assert(bounds2.cone_apex[0] == 0 && bounds2.cone_apex[1] == 0 && bounds2.cone_apex[2] == 0); + assert(bounds2.cone_axis[0] == 0 && bounds2.cone_axis[1] == 0 && bounds2.cone_axis[2] == 0); + assert(bounds2.cone_cutoff == 1); + assert(bounds2.cone_axis_s8[0] == 0 && bounds2.cone_axis_s8[1] == 0 && bounds2.cone_axis_s8[2] == 0); + assert(bounds2.cone_cutoff_s8 == 127); + + // however, the bounding sphere needs to be in tact (here we only check bbox for simplicity) + assert(bounds2.center[0] - bounds2.radius <= 0 && bounds2.center[0] + bounds2.radius >= 1); + assert(bounds2.center[1] - bounds2.radius <= 0 && bounds2.center[1] + bounds2.radius >= 1); + assert(bounds2.center[2] - bounds2.radius <= 0 && bounds2.center[2] + bounds2.radius >= 1); +} + +static void sphereBounds() +{ + const float vbr[] = { + 0, 0, 0, 0, + 0, 1, 0, 1, + 0, 0, 1, 2, + 1, 0, 1, 3 // + }; + + // without the radius, the center is inside the tetrahedron + meshopt_Bounds bounds = meshopt_computeSphereBounds(vbr, 4, sizeof(float) * 4, NULL, 0); + assert(fabsf(bounds.center[0] - 0.5f) < 1e-2f); + assert(fabsf(bounds.center[1] - 0.5f) < 1e-2f); + assert(fabsf(bounds.center[2] - 0.5f) < 1e-2f); + assert(bounds.radius < 0.87f); + + // when using the radius, the last sphere envelops the entire set + meshopt_Bounds boundsr = meshopt_computeSphereBounds(vbr, 4, sizeof(float) * 4, vbr + 3, sizeof(float) * 4); + assert(fabsf(boundsr.center[0] - 1.f) < 1e-2f); + assert(fabsf(boundsr.center[1] - 0.f) < 1e-2f); + assert(fabsf(boundsr.center[2] - 1.f) < 1e-2f); + assert(fabsf(boundsr.radius - 3.f) < 1e-2f); +} + +static void meshletsEmpty() +{ + const float vbd[4 * 3] = {}; + + meshopt_Meshlet ml[1]; + unsigned int mv[4]; + unsigned char mt[8]; + size_t mc = meshopt_buildMeshlets(ml, mv, mt, NULL, 0, vbd, 4, sizeof(float) * 3, 64, 64, 0.f); + assert(mc == 0); +} + +static void meshletsDense() +{ + const float vbd[4 * 3] = {}; + const unsigned int ibd[6] = {0, 2, 1, 1, 2, 3}; + + meshopt_Meshlet ml[1]; + unsigned int mv[4]; + unsigned char mt[8]; + size_t mc = meshopt_buildMeshlets(ml, mv, mt, ibd, 6, vbd, 4, sizeof(float) * 3, 64, 64, 0.f); + + assert(mc == 1); + assert(ml[0].triangle_count == 2); + assert(ml[0].vertex_count == 4); + + unsigned int tri0[3] = {mv[mt[0]], mv[mt[1]], mv[mt[2]]}; + unsigned int tri1[3] = {mv[mt[3]], mv[mt[4]], mv[mt[5]]}; + + // technically triangles could also be flipped in the meshlet but for now just assume they aren't + assert(memcmp(tri0, ibd + 0, 3 * sizeof(unsigned int)) == 0); + assert(memcmp(tri1, ibd + 3, 3 * sizeof(unsigned int)) == 0); +} + +static void meshletsSparse() +{ + const float vbd[16 * 3] = {}; + const unsigned int ibd[6] = {0, 7, 15, 15, 7, 3}; + + meshopt_Meshlet ml[1]; + unsigned int mv[4]; + unsigned char mt[8]; + size_t mc = meshopt_buildMeshlets(ml, mv, mt, ibd, 6, vbd, 16, sizeof(float) * 3, 64, 64, 0.f); + + assert(mc == 1); + assert(ml[0].triangle_count == 2); + assert(ml[0].vertex_count == 4); + + unsigned int tri0[3] = {mv[mt[0]], mv[mt[1]], mv[mt[2]]}; + unsigned int tri1[3] = {mv[mt[3]], mv[mt[4]], mv[mt[5]]}; + + // technically triangles could also be flipped in the meshlet but for now just assume they aren't + assert(memcmp(tri0, ibd + 0, 3 * sizeof(unsigned int)) == 0); + assert(memcmp(tri1, ibd + 3, 3 * sizeof(unsigned int)) == 0); +} + +static void meshletsFlex() +{ + // two tetrahedrons far apart + float vb[2 * 4 * 3] = { + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 10, 0, 0, 11, 0, 0, 10, 1, 0, 10, 0, 1 // + }; + + unsigned int ib[2 * 4 * 3] = { + 0, 1, 2, 0, 2, 3, 0, 3, 1, 1, 3, 2, + 4, 5, 6, 4, 6, 7, 4, 7, 5, 5, 7, 6 // + }; + + // up to 2 meshlets with min_triangles=4 + assert(meshopt_buildMeshletsBound(2 * 4 * 3, 16, 4) == 2); + + meshopt_Meshlet ml[2]; + unsigned int mv[2 * 16]; + unsigned char mt[2 * 8 * 3]; // 2 meshlets with up to 8 triangles + + // with regular function, we should get one meshlet (maxt=8) or two (maxt=4) + assert(meshopt_buildMeshlets(ml, mv, mt, ib, sizeof(ib) / sizeof(ib[0]), vb, 8, sizeof(float) * 3, 16, 8, 0.f) == 1); + assert(ml[0].triangle_count == 8); + assert(ml[0].vertex_count == 8); + + assert(meshopt_buildMeshlets(ml, mv, mt, ib, sizeof(ib) / sizeof(ib[0]), vb, 8, sizeof(float) * 3, 16, 4, 0.f) == 2); + assert(ml[0].triangle_count == 4); + assert(ml[0].vertex_count == 4); + assert(ml[1].triangle_count == 4); + assert(ml[1].vertex_count == 4); + + // with flex function and mint=4 maxt=8 we should get one meshlet if split_factor is zero, or large enough to accomodate both + assert(meshopt_buildMeshletsFlex(ml, mv, mt, ib, sizeof(ib) / sizeof(ib[0]), vb, 8, sizeof(float) * 3, 16, 4, 8, 0.f, 0.f) == 1); + assert(ml[0].triangle_count == 8); + assert(ml[0].vertex_count == 8); + + assert(meshopt_buildMeshletsFlex(ml, mv, mt, ib, sizeof(ib) / sizeof(ib[0]), vb, 8, sizeof(float) * 3, 16, 4, 8, 0.f, 10.f) == 1); + assert(ml[0].triangle_count == 8); + assert(ml[0].vertex_count == 8); + + // however, with a smaller split factor we should get two meshlets + assert(meshopt_buildMeshletsFlex(ml, mv, mt, ib, sizeof(ib) / sizeof(ib[0]), vb, 8, sizeof(float) * 3, 16, 4, 8, 0.f, 1.f) == 2); + assert(ml[0].triangle_count == 4); + assert(ml[0].vertex_count == 4); + assert(ml[1].triangle_count == 4); + assert(ml[1].vertex_count == 4); +} + +static void meshletsMax() +{ + float vb[16 * 16 * 3]; + unsigned int ib[15 * 15 * 2 * 3]; + + // 16x16 grid of vertices, 15x15 grid of triangles + for (int y = 0; y < 16; ++y) + for (int x = 0; x < 16; ++x) + { + vb[(y * 16 + x) * 3 + 0] = float(x); + vb[(y * 16 + x) * 3 + 1] = float(y); + vb[(y * 16 + x) * 3 + 2] = 0; + } + + for (int y = 0; y < 15; ++y) + for (int x = 0; x < 15; ++x) + { + ib[(y * 15 + x) * 2 * 3 + 0] = (y + 0) * 16 + (x + 0); + ib[(y * 15 + x) * 2 * 3 + 1] = (y + 0) * 16 + (x + 1); + ib[(y * 15 + x) * 2 * 3 + 2] = (y + 1) * 16 + (x + 0); + ib[(y * 15 + x) * 2 * 3 + 3] = (y + 1) * 16 + (x + 0); + ib[(y * 15 + x) * 2 * 3 + 4] = (y + 0) * 16 + (x + 1); + ib[(y * 15 + x) * 2 * 3 + 5] = (y + 1) * 16 + (x + 1); + } + + meshopt_Meshlet ml[1]; + unsigned int mv[16 * 16]; + unsigned char mt[15 * 15 * 2 * 3 + 3]; + + size_t mc = meshopt_buildMeshlets(ml, mv, mt, ib, sizeof(ib) / sizeof(ib[0]), vb, 16 * 16, sizeof(float) * 3, 256, 512, 0.f); + assert(mc == 1); + assert(ml[0].triangle_count == 450); + assert(ml[0].vertex_count == 256); + + meshopt_optimizeMeshlet(mv, mt, ml[0].triangle_count, ml[0].vertex_count); + + // check sequential ordering of remapped indices + int vmax = -1; + + for (size_t i = 0; i < 450 * 3; ++i) + { + assert(mt[i] <= vmax + 1); + vmax = vmax < mt[i] ? mt[i] : vmax; + } +} + +static void extractMeshlet() +{ + // 15 and 1039 collide in low 10 bits + const unsigned int indices[] = {0, 7, 15, 1039, 7, 15}; + + unsigned int vertices[4]; + unsigned char triangles[6]; + size_t unique = meshopt_extractMeshletIndices(vertices, triangles, indices, 6); + assert(unique == 4); + + // verify the invariant: vertices[triangles[i]] == indices[i] + for (size_t i = 0; i < 6; ++i) + assert(vertices[triangles[i]] == indices[i]); + + assert(vertices[0] == 0 && vertices[1] == 7 && vertices[2] == 15 && vertices[3] == 1039); + assert(triangles[0] == 0 && triangles[1] == 1 && triangles[2] == 2 && triangles[3] == 3 && triangles[4] == 1 && triangles[5] == 2); +} + +static void meshletsSpatial() +{ + // two tetrahedrons far apart + float vb[2 * 4 * 3] = { + 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, + 10, 0, 0, 11, 0, 0, 10, 1, 0, 10, 0, 1 // + }; + + unsigned int ib[2 * 4 * 3] = { + 0, 1, 2, 0, 2, 3, 0, 3, 1, 1, 3, 2, + 4, 5, 6, 4, 6, 7, 4, 7, 5, 5, 7, 6 // + }; + + // up to 2 meshlets with min_triangles=4 + assert(meshopt_buildMeshletsBound(2 * 4 * 3, 16, 4) == 2); + + meshopt_Meshlet ml[2]; + unsigned int mv[2 * 16]; + unsigned char mt[2 * 8 * 3]; // 2 meshlets with up to 8 triangles + + // with strict limits, we should get one meshlet (maxt=8) or two (maxt=4) + assert(meshopt_buildMeshletsSpatial(ml, mv, mt, ib, sizeof(ib) / sizeof(ib[0]), vb, 8, sizeof(float) * 3, 16, 8, 8, 0.f) == 1); + assert(ml[0].triangle_count == 8); + assert(ml[0].vertex_count == 8); + + assert(meshopt_buildMeshletsSpatial(ml, mv, mt, ib, sizeof(ib) / sizeof(ib[0]), vb, 8, sizeof(float) * 3, 16, 4, 4, 0.f) == 2); + assert(ml[0].triangle_count == 4); + assert(ml[0].vertex_count == 4); + assert(ml[1].triangle_count == 4); + assert(ml[1].vertex_count == 4); + + // with maxv=4 we should get two meshlets since we can't accomodate both + assert(meshopt_buildMeshletsSpatial(ml, mv, mt, ib, sizeof(ib) / sizeof(ib[0]), vb, 8, sizeof(float) * 3, 4, 4, 8, 0.f) == 2); + assert(ml[0].triangle_count == 4); + assert(ml[0].vertex_count == 4); + assert(ml[1].triangle_count == 4); + assert(ml[1].vertex_count == 4); +} + +static void meshletsSpatialDeep() +{ + const int N = 400; + const size_t max_vertices = 4; + const size_t max_triangles = 4; + + float vb[(N + 1) * 3]; + unsigned int ib[N * 3]; + + vb[0] = vb[1] = vb[2] = 0; + + for (size_t i = 0; i < N; ++i) + { + vb[(i + 1) * 3 + 0] = vb[(i + 1) * 3 + 1] = vb[(i + 1) * 3 + 2] = powf(1.2f, float(i)); + + ib[i * 3 + 0] = 0; + ib[i * 3 + 1] = ib[i * 3 + 2] = unsigned(i + 1); + } + + size_t max_meshlets = meshopt_buildMeshletsBound(N * 3, max_vertices, max_triangles); + std::vector meshlets(max_meshlets); + std::vector meshlet_vertices(N * 3); + std::vector meshlet_triangles(N * 3); + + size_t result = meshopt_buildMeshletsSpatial(&meshlets[0], &meshlet_vertices[0], &meshlet_triangles[0], &ib[0], N * 3, &vb[0], N + 1, sizeof(float) * 3, max_vertices, max_triangles, max_triangles, 0.f); + assert(result == N); +} + +static void partitionBasic() +{ + // 0 1 2 + // 3 + // 4 5 6 7 8 + // 9 + // 10 11 12 + const unsigned int ci[] = { + 0, 1, 3, 4, 5, 6, + 1, 2, 3, 6, 7, 8, + 4, 5, 6, 9, 10, 11, + 6, 7, 8, 9, 11, 12 // + }; + + const unsigned int cc[4] = {6, 6, 6, 6}; + unsigned int part[4]; + + assert(meshopt_partitionClusters(part, ci, sizeof(ci) / sizeof(ci[0]), cc, 4, NULL, 13, 0, 1) == 4); + assert(part[0] == 0 && part[1] == 1 && part[2] == 2 && part[3] == 3); + + assert(meshopt_partitionClusters(part, ci, sizeof(ci) / sizeof(ci[0]), cc, 4, NULL, 13, 0, 2) == 2); + assert(part[0] == 0 && part[1] == 0 && part[2] == 1 && part[3] == 1); + + assert(meshopt_partitionClusters(part, ci, sizeof(ci) / sizeof(ci[0]), cc, 4, NULL, 13, 0, 4) == 1); + assert(part[0] == 0 && part[1] == 0 && part[2] == 0 && part[3] == 0); +} + +static void partitionSpatial() +{ + const unsigned int ci[] = { + 0, 1, 2, + 0, 3, 4, + 0, 5, 6 // + }; + + const float vb[] = { + 0, 0, 0, + 1, 0, 0, 0, 1, 0, + 0, 2, 0, 2, 0, 0, + -1, 0, 0, 0, -1, 0 // + }; + + const unsigned int cc[3] = {3, 3, 3}; + unsigned int part[3]; + + assert(meshopt_partitionClusters(part, ci, sizeof(ci) / sizeof(ci[0]), cc, 3, NULL, 7, 0, 2) == 2); + assert(part[0] == 0 && part[1] == 0 && part[2] == 1); + + assert(meshopt_partitionClusters(part, ci, sizeof(ci) / sizeof(ci[0]), cc, 3, vb, 7, sizeof(float) * 3, 2) == 2); + assert(part[0] == 0 && part[1] == 1 && part[2] == 0); +} + +static void partitionSpatialMerge() +{ + const unsigned int ci[] = { + 0, 1, 2, + 3, 4, 5, + 6, 7, 8 // + }; + + const float vb[] = { + 0, 0, 0, 1, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 2, 0, 2, 0, 0, + 10, 0, 0, 10, 1, 0, 10, 2, 0 // + }; + + const unsigned int cc[3] = {3, 3, 3}; + unsigned int part[3]; + + assert(meshopt_partitionClusters(part, ci, sizeof(ci) / sizeof(ci[0]), cc, 3, NULL, 9, 0, 2) == 3); + assert(part[0] == 0 && part[1] == 1 && part[2] == 2); + + assert(meshopt_partitionClusters(part, ci, sizeof(ci) / sizeof(ci[0]), cc, 3, vb, 9, sizeof(float) * 3, 2) == 2); + assert(part[0] == 0 && part[1] == 0 && part[2] == 1); +} + +static int remapCustomFalse(void*, unsigned int, unsigned int) +{ + return 0; +} + +static int remapCustomTrue(void*, unsigned int, unsigned int) +{ + return 1; +} + +static void remapCustom() +{ + const float vb[] = { + 0, 0, 0, + 1, 0, 0, + 0, 1, 0, + 0, 0, 1, + 1, 0, 0, + 0, -0.f, 1 // + }; + + unsigned int remap[6]; + size_t res; + + res = meshopt_generateVertexRemapCustom(remap, NULL, 6, vb, 6, sizeof(float) * 3, NULL, NULL); + assert(res == 4); + for (int i = 0; i < 4; ++i) + assert(remap[i] == unsigned(i)); + assert(remap[4] == 1); + assert(remap[5] == 3); + + res = meshopt_generateVertexRemapCustom(remap, NULL, 6, vb, 6, sizeof(float) * 3, remapCustomTrue, NULL); + assert(res == 4); + for (int i = 0; i < 4; ++i) + assert(remap[i] == unsigned(i)); + assert(remap[4] == 1); + assert(remap[5] == 3); + + res = meshopt_generateVertexRemapCustom(remap, NULL, 6, vb, 6, sizeof(float) * 3, remapCustomFalse, NULL); + assert(res == 6); + for (int i = 0; i < 6; ++i) + assert(remap[i] == unsigned(i)); +} + +static size_t allocCount; +static size_t freeCount; + +static void* customAlloc(size_t size) +{ + allocCount++; + + return malloc(size); +} + +static void customFree(void* ptr) +{ + freeCount++; + + free(ptr); +} + +static void customAllocator() +{ + meshopt_setAllocator(customAlloc, customFree); + + assert(allocCount == 0 && freeCount == 0); + + float vb[] = {1, 0, 0, 0, 1, 0, 0, 0, 1}; + unsigned int ib[] = {0, 1, 2}; + unsigned short ibs[] = {0, 1, 2}; + + // meshopt_computeClusterBounds doesn't allocate + meshopt_computeClusterBounds(ib, 3, vb, 3, 12); + assert(allocCount == 0 && freeCount == 0); + + // ... unless IndexAdapter is used + meshopt_computeClusterBounds(ibs, 3, vb, 3, 12); + assert(allocCount == 1 && freeCount == 1); + + // meshopt_optimizeVertexFetch allocates internal remap table and temporary storage for in-place remaps + meshopt_optimizeVertexFetch(vb, ib, 3, vb, 3, 12); + assert(allocCount == 3 && freeCount == 3); + + // ... plus one for IndexAdapter + meshopt_optimizeVertexFetch(vb, ibs, 3, vb, 3, 12); + assert(allocCount == 6 && freeCount == 6); + + meshopt_setAllocator(operator new, operator delete); + + // customAlloc & customFree should not get called anymore + meshopt_optimizeVertexFetch(vb, ib, 3, vb, 3, 12); + assert(allocCount == 6 && freeCount == 6); + + allocCount = freeCount = 0; +} + +static void emptyMesh() +{ + meshopt_optimizeVertexCache(NULL, NULL, 0, 0); + meshopt_optimizeVertexCacheFifo(NULL, NULL, 0, 0, 16); + meshopt_optimizeOverdraw(NULL, NULL, 0, NULL, 0, 12, 1.f); +} + +static void simplify() +{ + // 0 + // 1 2 + // 3 4 5 + unsigned int ib[] = { + 0, 2, 1, + 1, 2, 3, + 3, 2, 4, + 2, 5, 4 // + }; + + float vb[] = { + 0, 4, 0, + 0, 1, 0, + 2, 2, 0, + 0, 0, 0, + 1, 0, 0, + 4, 0, 0 // + }; + + unsigned int expected[] = { + 0, + 5, + 3, + }; + + float error; + assert(meshopt_simplify(ib, ib, 12, vb, 6, 12, 3, 1e-2f, 0, &error) == 3); + assert(error < 1e-4f); + assert(memcmp(ib, expected, sizeof(expected)) == 0); +} + +static void simplifyStuck() +{ + // tetrahedron can't be simplified due to collapse error restrictions + float vb1[] = {0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1}; + unsigned int ib1[] = {0, 1, 2, 0, 2, 3, 0, 3, 1, 2, 1, 3}; + + assert(meshopt_simplify(ib1, ib1, 12, vb1, 4, 12, 6, 1e-3f) == 12); + + // 5-vertex strip can't be simplified due to topology restriction since middle triangle has flipped winding + float vb2[] = {0, 0, 0, 1, 0, 0, 2, 0, 0, 0.5f, 1, 0, 1.5f, 1, 0}; + unsigned int ib2[] = {0, 1, 3, 3, 1, 4, 1, 2, 4}; // ok + unsigned int ib3[] = {0, 1, 3, 1, 3, 4, 1, 2, 4}; // flipped + + assert(meshopt_simplify(ib2, ib2, 9, vb2, 5, 12, 6, 1e-3f) == 6); + assert(meshopt_simplify(ib3, ib3, 9, vb2, 5, 12, 6, 1e-3f) == 9); + + // 4-vertex quad with a locked corner can't be simplified due to border error-induced restriction + float vb4[] = {0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0}; + unsigned int ib4[] = {0, 1, 3, 0, 3, 2}; + + assert(meshopt_simplify(ib4, ib4, 6, vb4, 4, 12, 3, 1e-3f) == 6); + + // 4-vertex quad with a locked corner can't be simplified due to border error-induced restriction + float vb5[] = {0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0}; + unsigned int ib5[] = {0, 1, 4, 0, 3, 2}; + + assert(meshopt_simplify(ib5, ib5, 6, vb5, 5, 12, 3, 1e-3f) == 6); +} + +static void simplifySloppyStuck() +{ + const float vb[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; + const unsigned int ib[] = {0, 1, 2, 0, 1, 2}; + + unsigned int* target = NULL; + + // simplifying down to 0 triangles results in 0 immediately + assert(meshopt_simplifySloppy(target, ib, 3, vb, 3, 12, 0, 0.f) == 0); + + // simplifying down to 2 triangles given that all triangles are degenerate results in 0 as well + assert(meshopt_simplifySloppy(target, ib, 6, vb, 3, 12, 6, 0.f) == 0); +} + +static void simplifySloppyLocks() +{ + // 0 + // 1 2 + // 3 4 5 + unsigned int ib[] = { + 0, 2, 1, + 1, 2, 3, + 3, 2, 4, + 2, 5, 4 // + }; + + float vb[] = { + 0, 4, 0, + 0, 1, 0, + 2, 2, 0, + 0, 0, 0, + 1, 0, 0, + 4, 0, 0 // + }; + + // lock spine + unsigned char locks[] = {1, 0, 1, 0, 0, 1}; + + unsigned int expected[] = { + 0, + 2, + 1, + 1, + 2, + 5, + }; + + float error; + assert(meshopt_simplifySloppy(ib, ib, 12, vb, 6, 12, locks, 3, 1.f, &error) == 6); + assert(error == 0.f); + assert(memcmp(ib, expected, sizeof(expected)) == 0); +} + +static void simplifyPointsStuck() +{ + const float vb[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; + + // simplifying down to 0 points results in 0 immediately + assert(meshopt_simplifyPoints(NULL, vb, 3, 12, NULL, 0, 0, 0) == 0); +} + +static void simplifyFlip() +{ + // this mesh has been constructed by taking a tessellated irregular grid with a square cutout + // and progressively collapsing edges until the only ones left violate border or flip constraints. + // there is only one valid non-flip collapse, so we validate that we take it; when flips are allowed, + // the wrong collapse is picked instead. + float vb[] = { + 1.000000f, 1.000000f, -1.000000f, + 1.000000f, 1.000000f, 1.000000f, + 1.000000f, -1.000000f, 1.000000f, + 1.000000f, -0.200000f, -0.200000f, + 1.000000f, 0.200000f, -0.200000f, + 1.000000f, -0.200000f, 0.200000f, + 1.000000f, 0.200000f, 0.200000f, + 1.000000f, 0.500000f, -0.500000f, + 1.000000f, -1.000000f, 0.000000f // + }; + + // the collapse we expect is 7 -> 0 + unsigned int ib[] = { + 7, 4, 3, + 1, 2, 5, + 7, 1, 6, + // gets removed + 7, 8, 0, + 7, 6, 4, + 8, 5, 2, + 8, 7, 3, + 8, 3, 5, + 5, 6, 1, + // gets removed + 7, 0, 1 // + }; + + unsigned int expected[] = { + 0, 4, 3, + 1, 2, 5, + 0, 1, 6, + 0, 6, 4, + 8, 5, 2, + 8, 0, 3, + 8, 3, 5, + 5, 6, 1 // + }; + + assert(meshopt_simplify(ib, ib, 30, vb, 9, 12, 3, 1e-3f) == 24); + assert(memcmp(ib, expected, sizeof(expected)) == 0); +} + +static void simplifyScale() +{ + const float vb[] = {0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3}; + + assert(meshopt_simplifyScale(vb, 4, 12) == 3.f); +} + +static void simplifyDegenerate() +{ + float vb[] = { + 0.000000f, 0.000000f, 0.000000f, + 0.000000f, 1.000000f, 0.000000f, + 0.000000f, 2.000000f, 0.000000f, + 1.000000f, 0.000000f, 0.000000f, + 2.000000f, 0.000000f, 0.000000f, + 1.000000f, 1.000000f, 0.000000f // + }; + + // 0 1 2 + // 3 5 + // 4 + + unsigned int ib[] = { + 0, 1, 3, + 3, 1, 5, + 1, 2, 5, + 3, 5, 4, + // these two degenerate triangles create a fake reverse edge + 1, 0, 1, + // which breaks border classification + 0, 3, 0 // + }; + + unsigned int expected[] = { + 0, 1, 4, + 4, 1, 2 // + }; + + assert(meshopt_simplify(ib, ib, 18, vb, 6, 12, 3, 1e-3f) == 6); + assert(memcmp(ib, expected, sizeof(expected)) == 0); +} + +static void simplifyLockBorder() +{ + float vb[] = { + 0.000000f, 0.000000f, 0.000000f, + 0.000000f, 1.000000f, 0.000000f, + 0.000000f, 2.000000f, 0.000000f, + 1.000000f, 0.000000f, 0.000000f, + 1.000000f, 1.000000f, 0.000000f, + 1.000000f, 2.000000f, 0.000000f, + 2.000000f, 0.000000f, 0.000000f, + 2.000000f, 1.000000f, 0.000000f, + 2.000000f, 2.000000f, 0.000000f // + }; + + // 0 1 2 + // 3 4 5 + // 6 7 8 + + unsigned int ib[] = { + 0, 1, 3, + 3, 1, 4, + 1, 2, 4, + 4, 2, 5, + 3, 4, 6, + 6, 4, 7, + 4, 5, 7, + 7, 5, 8 // + }; + + unsigned int expected[] = { + 0, 1, 3, + 1, 2, 3, + 3, 2, 5, + 6, 3, 7, + 3, 5, 7, + 7, 5, 8 // + }; + + assert(meshopt_simplify(ib, ib, 24, vb, 9, 12, 3, 1e-3f, meshopt_SimplifyLockBorder) == 18); + assert(memcmp(ib, expected, sizeof(expected)) == 0); +} + +static void simplifyAttr(bool skip_g) +{ + float vb[8 * 3][6]; + + for (int y = 0; y < 8; ++y) + { + // first four rows are a blue gradient, next four rows are a yellow gradient + float r = (y < 4) ? 0.8f + y * 0.05f : 0.f; + float g = (y < 4) ? 0.8f + y * 0.05f : 0.f; + float b = (y < 4) ? 0.f : 0.8f + (7 - y) * 0.05f; + + for (int x = 0; x < 3; ++x) + { + vb[y * 3 + x][0] = float(x); + vb[y * 3 + x][1] = float(y); + vb[y * 3 + x][2] = 0.03f * x + 0.03f * (y % 2) + (x == 2 && y == 7) * 0.03f; + vb[y * 3 + x][3] = r; + vb[y * 3 + x][4] = g; + vb[y * 3 + x][5] = b; + } + } + + unsigned int ib[7 * 2][6]; + + for (int y = 0; y < 7; ++y) + { + for (int x = 0; x < 2; ++x) + { + ib[y * 2 + x][0] = (y + 0) * 3 + (x + 0); + ib[y * 2 + x][1] = (y + 0) * 3 + (x + 1); + ib[y * 2 + x][2] = (y + 1) * 3 + (x + 0); + ib[y * 2 + x][3] = (y + 1) * 3 + (x + 0); + ib[y * 2 + x][4] = (y + 0) * 3 + (x + 1); + ib[y * 2 + x][5] = (y + 1) * 3 + (x + 1); + } + } + + float attr_weights[3] = {0.5f, skip_g ? 0.f : 0.5f, 0.5f}; + + // *0 1 *2 + // 3 4 5 + // 6 7 8 + // *9 10 *11 + // *12 13 *14 + // 15 16 17 + // 18 19 20 + // *21 22 *23 + unsigned int expected[3][6] = { + {0, 2, 11, 0, 11, 9}, + {9, 11, 12, 12, 11, 14}, + {12, 14, 23, 12, 23, 21}, + }; + + assert(meshopt_simplifyWithAttributes(ib[0], ib[0], 7 * 2 * 6, vb[0], 8 * 3, 6 * sizeof(float), vb[0] + 3, 6 * sizeof(float), attr_weights, 3, NULL, 6 * 3, 1e-2f) == 18); + assert(memcmp(ib, expected, sizeof(expected)) == 0); +} + +static void simplifyLockFlags() +{ + float vb[] = { + 0, 0, 0, + 0, 1, 0, + 0, 2, 0, + 1, 0, 0, + 1, 1, 0, + 1, 2, 0, + 2, 0, 0, + 2, 1, 0, + 2, 2, 0 // + }; + + unsigned char lock[9] = { + 1, 1, 1, + 1, 0, 1, + 1, 1, 1 // + }; + + // 0 1 2 + // 3 4 5 + // 6 7 8 + + unsigned int ib[] = { + 0, 1, 3, + 3, 1, 4, + 1, 2, 4, + 4, 2, 5, + 3, 4, 6, + 6, 4, 7, + 4, 5, 7, + 7, 5, 8 // + }; + + unsigned int expected[] = { + 0, 1, 3, + 1, 2, 3, + 3, 2, 5, + 6, 3, 7, + 3, 5, 7, + 7, 5, 8 // + }; + + assert(meshopt_simplifyWithAttributes(ib, ib, 24, vb, 9, 12, NULL, 0, NULL, 0, lock, 3, 1e-3f, 0) == 18); + assert(memcmp(ib, expected, sizeof(expected)) == 0); +} + +static void simplifyLockFlagsSeam() +{ + float vb[] = { + 0, 0, 0, + 0, 1, 0, + 0, 1, 0, + 0, 2, 0, + 1, 0, 0, + 1, 1, 0, + 1, 1, 0, + 1, 2, 0, + 2, 0, 0, + 2, 1, 0, + 2, 1, 0, + 2, 2, 0 // + }; + + unsigned char lock0[12] = { + 1, 0, 0, 1, + 0, 0, 0, 0, + 1, 0, 0, 1 // + }; + + unsigned char lock1[12] = { + 1, 0, 0, 1, + 1, 0, 0, 1, + 1, 0, 0, 1 // + }; + + unsigned char lock2[12] = { + 1, 0, 1, 1, + 1, 0, 1, 1, + 1, 0, 1, 1 // + }; + + unsigned char lock3[12] = { + 1, 1, 0, 1, + 1, 1, 0, 1, + 1, 1, 0, 1 // + }; + + // 0 1-2 3 + // 4 5-6 7 + // 8 9-10 11 + + unsigned int ib[] = { + 0, 1, 4, + 4, 1, 5, + 4, 5, 8, + 8, 5, 9, + 2, 3, 6, + 6, 3, 7, + 6, 7, 10, + 10, 7, 11 // + }; + + unsigned int res[24]; + // with no locks, we should be able to collapse the entire mesh (vertices 1-2 and 9-10 are locked but others can move towards them) + assert(meshopt_simplifyWithAttributes(res, ib, 24, vb, 12, 12, NULL, 0, NULL, 0, NULL, 0, 1.f, 0) == 0); + + // with corners locked, we should get two quads + assert(meshopt_simplifyWithAttributes(res, ib, 24, vb, 12, 12, NULL, 0, NULL, 0, lock0, 0, 1.f, 0) == 12); + + // with both sides locked, we can only collapse the seam spine + assert(meshopt_simplifyWithAttributes(res, ib, 24, vb, 12, 12, NULL, 0, NULL, 0, lock1, 0, 1.f, 0) == 18); + + // with seam spine locked, we can collapse nothing; note that we intentionally test two different lock configurations + // they each lock only one side of the seam spine, which should be equivalent + assert(meshopt_simplifyWithAttributes(res, ib, 24, vb, 12, 12, NULL, 0, NULL, 0, lock2, 0, 1.f, 0) == 24); + assert(meshopt_simplifyWithAttributes(res, ib, 24, vb, 12, 12, NULL, 0, NULL, 0, lock3, 0, 1.f, 0) == 24); +} + +static void simplifySparse() +{ + float vb[] = { + 0, 0, 100, + 0, 1, 0, + 0, 2, 100, + 1, 0, 0.1f, + 1, 1, 0.1f, + 1, 2, 0.1f, + 2, 0, 100, + 2, 1, 0, + 2, 2, 100 // + }; + + float vba[] = { + 100, + 0.5f, + 100, + 0.5f, + 0.5f, + 0, + 100, + 0.5f, + 100, + }; + + float aw[] = { + 0.5f}; + + unsigned char lock[9] = { + 8, 1, 8, + 1, 0, 1, + 8, 1, 8 // + }; + + // 1 + // 3 4 5 + // 7 + + unsigned int ib[] = { + 3, 1, 4, + 1, 5, 4, + 3, 4, 7, + 4, 5, 7 // + }; + + unsigned int res[12]; + + // vertices 3-4-5 are slightly elevated along Z which guides the collapses when only using geometry + unsigned int expected[] = { + 1, 5, 3, + 3, 5, 7 // + }; + + assert(meshopt_simplify(res, ib, 12, vb, 9, 12, 6, 1e-3f, meshopt_SimplifySparse) == 6); + assert(memcmp(res, expected, sizeof(expected)) == 0); + + // vertices 1-4-7 have a crease in the attribute value which guides the collapses the opposite way when weighing attributes sufficiently + unsigned int expecteda[] = { + 3, 1, 7, + 1, 5, 7 // + }; + + assert(meshopt_simplifyWithAttributes(res, ib, 12, vb, 9, 12, vba, sizeof(float), aw, 1, lock, 6, 1e-1f, meshopt_SimplifySparse) == 6); + assert(memcmp(res, expecteda, sizeof(expecteda)) == 0); + + // a final test validates that destination can alias when using sparsity + assert(meshopt_simplify(ib, ib, 12, vb, 9, 12, 6, 1e-3f, meshopt_SimplifySparse) == 6); + assert(memcmp(ib, expected, sizeof(expected)) == 0); +} + +static void simplifyErrorAbsolute() +{ + float vb[] = { + 0, 0, 0, + 0, 1, 0, + 0, 2, 0, + 1, 0, 0, + 1, 1, 1, + 1, 2, 0, + 2, 0, 0, + 2, 1, 0, + 2, 2, 0 // + }; + + // 0 1 2 + // 3 4 5 + // 6 7 8 + + unsigned int ib[] = { + 0, 1, 3, + 3, 1, 4, + 1, 2, 4, + 4, 2, 5, + 3, 4, 6, + 6, 4, 7, + 4, 5, 7, + 7, 5, 8 // + }; + + float error = 0.f; + assert(meshopt_simplify(ib, ib, 24, vb, 9, 12, 18, 2.f, meshopt_SimplifyLockBorder | meshopt_SimplifyErrorAbsolute, &error) == 18); + assert(fabsf(error - 0.85f) < 0.01f); +} + +static void simplifySeam() +{ + // xyz+attr + float vb[] = { + 0, 0, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 1, + 0, 2, 0, 1, + 1, 0, 0, 0, + 1, 1, 0.3f, 0, + 1, 1, 0.3f, 1, + 1, 2, 0, 1, + 2, 0, 0, 0, + 2, 1, 0.1f, 0, + 2, 1, 0.1f, 1, + 2, 2, 0, 1, + 3, 0, 0, 0, + 3, 1, 0, 0, + 3, 1, 0, 1, + 3, 2, 0, 1 // + }; + + // 0 1-2 3 + // 4 5-6 7 + // 8 9-10 11 + // 12 13-14 15 + + unsigned int ib[] = { + 0, 1, 4, + 4, 1, 5, + 2, 3, 6, + 6, 3, 7, + 4, 5, 8, + 8, 5, 9, + 6, 7, 10, + 10, 7, 11, + 8, 9, 12, + 12, 9, 13, + 10, 11, 14, + 14, 11, 15 // + }; + + // note: vertices 1-2 and 13-14 are classified as locked, because they are on a seam & a border + // 0 1-2 3 + // 5-6 + // 9-10 + // 12 13-14 15 + unsigned int expected[] = { + 0, 1, 13, + 2, 3, 14, + 0, 13, 12, + 14, 3, 15 // + }; + + unsigned int res[36]; + float error = 0.f; + + assert(meshopt_simplify(res, ib, 36, vb, 16, 16, 12, 1.f, 0, &error) == 12); + assert(memcmp(res, expected, sizeof(expected)) == 0); + assert(fabsf(error - 0.1f) < 0.01f); // note: the error is not zero because there is a difference in height between the seam vertices + + float aw = 1; + assert(meshopt_simplifyWithAttributes(res, ib, 36, vb, 16, 16, vb + 3, 16, &aw, 1, NULL, 12, 2.f, 0, &error) == 12); + assert(memcmp(res, expected, sizeof(expected)) == 0); + assert(fabsf(error - 0.1f) < 0.01f); // note: this is the same error as above because the attribute is constant on either side of the seam +} + +static void simplifySeamFake() +{ + // xyz+attr + float vb[] = { + 0, 0, 0, 0, + 1, 0, 0, 1, + 1, 0, 0, 2, + 0, 0, 0, 3 // + }; + + unsigned int ib[] = { + 0, 1, 2, + 2, 1, 3 // + }; + + assert(meshopt_simplify(ib, ib, 6, vb, 4, 16, 0, 1.f, 0, NULL) == 6); +} + +static void simplifySeamAttr() +{ + // xyz+attr + float vb[] = { + 0, 0, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 2, 0, 0, + 1, 0, 0, 1, + 1, 1, 0, 1, + 1, 1, 0, 1, + 1, 2, 0, 1, + 4, 0, 0, 2, + 4, 1, 0, 2, + 4, 1, 0, 2, + 4, 2, 0, 2 // + }; + + // 0 1-2 3 + // 4 5-6 7 + // 8 9-10 11 + + unsigned int ib[] = { + 0, 1, 4, + 4, 1, 5, + 2, 3, 6, + 6, 3, 7, + 4, 5, 8, + 8, 5, 9, + 6, 7, 10, + 10, 7, 11 // + }; + + // note: vertices 1-2 and 9-10 are classified as locked, because they are on a seam & a border + // 0 1-2 3 + // 4 7 + // 8 9-10 11 + unsigned int expected[] = { + 0, 1, 4, + 2, 3, 7, + 4, 1, 8, + 8, 1, 9, + 2, 7, 10, + 10, 7, 11 // + }; + + unsigned int res[24]; + float error = 0.f; + + float aw = 1; + assert(meshopt_simplifyWithAttributes(res, ib, 24, vb, 12, 16, vb + 3, 16, &aw, 1, NULL, 12, 2.f, meshopt_SimplifyLockBorder, &error) == 18); + assert(memcmp(res, expected, sizeof(expected)) == 0); + assert(fabsf(error - 0.35f) < 0.01f); +} + +static void simplifyDebug() +{ + // 0 + // 1 2 + // 3 4 5 + unsigned int ib[] = { + 0, 2, 1, + 1, 2, 3, + 3, 2, 4, + 2, 5, 4 // + }; + + float vb[] = { + 0, 4, 0, + 0, 1, 0, + 2, 2, 0, + 0, 0, 0, + 1, 0, 0, + 4, 0, 0 // + }; + + unsigned int expected[] = { + 0 | (9u << 28), + 5 | (9u << 28), + 3 | (9u << 28), + }; + + const unsigned int meshopt_SimplifyInternalDebug = 1 << 30; + + float error; + assert(meshopt_simplify(ib, ib, 12, vb, 6, 12, 3, 1e-2f, meshopt_SimplifyInternalDebug, &error) == 3); + assert(error < 1e-4f); + assert(memcmp(ib, expected, sizeof(expected)) == 0); +} + +static void simplifyPrune() +{ + // 0 + // 1 2 + // 3 4 5 + // + + // 6 7 8 (same position) + unsigned int ib[] = { + 3, 2, 4, + 0, 2, 1, + 1, 2, 3, + 2, 5, 4, + 6, 7, 8 // + }; + + float vb[] = { + 0, 4, 0, + 0, 1, 0, + 2, 2, 0, + 0, 0, 0, + 1, 0, 0, + 4, 0, 0, + 1, 1, 1, + 1, 1, 1, + 1, 1, 1 // + }; + + unsigned int expected[] = { + 0, + 5, + 3, + }; + + float error; + assert(meshopt_simplify(ib, ib, 15, vb, 9, 12, 3, 1e-2f, meshopt_SimplifyPrune, &error) == 3); + assert(error < 1e-4f); + assert(memcmp(ib, expected, sizeof(expected)) == 0); + + // re-run prune with and without sparsity on a small subset to make sure the component code correctly handles sparse subsets + assert(meshopt_simplify(ib, ib, 3, vb, 9, 12, 3, 1e-2f, meshopt_SimplifyPrune, &error) == 3); + assert(meshopt_simplify(ib, ib, 3, vb, 9, 12, 3, 1e-2f, meshopt_SimplifyPrune | meshopt_SimplifySparse, &error) == 3); + assert(memcmp(ib, expected, sizeof(expected)) == 0); +} + +static void simplifyPruneCleanup() +{ + unsigned int ib[] = { + 0, 1, 2, + 3, 4, 5, + 6, 7, 8 // + }; + + float vb[] = { + 0, 0, 0, + 0, 1, 0, + 1, 0, 0, + 0, 0, 1, + 0, 2, 1, + 2, 0, 1, + 0, 0, 2, + 0, 4, 2, + 4, 0, 2 // + }; + + unsigned int expected[] = { + 6, + 7, + 8, + }; + + float error; + assert(meshopt_simplify(ib, ib, 9, vb, 9, 12, 3, 1.f, meshopt_SimplifyLockBorder | meshopt_SimplifyPrune, &error) == 3); + assert(fabsf(error - 0.37f) < 0.01f); + assert(memcmp(ib, expected, sizeof(expected)) == 0); +} + +static void simplifyPruneFunc() +{ + unsigned int ib[] = { + 0, 1, 2, + 3, 4, 5, + 6, 7, 8 // + }; + + float vb[] = { + 0, 0, 0, + 0, 1, 0, + 1, 0, 0, + 0, 0, 1, + 0, 2, 1, + 2, 0, 1, + 0, 0, 2, + 0, 4, 2, + 4, 0, 2 // + }; + + unsigned int expected[] = { + 6, + 7, + 8, + }; + + assert(meshopt_simplifyPrune(ib, ib, 9, vb, 9, 12, 0.5f) == 3); + assert(memcmp(ib, expected, sizeof(expected)) == 0); +} + +static void simplifyUpdate() +{ + float vb[5][4] = { + {0, 0, 0, 0}, + {1, 1, 0, 0}, + {2, 0, 0, 0}, + {0.9f, 0.2f, 0.1f, 0.2f}, + {1.1f, 0.2f, 0.1f, 0.1f}, + }; + + // 1 + // 3 4 + // 0 2 + unsigned int ib[15] = { + 0, 1, 3, 3, 1, 4, 4, 1, 2, 0, 3, 2, 3, 4, 2 // + }; + + float attr_weight = 1.f; + + assert(meshopt_simplifyWithUpdate(ib, 15, vb[0], 5, 4 * sizeof(float), vb[0] + 3, 4 * sizeof(float), &attr_weight, 1, NULL, 9, 1.f) == 9); + + unsigned int expected[] = { + 0, 1, 3, 3, 1, 2, 0, 3, 2 // + }; + + assert(memcmp(ib, expected, sizeof(expected)) == 0); + + // border vertices haven't moved but may have small floating point drift + for (int i = 0; i < 3; ++i) + assert(fabsf(vb[i][3]) < 1e-6f); + + // center vertex got updated + assert(fabsf(vb[3][0] - 0.88f) < 1e-2f); + assert(fabsf(vb[3][1] - 0.19f) < 1e-2f); + assert(fabsf(vb[3][2] - 0.11f) < 1e-2f); + assert(fabsf(vb[3][3] - 0.18f) < 1e-2f); +} + +static void simplifyUpdateLocked(unsigned int options) +{ + float vb[5][4] = { + {0, 0, 0, 0}, + {1, 1, 0, 0}, + {2, 0, 0, 0}, + {0.9f, 0.2f, 0.1f, 0.2f}, + {1.1f, 0.2f, 0.1f, 0.1f}, + }; + + // 1 + // 3 4 + // 0 2 + unsigned int ib[15] = { + 0, 1, 3, 3, 1, 4, 4, 1, 2, 0, 3, 2, 3, 4, 2 // + }; + + float attr_weight = 1.f; + + unsigned char vertex_lock[5] = {0, 0, 0, 1, 0}; + + assert(meshopt_simplifyWithUpdate(ib, 15, vb[0], 5, 4 * sizeof(float), vb[0] + 3, 4 * sizeof(float), &attr_weight, 1, vertex_lock, 9, 1.f, options) == 9); + + unsigned int expected[] = { + 0, 1, 3, 3, 1, 2, 0, 3, 2 // + }; + + assert(memcmp(ib, expected, sizeof(expected)) == 0); + + for (int i = 0; i < 3; ++i) + assert(fabsf(vb[i][3]) < 1e-6f); + + // locking guarantees exact result + assert(vb[3][0] == 0.9f); + assert(vb[3][1] == 0.2f); + assert(vb[3][2] == 0.1f); + assert(vb[3][3] == 0.2f); +} + +static void simplifyFolds() +{ + const float vb[] = { + 0, 0, 0, 1, 0, 0, 2, 0, 0, + 0, 1, 0, 1, 1, 0, 2, 1, 0 // + }; + + // 0 1 2 + // 3 4 5 + flipped versions + const unsigned int ib[] = { + 0, 1, 3, 3, 1, 4, 1, 2, 4, 4, 2, 5, + 0, 3, 1, 3, 4, 1, 1, 4, 2, 4, 5, 2 // + }; + + unsigned int result[24]; + + // without fold preservation, the errors for all collapses are close to zero because there are no open edges to prevent this + assert(meshopt_simplify(result, ib, 24, vb, 6, 12, 0, 1e-3f) == 0); + assert(meshopt_simplify(result, ib, 24, vb, 6, 12, 0, 1e-3f, meshopt_SimplifyPreserveFolds) == 12); + + const unsigned int expected[] = { + 0, 2, 3, 3, 2, 5, 0, 3, 2, 3, 5, 2 // + }; + + assert(memcmp(result, expected, sizeof(expected)) == 0); +} + +static void filterTriangles() +{ + // v0/v3 match fully; v0/v4 match on prefix only + const unsigned int vb[] = { + 1, 0, 10, + 2, 0, 20, + 3, 0, 30, + 1, 1, 10, + 1, 1, 99 // + }; + + const unsigned int ib[] = { + 0, 1, 2, // + 3, 1, 2, // dup of (0,1,2) + 4, 1, 2, // dup prefix + 2, 1, 0, // opposite winding of (0,1,2) + 0, 4, 1 // degen prefix + }; + + // prefix only: vertex key is first 4 bytes + unsigned int sib[15]; + size_t slen = meshopt_filterIndexBuffer(sib, ib, 15, vb, 5, 4, 12); + + unsigned int expecteds[] = {0, 1, 2, 2, 1, 0}; + assert(slen == sizeof(expecteds) / sizeof(expecteds[0])); + assert(memcmp(sib, expecteds, sizeof(expecteds)) == 0); + + // prefix+suffix: vertex key is first 4 and last 4 bytes + const meshopt_Stream streams[] = {{vb + 0, 4, 12}, {vb + 2, 4, 12}}; + + unsigned int mib[15]; + size_t mlen = meshopt_filterIndexBufferMulti(mib, ib, 15, 5, streams, 2); + unsigned int expectedm[] = {0, 1, 2, 4, 1, 2, 2, 1, 0, 0, 4, 1}; + + assert(mlen == sizeof(expectedm) / sizeof(expectedm[0])); + assert(memcmp(mib, expectedm, sizeof(expectedm)) == 0); +} + +static void adjacency() +{ + // 0 1/4 + // 2/5 3 + const float vb[] = {0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0}; + const unsigned int ib[] = {0, 1, 2, 5, 4, 3}; + + unsigned int adjib[12]; + meshopt_generateAdjacencyIndexBuffer(adjib, ib, 6, vb, 6, 12); + + unsigned int expected[] = { + // patch 0 + 0, 0, + 1, 3, + 2, 2, + + // patch 1 + 5, 0, + 4, 4, + 3, 3 // + }; + + assert(memcmp(adjib, expected, sizeof(expected)) == 0); +} + +static void tessellation() +{ + // 0 1/4 + // 2/5 3 + const float vb[] = {0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0}; + const unsigned int ib[] = {0, 1, 2, 5, 4, 3}; + + unsigned int tessib[24]; + meshopt_generateTessellationIndexBuffer(tessib, ib, 6, vb, 6, 12); + + unsigned int expected[] = { + // patch 0 + 0, 1, 2, + 0, 1, + 4, 5, + 2, 0, + 0, 1, 2, + + // patch 1 + 5, 4, 3, + 2, 1, + 4, 3, + 3, 5, + 2, 1, 3 // + }; + + assert(memcmp(tessib, expected, sizeof(expected)) == 0); +} + +static void provoking() +{ + // 0 1 2 + // 3 4 5 + const unsigned int ib[] = { + 0, 1, 3, + 3, 1, 4, + 1, 2, 4, + 4, 2, 5, + 0, 2, 4 // + }; + + unsigned int pib[15]; + unsigned int pre[6 + 5]; // limit is vertex count + triangle count + size_t res = meshopt_generateProvokingIndexBuffer(pib, pre, ib, 15, 6); + + unsigned int expectedib[] = { + 0, 5, 1, + 1, 4, 0, + 2, 4, 1, + 3, 4, 2, + 4, 5, 2 // + }; + + unsigned int expectedre[] = { + 3, 1, 2, 5, 4, 0 // + }; + + assert(res == 6); + assert(memcmp(pib, expectedib, sizeof(expectedib)) == 0); + assert(memcmp(pre, expectedre, sizeof(expectedre)) == 0); +} + +static void quantizeFloat() +{ + volatile float zero = 0.f; // avoids div-by-zero warnings + + assert(meshopt_quantizeFloat(1.2345f, 23) == 1.2345f); + + assert(meshopt_quantizeFloat(1.2345f, 16) == 1.2344971f); + assert(meshopt_quantizeFloat(1.2345f, 8) == 1.2343750f); + assert(meshopt_quantizeFloat(1.2345f, 4) == 1.25f); + assert(meshopt_quantizeFloat(1.2345f, 1) == 1.0); + + assert(meshopt_quantizeFloat(1.f, 0) == 1.0f); + + assert(meshopt_quantizeFloat(1.f / zero, 0) == 1.f / zero); + assert(meshopt_quantizeFloat(-1.f / zero, 0) == -1.f / zero); + + float nanf = meshopt_quantizeFloat(zero / zero, 8); + assert(nanf != nanf); +} + +static void quantizeHalf() +{ + volatile float zero = 0.f; // avoids div-by-zero warnings + + // normal + assert(meshopt_quantizeHalf(1.2345f) == 0x3cf0); + + // overflow + assert(meshopt_quantizeHalf(65535.f) == 0x7c00); + assert(meshopt_quantizeHalf(-65535.f) == 0xfc00); + + // large + assert(meshopt_quantizeHalf(65000.f) == 0x7bef); + assert(meshopt_quantizeHalf(-65000.f) == 0xfbef); + + // small + assert(meshopt_quantizeHalf(0.125f) == 0x3000); + assert(meshopt_quantizeHalf(-0.125f) == 0xb000); + + // very small + assert(meshopt_quantizeHalf(1e-4f) == 0x068e); + assert(meshopt_quantizeHalf(-1e-4f) == 0x868e); + + // underflow + assert(meshopt_quantizeHalf(1e-5f) == 0x0000); + assert(meshopt_quantizeHalf(-1e-5f) == 0x8000); + + // exponent underflow + assert(meshopt_quantizeHalf(1e-20f) == 0x0000); + assert(meshopt_quantizeHalf(-1e-20f) == 0x8000); + + // exponent overflow + assert(meshopt_quantizeHalf(1e20f) == 0x7c00); + assert(meshopt_quantizeHalf(-1e20f) == 0xfc00); + + // inf + assert(meshopt_quantizeHalf(1.f / zero) == 0x7c00); + assert(meshopt_quantizeHalf(-1.f / zero) == 0xfc00); + + // nan + unsigned short nanh = meshopt_quantizeHalf(zero / zero); + assert(nanh == 0x7e00 || nanh == 0xfe00); +} + +static void dequantizeHalf() +{ + volatile float zero = 0.f; // avoids div-by-zero warnings + + // normal + assert(meshopt_dequantizeHalf(0x3cf0) == 1.234375f); + + // large + assert(meshopt_dequantizeHalf(0x7bef) == 64992.f); + assert(meshopt_dequantizeHalf(0xfbef) == -64992.f); + + // small + assert(meshopt_dequantizeHalf(0x3000) == 0.125f); + assert(meshopt_dequantizeHalf(0xb000) == -0.125f); + + // very small + assert(meshopt_dequantizeHalf(0x068e) == 1.00016594e-4f); + assert(meshopt_dequantizeHalf(0x868e) == -1.00016594e-4f); + + // denormal + assert(meshopt_dequantizeHalf(0x00ff) == 0.f); + assert(meshopt_dequantizeHalf(0x80ff) == 0.f); // actually this is -0.f + assert(1.f / meshopt_dequantizeHalf(0x80ff) == -1.f / zero); + + // inf + assert(meshopt_dequantizeHalf(0x7c00) == 1.f / zero); + assert(meshopt_dequantizeHalf(0xfc00) == -1.f / zero); + + // nan + float nanf = meshopt_dequantizeHalf(0x7e00); + assert(nanf != nanf); +} + +static int validatePositionExponent(const float minv[3], const float maxv[3], int min_exponent, int max_bits) +{ + int exponent = meshopt_computePositionExponent(minv, maxv, min_exponent, max_bits); + + const int anchor_min = -(1 << 23); + const int anchor_max = (1 << 23) - 1; + const unsigned int range_max = (1 << max_bits) - 1; + + float scale = ldexpf(1.f, -exponent); + + for (int k = 0; k < 3; ++k) + { + float lo = minv[k] * scale; + float hi = maxv[k] * scale; + + // lo must fit in 24-bit signed under both round-to-nearest tie-break choices + int lo_neg = int(ceilf(lo - 0.5f)); // round half toward -inf + int lo_pos = int(floorf(lo + 0.5f)); // round half toward +inf + assert(anchor_min <= lo_neg); + assert(lo_neg <= lo_pos); + assert(lo_pos <= anchor_max); + + // hi must fit in 24-bit signed under both round-to-nearest tie-break choices + int hi_neg = int(ceilf(hi - 0.5f)); // round half toward -inf + int hi_pos = int(floorf(hi + 0.5f)); // round half toward +inf + assert(anchor_min <= hi_neg); + assert(hi_neg <= hi_pos); + assert(hi_pos <= anchor_max); + + // largest hi - smallest lo must fit in max_bits (covers both round-to-nearest tie-break choices) + assert(unsigned(hi_pos - lo_neg) <= range_max); + } + + return exponent; +} + +static void computePositionExponent() +{ + const float minv[3] = {-1.f, 2.f, 10.f}; + const float maxv[3] = {1.f, 2.5f, 20.f}; + + assert(validatePositionExponent(minv, maxv, -16, 16) == -12); + assert(validatePositionExponent(minv, maxv, -10, 16) == -10); + + const float minirr[3] = {-3.14159265f, -2.7182818f, -1.41321356f}; + const float maxirr[3] = {3.14159265f, 2.7182818f, 1.41321356f}; + + assert(validatePositionExponent(minirr, maxirr, -16, 16) == -13); + + const float point[3] = {1000000.f, 0.f, 0.f}; + assert(validatePositionExponent(point, point, -16, 16) == -3); + + const float negpoint[3] = {-1000000.f, 0.f, 0.f}; + assert(validatePositionExponent(negpoint, negpoint, -16, 16) == -3); + + const float asym_min[3] = {0.f, -1000.f, 0.f}; + const float asym_max[3] = {1e-3f, 1000.f, 1.f}; + assert(validatePositionExponent(asym_min, asym_max, -16, 16) == -5); + + const float zero[3] = {0.f, 0.f, 0.f}; + assert(validatePositionExponent(zero, zero, -16, 16) == -16); + + const float range_min[3] = {0.f, 0.f, 0.f}; + const float range_max[3] = {65535.f, 0.f, 0.f}; + assert(validatePositionExponent(range_min, range_max, 0, 16) == 0); + + const float range_over[3] = {65536.f, 0.f, 0.f}; + assert(validatePositionExponent(range_min, range_over, 0, 16) == 1); + + const float range_dgf_min[3] = {0.4f, 0.f, 0.f}; + const float range_dgf_max[3] = {65535.5625f, 0.f, 0.f}; + assert(validatePositionExponent(range_dgf_min, range_dgf_max, 0, 16) == 1); + + const float range_half_min[3] = {-1.f, 0.f, 0.f}; + const float range_half_max[3] = {131069.f, 0.f, 0.f}; + assert(validatePositionExponent(range_half_min, range_half_max, 0, 16) == 2); + + const float anchor_max[3] = {8388607.f, 0.f, 0.f}; + assert(validatePositionExponent(anchor_max, anchor_max, 0, 16) == 0); + + const float anchor_over[3] = {8388608.f, 0.f, 0.f}; + assert(validatePositionExponent(anchor_over, anchor_over, 0, 16) == 1); + + const float anchor_min[3] = {-8388608.f, 0.f, 0.f}; + assert(validatePositionExponent(anchor_min, anchor_min, 0, 16) == 1); // note: symmetric range; 0 would be acceptable + + const float posunit[3] = {1.f, 0.f, 0.f}; + assert(validatePositionExponent(posunit, posunit, -23, 16) == -22); + + const float negunit[3] = {-1.f, 0.f, 0.f}; + assert(validatePositionExponent(negunit, negunit, -23, 16) == -22); // note: symmetric range; -23 would be acceptable + + const float ties[3] = {0.5f, -0.5f, 2.5f}; + assert(validatePositionExponent(ties, ties, 0, 16) == 0); + + const float tie_min[3] = {-0.5f, 0.f, 0.f}; + const float tie_max[3] = {0.5f, 0.f, 0.f}; + assert(validatePositionExponent(tie_min, tie_max, 0, 16) == 0); + + const float bits21_max[3] = {2097151.f, 0.f, 0.f}; + assert(validatePositionExponent(range_min, bits21_max, 0, 21) == 0); + + const float anchor_half[3] = {8388607.5f, 0.f, 0.f}; + assert(validatePositionExponent(anchor_half, anchor_half, 0, 16) == 1); +} + +static void encodeMeshletBound() +{ + const unsigned char triangles[5 * 3] = { + 0, 1, 2, + 2, 1, 3, + 3, 5, 4, + 2, 0, 6, + 6, 6, 6 // + }; + + const unsigned int vertices[7] = { + 5, + 12, + 140, + 0, + 12389, + 123456789, + 7, + }; + + size_t bound = meshopt_encodeMeshletBound(7, 5); + + unsigned char enc[256]; + size_t size = meshopt_encodeMeshlet(enc, sizeof(enc), vertices, 7, triangles, 5); + assert(size > 0 && size <= bound); + + assert(meshopt_encodeMeshlet(enc, size - 1, vertices, 7, triangles, 5) == 0); +} + +template +static void validateDecodeMeshlet(const unsigned char* data, size_t size, const unsigned int* vertices, size_t vertex_count, const unsigned char* triangles, size_t triangle_count) +{ + V rv[VS]; + T rt[TS]; + + int rc = meshopt_decodeMeshlet(rv, vertex_count, rt, triangle_count, data, size); + assert(rc == 0); + + for (size_t j = 0; j < vertex_count; ++j) + assert(rv[j] == V(vertices[j])); + + for (size_t j = 0; j < triangle_count; ++j) + { + unsigned int a = triangles[j * 3 + 0]; + unsigned int b = triangles[j * 3 + 1]; + unsigned int c = triangles[j * 3 + 2]; + + unsigned int tri = sizeof(T) == 1 ? rt[j * 3] | (rt[j * 3 + 1] << 8) | (rt[j * 3 + 2] << 16) : rt[j]; + + unsigned int abc = (a << 0) | (b << 8) | (c << 16); + unsigned int bca = (b << 0) | (c << 8) | (a << 16); + unsigned int cba = (c << 0) | (a << 8) | (b << 16); + + assert(tri == abc || tri == bca || tri == cba); + } +} + +static void decodeMeshletSafety() +{ + const unsigned char triangles[5 * 3] = { + 0, 1, 2, + 2, 1, 3, + 3, 5, 4, + 2, 0, 6, + 6, 6, 6 // + }; + + const unsigned int vertices[7] = { + 5, + 12, + 140, + 0, + 12389, + 123456789, + 7, + }; + + unsigned char encb[256]; + size_t size = meshopt_encodeMeshlet(encb, sizeof(encb), vertices, 7, triangles, 5); + assert(size > 0); + + // move encoded buffer to the end to make sure any over-reads trigger sanitizers + unsigned char* enc = encb + sizeof(encb) - size; + memmove(enc, encb, size); + + validateDecodeMeshlet(enc, size, vertices, 7, triangles, 5); + + // decodeMeshlet uses aligned 32-bit writes => must round vertex/triangle storage up when using short/char decoding + // note the +1 in triangle storage is because align(5*3, 4) = 16; it's up to +3 in general case + validateDecodeMeshlet(enc, size, vertices, 7, triangles, 5); + validateDecodeMeshlet(enc, size, vertices, 7, triangles, 5); + validateDecodeMeshlet(enc, size, vertices, 7, triangles, 5); + + // any truncated input should not be decodable; we check both prefix and suffix truncation + unsigned int rv[7], rt[5]; + + for (size_t i = 1; i < size; ++i) + assert(meshopt_decodeMeshlet(rv, 7, rt, 5, enc, i) < 0); + for (size_t i = 1; i < size; ++i) + assert(meshopt_decodeMeshlet(rv, 7, rt, 5, enc + i, size - i) < 0); + + // because SIMD implementation is specialized by size, we need to test truncated inputs for short representations + unsigned short rvs[7 + 1]; // 32b alignment + unsigned char rts[5 * 3 + 1]; // 32b alignment + + for (size_t i = 1; i < size; ++i) + assert(meshopt_decodeMeshlet(rvs, 7, rts, 5, enc, i) < 0); + for (size_t i = 1; i < size; ++i) + assert(meshopt_decodeMeshlet(rvs, 7, rts, 5, enc + i, size - i) < 0); + + // when using decodeMeshletRaw, the output buffer sizes must be 16b aligned + unsigned int rvr[8], rtr[8]; + + for (size_t i = 1; i < size; ++i) + assert(meshopt_decodeMeshletRaw(rvr, 7, rtr, 5, enc, i) < 0); + for (size_t i = 1; i < size; ++i) + assert(meshopt_decodeMeshletRaw(rvr, 7, rtr, 5, enc + i, size - i) < 0); + + // otherwise, decodeMeshlet and decodeMeshletRaw should agree + int rc = meshopt_decodeMeshlet(rv, 7, rt, 5, enc, size); + int rcr = meshopt_decodeMeshletRaw(rvr, 7, rtr, 5, enc, size); + + assert(rc == 0 && rcr == 0); + assert(memcmp(rv, rvr, 7 * sizeof(unsigned int)) == 0); + assert(memcmp(rt, rtr, 5 * sizeof(unsigned int)) == 0); +} + +static void decodeMeshletBasic() +{ + const unsigned char triangles[5 * 3] = { + 0, 1, 2, + 2, 1, 3, + 4, 3, 5, + 2, 0, 6, + 6, 6, 6 // + }; + + const unsigned int vertices[7] = { + 5, + 12, + 140, + 0, + 12389, + 123456789, + 7, + }; + + const unsigned char encoded[46] = { + 0x0a, 0x0c, 0xfe, 0x19, 0x01, 0xc8, 0x60, 0x00, 0x00, 0x5e, 0x39, 0xb7, 0x0e, 0x1d, 0x9a, 0xb7, + 0x0e, 0x00, 0x00, 0x00, 0x00, 0x04, 0x03, 0x05, 0x02, 0x00, 0x06, 0x06, 0x06, 0x06, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87, 0xff, 0x2c, 0xff, 0x0f // + }; + + unsigned int rv[7]; + unsigned char rt[5 * 3 + 1]; + int rc = meshopt_decodeMeshlet(rv, 7, rt, 5, encoded, sizeof(encoded)); + + assert(rc == 0); + assert(memcmp(rv, vertices, sizeof(vertices)) == 0); + assert(memcmp(rt, triangles, sizeof(triangles)) == 0); +} + +static void decodeMeshletTypical() +{ + const unsigned char triangles[44 * 3] = { + 0, 1, 2, 0, 2, 3, 3, 2, 4, 3, 4, 5, 0, 3, 6, 6, 3, 5, 0, 6, 7, 7, 6, 8, + 8, 6, 5, 7, 8, 9, 8, 5, 10, 10, 5, 4, 10, 4, 11, 11, 4, 12, 11, 12, 13, 10, 11, 14, + 10, 14, 8, 14, 11, 15, 15, 11, 13, 15, 13, 16, 15, 16, 14, 16, 13, 17, 14, 16, 18, 14, 18, 8, + 18, 16, 19, 19, 16, 20, 20, 16, 17, 20, 17, 21, 20, 21, 22, 20, 22, 19, 22, 21, 23, 19, 22, 24, + 19, 24, 25, 19, 25, 18, 18, 25, 26, 18, 26, 8, 8, 26, 9, 22, 23, 27, 22, 27, 24, 27, 23, 28, + 27, 28, 29, 27, 29, 24, 29, 28, 30, 24, 29, 31 // + }; + + const unsigned int vertices[32] = { + 10, 11, 9, 12, 8, 13, 14, 15, 16, 17, 18, 19, 0, 20, 21, 22, + 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38 // + }; + + const unsigned char encoded[53] = { + 0x14, 0x05, 0x04, 0x09, 0x08, 0x27, 0x26, 0x05, 0x05, 0x04, 0x08, 0x0d, 0x0e, 0x08, 0x11, 0x13, + 0x12, 0x08, 0x09, 0x16, 0x17, 0x18, 0x18, 0x0d, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x0c, + 0x02, 0x38, 0x24, 0x43, 0x34, 0x20, 0x80, 0x61, 0x03, 0x61, 0x16, 0x26, 0x03, 0x10, 0x66, 0x10, + 0x12, 0xe3, 0x61, 0x10, 0x66 // + }; + + unsigned int rv[32]; + unsigned char rt[44 * 3]; + int rc = meshopt_decodeMeshlet(rv, 32, rt, 44, encoded, sizeof(encoded)); + + assert(rc == 0); + assert(memcmp(rv, vertices, sizeof(vertices)) == 0); + assert(memcmp(rt, triangles, sizeof(triangles)) == 0); +} + +static void opacityMap() +{ + const size_t triangle_count = 6; + const size_t vertex_count = 8; + + const unsigned int indices[triangle_count * 3] = { + // 4 corner triangles + 0, 4, 7, + 1, 5, 4, + 2, 6, 5, + 3, 7, 6, + + // 2 center triangles (2x larger) + 4, 6, 5, // note: this triangle is flipped from its correct orientation to produce the same OMM to test compaction + 4, 6, 7 // + }; + + const float uvs[vertex_count * 2] = { + 0.f, 0.f, + 1.f, 0.f, + 1.f, 1.f, + 0.f, 1.f, + 0.5f, 0.f, + 1.f, 0.5f, + 0.5f, 1.f, + 0.f, 0.5f // + }; + + const unsigned int texture_size = 32; + unsigned char texture[texture_size * texture_size]; + + float center = float(texture_size) * 0.5f; + float radius = 10.f; + + for (unsigned int y = 0; y < texture_size; ++y) + for (unsigned int x = 0; x < texture_size; ++x) + { + float dx = float(x) + 0.5f - center; + float dy = float(y) + 0.5f - center; + float dc = radius - sqrtf(dx * dx + dy * dy); + + texture[y * texture_size + x] = (unsigned char)meshopt_quantizeUnorm(dc + 0.5f, 8); + } + + // subdivision parameters + const float target_edge = 2.5f; + const int max_level = 4; + + // state histogram for testing + int histogram[2][4] = {}; + + for (int k = 0; k < 2; ++k) + { + int states = 2 << k; + + unsigned char levels[triangle_count]; + unsigned int sources[triangle_count]; + int omm_indices[triangle_count]; + + // compute level and source triangle per OMM; note that this can also deduplicate OMMs based on UVs but in our case the UVs are unique + size_t omm_count = meshopt_opacityMapMeasure(levels, sources, omm_indices, indices, triangle_count * 3, uvs, vertex_count, sizeof(float) * 2, texture_size, texture_size, max_level, target_edge); + assert(omm_count <= triangle_count); + + // validate expected levels/special indices based on underlying test data; depends on implementation specifics, might change + assert(levels[0] == 2 && levels[1] == 2 && levels[2] == 2 && levels[3] == 2); + assert(levels[4] == 3 && levels[5] == 3); + + // layout OMM data + std::vector offsets(omm_count); + size_t data_size = 0; + + for (size_t i = 0; i < omm_count; ++i) + { + offsets[i] = unsigned(data_size); + data_size += meshopt_opacityMapEntrySize(levels[i], states); + } + + std::vector data(data_size); + + for (size_t i = 0; i < omm_count; ++i) + { + unsigned int tri = sources[i]; + assert(tri < triangle_count); + + const float* uv0 = &uvs[indices[tri * 3 + 0] * 2]; + const float* uv1 = &uvs[indices[tri * 3 + 1] * 2]; + const float* uv2 = &uvs[indices[tri * 3 + 2] * 2]; + + meshopt_opacityMapRasterize(&data[offsets[i]], levels[i], states, uv0, uv1, uv2, texture, 1, texture_size, texture_size, texture_size); + + size_t micro_triangle_count = size_t(1) << (levels[i] * 2); + + for (size_t j = 0; j < micro_triangle_count; ++j) + { + unsigned char byte = data[offsets[i] + (j >> (states == 2 ? 3 : 2))]; + int state = (byte >> (states == 2 ? j & 7 : (j & 3) * 2)) & (states - 1); + histogram[k][state]++; + } + } + + // compact OMMs; note, this can be done per mesh but for optimal reuse this should be done for all meshes in model/scene at once + size_t compact_count = meshopt_opacityMapCompact(&data[0], data.size(), levels, &offsets[0], omm_count, omm_indices, triangle_count, states); + assert(compact_count <= omm_count); + data.resize(compact_count == 0 ? 0 : offsets[compact_count - 1] + meshopt_opacityMapEntrySize(levels[compact_count - 1], states)); + + // after compaction, some OMM indices may be replaced with a special index (-4..-1) + for (size_t i = 0; i < triangle_count; ++i) + assert(omm_indices[i] < 0 || size_t(omm_indices[i]) < compact_count); + + // validate expected levels/special indices based on underlying test data; depends on implementation specifics, might change + assert(levels[0] == 3 && levels[1] == 3); // note: OMM data got compacted so we only have 3-level OMMs left + assert(omm_indices[0] == -1 && omm_indices[1] == -1 && omm_indices[2] == -1 && omm_indices[3] == -1); + assert(compact_count == 1 && omm_indices[4] == 0 && omm_indices[5] == 0); // we force the two center triangles to use opposite orientations to make sure their data matches + } + + // validate expected histogram based on underlying test data; depends on rasterization specifics, might change + assert(histogram[0][0] == histogram[1][0] + histogram[1][2]); + assert(histogram[0][1] == histogram[1][1] + histogram[1][3]); + + float opaque = float(histogram[0][1]) / float(histogram[0][0] + histogram[0][1]); + float known = float(histogram[1][0] + histogram[1][1]) / float(histogram[1][0] + histogram[1][1] + histogram[1][2] + histogram[1][3]); + + assert(fabsf(opaque - 0.36f) < 1e-2f); + assert(fabsf(known - 0.76f) < 1e-2f); +} + +static void opacityMapRasterize0() +{ + const unsigned int texture_size = 32; + unsigned char texture[texture_size * texture_size]; + + float center = float(texture_size) * 0.5f; + float radius = 10.f; + + for (unsigned int y = 0; y < texture_size; ++y) + for (unsigned int x = 0; x < texture_size; ++x) + { + float dx = float(x) + 0.5f - center; + float dy = float(y) + 0.5f - center; + float dc = radius - sqrtf(dx * dx + dy * dy); + + texture[y * texture_size + x] = (unsigned char)meshopt_quantizeUnorm(dc + 0.5f, 8); + } + + const float uv0[2] = {0.f, 0.f}; + const float uv1[2] = {1.f, 0.f}; + const float uv2[2] = {0.f, 1.f}; + const float uv3[2] = {0.5f, 0.f}; + const float uv4[2] = {0.f, 0.5f}; + const float uv5[2] = {0.5f, 0.5f}; + + unsigned char r0 = 0, r1 = 0, r2 = 0; + meshopt_opacityMapRasterize(&r0, 0, 4, uv0, uv1, uv2, texture, 1, texture_size, texture_size, texture_size); + meshopt_opacityMapRasterize(&r1, 0, 4, uv0, uv3, uv4, texture, 1, texture_size, texture_size, texture_size); + meshopt_opacityMapRasterize(&r2, 0, 4, uv5, uv3, uv4, texture, 1, texture_size, texture_size, texture_size); + + assert(r0 == 2); // unknown-transparent + assert(r1 == 0); // transparent + assert(r2 == 3); // unknown-opaque +} + +static int opacityMapSpecialIndex(int level, int states, unsigned char value) +{ + std::vector data(meshopt_opacityMapEntrySize(level, states), value); + unsigned char levels[1] = {(unsigned char)level}; + unsigned int offsets[1] = {0}; + int omm_indices[1] = {0}; + + size_t compact_count = meshopt_opacityMapCompact(&data[0], data.size(), levels, offsets, 1, omm_indices, 1, states); + assert(compact_count == 0); + return omm_indices[0]; +} + +static void opacityMapSpecial() +{ + for (int level = 0; level <= 2; ++level) + { + assert(opacityMapSpecialIndex(level, 2, 0x00) == -1); + assert(opacityMapSpecialIndex(level, 2, 0xff) == -2); + + assert(opacityMapSpecialIndex(level, 4, 0x00) == -1); + assert(opacityMapSpecialIndex(level, 4, 0x55) == -2); + assert(opacityMapSpecialIndex(level, 4, 0xaa) == -3); + assert(opacityMapSpecialIndex(level, 4, 0xff) == -4); + } +} + +static void tangentsBasic() +{ + struct Vertex + { + float px, py, pz; + float nx, ny, nz; + float tx, ty; + }; + + // unindexed quad with matching corner data for shared diagonal + const Vertex vertices[] = { + {0, 0, 0, -0.28f, 0, 0.96f, 0, 0}, // diag 1 + {1, 0, 0, 0.28f, 0, 0.96f, 1, 0}, + {1, 1, 0, 0.28f, 0, 0.96f, 1, 1}, // diag 2 + {0, 0, 0, -0.28f, 0, 0.96f, 0, 0}, // diag 1 + {1, 1, 0, 0.28f, 0, 0.96f, 1, 1}, // diag 2 + {0, 1, 0, -0.28f, 0, 0.96f, 0, 1}, + }; + + // unindexed input: indices == NULL, vertex_count == index_count + float tangents[6 * 4]; + meshopt_generateTangents(tangents, NULL, 6, &vertices[0].px, 6, sizeof(Vertex), &vertices[0].nx, sizeof(Vertex), &vertices[0].tx, sizeof(Vertex), 0); + + // (1, 0, 0) reprojected onto tilted normals + const float left[4] = {0.96f, 0.f, 0.28f, 1.f}; + const float right[4] = {0.96f, 0.f, -0.28f, 1.f}; + + const float* expected[6] = {left, right, right, left, right, left}; + + for (size_t i = 0; i < 6; ++i) + for (size_t k = 0; k < 4; ++k) + assert(fabsf(tangents[i * 4 + k] - expected[i][k]) < 1e-3f); + + // shared vertices get the same tangent vector + for (int k = 0; k < 4; ++k) + { + assert(tangents[0 * 4 + k] == tangents[3 * 4 + k]); // diag 1 + assert(tangents[2 * 4 + k] == tangents[4 * 4 + k]); // diag 2 + } +} + +static void tangentDegenerate() +{ + struct Vertex + { + float px, py, pz; + float nx, ny, nz; + float tx, ty; + }; + + const Vertex vertices[] = { + {0, 0, 0, 0, 0, 1, 0, 0}, + {1, 1, 0, 0, 0, 1, 1, 1}, + {2, 2, 0, 0, 0, 1, 2, 2}, + {-1, -2, 1, 0, 0, 1, 0, -2}, + {0, 1, 1, 0, 0, 1, 1, 0}, + {-1, 0, 0, 0, 0, 1, 1, 0}, + }; + + const unsigned int indices[] = { + // outer, positive + 0, 3, 1, + // inner, degenerate UVs + 0, 1, 2, + // outer, positive + 1, 4, 2, + // outer, negative + 2, 5, 0 // + }; + + float tangents[12 * 4]; + meshopt_generateTangents(tangents, indices, 12, &vertices[0].px, 6, sizeof(Vertex), &vertices[0].nx, sizeof(Vertex), &vertices[0].tx, sizeof(Vertex), meshopt_TangentCompatible); + + const float vx = 0.0836f; + const float vy = 0.9965f; + const float expected[12][4] = { + // outer, positive + {1.f, 0.f, 0.f, 1.f}, // 0 + {1.f, 0.f, 0.f, 1.f}, // 3 + {vx, vy, 0.f, 1.f}, // 1 + // inner, degenerate UVs + {1.f, 0.f, 0.f, 1.f}, // 0 + {vx, vy, 0.f, 1.f}, // 1 + {0.f, 1.f, 0.f, 1.f}, // 2 + // outer, positive + {vx, vy, 0.f, 1.f}, // 1 + {0.f, 1.f, 0.f, 1.f}, // 4 + {0.f, 1.f, 0.f, 1.f}, // 2 + // outer, negative + {-1.f, 0.f, 0.f, -1.f}, // 2 (split) + {-1.f, 0.f, 0.f, -1.f}, // 5 + {-1.f, 0.f, 0.f, -1.f}, // 0 (split) + }; + + for (size_t i = 0; i < 12; ++i) + for (size_t k = 0; k < 4; ++k) + assert(fabsf(tangents[i * 4 + k] - expected[i][k]) < 1e-3f); +} + +static void normalsBasic() +{ + const float vertices[][3] = { + {-1.f, -0.57735f, 0.f}, + {1.f, -0.57735f, 0.f}, + {0.f, 1.15470f, 0.f}, + {0.f, 0.f, 0.38f}, + }; + + // flattened tetrahedron (apex is closer to the base so that we can get soft side edges) + const unsigned int indices[] = {0, 2, 1, 0, 1, 3, 1, 2, 3, 2, 0, 3}; + + float normals[12 * 3]; + meshopt_generateNormals(normals, indices, 12, vertices[0], 4, sizeof(vertices[0]), 3.14159265f / 3.f, 0.f); + + const float base[3] = {0.f, 0.f, -1.f}; + const float side0[3] = {-0.2707f, -0.1563f, 0.9499f}; + const float side1[3] = {0.2707f, -0.1563f, 0.9499f}; + const float side2[3] = {0.f, 0.3126f, 0.9499f}; + const float apex[3] = {0.f, 0.f, 1.f}; + + const float* expected[12] = {base, base, base, side0, side1, apex, side1, side2, apex, side2, side0, apex}; + + for (size_t i = 0; i < 12; ++i) + for (size_t k = 0; k < 3; ++k) + assert(fabsf(normals[i * 3 + k] - expected[i][k]) < 1e-3f); + + // shared vertices around soft side edges get the same normal vector + for (size_t k = 0; k < 3; ++k) + { + assert(normals[3 * 3 + k] == normals[10 * 3 + k]); // side0 + assert(normals[4 * 3 + k] == normals[6 * 3 + k]); // side1 + assert(normals[7 * 3 + k] == normals[9 * 3 + k]); // side2 + assert(normals[5 * 3 + k] == normals[8 * 3 + k]); // apex + assert(normals[5 * 3 + k] == normals[11 * 3 + k]); // apex + } +} + +static void normalsDegenerate() +{ + const float vertices[][3] = { + {0.f, 0.f, 0.f}, + {0.f, 1.f, 0.f}, + {1.f, 0.f, 0.f}, + {2.f, 0.f, 0.f}, + {0.f, 0.5f, 0.f}, + }; + + // degenerate triangle connects the triangles across a hard edge + const unsigned int indices[] = {0, 1, 2, 0, 2, 3, 0, 3, 4}; + + float normals[9 * 3]; + meshopt_generateNormals(normals, indices, 9, vertices[0], 5, sizeof(vertices[0]), 3.14159265f * 3.f / 4.f, 0.f); + + const float negative[3] = {0.f, 0.f, -1.f}; + const float zero[3] = {0.f, 0.f, 0.f}; + const float positive[3] = {0.f, 0.f, 1.f}; + const float* expected[9] = {negative, negative, negative, zero, zero, zero, positive, positive, positive}; + + for (size_t i = 0; i < 9; ++i) + for (size_t k = 0; k < 3; ++k) + assert(fabsf(normals[i * 3 + k] - expected[i][k]) < 1e-3f); +} + +void runTests() +{ + decodeIndexV0(); + decodeIndexV1(); + decodeIndexV1More(); + decodeIndexV1ThreeEdges(); + decodeIndex16(); + encodeIndexMemorySafe(); + decodeIndexMemorySafe(); + decodeIndexRejectExtraBytes(); + decodeIndexRejectMalformedHeaders(); + decodeIndexRejectInvalidVersion(); + decodeIndexMalformedVByte(); + roundtripIndexTricky(); + encodeIndexEmpty(); + + decodeIndexSequence(); + decodeIndexSequence16(); + encodeIndexSequenceMemorySafe(); + decodeIndexSequenceMemorySafe(); + decodeIndexSequenceRejectExtraBytes(); + decodeIndexSequenceRejectMalformedHeaders(); + decodeIndexSequenceRejectInvalidVersion(); + encodeIndexSequenceEmpty(); + + decodeVertexV0(); + decodeVertexV0More(); + decodeVertexV0Mode2(); + decodeVertexV1(); + decodeVertexV1Custom(); + decodeVertexV1Deltas(); + + for (int version = 0; version <= 1; ++version) + { + meshopt_encodeVertexVersion(version); + + decodeVertexMemorySafe(); + decodeVertexRejectExtraBytes(); + decodeVertexRejectMalformedHeaders(); + decodeVertexBitGroups(); + decodeVertexBitGroupSentinels(); + decodeVertexBitGroupSentinelCount(); + decodeVertexDeltas(); + decodeVertexBitXor(); + decodeVertexLarge(); + decodeVertexSmall(); + encodeVertexEmpty(); + encodeVertexMemorySafe(); + } + + decodeVersion(); + + decodeFilterOct8(); + decodeFilterOct12(); + decodeFilterQuat12(); + decodeFilterExp(); + + encodeFilterOct8(); + encodeFilterOct12(); + encodeFilterQuat12(); + encodeFilterExp(); + encodeFilterExpZero(); + encodeFilterExpOverflow(); + encodeFilterExpZeroShared(); + encodeFilterExpAlias(); + encodeFilterExpClamp(); + encodeFilterColor8(); + encodeFilterColor12(); + + clusterBoundsDegenerate(); + sphereBounds(); + + extractMeshlet(); + + meshletsEmpty(); + meshletsDense(); + meshletsSparse(); + meshletsFlex(); + meshletsMax(); + meshletsSpatial(); + meshletsSpatialDeep(); + + partitionBasic(); + partitionSpatial(); + partitionSpatialMerge(); + + remapCustom(); + + customAllocator(); + + emptyMesh(); + + simplify(); + simplifyStuck(); + simplifySloppyStuck(); + simplifySloppyLocks(); + simplifyPointsStuck(); + simplifyFlip(); + simplifyScale(); + simplifyDegenerate(); + simplifyLockBorder(); + simplifyAttr(/* skip_g= */ false); + simplifyAttr(/* skip_g= */ true); + simplifyLockFlags(); + simplifyLockFlagsSeam(); + simplifySparse(); + simplifyErrorAbsolute(); + simplifySeam(); + simplifySeamFake(); + simplifySeamAttr(); + simplifyDebug(); + simplifyPrune(); + simplifyPruneCleanup(); + simplifyPruneFunc(); + simplifyUpdate(); + simplifyUpdateLocked(0); + simplifyUpdateLocked(meshopt_SimplifySparse); + simplifyFolds(); + + filterTriangles(); + adjacency(); + tessellation(); + provoking(); + + quantizeFloat(); + quantizeHalf(); + dequantizeHalf(); + computePositionExponent(); + + encodeMeshletBound(); + decodeMeshletSafety(); + decodeMeshletBasic(); + decodeMeshletTypical(); + + opacityMap(); + opacityMapRasterize0(); + opacityMapSpecial(); + + tangentsBasic(); + tangentDegenerate(); + normalsBasic(); + normalsDegenerate(); +} diff --git a/Lib/meshoptimizer/extern/.clang-format b/Lib/meshoptimizer/extern/.clang-format new file mode 100644 index 0000000..e384528 --- /dev/null +++ b/Lib/meshoptimizer/extern/.clang-format @@ -0,0 +1 @@ +DisableFormat: true diff --git a/Lib/meshoptimizer/extern/cgltf.h b/Lib/meshoptimizer/extern/cgltf.h new file mode 100644 index 0000000..316a11d --- /dev/null +++ b/Lib/meshoptimizer/extern/cgltf.h @@ -0,0 +1,7240 @@ +/** + * cgltf - a single-file glTF 2.0 parser written in C99. + * + * Version: 1.15 + * + * Website: https://github.com/jkuhlmann/cgltf + * + * Distributed under the MIT License, see notice at the end of this file. + * + * Building: + * Include this file where you need the struct and function + * declarations. Have exactly one source file where you define + * `CGLTF_IMPLEMENTATION` before including this file to get the + * function definitions. + * + * Reference: + * `cgltf_result cgltf_parse(const cgltf_options*, const void*, + * cgltf_size, cgltf_data**)` parses both glTF and GLB data. If + * this function returns `cgltf_result_success`, you have to call + * `cgltf_free()` on the created `cgltf_data*` variable. + * Note that contents of external files for buffers and images are not + * automatically loaded. You'll need to read these files yourself using + * URIs in the `cgltf_data` structure. + * + * `cgltf_options` is the struct passed to `cgltf_parse()` to control + * parts of the parsing process. You can use it to force the file type + * and provide memory allocation as well as file operation callbacks. + * Should be zero-initialized to trigger default behavior. + * + * `cgltf_data` is the struct allocated and filled by `cgltf_parse()`. + * It generally mirrors the glTF format as described by the spec (see + * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0). + * + * `void cgltf_free(cgltf_data*)` frees the allocated `cgltf_data` + * variable. + * + * `cgltf_result cgltf_load_buffers(const cgltf_options*, cgltf_data*, + * const char* gltf_path)` can be optionally called to open and read buffer + * files using the `FILE*` APIs. The `gltf_path` argument is the path to + * the original glTF file, which allows the parser to resolve the path to + * buffer files. + * + * `cgltf_result cgltf_load_buffer_base64(const cgltf_options* options, + * cgltf_size size, const char* base64, void** out_data)` decodes + * base64-encoded data content. Used internally by `cgltf_load_buffers()`. + * This is useful when decoding data URIs in images. + * + * `cgltf_result cgltf_parse_file(const cgltf_options* options, const + * char* path, cgltf_data** out_data)` can be used to open the given + * file using `FILE*` APIs and parse the data using `cgltf_parse()`. + * + * `cgltf_result cgltf_validate(cgltf_data*)` can be used to do additional + * checks to make sure the parsed glTF data is valid. + * + * `cgltf_node_transform_local` converts the translation / rotation / scale properties of a node + * into a mat4. + * + * `cgltf_node_transform_world` calls `cgltf_node_transform_local` on every ancestor in order + * to compute the root-to-node transformation. + * + * `cgltf_accessor_unpack_floats` reads in the data from an accessor, applies sparse data (if any), + * and converts them to floating point. Assumes that `cgltf_load_buffers` has already been called. + * By passing null for the output pointer, users can find out how many floats are required in the + * output buffer. + * + * `cgltf_accessor_unpack_indices` reads in the index data from an accessor. Assumes that + * `cgltf_load_buffers` has already been called. By passing null for the output pointer, users can + * find out how many indices are required in the output buffer. Returns 0 if the accessor is + * sparse or if the output component size is less than the accessor's component size. + * + * `cgltf_num_components` is a tiny utility that tells you the dimensionality of + * a certain accessor type. This can be used before `cgltf_accessor_unpack_floats` to help allocate + * the necessary amount of memory. `cgltf_component_size` and `cgltf_calc_size` exist for + * similar purposes. + * + * `cgltf_accessor_read_float` reads a certain element from a non-sparse accessor and converts it to + * floating point, assuming that `cgltf_load_buffers` has already been called. The passed-in element + * size is the number of floats in the output buffer, which should be in the range [1, 16]. Returns + * false if the passed-in element_size is too small, or if the accessor is sparse. + * + * `cgltf_accessor_read_uint` is similar to its floating-point counterpart, but limited to reading + * vector types and does not support matrix types. The passed-in element size is the number of uints + * in the output buffer, which should be in the range [1, 4]. Returns false if the passed-in + * element_size is too small, or if the accessor is sparse. + * + * `cgltf_accessor_read_index` is similar to its floating-point counterpart, but it returns size_t + * and only works with single-component data types. + * + * `cgltf_copy_extras_json` allows users to retrieve the "extras" data that can be attached to many + * glTF objects (which can be arbitrary JSON data). This is a legacy function, consider using + * cgltf_extras::data directly instead. You can parse this data using your own JSON parser + * or, if you've included the cgltf implementation using the integrated JSMN JSON parser. + */ +#ifndef CGLTF_H_INCLUDED__ +#define CGLTF_H_INCLUDED__ + +#include +#include /* For uint8_t, uint32_t */ + +#ifdef __cplusplus +extern "C" { +#endif + +typedef size_t cgltf_size; +typedef long long int cgltf_ssize; +typedef float cgltf_float; +typedef int cgltf_int; +typedef unsigned int cgltf_uint; +typedef int cgltf_bool; + +typedef enum cgltf_file_type +{ + cgltf_file_type_invalid, + cgltf_file_type_gltf, + cgltf_file_type_glb, + cgltf_file_type_max_enum +} cgltf_file_type; + +typedef enum cgltf_result +{ + cgltf_result_success, + cgltf_result_data_too_short, + cgltf_result_unknown_format, + cgltf_result_invalid_json, + cgltf_result_invalid_gltf, + cgltf_result_invalid_options, + cgltf_result_file_not_found, + cgltf_result_io_error, + cgltf_result_out_of_memory, + cgltf_result_legacy_gltf, + cgltf_result_max_enum +} cgltf_result; + +typedef struct cgltf_memory_options +{ + void* (*alloc_func)(void* user, cgltf_size size); + void (*free_func) (void* user, void* ptr); + void* user_data; +} cgltf_memory_options; + +typedef struct cgltf_file_options +{ + cgltf_result(*read)(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, const char* path, cgltf_size* size, void** data); + void (*release)(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, void* data, cgltf_size size); + void* user_data; +} cgltf_file_options; + +typedef struct cgltf_options +{ + cgltf_file_type type; /* invalid == auto detect */ + cgltf_size json_token_count; /* 0 == auto */ + cgltf_memory_options memory; + cgltf_file_options file; +} cgltf_options; + +typedef enum cgltf_buffer_view_type +{ + cgltf_buffer_view_type_invalid, + cgltf_buffer_view_type_indices, + cgltf_buffer_view_type_vertices, + cgltf_buffer_view_type_max_enum +} cgltf_buffer_view_type; + +typedef enum cgltf_attribute_type +{ + cgltf_attribute_type_invalid, + cgltf_attribute_type_position, + cgltf_attribute_type_normal, + cgltf_attribute_type_tangent, + cgltf_attribute_type_texcoord, + cgltf_attribute_type_color, + cgltf_attribute_type_joints, + cgltf_attribute_type_weights, + cgltf_attribute_type_custom, + cgltf_attribute_type_max_enum +} cgltf_attribute_type; + +typedef enum cgltf_component_type +{ + cgltf_component_type_invalid, + cgltf_component_type_r_8, /* BYTE */ + cgltf_component_type_r_8u, /* UNSIGNED_BYTE */ + cgltf_component_type_r_16, /* SHORT */ + cgltf_component_type_r_16u, /* UNSIGNED_SHORT */ + cgltf_component_type_r_32u, /* UNSIGNED_INT */ + cgltf_component_type_r_32f, /* FLOAT */ + cgltf_component_type_max_enum +} cgltf_component_type; + +typedef enum cgltf_type +{ + cgltf_type_invalid, + cgltf_type_scalar, + cgltf_type_vec2, + cgltf_type_vec3, + cgltf_type_vec4, + cgltf_type_mat2, + cgltf_type_mat3, + cgltf_type_mat4, + cgltf_type_max_enum +} cgltf_type; + +typedef enum cgltf_primitive_type +{ + cgltf_primitive_type_invalid, + cgltf_primitive_type_points, + cgltf_primitive_type_lines, + cgltf_primitive_type_line_loop, + cgltf_primitive_type_line_strip, + cgltf_primitive_type_triangles, + cgltf_primitive_type_triangle_strip, + cgltf_primitive_type_triangle_fan, + cgltf_primitive_type_max_enum +} cgltf_primitive_type; + +typedef enum cgltf_alpha_mode +{ + cgltf_alpha_mode_opaque, + cgltf_alpha_mode_mask, + cgltf_alpha_mode_blend, + cgltf_alpha_mode_max_enum +} cgltf_alpha_mode; + +typedef enum cgltf_animation_path_type { + cgltf_animation_path_type_invalid, + cgltf_animation_path_type_translation, + cgltf_animation_path_type_rotation, + cgltf_animation_path_type_scale, + cgltf_animation_path_type_weights, + cgltf_animation_path_type_max_enum +} cgltf_animation_path_type; + +typedef enum cgltf_interpolation_type { + cgltf_interpolation_type_linear, + cgltf_interpolation_type_step, + cgltf_interpolation_type_cubic_spline, + cgltf_interpolation_type_max_enum +} cgltf_interpolation_type; + +typedef enum cgltf_camera_type { + cgltf_camera_type_invalid, + cgltf_camera_type_perspective, + cgltf_camera_type_orthographic, + cgltf_camera_type_max_enum +} cgltf_camera_type; + +typedef enum cgltf_light_type { + cgltf_light_type_invalid, + cgltf_light_type_directional, + cgltf_light_type_point, + cgltf_light_type_spot, + cgltf_light_type_max_enum +} cgltf_light_type; + +typedef enum cgltf_data_free_method { + cgltf_data_free_method_none, + cgltf_data_free_method_file_release, + cgltf_data_free_method_memory_free, + cgltf_data_free_method_max_enum +} cgltf_data_free_method; + +typedef struct cgltf_extras { + cgltf_size start_offset; /* this field is deprecated and will be removed in the future; use data instead */ + cgltf_size end_offset; /* this field is deprecated and will be removed in the future; use data instead */ + + char* data; +} cgltf_extras; + +typedef struct cgltf_extension { + char* name; + char* data; +} cgltf_extension; + +typedef struct cgltf_buffer +{ + char* name; + cgltf_size size; + char* uri; + void* data; /* loaded by cgltf_load_buffers */ + cgltf_data_free_method data_free_method; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_buffer; + +typedef enum cgltf_meshopt_compression_mode { + cgltf_meshopt_compression_mode_invalid, + cgltf_meshopt_compression_mode_attributes, + cgltf_meshopt_compression_mode_triangles, + cgltf_meshopt_compression_mode_indices, + cgltf_meshopt_compression_mode_max_enum +} cgltf_meshopt_compression_mode; + +typedef enum cgltf_meshopt_compression_filter { + cgltf_meshopt_compression_filter_none, + cgltf_meshopt_compression_filter_octahedral, + cgltf_meshopt_compression_filter_quaternion, + cgltf_meshopt_compression_filter_exponential, + cgltf_meshopt_compression_filter_color, + cgltf_meshopt_compression_filter_max_enum +} cgltf_meshopt_compression_filter; + +typedef struct cgltf_meshopt_compression +{ + cgltf_buffer* buffer; + cgltf_size offset; + cgltf_size size; + cgltf_size stride; + cgltf_size count; + cgltf_meshopt_compression_mode mode; + cgltf_meshopt_compression_filter filter; + cgltf_bool is_khr; +} cgltf_meshopt_compression; + +typedef struct cgltf_buffer_view +{ + char *name; + cgltf_buffer* buffer; + cgltf_size offset; + cgltf_size size; + cgltf_size stride; /* 0 == automatically determined by accessor */ + cgltf_buffer_view_type type; + void* data; /* overrides buffer->data if present, filled by extensions */ + cgltf_bool has_meshopt_compression; + cgltf_meshopt_compression meshopt_compression; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_buffer_view; + +typedef struct cgltf_accessor_sparse +{ + cgltf_size count; + cgltf_buffer_view* indices_buffer_view; + cgltf_size indices_byte_offset; + cgltf_component_type indices_component_type; + cgltf_buffer_view* values_buffer_view; + cgltf_size values_byte_offset; +} cgltf_accessor_sparse; + +typedef struct cgltf_accessor +{ + char* name; + cgltf_component_type component_type; + cgltf_bool normalized; + cgltf_type type; + cgltf_size offset; + cgltf_size count; + cgltf_size stride; + cgltf_buffer_view* buffer_view; + cgltf_bool has_min; + cgltf_float min[16]; + cgltf_bool has_max; + cgltf_float max[16]; + cgltf_bool is_sparse; + cgltf_accessor_sparse sparse; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_accessor; + +typedef struct cgltf_attribute +{ + char* name; + cgltf_attribute_type type; + cgltf_int index; + cgltf_accessor* data; +} cgltf_attribute; + +typedef struct cgltf_image +{ + char* name; + char* uri; + cgltf_buffer_view* buffer_view; + char* mime_type; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_image; + +typedef enum cgltf_filter_type { + cgltf_filter_type_undefined = 0, + cgltf_filter_type_nearest = 9728, + cgltf_filter_type_linear = 9729, + cgltf_filter_type_nearest_mipmap_nearest = 9984, + cgltf_filter_type_linear_mipmap_nearest = 9985, + cgltf_filter_type_nearest_mipmap_linear = 9986, + cgltf_filter_type_linear_mipmap_linear = 9987 +} cgltf_filter_type; + +typedef enum cgltf_wrap_mode { + cgltf_wrap_mode_clamp_to_edge = 33071, + cgltf_wrap_mode_mirrored_repeat = 33648, + cgltf_wrap_mode_repeat = 10497 +} cgltf_wrap_mode; + +typedef struct cgltf_sampler +{ + char* name; + cgltf_filter_type mag_filter; + cgltf_filter_type min_filter; + cgltf_wrap_mode wrap_s; + cgltf_wrap_mode wrap_t; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_sampler; + +typedef struct cgltf_texture +{ + char* name; + cgltf_image* image; + cgltf_sampler* sampler; + cgltf_bool has_basisu; + cgltf_image* basisu_image; + cgltf_bool has_webp; + cgltf_image* webp_image; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_texture; + +typedef struct cgltf_texture_transform +{ + cgltf_float offset[2]; + cgltf_float rotation; + cgltf_float scale[2]; + cgltf_bool has_texcoord; + cgltf_int texcoord; +} cgltf_texture_transform; + +typedef struct cgltf_texture_view +{ + cgltf_texture* texture; + cgltf_int texcoord; + cgltf_float scale; /* equivalent to strength for occlusion_texture */ + cgltf_bool has_transform; + cgltf_texture_transform transform; +} cgltf_texture_view; + +typedef struct cgltf_pbr_metallic_roughness +{ + cgltf_texture_view base_color_texture; + cgltf_texture_view metallic_roughness_texture; + + cgltf_float base_color_factor[4]; + cgltf_float metallic_factor; + cgltf_float roughness_factor; +} cgltf_pbr_metallic_roughness; + +typedef struct cgltf_pbr_specular_glossiness +{ + cgltf_texture_view diffuse_texture; + cgltf_texture_view specular_glossiness_texture; + + cgltf_float diffuse_factor[4]; + cgltf_float specular_factor[3]; + cgltf_float glossiness_factor; +} cgltf_pbr_specular_glossiness; + +typedef struct cgltf_clearcoat +{ + cgltf_texture_view clearcoat_texture; + cgltf_texture_view clearcoat_roughness_texture; + cgltf_texture_view clearcoat_normal_texture; + + cgltf_float clearcoat_factor; + cgltf_float clearcoat_roughness_factor; +} cgltf_clearcoat; + +typedef struct cgltf_transmission +{ + cgltf_texture_view transmission_texture; + cgltf_float transmission_factor; +} cgltf_transmission; + +typedef struct cgltf_ior +{ + cgltf_float ior; +} cgltf_ior; + +typedef struct cgltf_specular +{ + cgltf_texture_view specular_texture; + cgltf_texture_view specular_color_texture; + cgltf_float specular_color_factor[3]; + cgltf_float specular_factor; +} cgltf_specular; + +typedef struct cgltf_volume +{ + cgltf_texture_view thickness_texture; + cgltf_float thickness_factor; + cgltf_float attenuation_color[3]; + cgltf_float attenuation_distance; +} cgltf_volume; + +typedef struct cgltf_sheen +{ + cgltf_texture_view sheen_color_texture; + cgltf_float sheen_color_factor[3]; + cgltf_texture_view sheen_roughness_texture; + cgltf_float sheen_roughness_factor; +} cgltf_sheen; + +typedef struct cgltf_emissive_strength +{ + cgltf_float emissive_strength; +} cgltf_emissive_strength; + +typedef struct cgltf_iridescence +{ + cgltf_float iridescence_factor; + cgltf_texture_view iridescence_texture; + cgltf_float iridescence_ior; + cgltf_float iridescence_thickness_min; + cgltf_float iridescence_thickness_max; + cgltf_texture_view iridescence_thickness_texture; +} cgltf_iridescence; + +typedef struct cgltf_diffuse_transmission +{ + cgltf_texture_view diffuse_transmission_texture; + cgltf_float diffuse_transmission_factor; + cgltf_float diffuse_transmission_color_factor[3]; + cgltf_texture_view diffuse_transmission_color_texture; +} cgltf_diffuse_transmission; + +typedef struct cgltf_anisotropy +{ + cgltf_float anisotropy_strength; + cgltf_float anisotropy_rotation; + cgltf_texture_view anisotropy_texture; +} cgltf_anisotropy; + +typedef struct cgltf_dispersion +{ + cgltf_float dispersion; +} cgltf_dispersion; + +typedef struct cgltf_material +{ + char* name; + cgltf_bool has_pbr_metallic_roughness; + cgltf_bool has_pbr_specular_glossiness; + cgltf_bool has_clearcoat; + cgltf_bool has_transmission; + cgltf_bool has_volume; + cgltf_bool has_ior; + cgltf_bool has_specular; + cgltf_bool has_sheen; + cgltf_bool has_emissive_strength; + cgltf_bool has_iridescence; + cgltf_bool has_diffuse_transmission; + cgltf_bool has_anisotropy; + cgltf_bool has_dispersion; + cgltf_pbr_metallic_roughness pbr_metallic_roughness; + cgltf_pbr_specular_glossiness pbr_specular_glossiness; + cgltf_clearcoat clearcoat; + cgltf_ior ior; + cgltf_specular specular; + cgltf_sheen sheen; + cgltf_transmission transmission; + cgltf_volume volume; + cgltf_emissive_strength emissive_strength; + cgltf_iridescence iridescence; + cgltf_diffuse_transmission diffuse_transmission; + cgltf_anisotropy anisotropy; + cgltf_dispersion dispersion; + cgltf_texture_view normal_texture; + cgltf_texture_view occlusion_texture; + cgltf_texture_view emissive_texture; + cgltf_float emissive_factor[3]; + cgltf_alpha_mode alpha_mode; + cgltf_float alpha_cutoff; + cgltf_bool double_sided; + cgltf_bool unlit; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_material; + +typedef struct cgltf_material_mapping +{ + cgltf_size variant; + cgltf_material* material; + cgltf_extras extras; +} cgltf_material_mapping; + +typedef struct cgltf_morph_target { + cgltf_attribute* attributes; + cgltf_size attributes_count; +} cgltf_morph_target; + +typedef struct cgltf_draco_mesh_compression { + cgltf_buffer_view* buffer_view; + cgltf_attribute* attributes; + cgltf_size attributes_count; +} cgltf_draco_mesh_compression; + +typedef struct cgltf_mesh_gpu_instancing { + cgltf_attribute* attributes; + cgltf_size attributes_count; +} cgltf_mesh_gpu_instancing; + +typedef struct cgltf_primitive { + cgltf_primitive_type type; + cgltf_accessor* indices; + cgltf_material* material; + cgltf_attribute* attributes; + cgltf_size attributes_count; + cgltf_morph_target* targets; + cgltf_size targets_count; + cgltf_extras extras; + cgltf_bool has_draco_mesh_compression; + cgltf_draco_mesh_compression draco_mesh_compression; + cgltf_material_mapping* mappings; + cgltf_size mappings_count; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_primitive; + +typedef struct cgltf_mesh { + char* name; + cgltf_primitive* primitives; + cgltf_size primitives_count; + cgltf_float* weights; + cgltf_size weights_count; + char** target_names; + cgltf_size target_names_count; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_mesh; + +typedef struct cgltf_node cgltf_node; + +typedef struct cgltf_skin { + char* name; + cgltf_node** joints; + cgltf_size joints_count; + cgltf_node* skeleton; + cgltf_accessor* inverse_bind_matrices; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_skin; + +typedef struct cgltf_camera_perspective { + cgltf_bool has_aspect_ratio; + cgltf_float aspect_ratio; + cgltf_float yfov; + cgltf_bool has_zfar; + cgltf_float zfar; + cgltf_float znear; + cgltf_extras extras; +} cgltf_camera_perspective; + +typedef struct cgltf_camera_orthographic { + cgltf_float xmag; + cgltf_float ymag; + cgltf_float zfar; + cgltf_float znear; + cgltf_extras extras; +} cgltf_camera_orthographic; + +typedef struct cgltf_camera { + char* name; + cgltf_camera_type type; + union { + cgltf_camera_perspective perspective; + cgltf_camera_orthographic orthographic; + } data; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_camera; + +typedef struct cgltf_light { + char* name; + cgltf_float color[3]; + cgltf_float intensity; + cgltf_light_type type; + cgltf_float range; + cgltf_float spot_inner_cone_angle; + cgltf_float spot_outer_cone_angle; + cgltf_extras extras; +} cgltf_light; + +struct cgltf_node { + char* name; + cgltf_node* parent; + cgltf_node** children; + cgltf_size children_count; + cgltf_skin* skin; + cgltf_mesh* mesh; + cgltf_camera* camera; + cgltf_light* light; + cgltf_float* weights; + cgltf_size weights_count; + cgltf_bool has_translation; + cgltf_bool has_rotation; + cgltf_bool has_scale; + cgltf_bool has_matrix; + cgltf_float translation[3]; + cgltf_float rotation[4]; + cgltf_float scale[3]; + cgltf_float matrix[16]; + cgltf_extras extras; + cgltf_bool has_mesh_gpu_instancing; + cgltf_mesh_gpu_instancing mesh_gpu_instancing; + cgltf_size extensions_count; + cgltf_extension* extensions; +}; + +typedef struct cgltf_scene { + char* name; + cgltf_node** nodes; + cgltf_size nodes_count; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_scene; + +typedef struct cgltf_animation_sampler { + cgltf_accessor* input; + cgltf_accessor* output; + cgltf_interpolation_type interpolation; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_animation_sampler; + +typedef struct cgltf_animation_channel { + cgltf_animation_sampler* sampler; + cgltf_node* target_node; + cgltf_animation_path_type target_path; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_animation_channel; + +typedef struct cgltf_animation { + char* name; + cgltf_animation_sampler* samplers; + cgltf_size samplers_count; + cgltf_animation_channel* channels; + cgltf_size channels_count; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_animation; + +typedef struct cgltf_material_variant +{ + char* name; + cgltf_extras extras; +} cgltf_material_variant; + +typedef struct cgltf_asset { + char* copyright; + char* generator; + char* version; + char* min_version; + cgltf_extras extras; + cgltf_size extensions_count; + cgltf_extension* extensions; +} cgltf_asset; + +typedef struct cgltf_data +{ + cgltf_file_type file_type; + void* file_data; + cgltf_size file_size; + + cgltf_asset asset; + + cgltf_mesh* meshes; + cgltf_size meshes_count; + + cgltf_material* materials; + cgltf_size materials_count; + + cgltf_accessor* accessors; + cgltf_size accessors_count; + + cgltf_buffer_view* buffer_views; + cgltf_size buffer_views_count; + + cgltf_buffer* buffers; + cgltf_size buffers_count; + + cgltf_image* images; + cgltf_size images_count; + + cgltf_texture* textures; + cgltf_size textures_count; + + cgltf_sampler* samplers; + cgltf_size samplers_count; + + cgltf_skin* skins; + cgltf_size skins_count; + + cgltf_camera* cameras; + cgltf_size cameras_count; + + cgltf_light* lights; + cgltf_size lights_count; + + cgltf_node* nodes; + cgltf_size nodes_count; + + cgltf_scene* scenes; + cgltf_size scenes_count; + + cgltf_scene* scene; + + cgltf_animation* animations; + cgltf_size animations_count; + + cgltf_material_variant* variants; + cgltf_size variants_count; + + cgltf_extras extras; + + cgltf_size data_extensions_count; + cgltf_extension* data_extensions; + + char** extensions_used; + cgltf_size extensions_used_count; + + char** extensions_required; + cgltf_size extensions_required_count; + + const char* json; + cgltf_size json_size; + + const void* bin; + cgltf_size bin_size; + + cgltf_memory_options memory; + cgltf_file_options file; +} cgltf_data; + +cgltf_result cgltf_parse( + const cgltf_options* options, + const void* data, + cgltf_size size, + cgltf_data** out_data); + +cgltf_result cgltf_parse_file( + const cgltf_options* options, + const char* path, + cgltf_data** out_data); + +cgltf_result cgltf_load_buffers( + const cgltf_options* options, + cgltf_data* data, + const char* gltf_path); + +cgltf_result cgltf_load_buffer_base64(const cgltf_options* options, cgltf_size size, const char* base64, void** out_data); + +cgltf_size cgltf_decode_string(char* string); +cgltf_size cgltf_decode_uri(char* uri); + +cgltf_result cgltf_validate(cgltf_data* data); + +void cgltf_free(cgltf_data* data); + +void cgltf_node_transform_local(const cgltf_node* node, cgltf_float* out_matrix); +void cgltf_node_transform_world(const cgltf_node* node, cgltf_float* out_matrix); + +const uint8_t* cgltf_buffer_view_data(const cgltf_buffer_view* view); + +const cgltf_accessor* cgltf_find_accessor(const cgltf_primitive* prim, cgltf_attribute_type type, cgltf_int index); + +cgltf_bool cgltf_accessor_read_float(const cgltf_accessor* accessor, cgltf_size index, cgltf_float* out, cgltf_size element_size); +cgltf_bool cgltf_accessor_read_uint(const cgltf_accessor* accessor, cgltf_size index, cgltf_uint* out, cgltf_size element_size); +cgltf_size cgltf_accessor_read_index(const cgltf_accessor* accessor, cgltf_size index); + +cgltf_size cgltf_num_components(cgltf_type type); +cgltf_size cgltf_component_size(cgltf_component_type component_type); +cgltf_size cgltf_calc_size(cgltf_type type, cgltf_component_type component_type); + +cgltf_size cgltf_accessor_unpack_floats(const cgltf_accessor* accessor, cgltf_float* out, cgltf_size float_count); +cgltf_size cgltf_accessor_unpack_indices(const cgltf_accessor* accessor, void* out, cgltf_size out_component_size, cgltf_size index_count); + +/* this function is deprecated and will be removed in the future; use cgltf_extras::data instead */ +cgltf_result cgltf_copy_extras_json(const cgltf_data* data, const cgltf_extras* extras, char* dest, cgltf_size* dest_size); + +cgltf_size cgltf_mesh_index(const cgltf_data* data, const cgltf_mesh* object); +cgltf_size cgltf_material_index(const cgltf_data* data, const cgltf_material* object); +cgltf_size cgltf_accessor_index(const cgltf_data* data, const cgltf_accessor* object); +cgltf_size cgltf_buffer_view_index(const cgltf_data* data, const cgltf_buffer_view* object); +cgltf_size cgltf_buffer_index(const cgltf_data* data, const cgltf_buffer* object); +cgltf_size cgltf_image_index(const cgltf_data* data, const cgltf_image* object); +cgltf_size cgltf_texture_index(const cgltf_data* data, const cgltf_texture* object); +cgltf_size cgltf_sampler_index(const cgltf_data* data, const cgltf_sampler* object); +cgltf_size cgltf_skin_index(const cgltf_data* data, const cgltf_skin* object); +cgltf_size cgltf_camera_index(const cgltf_data* data, const cgltf_camera* object); +cgltf_size cgltf_light_index(const cgltf_data* data, const cgltf_light* object); +cgltf_size cgltf_node_index(const cgltf_data* data, const cgltf_node* object); +cgltf_size cgltf_scene_index(const cgltf_data* data, const cgltf_scene* object); +cgltf_size cgltf_animation_index(const cgltf_data* data, const cgltf_animation* object); +cgltf_size cgltf_animation_sampler_index(const cgltf_animation* animation, const cgltf_animation_sampler* object); +cgltf_size cgltf_animation_channel_index(const cgltf_animation* animation, const cgltf_animation_channel* object); + +#ifdef __cplusplus +} +#endif + +#endif /* #ifndef CGLTF_H_INCLUDED__ */ + +/* + * + * Stop now, if you are only interested in the API. + * Below, you find the implementation. + * + */ + +#if defined(__INTELLISENSE__) || defined(__JETBRAINS_IDE__) +/* This makes MSVC/CLion intellisense work. */ +#define CGLTF_IMPLEMENTATION +#endif + +#ifdef CGLTF_IMPLEMENTATION + +#include /* For assert */ +#include /* For strncpy */ +#include /* For fopen */ +#include /* For UINT_MAX etc */ +#include /* For FLT_MAX */ + +#if !defined(CGLTF_MALLOC) || !defined(CGLTF_FREE) || !defined(CGLTF_ATOI) || !defined(CGLTF_ATOF) || !defined(CGLTF_ATOLL) +#include /* For malloc, free, atoi, atof */ +#endif + +/* JSMN_PARENT_LINKS is necessary to make parsing large structures linear in input size */ +#define JSMN_PARENT_LINKS + +/* JSMN_STRICT is necessary to reject invalid JSON documents */ +#define JSMN_STRICT + +/* + * -- jsmn.h start -- + * Source: https://github.com/zserge/jsmn + * License: MIT + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1, + JSMN_ARRAY = 2, + JSMN_STRING = 3, + JSMN_PRIMITIVE = 4 +} jsmntype_t; +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; +typedef struct { + jsmntype_t type; + ptrdiff_t start; + ptrdiff_t end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; +typedef struct { + size_t pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g parent object or array */ +} jsmn_parser; +static void jsmn_init(jsmn_parser *parser); +static int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, jsmntok_t *tokens, size_t num_tokens); +/* + * -- jsmn.h end -- + */ + + +#ifndef CGLTF_CONSTS +#define GlbHeaderSize 12 +#define GlbChunkHeaderSize 8 +static const uint32_t GlbVersion = 2; +static const uint32_t GlbMagic = 0x46546C67; +static const uint32_t GlbMagicJsonChunk = 0x4E4F534A; +static const uint32_t GlbMagicBinChunk = 0x004E4942; +#define CGLTF_CONSTS +#endif + +#ifndef CGLTF_MALLOC +#define CGLTF_MALLOC(size) malloc(size) +#endif +#ifndef CGLTF_FREE +#define CGLTF_FREE(ptr) free(ptr) +#endif +#ifndef CGLTF_ATOI +#define CGLTF_ATOI(str) atoi(str) +#endif +#ifndef CGLTF_ATOF +#define CGLTF_ATOF(str) atof(str) +#endif +#ifndef CGLTF_ATOLL +#define CGLTF_ATOLL(str) atoll(str) +#endif +#ifndef CGLTF_VALIDATE_ENABLE_ASSERTS +#define CGLTF_VALIDATE_ENABLE_ASSERTS 0 +#endif + +static void* cgltf_default_alloc(void* user, cgltf_size size) +{ + (void)user; + return CGLTF_MALLOC(size); +} + +static void cgltf_default_free(void* user, void* ptr) +{ + (void)user; + CGLTF_FREE(ptr); +} + +static void* cgltf_calloc(cgltf_options* options, size_t element_size, cgltf_size count) +{ + if (SIZE_MAX / element_size < count) + { + return NULL; + } + void* result = options->memory.alloc_func(options->memory.user_data, element_size * count); + if (!result) + { + return NULL; + } + memset(result, 0, element_size * count); + return result; +} + +static cgltf_result cgltf_default_file_read(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, const char* path, cgltf_size* size, void** data) +{ + (void)file_options; + void* (*memory_alloc)(void*, cgltf_size) = memory_options->alloc_func ? memory_options->alloc_func : &cgltf_default_alloc; + void (*memory_free)(void*, void*) = memory_options->free_func ? memory_options->free_func : &cgltf_default_free; + + FILE* file = fopen(path, "rb"); + if (!file) + { + return cgltf_result_file_not_found; + } + + cgltf_size file_size = size ? *size : 0; + + if (file_size == 0) + { + fseek(file, 0, SEEK_END); + +#ifdef _MSC_VER + __int64 length = _ftelli64(file); +#else + long length = ftell(file); +#endif + + if (length < 0) + { + fclose(file); + return cgltf_result_io_error; + } + + fseek(file, 0, SEEK_SET); + file_size = (cgltf_size)length; + } + + char* file_data = (char*)memory_alloc(memory_options->user_data, file_size); + if (!file_data) + { + fclose(file); + return cgltf_result_out_of_memory; + } + + cgltf_size read_size = fread(file_data, 1, file_size, file); + + fclose(file); + + if (read_size != file_size) + { + memory_free(memory_options->user_data, file_data); + return cgltf_result_io_error; + } + + if (size) + { + *size = file_size; + } + if (data) + { + *data = file_data; + } + + return cgltf_result_success; +} + +static void cgltf_default_file_release(const struct cgltf_memory_options* memory_options, const struct cgltf_file_options* file_options, void* data, cgltf_size size) +{ + (void)file_options; + (void)size; + void (*memfree)(void*, void*) = memory_options->free_func ? memory_options->free_func : &cgltf_default_free; + memfree(memory_options->user_data, data); +} + +static cgltf_result cgltf_parse_json(cgltf_options* options, const uint8_t* json_chunk, cgltf_size size, cgltf_data** out_data); + +cgltf_result cgltf_parse(const cgltf_options* options, const void* data, cgltf_size size, cgltf_data** out_data) +{ + if (size < GlbHeaderSize) + { + return cgltf_result_data_too_short; + } + + if (options == NULL) + { + return cgltf_result_invalid_options; + } + + cgltf_options fixed_options = *options; + if (fixed_options.memory.alloc_func == NULL) + { + fixed_options.memory.alloc_func = &cgltf_default_alloc; + } + if (fixed_options.memory.free_func == NULL) + { + fixed_options.memory.free_func = &cgltf_default_free; + } + + uint32_t tmp; + // Magic + memcpy(&tmp, data, 4); + if (tmp != GlbMagic) + { + if (fixed_options.type == cgltf_file_type_invalid) + { + fixed_options.type = cgltf_file_type_gltf; + } + else if (fixed_options.type == cgltf_file_type_glb) + { + return cgltf_result_unknown_format; + } + } + + if (fixed_options.type == cgltf_file_type_gltf) + { + cgltf_result json_result = cgltf_parse_json(&fixed_options, (const uint8_t*)data, size, out_data); + if (json_result != cgltf_result_success) + { + return json_result; + } + + (*out_data)->file_type = cgltf_file_type_gltf; + + return cgltf_result_success; + } + + const uint8_t* ptr = (const uint8_t*)data; + // Version + memcpy(&tmp, ptr + 4, 4); + uint32_t version = tmp; + if (version != GlbVersion) + { + return version < GlbVersion ? cgltf_result_legacy_gltf : cgltf_result_unknown_format; + } + + // Total length + memcpy(&tmp, ptr + 8, 4); + if (tmp > size) + { + return cgltf_result_data_too_short; + } + + const uint8_t* json_chunk = ptr + GlbHeaderSize; + + if (GlbHeaderSize + GlbChunkHeaderSize > size) + { + return cgltf_result_data_too_short; + } + + // JSON chunk: length + uint32_t json_length; + memcpy(&json_length, json_chunk, 4); + if (json_length > size - GlbHeaderSize - GlbChunkHeaderSize) + { + return cgltf_result_data_too_short; + } + + // JSON chunk: magic + memcpy(&tmp, json_chunk + 4, 4); + if (tmp != GlbMagicJsonChunk) + { + return cgltf_result_unknown_format; + } + + json_chunk += GlbChunkHeaderSize; + + const void* bin = NULL; + cgltf_size bin_size = 0; + + if (GlbChunkHeaderSize <= size - GlbHeaderSize - GlbChunkHeaderSize - json_length) + { + // We can read another chunk + const uint8_t* bin_chunk = json_chunk + json_length; + + // Bin chunk: length + uint32_t bin_length; + memcpy(&bin_length, bin_chunk, 4); + if (bin_length > size - GlbHeaderSize - GlbChunkHeaderSize - json_length - GlbChunkHeaderSize) + { + return cgltf_result_data_too_short; + } + + // Bin chunk: magic + memcpy(&tmp, bin_chunk + 4, 4); + if (tmp != GlbMagicBinChunk) + { + return cgltf_result_unknown_format; + } + + bin_chunk += GlbChunkHeaderSize; + + bin = bin_chunk; + bin_size = bin_length; + } + + cgltf_result json_result = cgltf_parse_json(&fixed_options, json_chunk, json_length, out_data); + if (json_result != cgltf_result_success) + { + return json_result; + } + + (*out_data)->file_type = cgltf_file_type_glb; + (*out_data)->bin = bin; + (*out_data)->bin_size = bin_size; + + return cgltf_result_success; +} + +cgltf_result cgltf_parse_file(const cgltf_options* options, const char* path, cgltf_data** out_data) +{ + if (options == NULL) + { + return cgltf_result_invalid_options; + } + + cgltf_result (*file_read)(const struct cgltf_memory_options*, const struct cgltf_file_options*, const char*, cgltf_size*, void**) = options->file.read ? options->file.read : &cgltf_default_file_read; + void (*file_release)(const struct cgltf_memory_options*, const struct cgltf_file_options*, void* data, cgltf_size size) = options->file.release ? options->file.release : cgltf_default_file_release; + + void* file_data = NULL; + cgltf_size file_size = 0; + cgltf_result result = file_read(&options->memory, &options->file, path, &file_size, &file_data); + if (result != cgltf_result_success) + { + return result; + } + + result = cgltf_parse(options, file_data, file_size, out_data); + + if (result != cgltf_result_success) + { + file_release(&options->memory, &options->file, file_data, file_size); + return result; + } + + (*out_data)->file_data = file_data; + (*out_data)->file_size = file_size; + + return cgltf_result_success; +} + +static void cgltf_combine_paths(char* path, const char* base, const char* uri) +{ + const char* s0 = strrchr(base, '/'); + const char* s1 = strrchr(base, '\\'); + const char* slash = s0 ? (s1 && s1 > s0 ? s1 : s0) : s1; + + if (slash) + { + size_t prefix = slash - base + 1; + + strncpy(path, base, prefix); + strcpy(path + prefix, uri); + } + else + { + strcpy(path, uri); + } +} + +static cgltf_result cgltf_load_buffer_file(const cgltf_options* options, cgltf_size size, const char* uri, const char* gltf_path, void** out_data) +{ + void* (*memory_alloc)(void*, cgltf_size) = options->memory.alloc_func ? options->memory.alloc_func : &cgltf_default_alloc; + void (*memory_free)(void*, void*) = options->memory.free_func ? options->memory.free_func : &cgltf_default_free; + cgltf_result (*file_read)(const struct cgltf_memory_options*, const struct cgltf_file_options*, const char*, cgltf_size*, void**) = options->file.read ? options->file.read : &cgltf_default_file_read; + + char* path = (char*)memory_alloc(options->memory.user_data, strlen(uri) + strlen(gltf_path) + 1); + if (!path) + { + return cgltf_result_out_of_memory; + } + + cgltf_combine_paths(path, gltf_path, uri); + + // after combining, the tail of the resulting path is a uri; decode_uri converts it into path + cgltf_decode_uri(path + strlen(path) - strlen(uri)); + + void* file_data = NULL; + cgltf_result result = file_read(&options->memory, &options->file, path, &size, &file_data); + + memory_free(options->memory.user_data, path); + + *out_data = (result == cgltf_result_success) ? file_data : NULL; + + return result; +} + +cgltf_result cgltf_load_buffer_base64(const cgltf_options* options, cgltf_size size, const char* base64, void** out_data) +{ + void* (*memory_alloc)(void*, cgltf_size) = options->memory.alloc_func ? options->memory.alloc_func : &cgltf_default_alloc; + void (*memory_free)(void*, void*) = options->memory.free_func ? options->memory.free_func : &cgltf_default_free; + + unsigned char* data = (unsigned char*)memory_alloc(options->memory.user_data, size); + if (!data) + { + return cgltf_result_out_of_memory; + } + + unsigned int buffer = 0; + unsigned int buffer_bits = 0; + + for (cgltf_size i = 0; i < size; ++i) + { + while (buffer_bits < 8) + { + char ch = *base64++; + + int index = + (unsigned)(ch - 'A') < 26 ? (ch - 'A') : + (unsigned)(ch - 'a') < 26 ? (ch - 'a') + 26 : + (unsigned)(ch - '0') < 10 ? (ch - '0') + 52 : + ch == '+' ? 62 : + ch == '/' ? 63 : + -1; + + if (index < 0) + { + memory_free(options->memory.user_data, data); + return cgltf_result_io_error; + } + + buffer = (buffer << 6) | index; + buffer_bits += 6; + } + + data[i] = (unsigned char)(buffer >> (buffer_bits - 8)); + buffer_bits -= 8; + } + + *out_data = data; + + return cgltf_result_success; +} + +static int cgltf_unhex(char ch) +{ + return + (unsigned)(ch - '0') < 10 ? (ch - '0') : + (unsigned)(ch - 'A') < 6 ? (ch - 'A') + 10 : + (unsigned)(ch - 'a') < 6 ? (ch - 'a') + 10 : + -1; +} + +cgltf_size cgltf_decode_string(char* string) +{ + char* read = string + strcspn(string, "\\"); + if (*read == 0) + { + return read - string; + } + char* write = string; + char* last = string; + + for (;;) + { + // Copy characters since last escaped sequence + cgltf_size written = read - last; + memmove(write, last, written); + write += written; + + if (*read++ == 0) + { + break; + } + + // jsmn already checked that all escape sequences are valid + switch (*read++) + { + case '\"': *write++ = '\"'; break; + case '/': *write++ = '/'; break; + case '\\': *write++ = '\\'; break; + case 'b': *write++ = '\b'; break; + case 'f': *write++ = '\f'; break; + case 'r': *write++ = '\r'; break; + case 'n': *write++ = '\n'; break; + case 't': *write++ = '\t'; break; + case 'u': + { + // UCS-2 codepoint \uXXXX to UTF-8 + int character = 0; + for (cgltf_size i = 0; i < 4; ++i) + { + character = (character << 4) + cgltf_unhex(*read++); + } + + if (character <= 0x7F) + { + *write++ = character & 0xFF; + } + else if (character <= 0x7FF) + { + *write++ = 0xC0 | ((character >> 6) & 0xFF); + *write++ = 0x80 | (character & 0x3F); + } + else + { + *write++ = 0xE0 | ((character >> 12) & 0xFF); + *write++ = 0x80 | ((character >> 6) & 0x3F); + *write++ = 0x80 | (character & 0x3F); + } + break; + } + default: + break; + } + + last = read; + read += strcspn(read, "\\"); + } + + *write = 0; + return write - string; +} + +cgltf_size cgltf_decode_uri(char* uri) +{ + char* write = uri; + char* i = uri; + + while (*i) + { + if (*i == '%') + { + int ch1 = cgltf_unhex(i[1]); + + if (ch1 >= 0) + { + int ch2 = cgltf_unhex(i[2]); + + if (ch2 >= 0) + { + *write++ = (char)(ch1 * 16 + ch2); + i += 3; + continue; + } + } + } + + *write++ = *i++; + } + + *write = 0; + return write - uri; +} + +cgltf_result cgltf_load_buffers(const cgltf_options* options, cgltf_data* data, const char* gltf_path) +{ + if (options == NULL) + { + return cgltf_result_invalid_options; + } + + if (data->buffers_count && data->buffers[0].data == NULL && data->buffers[0].uri == NULL && data->bin) + { + if (data->bin_size < data->buffers[0].size) + { + return cgltf_result_data_too_short; + } + + data->buffers[0].data = (void*)data->bin; + data->buffers[0].data_free_method = cgltf_data_free_method_none; + } + + for (cgltf_size i = 0; i < data->buffers_count; ++i) + { + if (data->buffers[i].data) + { + continue; + } + + const char* uri = data->buffers[i].uri; + + if (uri == NULL) + { + continue; + } + + if (strncmp(uri, "data:", 5) == 0) + { + const char* comma = strchr(uri, ','); + + if (comma && comma - uri >= 7 && strncmp(comma - 7, ";base64", 7) == 0) + { + cgltf_result res = cgltf_load_buffer_base64(options, data->buffers[i].size, comma + 1, &data->buffers[i].data); + data->buffers[i].data_free_method = cgltf_data_free_method_memory_free; + + if (res != cgltf_result_success) + { + return res; + } + } + else + { + return cgltf_result_unknown_format; + } + } + else if (strstr(uri, "://") == NULL && gltf_path) + { + cgltf_result res = cgltf_load_buffer_file(options, data->buffers[i].size, uri, gltf_path, &data->buffers[i].data); + data->buffers[i].data_free_method = cgltf_data_free_method_file_release; + + if (res != cgltf_result_success) + { + return res; + } + } + else + { + return cgltf_result_unknown_format; + } + } + + return cgltf_result_success; +} + +static cgltf_size cgltf_calc_index_bound(cgltf_buffer_view* buffer_view, cgltf_size offset, cgltf_component_type component_type, cgltf_size count) +{ + char* data = (char*)buffer_view->buffer->data + offset + buffer_view->offset; + cgltf_size bound = 0; + + switch (component_type) + { + case cgltf_component_type_r_8u: + for (size_t i = 0; i < count; ++i) + { + cgltf_size v = ((unsigned char*)data)[i]; + bound = bound > v ? bound : v; + } + break; + + case cgltf_component_type_r_16u: + for (size_t i = 0; i < count; ++i) + { + cgltf_size v = ((unsigned short*)data)[i]; + bound = bound > v ? bound : v; + } + break; + + case cgltf_component_type_r_32u: + for (size_t i = 0; i < count; ++i) + { + cgltf_size v = ((unsigned int*)data)[i]; + bound = bound > v ? bound : v; + } + break; + + default: + ; + } + + return bound; +} + +#if CGLTF_VALIDATE_ENABLE_ASSERTS +#define CGLTF_ASSERT_IF(cond, result) assert(!(cond)); if (cond) return result; +#else +#define CGLTF_ASSERT_IF(cond, result) if (cond) return result; +#endif + +cgltf_result cgltf_validate(cgltf_data* data) +{ + for (cgltf_size i = 0; i < data->accessors_count; ++i) + { + cgltf_accessor* accessor = &data->accessors[i]; + + CGLTF_ASSERT_IF(data->accessors[i].component_type == cgltf_component_type_invalid, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(data->accessors[i].type == cgltf_type_invalid, cgltf_result_invalid_gltf); + + cgltf_size element_size = cgltf_calc_size(accessor->type, accessor->component_type); + + if (accessor->buffer_view) + { + cgltf_size req_size = accessor->offset + accessor->stride * (accessor->count - 1) + element_size; + + CGLTF_ASSERT_IF(accessor->buffer_view->size < req_size, cgltf_result_data_too_short); + } + + if (accessor->is_sparse) + { + cgltf_accessor_sparse* sparse = &accessor->sparse; + + cgltf_size indices_component_size = cgltf_component_size(sparse->indices_component_type); + cgltf_size indices_req_size = sparse->indices_byte_offset + indices_component_size * sparse->count; + cgltf_size values_req_size = sparse->values_byte_offset + element_size * sparse->count; + + CGLTF_ASSERT_IF(sparse->indices_buffer_view->size < indices_req_size || + sparse->values_buffer_view->size < values_req_size, cgltf_result_data_too_short); + + CGLTF_ASSERT_IF(sparse->indices_component_type != cgltf_component_type_r_8u && + sparse->indices_component_type != cgltf_component_type_r_16u && + sparse->indices_component_type != cgltf_component_type_r_32u, cgltf_result_invalid_gltf); + + if (sparse->indices_buffer_view->buffer->data) + { + cgltf_size index_bound = cgltf_calc_index_bound(sparse->indices_buffer_view, sparse->indices_byte_offset, sparse->indices_component_type, sparse->count); + + CGLTF_ASSERT_IF(index_bound >= accessor->count, cgltf_result_data_too_short); + } + } + } + + for (cgltf_size i = 0; i < data->buffer_views_count; ++i) + { + cgltf_size req_size = data->buffer_views[i].offset + data->buffer_views[i].size; + + CGLTF_ASSERT_IF(data->buffer_views[i].buffer && data->buffer_views[i].buffer->size < req_size, cgltf_result_data_too_short); + + if (data->buffer_views[i].has_meshopt_compression) + { + cgltf_meshopt_compression* mc = &data->buffer_views[i].meshopt_compression; + + CGLTF_ASSERT_IF(mc->buffer == NULL || mc->buffer->size < mc->offset + mc->size, cgltf_result_data_too_short); + + CGLTF_ASSERT_IF(data->buffer_views[i].stride && mc->stride != data->buffer_views[i].stride, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(data->buffer_views[i].size != mc->stride * mc->count, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->mode == cgltf_meshopt_compression_mode_invalid, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->mode == cgltf_meshopt_compression_mode_attributes && !(mc->stride % 4 == 0 && mc->stride <= 256), cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->mode == cgltf_meshopt_compression_mode_triangles && mc->count % 3 != 0, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF((mc->mode == cgltf_meshopt_compression_mode_triangles || mc->mode == cgltf_meshopt_compression_mode_indices) && mc->stride != 2 && mc->stride != 4, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF((mc->mode == cgltf_meshopt_compression_mode_triangles || mc->mode == cgltf_meshopt_compression_mode_indices) && mc->filter != cgltf_meshopt_compression_filter_none, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(mc->filter == cgltf_meshopt_compression_filter_octahedral && mc->stride != 4 && mc->stride != 8, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(mc->filter == cgltf_meshopt_compression_filter_quaternion && mc->stride != 8, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(mc->filter == cgltf_meshopt_compression_filter_color && mc->stride != 4 && mc->stride != 8, cgltf_result_invalid_gltf); + } + } + + for (cgltf_size i = 0; i < data->meshes_count; ++i) + { + if (data->meshes[i].weights) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives_count && data->meshes[i].primitives[0].targets_count != data->meshes[i].weights_count, cgltf_result_invalid_gltf); + } + + if (data->meshes[i].target_names) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives_count && data->meshes[i].primitives[0].targets_count != data->meshes[i].target_names_count, cgltf_result_invalid_gltf); + } + + for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].type == cgltf_primitive_type_invalid, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].targets_count != data->meshes[i].primitives[0].targets_count, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].attributes_count == 0, cgltf_result_invalid_gltf); + + cgltf_accessor* first = data->meshes[i].primitives[j].attributes[0].data; + + CGLTF_ASSERT_IF(first->count == 0, cgltf_result_invalid_gltf); + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].attributes[k].data->count != first->count, cgltf_result_invalid_gltf); + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k) + { + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].targets[k].attributes[m].data->count != first->count, cgltf_result_invalid_gltf); + } + } + + cgltf_accessor* indices = data->meshes[i].primitives[j].indices; + + CGLTF_ASSERT_IF(indices && + indices->component_type != cgltf_component_type_r_8u && + indices->component_type != cgltf_component_type_r_16u && + indices->component_type != cgltf_component_type_r_32u, cgltf_result_invalid_gltf); + + CGLTF_ASSERT_IF(indices && indices->type != cgltf_type_scalar, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(indices && indices->stride != cgltf_component_size(indices->component_type), cgltf_result_invalid_gltf); + + if (indices && indices->buffer_view && indices->buffer_view->buffer->data) + { + cgltf_size index_bound = cgltf_calc_index_bound(indices->buffer_view, indices->offset, indices->component_type, indices->count); + + CGLTF_ASSERT_IF(index_bound >= first->count, cgltf_result_data_too_short); + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].mappings_count; ++k) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].mappings[k].variant >= data->variants_count, cgltf_result_invalid_gltf); + } + } + } + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + if (data->nodes[i].weights && data->nodes[i].mesh) + { + CGLTF_ASSERT_IF(data->nodes[i].mesh->primitives_count && data->nodes[i].mesh->primitives[0].targets_count != data->nodes[i].weights_count, cgltf_result_invalid_gltf); + } + + if (data->nodes[i].has_mesh_gpu_instancing) + { + CGLTF_ASSERT_IF(data->nodes[i].mesh == NULL, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(data->nodes[i].mesh_gpu_instancing.attributes_count == 0, cgltf_result_invalid_gltf); + + cgltf_accessor* first = data->nodes[i].mesh_gpu_instancing.attributes[0].data; + + for (cgltf_size k = 0; k < data->nodes[i].mesh_gpu_instancing.attributes_count; ++k) + { + CGLTF_ASSERT_IF(data->nodes[i].mesh_gpu_instancing.attributes[k].data->count != first->count, cgltf_result_invalid_gltf); + } + } + } + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + cgltf_node* p1 = data->nodes[i].parent; + cgltf_node* p2 = p1 ? p1->parent : NULL; + + while (p1 && p2) + { + CGLTF_ASSERT_IF(p1 == p2, cgltf_result_invalid_gltf); + + p1 = p1->parent; + p2 = p2->parent ? p2->parent->parent : NULL; + } + } + + for (cgltf_size i = 0; i < data->scenes_count; ++i) + { + for (cgltf_size j = 0; j < data->scenes[i].nodes_count; ++j) + { + CGLTF_ASSERT_IF(data->scenes[i].nodes[j]->parent, cgltf_result_invalid_gltf); + } + } + + for (cgltf_size i = 0; i < data->animations_count; ++i) + { + for (cgltf_size j = 0; j < data->animations[i].channels_count; ++j) + { + cgltf_animation_channel* channel = &data->animations[i].channels[j]; + + if (!channel->target_node) + { + continue; + } + + cgltf_size components = 1; + + if (channel->target_path == cgltf_animation_path_type_weights) + { + CGLTF_ASSERT_IF(!channel->target_node->mesh || !channel->target_node->mesh->primitives_count, cgltf_result_invalid_gltf); + + components = channel->target_node->mesh->primitives[0].targets_count; + } + + cgltf_size values = channel->sampler->interpolation == cgltf_interpolation_type_cubic_spline ? 3 : 1; + + CGLTF_ASSERT_IF(channel->sampler->input->count * components * values != channel->sampler->output->count, cgltf_result_invalid_gltf); + } + } + + for (cgltf_size i = 0; i < data->variants_count; ++i) + { + CGLTF_ASSERT_IF(!data->variants[i].name, cgltf_result_invalid_gltf); + } + + return cgltf_result_success; +} + +cgltf_result cgltf_copy_extras_json(const cgltf_data* data, const cgltf_extras* extras, char* dest, cgltf_size* dest_size) +{ + cgltf_size json_size = extras->end_offset - extras->start_offset; + + if (!dest) + { + if (dest_size) + { + *dest_size = json_size + 1; + return cgltf_result_success; + } + return cgltf_result_invalid_options; + } + + if (*dest_size + 1 < json_size) + { + strncpy(dest, data->json + extras->start_offset, *dest_size - 1); + dest[*dest_size - 1] = 0; + } + else + { + strncpy(dest, data->json + extras->start_offset, json_size); + dest[json_size] = 0; + } + + return cgltf_result_success; +} + +static void cgltf_free_extras(cgltf_data* data, cgltf_extras* extras) +{ + data->memory.free_func(data->memory.user_data, extras->data); +} + +static void cgltf_free_extensions(cgltf_data* data, cgltf_extension* extensions, cgltf_size extensions_count) +{ + for (cgltf_size i = 0; i < extensions_count; ++i) + { + data->memory.free_func(data->memory.user_data, extensions[i].name); + data->memory.free_func(data->memory.user_data, extensions[i].data); + } + data->memory.free_func(data->memory.user_data, extensions); +} + +void cgltf_free(cgltf_data* data) +{ + if (!data) + { + return; + } + + void (*file_release)(const struct cgltf_memory_options*, const struct cgltf_file_options*, void* data, cgltf_size size) = data->file.release ? data->file.release : cgltf_default_file_release; + + data->memory.free_func(data->memory.user_data, data->asset.copyright); + data->memory.free_func(data->memory.user_data, data->asset.generator); + data->memory.free_func(data->memory.user_data, data->asset.version); + data->memory.free_func(data->memory.user_data, data->asset.min_version); + + cgltf_free_extensions(data, data->asset.extensions, data->asset.extensions_count); + cgltf_free_extras(data, &data->asset.extras); + + for (cgltf_size i = 0; i < data->accessors_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->accessors[i].name); + + cgltf_free_extensions(data, data->accessors[i].extensions, data->accessors[i].extensions_count); + cgltf_free_extras(data, &data->accessors[i].extras); + } + data->memory.free_func(data->memory.user_data, data->accessors); + + for (cgltf_size i = 0; i < data->buffer_views_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->buffer_views[i].name); + data->memory.free_func(data->memory.user_data, data->buffer_views[i].data); + + cgltf_free_extensions(data, data->buffer_views[i].extensions, data->buffer_views[i].extensions_count); + cgltf_free_extras(data, &data->buffer_views[i].extras); + } + data->memory.free_func(data->memory.user_data, data->buffer_views); + + for (cgltf_size i = 0; i < data->buffers_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->buffers[i].name); + + if (data->buffers[i].data_free_method == cgltf_data_free_method_file_release) + { + file_release(&data->memory, &data->file, data->buffers[i].data, data->buffers[i].size); + } + else if (data->buffers[i].data_free_method == cgltf_data_free_method_memory_free) + { + data->memory.free_func(data->memory.user_data, data->buffers[i].data); + } + + data->memory.free_func(data->memory.user_data, data->buffers[i].uri); + + cgltf_free_extensions(data, data->buffers[i].extensions, data->buffers[i].extensions_count); + cgltf_free_extras(data, &data->buffers[i].extras); + } + data->memory.free_func(data->memory.user_data, data->buffers); + + for (cgltf_size i = 0; i < data->meshes_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->meshes[i].name); + + for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j) + { + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k) + { + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].attributes[k].name); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].attributes); + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k) + { + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m) + { + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].targets[k].attributes[m].name); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].targets[k].attributes); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].targets); + + if (data->meshes[i].primitives[j].has_draco_mesh_compression) + { + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].draco_mesh_compression.attributes_count; ++k) + { + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].draco_mesh_compression.attributes[k].name); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].draco_mesh_compression.attributes); + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].mappings_count; ++k) + { + cgltf_free_extras(data, &data->meshes[i].primitives[j].mappings[k].extras); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives[j].mappings); + + cgltf_free_extensions(data, data->meshes[i].primitives[j].extensions, data->meshes[i].primitives[j].extensions_count); + cgltf_free_extras(data, &data->meshes[i].primitives[j].extras); + } + + data->memory.free_func(data->memory.user_data, data->meshes[i].primitives); + data->memory.free_func(data->memory.user_data, data->meshes[i].weights); + + for (cgltf_size j = 0; j < data->meshes[i].target_names_count; ++j) + { + data->memory.free_func(data->memory.user_data, data->meshes[i].target_names[j]); + } + + cgltf_free_extensions(data, data->meshes[i].extensions, data->meshes[i].extensions_count); + cgltf_free_extras(data, &data->meshes[i].extras); + + data->memory.free_func(data->memory.user_data, data->meshes[i].target_names); + } + + data->memory.free_func(data->memory.user_data, data->meshes); + + for (cgltf_size i = 0; i < data->materials_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->materials[i].name); + + cgltf_free_extensions(data, data->materials[i].extensions, data->materials[i].extensions_count); + cgltf_free_extras(data, &data->materials[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->materials); + + for (cgltf_size i = 0; i < data->images_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->images[i].name); + data->memory.free_func(data->memory.user_data, data->images[i].uri); + data->memory.free_func(data->memory.user_data, data->images[i].mime_type); + + cgltf_free_extensions(data, data->images[i].extensions, data->images[i].extensions_count); + cgltf_free_extras(data, &data->images[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->images); + + for (cgltf_size i = 0; i < data->textures_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->textures[i].name); + + cgltf_free_extensions(data, data->textures[i].extensions, data->textures[i].extensions_count); + cgltf_free_extras(data, &data->textures[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->textures); + + for (cgltf_size i = 0; i < data->samplers_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->samplers[i].name); + + cgltf_free_extensions(data, data->samplers[i].extensions, data->samplers[i].extensions_count); + cgltf_free_extras(data, &data->samplers[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->samplers); + + for (cgltf_size i = 0; i < data->skins_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->skins[i].name); + data->memory.free_func(data->memory.user_data, data->skins[i].joints); + + cgltf_free_extensions(data, data->skins[i].extensions, data->skins[i].extensions_count); + cgltf_free_extras(data, &data->skins[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->skins); + + for (cgltf_size i = 0; i < data->cameras_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->cameras[i].name); + + if (data->cameras[i].type == cgltf_camera_type_perspective) + { + cgltf_free_extras(data, &data->cameras[i].data.perspective.extras); + } + else if (data->cameras[i].type == cgltf_camera_type_orthographic) + { + cgltf_free_extras(data, &data->cameras[i].data.orthographic.extras); + } + + cgltf_free_extensions(data, data->cameras[i].extensions, data->cameras[i].extensions_count); + cgltf_free_extras(data, &data->cameras[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->cameras); + + for (cgltf_size i = 0; i < data->lights_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->lights[i].name); + + cgltf_free_extras(data, &data->lights[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->lights); + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->nodes[i].name); + data->memory.free_func(data->memory.user_data, data->nodes[i].children); + data->memory.free_func(data->memory.user_data, data->nodes[i].weights); + + if (data->nodes[i].has_mesh_gpu_instancing) + { + for (cgltf_size j = 0; j < data->nodes[i].mesh_gpu_instancing.attributes_count; ++j) + { + data->memory.free_func(data->memory.user_data, data->nodes[i].mesh_gpu_instancing.attributes[j].name); + } + + data->memory.free_func(data->memory.user_data, data->nodes[i].mesh_gpu_instancing.attributes); + } + + cgltf_free_extensions(data, data->nodes[i].extensions, data->nodes[i].extensions_count); + cgltf_free_extras(data, &data->nodes[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->nodes); + + for (cgltf_size i = 0; i < data->scenes_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->scenes[i].name); + data->memory.free_func(data->memory.user_data, data->scenes[i].nodes); + + cgltf_free_extensions(data, data->scenes[i].extensions, data->scenes[i].extensions_count); + cgltf_free_extras(data, &data->scenes[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->scenes); + + for (cgltf_size i = 0; i < data->animations_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->animations[i].name); + for (cgltf_size j = 0; j < data->animations[i].samplers_count; ++j) + { + cgltf_free_extensions(data, data->animations[i].samplers[j].extensions, data->animations[i].samplers[j].extensions_count); + cgltf_free_extras(data, &data->animations[i].samplers[j].extras); + } + data->memory.free_func(data->memory.user_data, data->animations[i].samplers); + + for (cgltf_size j = 0; j < data->animations[i].channels_count; ++j) + { + cgltf_free_extensions(data, data->animations[i].channels[j].extensions, data->animations[i].channels[j].extensions_count); + cgltf_free_extras(data, &data->animations[i].channels[j].extras); + } + data->memory.free_func(data->memory.user_data, data->animations[i].channels); + + cgltf_free_extensions(data, data->animations[i].extensions, data->animations[i].extensions_count); + cgltf_free_extras(data, &data->animations[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->animations); + + for (cgltf_size i = 0; i < data->variants_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->variants[i].name); + + cgltf_free_extras(data, &data->variants[i].extras); + } + + data->memory.free_func(data->memory.user_data, data->variants); + + cgltf_free_extensions(data, data->data_extensions, data->data_extensions_count); + cgltf_free_extras(data, &data->extras); + + for (cgltf_size i = 0; i < data->extensions_used_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->extensions_used[i]); + } + + data->memory.free_func(data->memory.user_data, data->extensions_used); + + for (cgltf_size i = 0; i < data->extensions_required_count; ++i) + { + data->memory.free_func(data->memory.user_data, data->extensions_required[i]); + } + + data->memory.free_func(data->memory.user_data, data->extensions_required); + + file_release(&data->memory, &data->file, data->file_data, data->file_size); + + data->memory.free_func(data->memory.user_data, data); +} + +void cgltf_node_transform_local(const cgltf_node* node, cgltf_float* out_matrix) +{ + cgltf_float* lm = out_matrix; + + if (node->has_matrix) + { + memcpy(lm, node->matrix, sizeof(float) * 16); + } + else + { + float tx = node->translation[0]; + float ty = node->translation[1]; + float tz = node->translation[2]; + + float qx = node->rotation[0]; + float qy = node->rotation[1]; + float qz = node->rotation[2]; + float qw = node->rotation[3]; + + float sx = node->scale[0]; + float sy = node->scale[1]; + float sz = node->scale[2]; + + lm[0] = (1 - 2 * qy*qy - 2 * qz*qz) * sx; + lm[1] = (2 * qx*qy + 2 * qz*qw) * sx; + lm[2] = (2 * qx*qz - 2 * qy*qw) * sx; + lm[3] = 0.f; + + lm[4] = (2 * qx*qy - 2 * qz*qw) * sy; + lm[5] = (1 - 2 * qx*qx - 2 * qz*qz) * sy; + lm[6] = (2 * qy*qz + 2 * qx*qw) * sy; + lm[7] = 0.f; + + lm[8] = (2 * qx*qz + 2 * qy*qw) * sz; + lm[9] = (2 * qy*qz - 2 * qx*qw) * sz; + lm[10] = (1 - 2 * qx*qx - 2 * qy*qy) * sz; + lm[11] = 0.f; + + lm[12] = tx; + lm[13] = ty; + lm[14] = tz; + lm[15] = 1.f; + } +} + +void cgltf_node_transform_world(const cgltf_node* node, cgltf_float* out_matrix) +{ + cgltf_float* lm = out_matrix; + cgltf_node_transform_local(node, lm); + + const cgltf_node* parent = node->parent; + + while (parent) + { + float pm[16]; + cgltf_node_transform_local(parent, pm); + + for (int i = 0; i < 4; ++i) + { + float l0 = lm[i * 4 + 0]; + float l1 = lm[i * 4 + 1]; + float l2 = lm[i * 4 + 2]; + + float r0 = l0 * pm[0] + l1 * pm[4] + l2 * pm[8]; + float r1 = l0 * pm[1] + l1 * pm[5] + l2 * pm[9]; + float r2 = l0 * pm[2] + l1 * pm[6] + l2 * pm[10]; + + lm[i * 4 + 0] = r0; + lm[i * 4 + 1] = r1; + lm[i * 4 + 2] = r2; + } + + lm[12] += pm[12]; + lm[13] += pm[13]; + lm[14] += pm[14]; + + parent = parent->parent; + } +} + +static cgltf_ssize cgltf_component_read_integer(const void* in, cgltf_component_type component_type) +{ + switch (component_type) + { + case cgltf_component_type_r_16: + return *((const int16_t*) in); + case cgltf_component_type_r_16u: + return *((const uint16_t*) in); + case cgltf_component_type_r_32u: + return *((const uint32_t*) in); + case cgltf_component_type_r_8: + return *((const int8_t*) in); + case cgltf_component_type_r_8u: + return *((const uint8_t*) in); + default: + return 0; + } +} + +static cgltf_size cgltf_component_read_index(const void* in, cgltf_component_type component_type) +{ + switch (component_type) + { + case cgltf_component_type_r_16u: + return *((const uint16_t*) in); + case cgltf_component_type_r_32u: + return *((const uint32_t*) in); + case cgltf_component_type_r_8u: + return *((const uint8_t*) in); + default: + return 0; + } +} + +static cgltf_float cgltf_component_read_float(const void* in, cgltf_component_type component_type, cgltf_bool normalized) +{ + if (component_type == cgltf_component_type_r_32f) + { + return *((const float*) in); + } + + if (normalized) + { + switch (component_type) + { + // note: glTF spec doesn't currently define normalized conversions for 32-bit integers + case cgltf_component_type_r_16: + return *((const int16_t*) in) / (cgltf_float)32767; + case cgltf_component_type_r_16u: + return *((const uint16_t*) in) / (cgltf_float)65535; + case cgltf_component_type_r_8: + return *((const int8_t*) in) / (cgltf_float)127; + case cgltf_component_type_r_8u: + return *((const uint8_t*) in) / (cgltf_float)255; + default: + return 0; + } + } + + return (cgltf_float)cgltf_component_read_integer(in, component_type); +} + +static cgltf_bool cgltf_element_read_float(const uint8_t* element, cgltf_type type, cgltf_component_type component_type, cgltf_bool normalized, cgltf_float* out, cgltf_size element_size) +{ + cgltf_size num_components = cgltf_num_components(type); + + if (element_size < num_components) { + return 0; + } + + // There are three special cases for component extraction, see #data-alignment in the 2.0 spec. + + cgltf_size component_size = cgltf_component_size(component_type); + + if (type == cgltf_type_mat2 && component_size == 1) + { + out[0] = cgltf_component_read_float(element, component_type, normalized); + out[1] = cgltf_component_read_float(element + 1, component_type, normalized); + out[2] = cgltf_component_read_float(element + 4, component_type, normalized); + out[3] = cgltf_component_read_float(element + 5, component_type, normalized); + return 1; + } + + if (type == cgltf_type_mat3 && component_size == 1) + { + out[0] = cgltf_component_read_float(element, component_type, normalized); + out[1] = cgltf_component_read_float(element + 1, component_type, normalized); + out[2] = cgltf_component_read_float(element + 2, component_type, normalized); + out[3] = cgltf_component_read_float(element + 4, component_type, normalized); + out[4] = cgltf_component_read_float(element + 5, component_type, normalized); + out[5] = cgltf_component_read_float(element + 6, component_type, normalized); + out[6] = cgltf_component_read_float(element + 8, component_type, normalized); + out[7] = cgltf_component_read_float(element + 9, component_type, normalized); + out[8] = cgltf_component_read_float(element + 10, component_type, normalized); + return 1; + } + + if (type == cgltf_type_mat3 && component_size == 2) + { + out[0] = cgltf_component_read_float(element, component_type, normalized); + out[1] = cgltf_component_read_float(element + 2, component_type, normalized); + out[2] = cgltf_component_read_float(element + 4, component_type, normalized); + out[3] = cgltf_component_read_float(element + 8, component_type, normalized); + out[4] = cgltf_component_read_float(element + 10, component_type, normalized); + out[5] = cgltf_component_read_float(element + 12, component_type, normalized); + out[6] = cgltf_component_read_float(element + 16, component_type, normalized); + out[7] = cgltf_component_read_float(element + 18, component_type, normalized); + out[8] = cgltf_component_read_float(element + 20, component_type, normalized); + return 1; + } + + for (cgltf_size i = 0; i < num_components; ++i) + { + out[i] = cgltf_component_read_float(element + component_size * i, component_type, normalized); + } + return 1; +} + +const uint8_t* cgltf_buffer_view_data(const cgltf_buffer_view* view) +{ + if (view->data) + return (const uint8_t*)view->data; + + if (!view->buffer->data) + return NULL; + + const uint8_t* result = (const uint8_t*)view->buffer->data; + result += view->offset; + return result; +} + +const cgltf_accessor* cgltf_find_accessor(const cgltf_primitive* prim, cgltf_attribute_type type, cgltf_int index) +{ + for (cgltf_size i = 0; i < prim->attributes_count; ++i) + { + const cgltf_attribute* attr = &prim->attributes[i]; + if (attr->type == type && attr->index == index) + return attr->data; + } + + return NULL; +} + +static const uint8_t* cgltf_find_sparse_index(const cgltf_accessor* accessor, cgltf_size needle) +{ + const cgltf_accessor_sparse* sparse = &accessor->sparse; + const uint8_t* index_data = cgltf_buffer_view_data(sparse->indices_buffer_view); + const uint8_t* value_data = cgltf_buffer_view_data(sparse->values_buffer_view); + + if (index_data == NULL || value_data == NULL) + return NULL; + + index_data += sparse->indices_byte_offset; + value_data += sparse->values_byte_offset; + + cgltf_size index_stride = cgltf_component_size(sparse->indices_component_type); + + cgltf_size offset = 0; + cgltf_size length = sparse->count; + + while (length) + { + cgltf_size rem = length % 2; + length /= 2; + + cgltf_size index = cgltf_component_read_index(index_data + (offset + length) * index_stride, sparse->indices_component_type); + offset += index < needle ? length + rem : 0; + } + + if (offset == sparse->count) + return NULL; + + cgltf_size index = cgltf_component_read_index(index_data + offset * index_stride, sparse->indices_component_type); + return index == needle ? value_data + offset * accessor->stride : NULL; +} + +cgltf_bool cgltf_accessor_read_float(const cgltf_accessor* accessor, cgltf_size index, cgltf_float* out, cgltf_size element_size) +{ + if (accessor->is_sparse) + { + const uint8_t* element = cgltf_find_sparse_index(accessor, index); + if (element) + return cgltf_element_read_float(element, accessor->type, accessor->component_type, accessor->normalized, out, element_size); + } + if (accessor->buffer_view == NULL) + { + memset(out, 0, element_size * sizeof(cgltf_float)); + return 1; + } + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; + } + element += accessor->offset + accessor->stride * index; + return cgltf_element_read_float(element, accessor->type, accessor->component_type, accessor->normalized, out, element_size); +} + +cgltf_size cgltf_accessor_unpack_floats(const cgltf_accessor* accessor, cgltf_float* out, cgltf_size float_count) +{ + cgltf_size floats_per_element = cgltf_num_components(accessor->type); + cgltf_size available_floats = accessor->count * floats_per_element; + if (out == NULL) + { + return available_floats; + } + + float_count = available_floats < float_count ? available_floats : float_count; + cgltf_size element_count = float_count / floats_per_element; + + // First pass: convert each element in the base accessor. + if (accessor->buffer_view == NULL) + { + memset(out, 0, element_count * floats_per_element * sizeof(cgltf_float)); + } + else + { + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; + } + element += accessor->offset; + + if (accessor->component_type == cgltf_component_type_r_32f && accessor->stride == floats_per_element * sizeof(cgltf_float)) + { + memcpy(out, element, element_count * floats_per_element * sizeof(cgltf_float)); + } + else + { + cgltf_float* dest = out; + + for (cgltf_size index = 0; index < element_count; index++, dest += floats_per_element, element += accessor->stride) + { + if (!cgltf_element_read_float(element, accessor->type, accessor->component_type, accessor->normalized, dest, floats_per_element)) + { + return 0; + } + } + } + } + + // Second pass: write out each element in the sparse accessor. + if (accessor->is_sparse) + { + const cgltf_accessor_sparse* sparse = &accessor->sparse; + + const uint8_t* index_data = cgltf_buffer_view_data(sparse->indices_buffer_view); + const uint8_t* reader_head = cgltf_buffer_view_data(sparse->values_buffer_view); + + if (index_data == NULL || reader_head == NULL) + { + return 0; + } + + index_data += sparse->indices_byte_offset; + reader_head += sparse->values_byte_offset; + + cgltf_size index_stride = cgltf_component_size(sparse->indices_component_type); + for (cgltf_size reader_index = 0; reader_index < sparse->count; reader_index++, index_data += index_stride, reader_head += accessor->stride) + { + size_t writer_index = cgltf_component_read_index(index_data, sparse->indices_component_type); + float* writer_head = out + writer_index * floats_per_element; + + if (!cgltf_element_read_float(reader_head, accessor->type, accessor->component_type, accessor->normalized, writer_head, floats_per_element)) + { + return 0; + } + } + } + + return element_count * floats_per_element; +} + +static cgltf_uint cgltf_component_read_uint(const void* in, cgltf_component_type component_type) +{ + switch (component_type) + { + case cgltf_component_type_r_8: + return *((const int8_t*) in); + + case cgltf_component_type_r_8u: + return *((const uint8_t*) in); + + case cgltf_component_type_r_16: + return *((const int16_t*) in); + + case cgltf_component_type_r_16u: + return *((const uint16_t*) in); + + case cgltf_component_type_r_32u: + return *((const uint32_t*) in); + + default: + return 0; + } +} + +static cgltf_bool cgltf_element_read_uint(const uint8_t* element, cgltf_type type, cgltf_component_type component_type, cgltf_uint* out, cgltf_size element_size) +{ + cgltf_size num_components = cgltf_num_components(type); + + if (element_size < num_components) + { + return 0; + } + + // Reading integer matrices is not a valid use case + if (type == cgltf_type_mat2 || type == cgltf_type_mat3 || type == cgltf_type_mat4) + { + return 0; + } + + cgltf_size component_size = cgltf_component_size(component_type); + + for (cgltf_size i = 0; i < num_components; ++i) + { + out[i] = cgltf_component_read_uint(element + component_size * i, component_type); + } + return 1; +} + +cgltf_bool cgltf_accessor_read_uint(const cgltf_accessor* accessor, cgltf_size index, cgltf_uint* out, cgltf_size element_size) +{ + if (accessor->is_sparse) + { + const uint8_t* element = cgltf_find_sparse_index(accessor, index); + if (element) + return cgltf_element_read_uint(element, accessor->type, accessor->component_type, out, element_size); + } + if (accessor->buffer_view == NULL) + { + memset(out, 0, element_size * sizeof(cgltf_uint)); + return 1; + } + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; + } + element += accessor->offset + accessor->stride * index; + return cgltf_element_read_uint(element, accessor->type, accessor->component_type, out, element_size); +} + +cgltf_size cgltf_accessor_read_index(const cgltf_accessor* accessor, cgltf_size index) +{ + if (accessor->is_sparse) + { + const uint8_t* element = cgltf_find_sparse_index(accessor, index); + if (element) + return cgltf_component_read_index(element, accessor->component_type); + } + if (accessor->buffer_view == NULL) + { + return 0; + } + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; // This is an error case, but we can't communicate the error with existing interface. + } + element += accessor->offset + accessor->stride * index; + return cgltf_component_read_index(element, accessor->component_type); +} + +cgltf_size cgltf_mesh_index(const cgltf_data* data, const cgltf_mesh* object) +{ + assert(object && (cgltf_size)(object - data->meshes) < data->meshes_count); + return (cgltf_size)(object - data->meshes); +} + +cgltf_size cgltf_material_index(const cgltf_data* data, const cgltf_material* object) +{ + assert(object && (cgltf_size)(object - data->materials) < data->materials_count); + return (cgltf_size)(object - data->materials); +} + +cgltf_size cgltf_accessor_index(const cgltf_data* data, const cgltf_accessor* object) +{ + assert(object && (cgltf_size)(object - data->accessors) < data->accessors_count); + return (cgltf_size)(object - data->accessors); +} + +cgltf_size cgltf_buffer_view_index(const cgltf_data* data, const cgltf_buffer_view* object) +{ + assert(object && (cgltf_size)(object - data->buffer_views) < data->buffer_views_count); + return (cgltf_size)(object - data->buffer_views); +} + +cgltf_size cgltf_buffer_index(const cgltf_data* data, const cgltf_buffer* object) +{ + assert(object && (cgltf_size)(object - data->buffers) < data->buffers_count); + return (cgltf_size)(object - data->buffers); +} + +cgltf_size cgltf_image_index(const cgltf_data* data, const cgltf_image* object) +{ + assert(object && (cgltf_size)(object - data->images) < data->images_count); + return (cgltf_size)(object - data->images); +} + +cgltf_size cgltf_texture_index(const cgltf_data* data, const cgltf_texture* object) +{ + assert(object && (cgltf_size)(object - data->textures) < data->textures_count); + return (cgltf_size)(object - data->textures); +} + +cgltf_size cgltf_sampler_index(const cgltf_data* data, const cgltf_sampler* object) +{ + assert(object && (cgltf_size)(object - data->samplers) < data->samplers_count); + return (cgltf_size)(object - data->samplers); +} + +cgltf_size cgltf_skin_index(const cgltf_data* data, const cgltf_skin* object) +{ + assert(object && (cgltf_size)(object - data->skins) < data->skins_count); + return (cgltf_size)(object - data->skins); +} + +cgltf_size cgltf_camera_index(const cgltf_data* data, const cgltf_camera* object) +{ + assert(object && (cgltf_size)(object - data->cameras) < data->cameras_count); + return (cgltf_size)(object - data->cameras); +} + +cgltf_size cgltf_light_index(const cgltf_data* data, const cgltf_light* object) +{ + assert(object && (cgltf_size)(object - data->lights) < data->lights_count); + return (cgltf_size)(object - data->lights); +} + +cgltf_size cgltf_node_index(const cgltf_data* data, const cgltf_node* object) +{ + assert(object && (cgltf_size)(object - data->nodes) < data->nodes_count); + return (cgltf_size)(object - data->nodes); +} + +cgltf_size cgltf_scene_index(const cgltf_data* data, const cgltf_scene* object) +{ + assert(object && (cgltf_size)(object - data->scenes) < data->scenes_count); + return (cgltf_size)(object - data->scenes); +} + +cgltf_size cgltf_animation_index(const cgltf_data* data, const cgltf_animation* object) +{ + assert(object && (cgltf_size)(object - data->animations) < data->animations_count); + return (cgltf_size)(object - data->animations); +} + +cgltf_size cgltf_animation_sampler_index(const cgltf_animation* animation, const cgltf_animation_sampler* object) +{ + assert(object && (cgltf_size)(object - animation->samplers) < animation->samplers_count); + return (cgltf_size)(object - animation->samplers); +} + +cgltf_size cgltf_animation_channel_index(const cgltf_animation* animation, const cgltf_animation_channel* object) +{ + assert(object && (cgltf_size)(object - animation->channels) < animation->channels_count); + return (cgltf_size)(object - animation->channels); +} + +cgltf_size cgltf_accessor_unpack_indices(const cgltf_accessor* accessor, void* out, cgltf_size out_component_size, cgltf_size index_count) +{ + if (out == NULL) + { + return accessor->count; + } + + cgltf_size numbers_per_element = cgltf_num_components(accessor->type); + cgltf_size available_numbers = accessor->count * numbers_per_element; + + index_count = available_numbers < index_count ? available_numbers : index_count; + cgltf_size index_component_size = cgltf_component_size(accessor->component_type); + + if (accessor->is_sparse) + { + return 0; + } + if (accessor->buffer_view == NULL) + { + return 0; + } + if (index_component_size > out_component_size) + { + return 0; + } + const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); + if (element == NULL) + { + return 0; + } + element += accessor->offset; + + if (index_component_size == out_component_size && accessor->stride == out_component_size * numbers_per_element) + { + memcpy(out, element, index_count * index_component_size); + return index_count; + } + + // Data couldn't be copied with memcpy due to stride being larger than the component size. + // OR + // The component size of the output array is larger than the component size of the index data, so index data will be padded. + switch (out_component_size) + { + case 1: + for (cgltf_size index = 0; index < index_count; index++, element += accessor->stride) + { + ((uint8_t*)out)[index] = (uint8_t)cgltf_component_read_index(element, accessor->component_type); + } + break; + case 2: + for (cgltf_size index = 0; index < index_count; index++, element += accessor->stride) + { + ((uint16_t*)out)[index] = (uint16_t)cgltf_component_read_index(element, accessor->component_type); + } + break; + case 4: + for (cgltf_size index = 0; index < index_count; index++, element += accessor->stride) + { + ((uint32_t*)out)[index] = (uint32_t)cgltf_component_read_index(element, accessor->component_type); + } + break; + default: + return 0; + } + + return index_count; +} + +#define CGLTF_ERROR_JSON -1 +#define CGLTF_ERROR_NOMEM -2 +#define CGLTF_ERROR_LEGACY -3 + +#define CGLTF_CHECK_TOKTYPE(tok_, type_) if ((tok_).type != (type_)) { return CGLTF_ERROR_JSON; } +#define CGLTF_CHECK_TOKTYPE_RET(tok_, type_, ret_) if ((tok_).type != (type_)) { return ret_; } +#define CGLTF_CHECK_KEY(tok_) if ((tok_).type != JSMN_STRING || (tok_).size == 0) { return CGLTF_ERROR_JSON; } /* checking size for 0 verifies that a value follows the key */ + +#define CGLTF_PTRINDEX(type, idx) (type*)((cgltf_size)idx + 1) +#define CGLTF_PTRFIXUP(var, data, size) if (var) { if ((cgltf_size)var > size) { return CGLTF_ERROR_JSON; } var = &data[(cgltf_size)var-1]; } +#define CGLTF_PTRFIXUP_REQ(var, data, size) if (!var || (cgltf_size)var > size) { return CGLTF_ERROR_JSON; } var = &data[(cgltf_size)var-1]; + +static int cgltf_json_strcmp(jsmntok_t const* tok, const uint8_t* json_chunk, const char* str) +{ + CGLTF_CHECK_TOKTYPE(*tok, JSMN_STRING); + size_t const str_len = strlen(str); + size_t const name_length = (size_t)(tok->end - tok->start); + return (str_len == name_length) ? strncmp((const char*)json_chunk + tok->start, str, str_len) : 128; +} + +static int cgltf_json_to_int(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + CGLTF_CHECK_TOKTYPE(*tok, JSMN_PRIMITIVE); + char tmp[128]; + int size = (size_t)(tok->end - tok->start) < sizeof(tmp) ? (int)(tok->end - tok->start) : (int)(sizeof(tmp) - 1); + strncpy(tmp, (const char*)json_chunk + tok->start, size); + tmp[size] = 0; + return CGLTF_ATOI(tmp); +} + +static cgltf_size cgltf_json_to_size(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + CGLTF_CHECK_TOKTYPE_RET(*tok, JSMN_PRIMITIVE, 0); + char tmp[128]; + int size = (size_t)(tok->end - tok->start) < sizeof(tmp) ? (int)(tok->end - tok->start) : (int)(sizeof(tmp) - 1); + strncpy(tmp, (const char*)json_chunk + tok->start, size); + tmp[size] = 0; + long long res = CGLTF_ATOLL(tmp); + return res < 0 ? 0 : (cgltf_size)res; +} + +static cgltf_float cgltf_json_to_float(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + CGLTF_CHECK_TOKTYPE(*tok, JSMN_PRIMITIVE); + char tmp[128]; + int size = (size_t)(tok->end - tok->start) < sizeof(tmp) ? (int)(tok->end - tok->start) : (int)(sizeof(tmp) - 1); + strncpy(tmp, (const char*)json_chunk + tok->start, size); + tmp[size] = 0; + return (cgltf_float)CGLTF_ATOF(tmp); +} + +static cgltf_bool cgltf_json_to_bool(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + int size = (int)(tok->end - tok->start); + return size == 4 && memcmp(json_chunk + tok->start, "true", 4) == 0; +} + +static int cgltf_skip_json(jsmntok_t const* tokens, int i) +{ + int end = i + 1; + + while (i < end) + { + switch (tokens[i].type) + { + case JSMN_OBJECT: + end += tokens[i].size * 2; + break; + + case JSMN_ARRAY: + end += tokens[i].size; + break; + + case JSMN_PRIMITIVE: + case JSMN_STRING: + break; + + default: + return -1; + } + + i++; + } + + return i; +} + +static void cgltf_fill_float_array(float* out_array, int size, float value) +{ + for (int j = 0; j < size; ++j) + { + out_array[j] = value; + } +} + +static int cgltf_parse_json_float_array(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, float* out_array, int size) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY); + if (tokens[i].size != size) + { + return CGLTF_ERROR_JSON; + } + ++i; + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_array[j] = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + return i; +} + +static int cgltf_parse_json_string(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, char** out_string) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_STRING); + if (*out_string) + { + return CGLTF_ERROR_JSON; + } + int size = (int)(tokens[i].end - tokens[i].start); + char* result = (char*)options->memory.alloc_func(options->memory.user_data, size + 1); + if (!result) + { + return CGLTF_ERROR_NOMEM; + } + strncpy(result, (const char*)json_chunk + tokens[i].start, size); + result[size] = 0; + *out_string = result; + return i + 1; +} + +static int cgltf_parse_json_array(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, size_t element_size, void** out_array, cgltf_size* out_size) +{ + (void)json_chunk; + if (tokens[i].type != JSMN_ARRAY) + { + return tokens[i].type == JSMN_OBJECT ? CGLTF_ERROR_LEGACY : CGLTF_ERROR_JSON; + } + if (*out_array) + { + return CGLTF_ERROR_JSON; + } + int size = tokens[i].size; + void* result = cgltf_calloc(options, element_size, size); + if (!result) + { + return CGLTF_ERROR_NOMEM; + } + *out_array = result; + *out_size = size; + return i + 1; +} + +static int cgltf_parse_json_string_array(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, char*** out_array, cgltf_size* out_size) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY); + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(char*), (void**)out_array, out_size); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < *out_size; ++j) + { + i = cgltf_parse_json_string(options, tokens, i, json_chunk, j + (*out_array)); + if (i < 0) + { + return i; + } + } + return i; +} + +static void cgltf_parse_attribute_type(const char* name, cgltf_attribute_type* out_type, int* out_index) +{ + if (*name == '_') + { + *out_type = cgltf_attribute_type_custom; + return; + } + + const char* us = strchr(name, '_'); + size_t len = us ? (size_t)(us - name) : strlen(name); + + if (len == 8 && strncmp(name, "POSITION", 8) == 0) + { + *out_type = cgltf_attribute_type_position; + } + else if (len == 6 && strncmp(name, "NORMAL", 6) == 0) + { + *out_type = cgltf_attribute_type_normal; + } + else if (len == 7 && strncmp(name, "TANGENT", 7) == 0) + { + *out_type = cgltf_attribute_type_tangent; + } + else if (len == 8 && strncmp(name, "TEXCOORD", 8) == 0) + { + *out_type = cgltf_attribute_type_texcoord; + } + else if (len == 5 && strncmp(name, "COLOR", 5) == 0) + { + *out_type = cgltf_attribute_type_color; + } + else if (len == 6 && strncmp(name, "JOINTS", 6) == 0) + { + *out_type = cgltf_attribute_type_joints; + } + else if (len == 7 && strncmp(name, "WEIGHTS", 7) == 0) + { + *out_type = cgltf_attribute_type_weights; + } + else + { + *out_type = cgltf_attribute_type_invalid; + } + + if (us && *out_type != cgltf_attribute_type_invalid) + { + *out_index = CGLTF_ATOI(us + 1); + if (*out_index < 0) + { + *out_type = cgltf_attribute_type_invalid; + *out_index = 0; + } + } +} + +static int cgltf_parse_json_attribute_list(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_attribute** out_attributes, cgltf_size* out_attributes_count) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + if (*out_attributes) + { + return CGLTF_ERROR_JSON; + } + + *out_attributes_count = tokens[i].size; + *out_attributes = (cgltf_attribute*)cgltf_calloc(options, sizeof(cgltf_attribute), *out_attributes_count); + ++i; + + if (!*out_attributes) + { + return CGLTF_ERROR_NOMEM; + } + + for (cgltf_size j = 0; j < *out_attributes_count; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + i = cgltf_parse_json_string(options, tokens, i, json_chunk, &(*out_attributes)[j].name); + if (i < 0) + { + return CGLTF_ERROR_JSON; + } + + cgltf_parse_attribute_type((*out_attributes)[j].name, &(*out_attributes)[j].type, &(*out_attributes)[j].index); + + (*out_attributes)[j].data = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + + return i; +} + +static int cgltf_parse_json_extras(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_extras* out_extras) +{ + if (out_extras->data) + { + return CGLTF_ERROR_JSON; + } + + /* fill deprecated fields for now, this will be removed in the future */ + out_extras->start_offset = tokens[i].start; + out_extras->end_offset = tokens[i].end; + + size_t start = tokens[i].start; + size_t size = tokens[i].end - start; + out_extras->data = (char*)options->memory.alloc_func(options->memory.user_data, size + 1); + if (!out_extras->data) + { + return CGLTF_ERROR_NOMEM; + } + strncpy(out_extras->data, (const char*)json_chunk + start, size); + out_extras->data[size] = '\0'; + + i = cgltf_skip_json(tokens, i); + return i; +} + +static int cgltf_parse_json_unprocessed_extension(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_extension* out_extension) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_STRING); + CGLTF_CHECK_TOKTYPE(tokens[i+1], JSMN_OBJECT); + if (out_extension->name) + { + return CGLTF_ERROR_JSON; + } + + cgltf_size name_length = tokens[i].end - tokens[i].start; + out_extension->name = (char*)options->memory.alloc_func(options->memory.user_data, name_length + 1); + if (!out_extension->name) + { + return CGLTF_ERROR_NOMEM; + } + strncpy(out_extension->name, (const char*)json_chunk + tokens[i].start, name_length); + out_extension->name[name_length] = 0; + i++; + + size_t start = tokens[i].start; + size_t size = tokens[i].end - start; + out_extension->data = (char*)options->memory.alloc_func(options->memory.user_data, size + 1); + if (!out_extension->data) + { + return CGLTF_ERROR_NOMEM; + } + strncpy(out_extension->data, (const char*)json_chunk + start, size); + out_extension->data[size] = '\0'; + + i = cgltf_skip_json(tokens, i); + + return i; +} + +static int cgltf_parse_json_unprocessed_extensions(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_size* out_extensions_count, cgltf_extension** out_extensions) +{ + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(*out_extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + *out_extensions_count = 0; + *out_extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!*out_extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + + for (int j = 0; j < extensions_size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + cgltf_size extension_index = (*out_extensions_count)++; + cgltf_extension* extension = &((*out_extensions)[extension_index]); + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, extension); + + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_draco_mesh_compression(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_draco_mesh_compression* out_draco_mesh_compression) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "attributes") == 0) + { + i = cgltf_parse_json_attribute_list(options, tokens, i + 1, json_chunk, &out_draco_mesh_compression->attributes, &out_draco_mesh_compression->attributes_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "bufferView") == 0) + { + ++i; + out_draco_mesh_compression->buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_mesh_gpu_instancing(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_mesh_gpu_instancing* out_mesh_gpu_instancing) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "attributes") == 0) + { + i = cgltf_parse_json_attribute_list(options, tokens, i + 1, json_chunk, &out_mesh_gpu_instancing->attributes, &out_mesh_gpu_instancing->attributes_count); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_material_mapping_data(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_material_mapping* out_mappings, cgltf_size* offset) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int obj_size = tokens[i].size; + ++i; + + int material = -1; + int variants_tok = -1; + int extras_tok = -1; + + for (int k = 0; k < obj_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "material") == 0) + { + ++i; + material = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "variants") == 0) + { + variants_tok = i+1; + CGLTF_CHECK_TOKTYPE(tokens[variants_tok], JSMN_ARRAY); + + i = cgltf_skip_json(tokens, i+1); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + extras_tok = i + 1; + i = cgltf_skip_json(tokens, extras_tok); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + if (material < 0 || variants_tok < 0) + { + return CGLTF_ERROR_JSON; + } + + if (out_mappings) + { + for (int k = 0; k < tokens[variants_tok].size; ++k) + { + int variant = cgltf_json_to_int(&tokens[variants_tok + 1 + k], json_chunk); + if (variant < 0) + return variant; + + out_mappings[*offset].material = CGLTF_PTRINDEX(cgltf_material, material); + out_mappings[*offset].variant = variant; + + if (extras_tok >= 0) + { + int e = cgltf_parse_json_extras(options, tokens, extras_tok, json_chunk, &out_mappings[*offset].extras); + if (e < 0) + return e; + } + + (*offset)++; + } + } + else + { + (*offset) += tokens[variants_tok].size; + } + } + + return i; +} + +static int cgltf_parse_json_material_mappings(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_primitive* out_prim) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "mappings") == 0) + { + if (out_prim->mappings) + { + return CGLTF_ERROR_JSON; + } + + cgltf_size mappings_offset = 0; + int k = cgltf_parse_json_material_mapping_data(options, tokens, i + 1, json_chunk, NULL, &mappings_offset); + if (k < 0) + { + return k; + } + + out_prim->mappings_count = mappings_offset; + out_prim->mappings = (cgltf_material_mapping*)cgltf_calloc(options, sizeof(cgltf_material_mapping), out_prim->mappings_count); + + mappings_offset = 0; + i = cgltf_parse_json_material_mapping_data(options, tokens, i + 1, json_chunk, out_prim->mappings, &mappings_offset); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static cgltf_primitive_type cgltf_json_to_primitive_type(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + int type = cgltf_json_to_int(tok, json_chunk); + + switch (type) + { + case 0: + return cgltf_primitive_type_points; + case 1: + return cgltf_primitive_type_lines; + case 2: + return cgltf_primitive_type_line_loop; + case 3: + return cgltf_primitive_type_line_strip; + case 4: + return cgltf_primitive_type_triangles; + case 5: + return cgltf_primitive_type_triangle_strip; + case 6: + return cgltf_primitive_type_triangle_fan; + default: + return cgltf_primitive_type_invalid; + } +} + +static int cgltf_parse_json_primitive(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_primitive* out_prim) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_prim->type = cgltf_primitive_type_triangles; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "mode") == 0) + { + ++i; + out_prim->type = cgltf_json_to_primitive_type(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "indices") == 0) + { + ++i; + out_prim->indices = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "material") == 0) + { + ++i; + out_prim->material = CGLTF_PTRINDEX(cgltf_material, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "attributes") == 0) + { + i = cgltf_parse_json_attribute_list(options, tokens, i + 1, json_chunk, &out_prim->attributes, &out_prim->attributes_count); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "targets") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_morph_target), (void**)&out_prim->targets, &out_prim->targets_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_prim->targets_count; ++k) + { + i = cgltf_parse_json_attribute_list(options, tokens, i, json_chunk, &out_prim->targets[k].attributes, &out_prim->targets[k].attributes_count); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_prim->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_prim->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_prim->extensions_count = 0; + out_prim->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_prim->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_draco_mesh_compression") == 0) + { + out_prim->has_draco_mesh_compression = 1; + i = cgltf_parse_json_draco_mesh_compression(options, tokens, i + 1, json_chunk, &out_prim->draco_mesh_compression); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_variants") == 0) + { + i = cgltf_parse_json_material_mappings(options, tokens, i + 1, json_chunk, out_prim); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_prim->extensions[out_prim->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_mesh(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_mesh* out_mesh) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_mesh->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "primitives") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_primitive), (void**)&out_mesh->primitives, &out_mesh->primitives_count); + if (i < 0) + { + return i; + } + + for (cgltf_size prim_index = 0; prim_index < out_mesh->primitives_count; ++prim_index) + { + i = cgltf_parse_json_primitive(options, tokens, i, json_chunk, &out_mesh->primitives[prim_index]); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "weights") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_float), (void**)&out_mesh->weights, &out_mesh->weights_count); + if (i < 0) + { + return i; + } + + i = cgltf_parse_json_float_array(tokens, i - 1, json_chunk, out_mesh->weights, (int)out_mesh->weights_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + ++i; + + out_mesh->extras.start_offset = tokens[i].start; + out_mesh->extras.end_offset = tokens[i].end; + + if (tokens[i].type == JSMN_OBJECT) + { + int extras_size = tokens[i].size; + ++i; + + for (int k = 0; k < extras_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "targetNames") == 0 && tokens[i+1].type == JSMN_ARRAY) + { + i = cgltf_parse_json_string_array(options, tokens, i + 1, json_chunk, &out_mesh->target_names, &out_mesh->target_names_count); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i); + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_mesh->extensions_count, &out_mesh->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_meshes(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_mesh), (void**)&out_data->meshes, &out_data->meshes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->meshes_count; ++j) + { + i = cgltf_parse_json_mesh(options, tokens, i, json_chunk, &out_data->meshes[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static cgltf_component_type cgltf_json_to_component_type(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + int type = cgltf_json_to_int(tok, json_chunk); + + switch (type) + { + case 5120: + return cgltf_component_type_r_8; + case 5121: + return cgltf_component_type_r_8u; + case 5122: + return cgltf_component_type_r_16; + case 5123: + return cgltf_component_type_r_16u; + case 5125: + return cgltf_component_type_r_32u; + case 5126: + return cgltf_component_type_r_32f; + default: + return cgltf_component_type_invalid; + } +} + +static int cgltf_parse_json_accessor_sparse(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_accessor_sparse* out_sparse) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0) + { + ++i; + out_sparse->count = cgltf_json_to_size(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "indices") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int indices_size = tokens[i].size; + ++i; + + for (int k = 0; k < indices_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_sparse->indices_buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_sparse->indices_byte_offset = cgltf_json_to_size(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "componentType") == 0) + { + ++i; + out_sparse->indices_component_type = cgltf_json_to_component_type(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "values") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int values_size = tokens[i].size; + ++i; + + for (int k = 0; k < values_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_sparse->values_buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_sparse->values_byte_offset = cgltf_json_to_size(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_accessor(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_accessor* out_accessor) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_accessor->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_accessor->buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_accessor->offset = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "componentType") == 0) + { + ++i; + out_accessor->component_type = cgltf_json_to_component_type(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "normalized") == 0) + { + ++i; + out_accessor->normalized = cgltf_json_to_bool(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0) + { + ++i; + out_accessor->count = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "type") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "SCALAR") == 0) + { + out_accessor->type = cgltf_type_scalar; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "VEC2") == 0) + { + out_accessor->type = cgltf_type_vec2; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "VEC3") == 0) + { + out_accessor->type = cgltf_type_vec3; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "VEC4") == 0) + { + out_accessor->type = cgltf_type_vec4; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "MAT2") == 0) + { + out_accessor->type = cgltf_type_mat2; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "MAT3") == 0) + { + out_accessor->type = cgltf_type_mat3; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "MAT4") == 0) + { + out_accessor->type = cgltf_type_mat4; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "min") == 0) + { + ++i; + out_accessor->has_min = 1; + // note: we can't parse the precise number of elements since type may not have been computed yet + int min_size = tokens[i].size > 16 ? 16 : tokens[i].size; + i = cgltf_parse_json_float_array(tokens, i, json_chunk, out_accessor->min, min_size); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "max") == 0) + { + ++i; + out_accessor->has_max = 1; + // note: we can't parse the precise number of elements since type may not have been computed yet + int max_size = tokens[i].size > 16 ? 16 : tokens[i].size; + i = cgltf_parse_json_float_array(tokens, i, json_chunk, out_accessor->max, max_size); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "sparse") == 0) + { + out_accessor->is_sparse = 1; + i = cgltf_parse_json_accessor_sparse(tokens, i + 1, json_chunk, &out_accessor->sparse); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_accessor->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_accessor->extensions_count, &out_accessor->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_texture_transform(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture_transform* out_texture_transform) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "offset") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_texture_transform->offset, 2); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "rotation") == 0) + { + ++i; + out_texture_transform->rotation = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scale") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_texture_transform->scale, 2); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "texCoord") == 0) + { + ++i; + out_texture_transform->has_texcoord = 1; + out_texture_transform->texcoord = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_texture_view(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture_view* out_texture_view) +{ + (void)options; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_texture_view->scale = 1.0f; + cgltf_fill_float_array(out_texture_view->transform.scale, 2, 1.0f); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "index") == 0) + { + ++i; + out_texture_view->texture = CGLTF_PTRINDEX(cgltf_texture, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "texCoord") == 0) + { + ++i; + out_texture_view->texcoord = cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scale") == 0) + { + ++i; + out_texture_view->scale = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "strength") == 0) + { + ++i; + out_texture_view->scale = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int extensions_size = tokens[i].size; + + ++i; + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_texture_transform") == 0) + { + out_texture_view->has_transform = 1; + i = cgltf_parse_json_texture_transform(tokens, i + 1, json_chunk, &out_texture_view->transform); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_pbr_metallic_roughness(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_pbr_metallic_roughness* out_pbr) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "metallicFactor") == 0) + { + ++i; + out_pbr->metallic_factor = + cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "roughnessFactor") == 0) + { + ++i; + out_pbr->roughness_factor = + cgltf_json_to_float(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "baseColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_pbr->base_color_factor, 4); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "baseColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->base_color_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "metallicRoughnessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->metallic_roughness_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_pbr_specular_glossiness(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_pbr_specular_glossiness* out_pbr) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "diffuseFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_pbr->diffuse_factor, 4); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_pbr->specular_factor, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "glossinessFactor") == 0) + { + ++i; + out_pbr->glossiness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "diffuseTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->diffuse_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularGlossinessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->specular_glossiness_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_clearcoat(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_clearcoat* out_clearcoat) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatFactor") == 0) + { + ++i; + out_clearcoat->clearcoat_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatRoughnessFactor") == 0) + { + ++i; + out_clearcoat->clearcoat_roughness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_clearcoat->clearcoat_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatRoughnessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_clearcoat->clearcoat_roughness_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "clearcoatNormalTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_clearcoat->clearcoat_normal_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_ior(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_ior* out_ior) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Default values + out_ior->ior = 1.5f; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "ior") == 0) + { + ++i; + out_ior->ior = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_specular(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_specular* out_specular) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Default values + out_specular->specular_factor = 1.0f; + cgltf_fill_float_array(out_specular->specular_color_factor, 3, 1.0f); + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "specularFactor") == 0) + { + ++i; + out_specular->specular_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_specular->specular_color_factor, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_specular->specular_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "specularColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_specular->specular_color_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_transmission(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_transmission* out_transmission) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "transmissionFactor") == 0) + { + ++i; + out_transmission->transmission_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "transmissionTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_transmission->transmission_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_volume(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_volume* out_volume) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "thicknessFactor") == 0) + { + ++i; + out_volume->thickness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "thicknessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_volume->thickness_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "attenuationColor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_volume->attenuation_color, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "attenuationDistance") == 0) + { + ++i; + out_volume->attenuation_distance = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_sheen(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_sheen* out_sheen) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_sheen->sheen_color_factor, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_sheen->sheen_color_texture); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenRoughnessFactor") == 0) + { + ++i; + out_sheen->sheen_roughness_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "sheenRoughnessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_sheen->sheen_roughness_texture); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_emissive_strength(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_emissive_strength* out_emissive_strength) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Default + out_emissive_strength->emissive_strength = 1.f; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "emissiveStrength") == 0) + { + ++i; + out_emissive_strength->emissive_strength = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_iridescence(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_iridescence* out_iridescence) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Default + out_iridescence->iridescence_ior = 1.3f; + out_iridescence->iridescence_thickness_min = 100.f; + out_iridescence->iridescence_thickness_max = 400.f; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceFactor") == 0) + { + ++i; + out_iridescence->iridescence_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_iridescence->iridescence_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceIor") == 0) + { + ++i; + out_iridescence->iridescence_ior = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceThicknessMinimum") == 0) + { + ++i; + out_iridescence->iridescence_thickness_min = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceThicknessMaximum") == 0) + { + ++i; + out_iridescence->iridescence_thickness_max = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "iridescenceThicknessTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_iridescence->iridescence_thickness_texture); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_diffuse_transmission(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_diffuse_transmission* out_diff_transmission) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Defaults + cgltf_fill_float_array(out_diff_transmission->diffuse_transmission_color_factor, 3, 1.0f); + out_diff_transmission->diffuse_transmission_factor = 0.f; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionFactor") == 0) + { + ++i; + out_diff_transmission->diffuse_transmission_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_diff_transmission->diffuse_transmission_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_diff_transmission->diffuse_transmission_color_factor, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_diff_transmission->diffuse_transmission_color_texture); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_anisotropy(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_anisotropy* out_anisotropy) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "anisotropyStrength") == 0) + { + ++i; + out_anisotropy->anisotropy_strength = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "anisotropyRotation") == 0) + { + ++i; + out_anisotropy->anisotropy_rotation = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "anisotropyTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_anisotropy->anisotropy_texture); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_dispersion(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_dispersion* out_dispersion) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "dispersion") == 0) + { + ++i; + out_dispersion->dispersion = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_image(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_image* out_image) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "uri") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->uri); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0) + { + ++i; + out_image->buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "mimeType") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->mime_type); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_image->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_image->extensions_count, &out_image->extensions); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_sampler(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_sampler* out_sampler) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_sampler->wrap_s = cgltf_wrap_mode_repeat; + out_sampler->wrap_t = cgltf_wrap_mode_repeat; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_sampler->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "magFilter") == 0) + { + ++i; + out_sampler->mag_filter + = (cgltf_filter_type)cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "minFilter") == 0) + { + ++i; + out_sampler->min_filter + = (cgltf_filter_type)cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "wrapS") == 0) + { + ++i; + out_sampler->wrap_s + = (cgltf_wrap_mode)cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "wrapT") == 0) + { + ++i; + out_sampler->wrap_t + = (cgltf_wrap_mode)cgltf_json_to_int(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_sampler->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_sampler->extensions_count, &out_sampler->extensions); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_texture(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture* out_texture) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_texture->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "sampler") == 0) + { + ++i; + out_texture->sampler = CGLTF_PTRINDEX(cgltf_sampler, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0) + { + ++i; + out_texture->image = CGLTF_PTRINDEX(cgltf_image, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_texture->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if (out_texture->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + ++i; + out_texture->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + out_texture->extensions_count = 0; + + if (!out_texture->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_texture_basisu") == 0) + { + out_texture->has_basisu = 1; + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int num_properties = tokens[i].size; + ++i; + + for (int t = 0; t < num_properties; ++t) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0) + { + ++i; + out_texture->basisu_image = CGLTF_PTRINDEX(cgltf_image, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "EXT_texture_webp") == 0) + { + out_texture->has_webp = 1; + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int num_properties = tokens[i].size; + ++i; + + for (int t = 0; t < num_properties; ++t) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0) + { + ++i; + out_texture->webp_image = CGLTF_PTRINDEX(cgltf_image, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_texture->extensions[out_texture->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_material(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_material* out_material) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + cgltf_fill_float_array(out_material->pbr_metallic_roughness.base_color_factor, 4, 1.0f); + out_material->pbr_metallic_roughness.metallic_factor = 1.0f; + out_material->pbr_metallic_roughness.roughness_factor = 1.0f; + + cgltf_fill_float_array(out_material->pbr_specular_glossiness.diffuse_factor, 4, 1.0f); + cgltf_fill_float_array(out_material->pbr_specular_glossiness.specular_factor, 3, 1.0f); + out_material->pbr_specular_glossiness.glossiness_factor = 1.0f; + + cgltf_fill_float_array(out_material->volume.attenuation_color, 3, 1.0f); + out_material->volume.attenuation_distance = FLT_MAX; + + out_material->alpha_cutoff = 0.5f; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_material->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "pbrMetallicRoughness") == 0) + { + out_material->has_pbr_metallic_roughness = 1; + i = cgltf_parse_json_pbr_metallic_roughness(options, tokens, i + 1, json_chunk, &out_material->pbr_metallic_roughness); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "emissiveFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_material->emissive_factor, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "normalTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, + &out_material->normal_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "occlusionTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, + &out_material->occlusion_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "emissiveTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, + &out_material->emissive_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "alphaMode") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens + i, json_chunk, "OPAQUE") == 0) + { + out_material->alpha_mode = cgltf_alpha_mode_opaque; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "MASK") == 0) + { + out_material->alpha_mode = cgltf_alpha_mode_mask; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "BLEND") == 0) + { + out_material->alpha_mode = cgltf_alpha_mode_blend; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "alphaCutoff") == 0) + { + ++i; + out_material->alpha_cutoff = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "doubleSided") == 0) + { + ++i; + out_material->double_sided = + cgltf_json_to_bool(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_material->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_material->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + ++i; + out_material->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + out_material->extensions_count= 0; + + if (!out_material->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_pbrSpecularGlossiness") == 0) + { + out_material->has_pbr_specular_glossiness = 1; + i = cgltf_parse_json_pbr_specular_glossiness(options, tokens, i + 1, json_chunk, &out_material->pbr_specular_glossiness); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_unlit") == 0) + { + out_material->unlit = 1; + i = cgltf_skip_json(tokens, i+1); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_clearcoat") == 0) + { + out_material->has_clearcoat = 1; + i = cgltf_parse_json_clearcoat(options, tokens, i + 1, json_chunk, &out_material->clearcoat); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_ior") == 0) + { + out_material->has_ior = 1; + i = cgltf_parse_json_ior(tokens, i + 1, json_chunk, &out_material->ior); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_specular") == 0) + { + out_material->has_specular = 1; + i = cgltf_parse_json_specular(options, tokens, i + 1, json_chunk, &out_material->specular); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_transmission") == 0) + { + out_material->has_transmission = 1; + i = cgltf_parse_json_transmission(options, tokens, i + 1, json_chunk, &out_material->transmission); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_volume") == 0) + { + out_material->has_volume = 1; + i = cgltf_parse_json_volume(options, tokens, i + 1, json_chunk, &out_material->volume); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_sheen") == 0) + { + out_material->has_sheen = 1; + i = cgltf_parse_json_sheen(options, tokens, i + 1, json_chunk, &out_material->sheen); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_emissive_strength") == 0) + { + out_material->has_emissive_strength = 1; + i = cgltf_parse_json_emissive_strength(tokens, i + 1, json_chunk, &out_material->emissive_strength); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_iridescence") == 0) + { + out_material->has_iridescence = 1; + i = cgltf_parse_json_iridescence(options, tokens, i + 1, json_chunk, &out_material->iridescence); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_diffuse_transmission") == 0) + { + out_material->has_diffuse_transmission = 1; + i = cgltf_parse_json_diffuse_transmission(options, tokens, i + 1, json_chunk, &out_material->diffuse_transmission); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_anisotropy") == 0) + { + out_material->has_anisotropy = 1; + i = cgltf_parse_json_anisotropy(options, tokens, i + 1, json_chunk, &out_material->anisotropy); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_dispersion") == 0) + { + out_material->has_dispersion = 1; + i = cgltf_parse_json_dispersion(tokens, i + 1, json_chunk, &out_material->dispersion); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_material->extensions[out_material->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_accessors(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_accessor), (void**)&out_data->accessors, &out_data->accessors_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->accessors_count; ++j) + { + i = cgltf_parse_json_accessor(options, tokens, i, json_chunk, &out_data->accessors[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_materials(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_material), (void**)&out_data->materials, &out_data->materials_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->materials_count; ++j) + { + i = cgltf_parse_json_material(options, tokens, i, json_chunk, &out_data->materials[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_images(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_image), (void**)&out_data->images, &out_data->images_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->images_count; ++j) + { + i = cgltf_parse_json_image(options, tokens, i, json_chunk, &out_data->images[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_textures(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_texture), (void**)&out_data->textures, &out_data->textures_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->textures_count; ++j) + { + i = cgltf_parse_json_texture(options, tokens, i, json_chunk, &out_data->textures[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_samplers(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_sampler), (void**)&out_data->samplers, &out_data->samplers_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->samplers_count; ++j) + { + i = cgltf_parse_json_sampler(options, tokens, i, json_chunk, &out_data->samplers[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_meshopt_compression(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_meshopt_compression* out_meshopt_compression) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "buffer") == 0) + { + ++i; + out_meshopt_compression->buffer = CGLTF_PTRINDEX(cgltf_buffer, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_meshopt_compression->offset = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteLength") == 0) + { + ++i; + out_meshopt_compression->size = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteStride") == 0) + { + ++i; + out_meshopt_compression->stride = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0) + { + ++i; + out_meshopt_compression->count = cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "mode") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "ATTRIBUTES") == 0) + { + out_meshopt_compression->mode = cgltf_meshopt_compression_mode_attributes; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "TRIANGLES") == 0) + { + out_meshopt_compression->mode = cgltf_meshopt_compression_mode_triangles; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "INDICES") == 0) + { + out_meshopt_compression->mode = cgltf_meshopt_compression_mode_indices; + } + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "filter") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "NONE") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_none; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "OCTAHEDRAL") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_octahedral; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "QUATERNION") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_quaternion; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "EXPONENTIAL") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_exponential; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "COLOR") == 0) + { + out_meshopt_compression->filter = cgltf_meshopt_compression_filter_color; + } + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_buffer_view(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_buffer_view* out_buffer_view) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_buffer_view->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "buffer") == 0) + { + ++i; + out_buffer_view->buffer = CGLTF_PTRINDEX(cgltf_buffer, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0) + { + ++i; + out_buffer_view->offset = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteLength") == 0) + { + ++i; + out_buffer_view->size = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteStride") == 0) + { + ++i; + out_buffer_view->stride = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "target") == 0) + { + ++i; + int type = cgltf_json_to_int(tokens+i, json_chunk); + switch (type) + { + case 34962: + type = cgltf_buffer_view_type_vertices; + break; + case 34963: + type = cgltf_buffer_view_type_indices; + break; + default: + type = cgltf_buffer_view_type_invalid; + break; + } + out_buffer_view->type = (cgltf_buffer_view_type)type; + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_buffer_view->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_buffer_view->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_buffer_view->extensions_count = 0; + out_buffer_view->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_buffer_view->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "EXT_meshopt_compression") == 0) + { + out_buffer_view->has_meshopt_compression = 1; + i = cgltf_parse_json_meshopt_compression(options, tokens, i + 1, json_chunk, &out_buffer_view->meshopt_compression); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_meshopt_compression") == 0) + { + out_buffer_view->has_meshopt_compression = 1; + out_buffer_view->meshopt_compression.is_khr = 1; + i = cgltf_parse_json_meshopt_compression(options, tokens, i + 1, json_chunk, &out_buffer_view->meshopt_compression); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_buffer_view->extensions[out_buffer_view->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_buffer_views(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_buffer_view), (void**)&out_data->buffer_views, &out_data->buffer_views_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->buffer_views_count; ++j) + { + i = cgltf_parse_json_buffer_view(options, tokens, i, json_chunk, &out_data->buffer_views[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_buffer(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_buffer* out_buffer) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_buffer->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteLength") == 0) + { + ++i; + out_buffer->size = + cgltf_json_to_size(tokens+i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "uri") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_buffer->uri); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_buffer->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_buffer->extensions_count, &out_buffer->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_buffers(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_buffer), (void**)&out_data->buffers, &out_data->buffers_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->buffers_count; ++j) + { + i = cgltf_parse_json_buffer(options, tokens, i, json_chunk, &out_data->buffers[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_skin(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_skin* out_skin) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_skin->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "joints") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_node*), (void**)&out_skin->joints, &out_skin->joints_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_skin->joints_count; ++k) + { + out_skin->joints[k] = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "skeleton") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_skin->skeleton = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "inverseBindMatrices") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_skin->inverse_bind_matrices = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_skin->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_skin->extensions_count, &out_skin->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_skins(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_skin), (void**)&out_data->skins, &out_data->skins_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->skins_count; ++j) + { + i = cgltf_parse_json_skin(options, tokens, i, json_chunk, &out_data->skins[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_camera(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_camera* out_camera) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_camera->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "perspective") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + if (out_camera->type != cgltf_camera_type_invalid) + { + return CGLTF_ERROR_JSON; + } + + out_camera->type = cgltf_camera_type_perspective; + + for (int k = 0; k < data_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "aspectRatio") == 0) + { + ++i; + out_camera->data.perspective.has_aspect_ratio = 1; + out_camera->data.perspective.aspect_ratio = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "yfov") == 0) + { + ++i; + out_camera->data.perspective.yfov = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "zfar") == 0) + { + ++i; + out_camera->data.perspective.has_zfar = 1; + out_camera->data.perspective.zfar = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "znear") == 0) + { + ++i; + out_camera->data.perspective.znear = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_camera->data.perspective.extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "orthographic") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + if (out_camera->type != cgltf_camera_type_invalid) + { + return CGLTF_ERROR_JSON; + } + + out_camera->type = cgltf_camera_type_orthographic; + + for (int k = 0; k < data_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "xmag") == 0) + { + ++i; + out_camera->data.orthographic.xmag = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "ymag") == 0) + { + ++i; + out_camera->data.orthographic.ymag = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "zfar") == 0) + { + ++i; + out_camera->data.orthographic.zfar = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "znear") == 0) + { + ++i; + out_camera->data.orthographic.znear = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_camera->data.orthographic.extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_camera->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_camera->extensions_count, &out_camera->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_cameras(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_camera), (void**)&out_data->cameras, &out_data->cameras_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->cameras_count; ++j) + { + i = cgltf_parse_json_camera(options, tokens, i, json_chunk, &out_data->cameras[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_light(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_light* out_light) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_light->color[0] = 1.f; + out_light->color[1] = 1.f; + out_light->color[2] = 1.f; + out_light->intensity = 1.f; + + out_light->spot_inner_cone_angle = 0.f; + out_light->spot_outer_cone_angle = 3.1415926535f / 4.0f; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_light->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "color") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_light->color, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "intensity") == 0) + { + ++i; + out_light->intensity = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "type") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens + i, json_chunk, "directional") == 0) + { + out_light->type = cgltf_light_type_directional; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "point") == 0) + { + out_light->type = cgltf_light_type_point; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "spot") == 0) + { + out_light->type = cgltf_light_type_spot; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "range") == 0) + { + ++i; + out_light->range = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "spot") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int k = 0; k < data_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "innerConeAngle") == 0) + { + ++i; + out_light->spot_inner_cone_angle = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "outerConeAngle") == 0) + { + ++i; + out_light->spot_outer_cone_angle = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_light->extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_lights(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_light), (void**)&out_data->lights, &out_data->lights_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->lights_count; ++j) + { + i = cgltf_parse_json_light(options, tokens, i, json_chunk, &out_data->lights[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_node(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_node* out_node) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + out_node->rotation[3] = 1.0f; + out_node->scale[0] = 1.0f; + out_node->scale[1] = 1.0f; + out_node->scale[2] = 1.0f; + out_node->matrix[0] = 1.0f; + out_node->matrix[5] = 1.0f; + out_node->matrix[10] = 1.0f; + out_node->matrix[15] = 1.0f; + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_node->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "children") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_node*), (void**)&out_node->children, &out_node->children_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_node->children_count; ++k) + { + out_node->children[k] = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "mesh") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->mesh = CGLTF_PTRINDEX(cgltf_mesh, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "skin") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->skin = CGLTF_PTRINDEX(cgltf_skin, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "camera") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->camera = CGLTF_PTRINDEX(cgltf_camera, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "translation") == 0) + { + out_node->has_translation = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->translation, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "rotation") == 0) + { + out_node->has_rotation = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->rotation, 4); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "scale") == 0) + { + out_node->has_scale = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->scale, 3); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "matrix") == 0) + { + out_node->has_matrix = 1; + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->matrix, 16); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "weights") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_float), (void**)&out_node->weights, &out_node->weights_count); + if (i < 0) + { + return i; + } + + i = cgltf_parse_json_float_array(tokens, i - 1, json_chunk, out_node->weights, (int)out_node->weights_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_node->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_node->extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_node->extensions_count= 0; + out_node->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_node->extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_lights_punctual") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int m = 0; m < data_size; ++m) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "light") == 0) + { + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE); + out_node->light = CGLTF_PTRINDEX(cgltf_light, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "EXT_mesh_gpu_instancing") == 0) + { + out_node->has_mesh_gpu_instancing = 1; + i = cgltf_parse_json_mesh_gpu_instancing(options, tokens, i + 1, json_chunk, &out_node->mesh_gpu_instancing); + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_node->extensions[out_node->extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_nodes(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_node), (void**)&out_data->nodes, &out_data->nodes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->nodes_count; ++j) + { + i = cgltf_parse_json_node(options, tokens, i, json_chunk, &out_data->nodes[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_scene(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_scene* out_scene) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_scene->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "nodes") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_node*), (void**)&out_scene->nodes, &out_scene->nodes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_scene->nodes_count; ++k) + { + out_scene->nodes[k] = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_scene->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_scene->extensions_count, &out_scene->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_scenes(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_scene), (void**)&out_data->scenes, &out_data->scenes_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->scenes_count; ++j) + { + i = cgltf_parse_json_scene(options, tokens, i, json_chunk, &out_data->scenes[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_animation_sampler(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_animation_sampler* out_sampler) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "input") == 0) + { + ++i; + out_sampler->input = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "output") == 0) + { + ++i; + out_sampler->output = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "interpolation") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens + i, json_chunk, "LINEAR") == 0) + { + out_sampler->interpolation = cgltf_interpolation_type_linear; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "STEP") == 0) + { + out_sampler->interpolation = cgltf_interpolation_type_step; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "CUBICSPLINE") == 0) + { + out_sampler->interpolation = cgltf_interpolation_type_cubic_spline; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_sampler->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_sampler->extensions_count, &out_sampler->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_animation_channel(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_animation_channel* out_channel) +{ + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "sampler") == 0) + { + ++i; + out_channel->sampler = CGLTF_PTRINDEX(cgltf_animation_sampler, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "target") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int target_size = tokens[i].size; + ++i; + + for (int k = 0; k < target_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "node") == 0) + { + ++i; + out_channel->target_node = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "path") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens+i, json_chunk, "translation") == 0) + { + out_channel->target_path = cgltf_animation_path_type_translation; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "rotation") == 0) + { + out_channel->target_path = cgltf_animation_path_type_rotation; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "scale") == 0) + { + out_channel->target_path = cgltf_animation_path_type_scale; + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "weights") == 0) + { + out_channel->target_path = cgltf_animation_path_type_weights; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_channel->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_channel->extensions_count, &out_channel->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_animation(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_animation* out_animation) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_animation->name); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "samplers") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_animation_sampler), (void**)&out_animation->samplers, &out_animation->samplers_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_animation->samplers_count; ++k) + { + i = cgltf_parse_json_animation_sampler(options, tokens, i, json_chunk, &out_animation->samplers[k]); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "channels") == 0) + { + i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_animation_channel), (void**)&out_animation->channels, &out_animation->channels_count); + if (i < 0) + { + return i; + } + + for (cgltf_size k = 0; k < out_animation->channels_count; ++k) + { + i = cgltf_parse_json_animation_channel(options, tokens, i, json_chunk, &out_animation->channels[k]); + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_animation->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_animation->extensions_count, &out_animation->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_animations(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_animation), (void**)&out_data->animations, &out_data->animations_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->animations_count; ++j) + { + i = cgltf_parse_json_animation(options, tokens, i, json_chunk, &out_data->animations[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_variant(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_material_variant* out_variant) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_variant->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_variant->extras); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_variants(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_material_variant), (void**)&out_data->variants, &out_data->variants_count); + if (i < 0) + { + return i; + } + + for (cgltf_size j = 0; j < out_data->variants_count; ++j) + { + i = cgltf_parse_json_variant(options, tokens, i, json_chunk, &out_data->variants[j]); + if (i < 0) + { + return i; + } + } + return i; +} + +static int cgltf_parse_json_asset(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_asset* out_asset) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "copyright") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->copyright); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "generator") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->generator); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "version") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->version); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "minVersion") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->min_version); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_asset->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_asset->extensions_count, &out_asset->extensions); + } + else + { + i = cgltf_skip_json(tokens, i+1); + } + + if (i < 0) + { + return i; + } + } + + if (out_asset->version && CGLTF_ATOF(out_asset->version) < 2) + { + return CGLTF_ERROR_LEGACY; + } + + return i; +} + +cgltf_size cgltf_num_components(cgltf_type type) { + switch (type) + { + case cgltf_type_vec2: + return 2; + case cgltf_type_vec3: + return 3; + case cgltf_type_vec4: + return 4; + case cgltf_type_mat2: + return 4; + case cgltf_type_mat3: + return 9; + case cgltf_type_mat4: + return 16; + case cgltf_type_invalid: + case cgltf_type_scalar: + default: + return 1; + } +} + +cgltf_size cgltf_component_size(cgltf_component_type component_type) { + switch (component_type) + { + case cgltf_component_type_r_8: + case cgltf_component_type_r_8u: + return 1; + case cgltf_component_type_r_16: + case cgltf_component_type_r_16u: + return 2; + case cgltf_component_type_r_32u: + case cgltf_component_type_r_32f: + return 4; + case cgltf_component_type_invalid: + default: + return 0; + } +} + +cgltf_size cgltf_calc_size(cgltf_type type, cgltf_component_type component_type) +{ + cgltf_size component_size = cgltf_component_size(component_type); + if (type == cgltf_type_mat2 && component_size == 1) + { + return 8 * component_size; + } + else if (type == cgltf_type_mat3 && (component_size == 1 || component_size == 2)) + { + return 12 * component_size; + } + return component_size * cgltf_num_components(type); +} + +static int cgltf_fixup_pointers(cgltf_data* out_data); + +static int cgltf_parse_json_root(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int size = tokens[i].size; + ++i; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "asset") == 0) + { + i = cgltf_parse_json_asset(options, tokens, i + 1, json_chunk, &out_data->asset); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "meshes") == 0) + { + i = cgltf_parse_json_meshes(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "accessors") == 0) + { + i = cgltf_parse_json_accessors(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "bufferViews") == 0) + { + i = cgltf_parse_json_buffer_views(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "buffers") == 0) + { + i = cgltf_parse_json_buffers(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "materials") == 0) + { + i = cgltf_parse_json_materials(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "images") == 0) + { + i = cgltf_parse_json_images(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "textures") == 0) + { + i = cgltf_parse_json_textures(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "samplers") == 0) + { + i = cgltf_parse_json_samplers(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "skins") == 0) + { + i = cgltf_parse_json_skins(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "cameras") == 0) + { + i = cgltf_parse_json_cameras(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "nodes") == 0) + { + i = cgltf_parse_json_nodes(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scenes") == 0) + { + i = cgltf_parse_json_scenes(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scene") == 0) + { + ++i; + out_data->scene = CGLTF_PTRINDEX(cgltf_scene, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "animations") == 0) + { + i = cgltf_parse_json_animations(options, tokens, i + 1, json_chunk, out_data); + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "extras") == 0) + { + i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_data->extras); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + if(out_data->data_extensions) + { + return CGLTF_ERROR_JSON; + } + + int extensions_size = tokens[i].size; + out_data->data_extensions_count = 0; + out_data->data_extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); + + if (!out_data->data_extensions) + { + return CGLTF_ERROR_NOMEM; + } + + ++i; + + for (int k = 0; k < extensions_size; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_lights_punctual") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int m = 0; m < data_size; ++m) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "lights") == 0) + { + i = cgltf_parse_json_lights(options, tokens, i + 1, json_chunk, out_data); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_variants") == 0) + { + ++i; + + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + + int data_size = tokens[i].size; + ++i; + + for (int m = 0; m < data_size; ++m) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "variants") == 0) + { + i = cgltf_parse_json_variants(options, tokens, i + 1, json_chunk, out_data); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + } + else + { + i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_data->data_extensions[out_data->data_extensions_count++])); + } + + if (i < 0) + { + return i; + } + } + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensionsUsed") == 0) + { + i = cgltf_parse_json_string_array(options, tokens, i + 1, json_chunk, &out_data->extensions_used, &out_data->extensions_used_count); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensionsRequired") == 0) + { + i = cgltf_parse_json_string_array(options, tokens, i + 1, json_chunk, &out_data->extensions_required, &out_data->extensions_required_count); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +cgltf_result cgltf_parse_json(cgltf_options* options, const uint8_t* json_chunk, cgltf_size size, cgltf_data** out_data) +{ + jsmn_parser parser = { 0, 0, 0 }; + + if (options->json_token_count == 0) + { + int token_count = jsmn_parse(&parser, (const char*)json_chunk, size, NULL, 0); + + if (token_count <= 0) + { + return cgltf_result_invalid_json; + } + + options->json_token_count = token_count; + } + + jsmntok_t* tokens = (jsmntok_t*)options->memory.alloc_func(options->memory.user_data, sizeof(jsmntok_t) * (options->json_token_count + 1)); + + if (!tokens) + { + return cgltf_result_out_of_memory; + } + + jsmn_init(&parser); + + int token_count = jsmn_parse(&parser, (const char*)json_chunk, size, tokens, options->json_token_count); + + if (token_count <= 0) + { + options->memory.free_func(options->memory.user_data, tokens); + return cgltf_result_invalid_json; + } + + // this makes sure that we always have an UNDEFINED token at the end of the stream + // for invalid JSON inputs this makes sure we don't perform out of bound reads of token data + tokens[token_count].type = JSMN_UNDEFINED; + + cgltf_data* data = (cgltf_data*)options->memory.alloc_func(options->memory.user_data, sizeof(cgltf_data)); + + if (!data) + { + options->memory.free_func(options->memory.user_data, tokens); + return cgltf_result_out_of_memory; + } + + memset(data, 0, sizeof(cgltf_data)); + data->memory = options->memory; + data->file = options->file; + + int i = cgltf_parse_json_root(options, tokens, 0, json_chunk, data); + + options->memory.free_func(options->memory.user_data, tokens); + + if (i < 0) + { + cgltf_free(data); + + switch (i) + { + case CGLTF_ERROR_NOMEM: return cgltf_result_out_of_memory; + case CGLTF_ERROR_LEGACY: return cgltf_result_legacy_gltf; + default: return cgltf_result_invalid_gltf; + } + } + + if (cgltf_fixup_pointers(data) < 0) + { + cgltf_free(data); + return cgltf_result_invalid_gltf; + } + + data->json = (const char*)json_chunk; + data->json_size = size; + + *out_data = data; + + return cgltf_result_success; +} + +static int cgltf_fixup_pointers(cgltf_data* data) +{ + for (cgltf_size i = 0; i < data->meshes_count; ++i) + { + for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j) + { + CGLTF_PTRFIXUP(data->meshes[i].primitives[j].indices, data->accessors, data->accessors_count); + CGLTF_PTRFIXUP(data->meshes[i].primitives[j].material, data->materials, data->materials_count); + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].attributes[k].data, data->accessors, data->accessors_count); + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k) + { + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].targets[k].attributes[m].data, data->accessors, data->accessors_count); + } + } + + if (data->meshes[i].primitives[j].has_draco_mesh_compression) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].draco_mesh_compression.buffer_view, data->buffer_views, data->buffer_views_count); + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].draco_mesh_compression.attributes_count; ++m) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].draco_mesh_compression.attributes[m].data, data->accessors, data->accessors_count); + } + } + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].mappings_count; ++k) + { + CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].mappings[k].material, data->materials, data->materials_count); + } + } + } + + for (cgltf_size i = 0; i < data->accessors_count; ++i) + { + CGLTF_PTRFIXUP(data->accessors[i].buffer_view, data->buffer_views, data->buffer_views_count); + + if (data->accessors[i].is_sparse) + { + CGLTF_PTRFIXUP_REQ(data->accessors[i].sparse.indices_buffer_view, data->buffer_views, data->buffer_views_count); + CGLTF_PTRFIXUP_REQ(data->accessors[i].sparse.values_buffer_view, data->buffer_views, data->buffer_views_count); + } + + if (data->accessors[i].buffer_view) + { + data->accessors[i].stride = data->accessors[i].buffer_view->stride; + } + + if (data->accessors[i].stride == 0) + { + data->accessors[i].stride = cgltf_calc_size(data->accessors[i].type, data->accessors[i].component_type); + } + } + + for (cgltf_size i = 0; i < data->textures_count; ++i) + { + CGLTF_PTRFIXUP(data->textures[i].image, data->images, data->images_count); + CGLTF_PTRFIXUP(data->textures[i].basisu_image, data->images, data->images_count); + CGLTF_PTRFIXUP(data->textures[i].webp_image, data->images, data->images_count); + CGLTF_PTRFIXUP(data->textures[i].sampler, data->samplers, data->samplers_count); + } + + for (cgltf_size i = 0; i < data->images_count; ++i) + { + CGLTF_PTRFIXUP(data->images[i].buffer_view, data->buffer_views, data->buffer_views_count); + } + + for (cgltf_size i = 0; i < data->materials_count; ++i) + { + CGLTF_PTRFIXUP(data->materials[i].normal_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].emissive_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].occlusion_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].pbr_metallic_roughness.base_color_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].pbr_metallic_roughness.metallic_roughness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].pbr_specular_glossiness.diffuse_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].pbr_specular_glossiness.specular_glossiness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].clearcoat.clearcoat_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].clearcoat.clearcoat_roughness_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].clearcoat.clearcoat_normal_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].specular.specular_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].specular.specular_color_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].transmission.transmission_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].volume.thickness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].sheen.sheen_color_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].sheen.sheen_roughness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].iridescence.iridescence_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].iridescence.iridescence_thickness_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].diffuse_transmission.diffuse_transmission_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].diffuse_transmission.diffuse_transmission_color_texture.texture, data->textures, data->textures_count); + + CGLTF_PTRFIXUP(data->materials[i].anisotropy.anisotropy_texture.texture, data->textures, data->textures_count); + } + + for (cgltf_size i = 0; i < data->buffer_views_count; ++i) + { + CGLTF_PTRFIXUP_REQ(data->buffer_views[i].buffer, data->buffers, data->buffers_count); + + if (data->buffer_views[i].has_meshopt_compression) + { + CGLTF_PTRFIXUP_REQ(data->buffer_views[i].meshopt_compression.buffer, data->buffers, data->buffers_count); + } + } + + for (cgltf_size i = 0; i < data->skins_count; ++i) + { + for (cgltf_size j = 0; j < data->skins[i].joints_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->skins[i].joints[j], data->nodes, data->nodes_count); + } + + CGLTF_PTRFIXUP(data->skins[i].skeleton, data->nodes, data->nodes_count); + CGLTF_PTRFIXUP(data->skins[i].inverse_bind_matrices, data->accessors, data->accessors_count); + } + + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + for (cgltf_size j = 0; j < data->nodes[i].children_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->nodes[i].children[j], data->nodes, data->nodes_count); + + if (data->nodes[i].children[j]->parent) + { + return CGLTF_ERROR_JSON; + } + + data->nodes[i].children[j]->parent = &data->nodes[i]; + } + + CGLTF_PTRFIXUP(data->nodes[i].mesh, data->meshes, data->meshes_count); + CGLTF_PTRFIXUP(data->nodes[i].skin, data->skins, data->skins_count); + CGLTF_PTRFIXUP(data->nodes[i].camera, data->cameras, data->cameras_count); + CGLTF_PTRFIXUP(data->nodes[i].light, data->lights, data->lights_count); + + if (data->nodes[i].has_mesh_gpu_instancing) + { + for (cgltf_size m = 0; m < data->nodes[i].mesh_gpu_instancing.attributes_count; ++m) + { + CGLTF_PTRFIXUP_REQ(data->nodes[i].mesh_gpu_instancing.attributes[m].data, data->accessors, data->accessors_count); + } + } + } + + for (cgltf_size i = 0; i < data->scenes_count; ++i) + { + for (cgltf_size j = 0; j < data->scenes[i].nodes_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->scenes[i].nodes[j], data->nodes, data->nodes_count); + + if (data->scenes[i].nodes[j]->parent) + { + return CGLTF_ERROR_JSON; + } + } + } + + CGLTF_PTRFIXUP(data->scene, data->scenes, data->scenes_count); + + for (cgltf_size i = 0; i < data->animations_count; ++i) + { + for (cgltf_size j = 0; j < data->animations[i].samplers_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->animations[i].samplers[j].input, data->accessors, data->accessors_count); + CGLTF_PTRFIXUP_REQ(data->animations[i].samplers[j].output, data->accessors, data->accessors_count); + } + + for (cgltf_size j = 0; j < data->animations[i].channels_count; ++j) + { + CGLTF_PTRFIXUP_REQ(data->animations[i].channels[j].sampler, data->animations[i].samplers, data->animations[i].samplers_count); + CGLTF_PTRFIXUP(data->animations[i].channels[j].target_node, data->nodes, data->nodes_count); + } + } + + return 0; +} + +/* + * -- jsmn.c start -- + * Source: https://github.com/zserge/jsmn + * License: MIT + * + * Copyright (c) 2010 Serge A. Zaitsev + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Allocates a fresh unused token from the token pull. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, + jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type, + ptrdiff_t start, ptrdiff_t end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + ptrdiff_t start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t' : case '\r' : case '\n' : case ' ' : + case ',' : case ']' : case '}' : + goto found; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + if (tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return 0; +} + +/** + * Fills next token with JSON string. + */ +static int jsmn_parse_string(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + + ptrdiff_t start = parser->pos; + + parser->pos++; + + /* Skip starting quote */ + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + if (tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': case '/' : case '\\' : case 'b' : + case 'f' : case 'r' : case 'n' : case 't' : + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { + /* If it isn't a hex character we have an error */ + if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +static int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, + jsmntok_t *tokens, size_t num_tokens) { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': case '[': + count++; + if (tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) + return JSMN_ERROR_NOMEM; + if (parser->toksuper != -1) { + tokens[parser->toksuper].size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': case ']': + if (tokens == NULL) + break; + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if (parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) { + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) { + if(token->type != type || parser->toksuper == -1) { + return JSMN_ERROR_INVAL; + } + break; + } + token = &tokens[token->parent]; + } +#else + for (i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) return JSMN_ERROR_INVAL; + for (; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + case '\t' : case '\r' : case '\n' : case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { +#ifdef JSMN_PARENT_LINKS + parser->toksuper = tokens[parser->toksuper].parent; +#else + for (i = parser->toknext - 1; i >= 0; i--) { + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + if (tokens[i].start != -1 && tokens[i].end == -1) { + parser->toksuper = i; + break; + } + } + } +#endif + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': case '0': case '1' : case '2': case '3' : case '4': + case '5': case '6': case '7' : case '8': case '9': + case 't': case 'f': case 'n' : + /* And they must not be keys of the object */ + if (tokens != NULL && parser->toksuper != -1) { + jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) { + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +static void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} +/* + * -- jsmn.c end -- + */ + +#endif /* #ifdef CGLTF_IMPLEMENTATION */ + +/* cgltf is distributed under MIT license: + * + * Copyright (c) 2018-2021 Johannes Kuhlmann + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ diff --git a/Lib/meshoptimizer/extern/fast_obj.h b/Lib/meshoptimizer/extern/fast_obj.h new file mode 100644 index 0000000..7967756 --- /dev/null +++ b/Lib/meshoptimizer/extern/fast_obj.h @@ -0,0 +1,1611 @@ +/* + * fast_obj + * + * Version 1.3 + * + * MIT License + * + * Copyright (c) 2018-2021 Richard Knight + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#ifndef FAST_OBJ_HDR +#define FAST_OBJ_HDR + +#define FAST_OBJ_VERSION_MAJOR 1 +#define FAST_OBJ_VERSION_MINOR 3 +#define FAST_OBJ_VERSION ((FAST_OBJ_VERSION_MAJOR << 8) | FAST_OBJ_VERSION_MINOR) + +#include + + +typedef struct +{ + /* Texture name from .mtl file */ + char* name; + + /* Resolved path to texture */ + char* path; + +} fastObjTexture; + + +typedef struct +{ + /* Material name */ + char* name; + + /* Parameters */ + float Ka[3]; /* Ambient */ + float Kd[3]; /* Diffuse */ + float Ks[3]; /* Specular */ + float Ke[3]; /* Emission */ + float Kt[3]; /* Transmittance */ + float Ns; /* Shininess */ + float Ni; /* Index of refraction */ + float Tf[3]; /* Transmission filter */ + float d; /* Disolve (alpha) */ + int illum; /* Illumination model */ + + /* Set for materials that don't come from the associated mtllib */ + int fallback; + + /* Texture map indices in fastObjMesh textures array */ + unsigned int map_Ka; + unsigned int map_Kd; + unsigned int map_Ks; + unsigned int map_Ke; + unsigned int map_Kt; + unsigned int map_Ns; + unsigned int map_Ni; + unsigned int map_d; + unsigned int map_bump; + +} fastObjMaterial; + +/* Allows user override to bigger indexable array */ +#ifndef FAST_OBJ_UINT_TYPE +#define FAST_OBJ_UINT_TYPE unsigned int +#endif + +typedef FAST_OBJ_UINT_TYPE fastObjUInt; + +typedef struct +{ + fastObjUInt p; + fastObjUInt t; + fastObjUInt n; + +} fastObjIndex; + + +typedef struct +{ + /* Group name */ + char* name; + + /* Number of faces */ + unsigned int face_count; + + /* First face in fastObjMesh face_* arrays */ + unsigned int face_offset; + + /* First index in fastObjMesh indices array */ + unsigned int index_offset; + +} fastObjGroup; + + +/* Note: a dummy zero-initialized value is added to the first index + of the positions, texcoords, normals and textures arrays. Hence, + valid indices into these arrays start from 1, with an index of 0 + indicating that the attribute is not present. */ +typedef struct +{ + /* Vertex data */ + unsigned int position_count; + float* positions; + + unsigned int texcoord_count; + float* texcoords; + + unsigned int normal_count; + float* normals; + + unsigned int color_count; + float* colors; + + /* Face data: one element for each face */ + unsigned int face_count; + unsigned int* face_vertices; + unsigned int* face_materials; + unsigned char* face_lines; + + /* Index data: one element for each face vertex */ + unsigned int index_count; + fastObjIndex* indices; + + /* Materials */ + unsigned int material_count; + fastObjMaterial* materials; + + /* Texture maps */ + unsigned int texture_count; + fastObjTexture* textures; + + /* Mesh objects ('o' tag in .obj file) */ + unsigned int object_count; + fastObjGroup* objects; + + /* Mesh groups ('g' tag in .obj file) */ + unsigned int group_count; + fastObjGroup* groups; + +} fastObjMesh; + +typedef struct +{ + void* (*file_open)(const char* path, void* user_data); + void (*file_close)(void* file, void* user_data); + size_t (*file_read)(void* file, void* dst, size_t bytes, void* user_data); + unsigned long (*file_size)(void* file, void* user_data); +} fastObjCallbacks; + +#ifdef __cplusplus +extern "C" { +#endif + +fastObjMesh* fast_obj_read(const char* path); +fastObjMesh* fast_obj_read_with_callbacks(const char* path, const fastObjCallbacks* callbacks, void* user_data); +void fast_obj_destroy(fastObjMesh* mesh); + +#ifdef __cplusplus +} +#endif + +#endif + + +#ifdef FAST_OBJ_IMPLEMENTATION + +#include +#include + +#ifndef FAST_OBJ_REALLOC +#define FAST_OBJ_REALLOC realloc +#endif + +#ifndef FAST_OBJ_FREE +#define FAST_OBJ_FREE free +#endif + +#ifdef _WIN32 +#define FAST_OBJ_SEPARATOR '\\' +#define FAST_OBJ_OTHER_SEP '/' +#else +#define FAST_OBJ_SEPARATOR '/' +#define FAST_OBJ_OTHER_SEP '\\' +#endif + + +/* Size of buffer to read into */ +#define BUFFER_SIZE 65536 + +/* Max supported power when parsing float */ +#define MAX_POWER 20 + +typedef struct +{ + /* Final mesh */ + fastObjMesh* mesh; + + /* Current object/group */ + fastObjGroup object; + fastObjGroup group; + + /* Current material index */ + unsigned int material; + + /* Current line in file */ + unsigned int line; + + /* Base path for materials/textures */ + char* base; + +} fastObjData; + + +static const +double POWER_10_POS[MAX_POWER] = +{ + 1.0e0, 1.0e1, 1.0e2, 1.0e3, 1.0e4, 1.0e5, 1.0e6, 1.0e7, 1.0e8, 1.0e9, + 1.0e10, 1.0e11, 1.0e12, 1.0e13, 1.0e14, 1.0e15, 1.0e16, 1.0e17, 1.0e18, 1.0e19, +}; + +static const +double POWER_10_NEG[MAX_POWER] = +{ + 1.0e0, 1.0e-1, 1.0e-2, 1.0e-3, 1.0e-4, 1.0e-5, 1.0e-6, 1.0e-7, 1.0e-8, 1.0e-9, + 1.0e-10, 1.0e-11, 1.0e-12, 1.0e-13, 1.0e-14, 1.0e-15, 1.0e-16, 1.0e-17, 1.0e-18, 1.0e-19, +}; + + +static void* memory_realloc(void* ptr, size_t bytes) +{ + return FAST_OBJ_REALLOC(ptr, bytes); +} + + +static +void memory_dealloc(void* ptr) +{ + FAST_OBJ_FREE(ptr); +} + + +#define array_clean(_arr) ((_arr) ? memory_dealloc(_array_header(_arr)), 0 : 0) +#define array_push(_arr, _val) (_array_mgrow(_arr, 1) ? ((_arr)[_array_size(_arr)++] = (_val), _array_size(_arr) - 1) : 0) +#define array_size(_arr) ((_arr) ? _array_size(_arr) : 0) +#define array_capacity(_arr) ((_arr) ? _array_capacity(_arr) : 0) +#define array_empty(_arr) (array_size(_arr) == 0) + +#define _array_header(_arr) ((fastObjUInt*)(_arr)-2) +#define _array_size(_arr) (_array_header(_arr)[0]) +#define _array_capacity(_arr) (_array_header(_arr)[1]) +#define _array_ngrow(_arr, _n) ((_arr) == 0 || (_array_size(_arr) + (_n) >= _array_capacity(_arr))) +#define _array_mgrow(_arr, _n) (_array_ngrow(_arr, _n) ? (_array_grow(_arr, _n) != 0) : 1) +#define _array_grow(_arr, _n) (*((void**)&(_arr)) = array_realloc(_arr, _n, sizeof(*(_arr)))) + + +static void* array_realloc(void* ptr, fastObjUInt n, fastObjUInt b) +{ + fastObjUInt sz = array_size(ptr); + fastObjUInt nsz = sz + n; + fastObjUInt cap = array_capacity(ptr); + fastObjUInt ncap = cap + cap / 2; + fastObjUInt* r; + + if (ncap < nsz) + ncap = nsz; + ncap = (ncap + 15) & ~15u; + + r = (fastObjUInt*)(memory_realloc(ptr ? _array_header(ptr) : 0, (size_t)b * ncap + 2 * sizeof(fastObjUInt))); + if (!r) + return 0; + + r[0] = sz; + r[1] = ncap; + + return (r + 2); +} + + +static +void* file_open(const char* path, void* user_data) +{ + (void)(user_data); + return fopen(path, "rb"); +} + + +static +void file_close(void* file, void* user_data) +{ + FILE* f; + (void)(user_data); + + f = (FILE*)(file); + fclose(f); +} + + +static +size_t file_read(void* file, void* dst, size_t bytes, void* user_data) +{ + FILE* f; + (void)(user_data); + + f = (FILE*)(file); + return fread(dst, 1, bytes, f); +} + + +static +unsigned long file_size(void* file, void* user_data) +{ + FILE* f; + long p; + long n; + (void)(user_data); + + f = (FILE*)(file); + + p = ftell(f); + fseek(f, 0, SEEK_END); + n = ftell(f); + fseek(f, p, SEEK_SET); + + if (n > 0) + return (unsigned long)(n); + else + return 0; +} + + +static +char* string_copy(const char* s, const char* e) +{ + size_t n; + char* p; + + n = (size_t)(e - s); + p = (char*)(memory_realloc(0, n + 1)); + if (p) + { + memcpy(p, s, n); + p[n] = '\0'; + } + + return p; +} + + +static +char* string_substr(const char* s, size_t a, size_t b) +{ + return string_copy(s + a, s + b); +} + + +static +char* string_concat(const char* a, const char* s, const char* e) +{ + size_t an; + size_t sn; + char* p; + + an = a ? strlen(a) : 0; + sn = (size_t)(e - s); + p = (char*)(memory_realloc(0, an + sn + 1)); + if (p) + { + if (a) + memcpy(p, a, an); + memcpy(p + an, s, sn); + p[an + sn] = '\0'; + } + + return p; +} + + +static +int string_equal(const char* a, const char* s, const char* e) +{ + size_t an = strlen(a); + size_t sn = (size_t)(e - s); + + return an == sn && memcmp(a, s, an) == 0; +} + + +static +void string_fix_separators(char* s) +{ + while (*s) + { + if (*s == FAST_OBJ_OTHER_SEP) + *s = FAST_OBJ_SEPARATOR; + s++; + } +} + + +static +int is_whitespace(char c) +{ + return (c == ' ' || c == '\t' || c == '\r'); +} + +static +int is_newline(char c) +{ + return (c == '\n'); +} + + +static +int is_digit(char c) +{ + return (c >= '0' && c <= '9'); +} + + +static +int is_exponent(char c) +{ + return (c == 'e' || c == 'E'); +} + + +static +const char* skip_name(const char* ptr) +{ + const char* s = ptr; + + while (!is_newline(*ptr)) + ptr++; + + while (ptr > s && is_whitespace(*(ptr - 1))) + ptr--; + + return ptr; +} + + +static +const char* skip_whitespace(const char* ptr) +{ + while (is_whitespace(*ptr)) + ptr++; + + return ptr; +} + + +static +const char* skip_line(const char* ptr) +{ + while (!is_newline(*ptr++)) + ; + + return ptr; +} + + +static +fastObjGroup object_default(void) +{ + fastObjGroup object; + + object.name = 0; + object.face_count = 0; + object.face_offset = 0; + object.index_offset = 0; + + return object; +} + + +static +void object_clean(fastObjGroup* object) +{ + memory_dealloc(object->name); +} + + +static +void flush_object(fastObjData* data) +{ + /* Add object if not empty */ + if (data->object.face_count > 0) + array_push(data->mesh->objects, data->object); + else + object_clean(&data->object); + + /* Reset for more data */ + data->object = object_default(); + data->object.face_offset = array_size(data->mesh->face_vertices); + data->object.index_offset = array_size(data->mesh->indices); +} + + + +static +fastObjGroup group_default(void) +{ + fastObjGroup group; + + group.name = 0; + group.face_count = 0; + group.face_offset = 0; + group.index_offset = 0; + + return group; +} + + +static +void group_clean(fastObjGroup* group) +{ + memory_dealloc(group->name); +} + + +static +void flush_group(fastObjData* data) +{ + /* Add group if not empty */ + if (data->group.face_count > 0) + array_push(data->mesh->groups, data->group); + else + group_clean(&data->group); + + /* Reset for more data */ + data->group = group_default(); + data->group.face_offset = array_size(data->mesh->face_vertices); + data->group.index_offset = array_size(data->mesh->indices); +} + + +static +const char* parse_int(const char* ptr, int* val) +{ + int sign; + int num; + + + if (*ptr == '-') + { + sign = -1; + ptr++; + } + else + { + sign = +1; + } + + num = 0; + while (is_digit(*ptr)) + num = 10 * num + (*ptr++ - '0'); + + *val = sign * num; + + return ptr; +} + + +static +const char* parse_float(const char* ptr, float* val) +{ + double sign; + double num; + double fra; + double div; + unsigned int eval; + const double* powers; + + + ptr = skip_whitespace(ptr); + + switch (*ptr) + { + case '+': + sign = 1.0; + ptr++; + break; + + case '-': + sign = -1.0; + ptr++; + break; + + default: + sign = 1.0; + break; + } + + + num = 0.0; + while (is_digit(*ptr)) + num = 10.0 * num + (double)(*ptr++ - '0'); + + if (*ptr == '.') + ptr++; + + fra = 0.0; + div = 1.0; + + while (is_digit(*ptr)) + { + fra = 10.0 * fra + (double)(*ptr++ - '0'); + div *= 10.0; + } + + num += fra / div; + + if (is_exponent(*ptr)) + { + ptr++; + + switch (*ptr) + { + case '+': + powers = POWER_10_POS; + ptr++; + break; + + case '-': + powers = POWER_10_NEG; + ptr++; + break; + + default: + powers = POWER_10_POS; + break; + } + + eval = 0; + while (is_digit(*ptr)) + eval = 10 * eval + (*ptr++ - '0'); + + num *= (eval >= MAX_POWER) ? 0.0 : powers[eval]; + } + + *val = (float)(sign * num); + + return ptr; +} + + +static +const char* parse_vertex(fastObjData* data, const char* ptr) +{ + unsigned int ii; + float v; + + + for (ii = 0; ii < 3; ii++) + { + ptr = parse_float(ptr, &v); + array_push(data->mesh->positions, v); + } + + + ptr = skip_whitespace(ptr); + if (!is_newline(*ptr)) + { + /* Fill the colors array until it matches the size of the positions array */ + for (ii = array_size(data->mesh->colors); ii < array_size(data->mesh->positions) - 3; ++ii) + { + array_push(data->mesh->colors, 1.0f); + } + + for (ii = 0; ii < 3; ++ii) + { + ptr = parse_float(ptr, &v); + array_push(data->mesh->colors, v); + } + } + + return ptr; +} + + +static +const char* parse_texcoord(fastObjData* data, const char* ptr) +{ + unsigned int ii; + float v; + + + for (ii = 0; ii < 2; ii++) + { + ptr = parse_float(ptr, &v); + array_push(data->mesh->texcoords, v); + } + + return ptr; +} + + +static +const char* parse_normal(fastObjData* data, const char* ptr) +{ + unsigned int ii; + float v; + + + for (ii = 0; ii < 3; ii++) + { + ptr = parse_float(ptr, &v); + array_push(data->mesh->normals, v); + } + + return ptr; +} + + +static +const char* parse_face(fastObjData* data, const char* ptr, unsigned char line) +{ + unsigned int count; + fastObjIndex vn; + int v; + int t; + int n; + + + ptr = skip_whitespace(ptr); + + count = 0; + while (!is_newline(*ptr)) + { + v = 0; + t = 0; + n = 0; + + ptr = parse_int(ptr, &v); + if (*ptr == '/') + { + ptr++; + if (*ptr != '/') + ptr = parse_int(ptr, &t); + + if (*ptr == '/') + { + ptr++; + ptr = parse_int(ptr, &n); + } + } + + if (v < 0) + vn.p = (array_size(data->mesh->positions) / 3) - (fastObjUInt)(-v); + else if (v > 0) + vn.p = (fastObjUInt)(v); + else + return ptr; /* Skip lines with no valid vertex index */ + + if (t < 0) + vn.t = (array_size(data->mesh->texcoords) / 2) - (fastObjUInt)(-t); + else if (t > 0) + vn.t = (fastObjUInt)(t); + else + vn.t = 0; + + if (n < 0) + vn.n = (array_size(data->mesh->normals) / 3) - (fastObjUInt)(-n); + else if (n > 0) + vn.n = (fastObjUInt)(n); + else + vn.n = 0; + + array_push(data->mesh->indices, vn); + count++; + + ptr = skip_whitespace(ptr); + } + + array_push(data->mesh->face_vertices, count); + array_push(data->mesh->face_materials, data->material); + + if (line || data->mesh->face_lines) + { + /* when line info exists, ensure it uses aligned indexing with other face data */ + size_t skipped = array_size(data->mesh->face_vertices) - array_size(data->mesh->face_lines); + while (--skipped > 0) + array_push(data->mesh->face_lines, 0); + + array_push(data->mesh->face_lines, line); + } + + data->group.face_count++; + data->object.face_count++; + + return ptr; +} + + +static +const char* parse_object(fastObjData* data, const char* ptr) +{ + const char* s; + const char* e; + + + ptr = skip_whitespace(ptr); + + s = ptr; + ptr = skip_name(ptr); + e = ptr; + + flush_object(data); + data->object.name = string_copy(s, e); + + return ptr; +} + + +static +const char* parse_group(fastObjData* data, const char* ptr) +{ + const char* s; + const char* e; + + + ptr = skip_whitespace(ptr); + + s = ptr; + ptr = skip_name(ptr); + e = ptr; + + flush_group(data); + data->group.name = string_copy(s, e); + + return ptr; +} + + +static +fastObjTexture map_default(void) +{ + fastObjTexture map; + + map.name = 0; + map.path = 0; + + return map; +} + + +static +fastObjMaterial mtl_default(void) +{ + fastObjMaterial mtl; + + mtl.name = 0; + + mtl.Ka[0] = 0.0; + mtl.Ka[1] = 0.0; + mtl.Ka[2] = 0.0; + mtl.Kd[0] = 1.0; + mtl.Kd[1] = 1.0; + mtl.Kd[2] = 1.0; + mtl.Ks[0] = 0.0; + mtl.Ks[1] = 0.0; + mtl.Ks[2] = 0.0; + mtl.Ke[0] = 0.0; + mtl.Ke[1] = 0.0; + mtl.Ke[2] = 0.0; + mtl.Kt[0] = 0.0; + mtl.Kt[1] = 0.0; + mtl.Kt[2] = 0.0; + mtl.Ns = 1.0; + mtl.Ni = 1.0; + mtl.Tf[0] = 1.0; + mtl.Tf[1] = 1.0; + mtl.Tf[2] = 1.0; + mtl.d = 1.0; + mtl.illum = 1; + + mtl.fallback = 0; + + mtl.map_Ka = 0; + mtl.map_Kd = 0; + mtl.map_Ks = 0; + mtl.map_Ke = 0; + mtl.map_Kt = 0; + mtl.map_Ns = 0; + mtl.map_Ni = 0; + mtl.map_d = 0; + mtl.map_bump = 0; + + return mtl; +} + + +static +const char* parse_usemtl(fastObjData* data, const char* ptr) +{ + const char* s; + const char* e; + unsigned int idx; + fastObjMaterial* mtl; + + + ptr = skip_whitespace(ptr); + + /* Parse the material name */ + s = ptr; + ptr = skip_name(ptr); + e = ptr; + + /* Find an existing material with the same name */ + idx = 0; + while (idx < array_size(data->mesh->materials)) + { + mtl = &data->mesh->materials[idx]; + if (mtl->name && string_equal(mtl->name, s, e)) + break; + + idx++; + } + + /* If doesn't exist, create a default one with this name + Note: this case happens when OBJ doesn't have its MTL */ + if (idx == array_size(data->mesh->materials)) + { + fastObjMaterial new_mtl = mtl_default(); + new_mtl.name = string_copy(s, e); + new_mtl.fallback = 1; + array_push(data->mesh->materials, new_mtl); + } + + data->material = idx; + + return ptr; +} + + +static +void map_clean(fastObjTexture* map) +{ + memory_dealloc(map->name); + memory_dealloc(map->path); +} + + +static +void mtl_clean(fastObjMaterial* mtl) +{ + memory_dealloc(mtl->name); +} + + +static +const char* read_mtl_int(const char* p, int* v) +{ + return parse_int(p, v); +} + + +static +const char* read_mtl_single(const char* p, float* v) +{ + return parse_float(p, v); +} + + +static +const char* read_mtl_triple(const char* p, float v[3]) +{ + p = read_mtl_single(p, &v[0]); + p = read_mtl_single(p, &v[1]); + p = read_mtl_single(p, &v[2]); + + return p; +} + + +static +const char* read_map(fastObjData* data, const char* ptr, unsigned int* idx) +{ + const char* s; + const char* e; + fastObjTexture* map; + + + ptr = skip_whitespace(ptr); + + /* Don't support options at present */ + if (*ptr == '-') + return ptr; + + + /* Read name */ + s = ptr; + ptr = skip_name(ptr); + e = ptr; + + /* Try to find an existing texture map with the same name */ + *idx = 1; /* skip dummy at index 0 */ + while (*idx < array_size(data->mesh->textures)) + { + map = &data->mesh->textures[*idx]; + if (map->name && string_equal(map->name, s, e)) + break; + + (*idx)++; + } + + /* Add it to the texture array if it didn't already exist */ + if (*idx == array_size(data->mesh->textures)) + { + fastObjTexture new_map = map_default(); + new_map.name = string_copy(s, e); + new_map.path = string_concat(data->base, s, e); + string_fix_separators(new_map.path); + array_push(data->mesh->textures, new_map); + } + + return e; +} + + +static +int read_mtllib(fastObjData* data, void* file, const fastObjCallbacks* callbacks, void* user_data) +{ + unsigned long n; + const char* s; + char* contents; + size_t l; + const char* p; + const char* e; + int found_d; + fastObjMaterial mtl; + + + /* Read entire file */ + n = callbacks->file_size(file, user_data); + + contents = (char*)(memory_realloc(0, n + 1)); + if (!contents) + return 0; + + l = callbacks->file_read(file, contents, n, user_data); + contents[l] = '\n'; + + mtl = mtl_default(); + + found_d = 0; + + p = contents; + e = contents + l; + while (p < e) + { + p = skip_whitespace(p); + + switch (*p) + { + case 'n': + p++; + if (p[0] == 'e' && + p[1] == 'w' && + p[2] == 'm' && + p[3] == 't' && + p[4] == 'l' && + is_whitespace(p[5])) + { + /* Push previous material (if there is one) */ + if (mtl.name) + { + array_push(data->mesh->materials, mtl); + mtl = mtl_default(); + } + + + /* Read name */ + p += 5; + + while (is_whitespace(*p)) + p++; + + s = p; + p = skip_name(p); + + mtl.name = string_copy(s, p); + } + break; + + case 'K': + if (p[1] == 'a') + p = read_mtl_triple(p + 2, mtl.Ka); + else if (p[1] == 'd') + p = read_mtl_triple(p + 2, mtl.Kd); + else if (p[1] == 's') + p = read_mtl_triple(p + 2, mtl.Ks); + else if (p[1] == 'e') + p = read_mtl_triple(p + 2, mtl.Ke); + else if (p[1] == 't') + p = read_mtl_triple(p + 2, mtl.Kt); + break; + + case 'N': + if (p[1] == 's') + p = read_mtl_single(p + 2, &mtl.Ns); + else if (p[1] == 'i') + p = read_mtl_single(p + 2, &mtl.Ni); + break; + + case 'T': + if (p[1] == 'r') + { + float Tr; + p = read_mtl_single(p + 2, &Tr); + if (!found_d) + { + /* Ignore Tr if we've already read d */ + mtl.d = 1.0f - Tr; + } + } + else if (p[1] == 'f') + p = read_mtl_triple(p + 2, mtl.Tf); + break; + + case 'd': + if (is_whitespace(p[1])) + { + p = read_mtl_single(p + 1, &mtl.d); + found_d = 1; + } + break; + + case 'i': + p++; + if (p[0] == 'l' && + p[1] == 'l' && + p[2] == 'u' && + p[3] == 'm' && + is_whitespace(p[4])) + { + p = read_mtl_int(p + 4, &mtl.illum); + } + break; + + case 'm': + p++; + if (p[0] == 'a' && + p[1] == 'p' && + p[2] == '_') + { + p += 3; + if (*p == 'K') + { + p++; + if (is_whitespace(p[1])) + { + if (*p == 'a') + p = read_map(data, p + 1, &mtl.map_Ka); + else if (*p == 'd') + p = read_map(data, p + 1, &mtl.map_Kd); + else if (*p == 's') + p = read_map(data, p + 1, &mtl.map_Ks); + else if (*p == 'e') + p = read_map(data, p + 1, &mtl.map_Ke); + else if (*p == 't') + p = read_map(data, p + 1, &mtl.map_Kt); + } + } + else if (*p == 'N') + { + p++; + if (is_whitespace(p[1])) + { + if (*p == 's') + p = read_map(data, p + 1, &mtl.map_Ns); + else if (*p == 'i') + p = read_map(data, p + 1, &mtl.map_Ni); + } + } + else if (*p == 'd') + { + p++; + if (is_whitespace(*p)) + p = read_map(data, p, &mtl.map_d); + } + else if ((p[0] == 'b' || p[0] == 'B') && + p[1] == 'u' && + p[2] == 'm' && + p[3] == 'p' && + is_whitespace(p[4])) + { + p = read_map(data, p + 4, &mtl.map_bump); + } + } + break; + + case '#': + break; + } + + p = skip_line(p); + } + + /* Push final material */ + if (mtl.name) + array_push(data->mesh->materials, mtl); + + memory_dealloc(contents); + + return 1; +} + + +static +const char* parse_mtllib(fastObjData* data, const char* ptr, const fastObjCallbacks* callbacks, void* user_data) +{ + const char* s; + const char* e; + char* lib; + void* file; + + + ptr = skip_whitespace(ptr); + + s = ptr; + ptr = skip_name(ptr); + e = ptr; + + lib = string_concat(data->base, s, e); + if (lib) + { + string_fix_separators(lib); + + file = callbacks->file_open(lib, user_data); + if (file) + { + read_mtllib(data, file, callbacks, user_data); + callbacks->file_close(file, user_data); + } + + memory_dealloc(lib); + } + + return ptr; +} + + +static +void parse_buffer(fastObjData* data, const char* ptr, const char* end, const fastObjCallbacks* callbacks, void* user_data) +{ + const char* p; + + + p = ptr; + while (p != end) + { + p = skip_whitespace(p); + + switch (*p) + { + case 'v': + p++; + + switch (*p++) + { + case ' ': + case '\t': + p = parse_vertex(data, p); + break; + + case 't': + p = parse_texcoord(data, p); + break; + + case 'n': + p = parse_normal(data, p); + break; + + default: + p--; /* roll p++ back in case *p was a newline */ + } + break; + + case 'f': + p++; + + switch (*p++) + { + case ' ': + case '\t': + p = parse_face(data, p, 0); + break; + + default: + p--; /* roll p++ back in case *p was a newline */ + } + break; + + case 'l': + p++; + + switch (*p++) + { + case ' ': + case '\t': + p = parse_face(data, p, 1); + break; + + default: + p--; /* roll p++ back in case *p was a newline */ + } + break; + + case 'o': + p++; + + switch (*p++) + { + case ' ': + case '\t': + p = parse_object(data, p); + break; + + default: + p--; /* roll p++ back in case *p was a newline */ + } + break; + + case 'g': + p++; + + switch (*p++) + { + case ' ': + case '\t': + p = parse_group(data, p); + break; + + default: + p--; /* roll p++ back in case *p was a newline */ + } + break; + + case 'm': + p++; + if (p[0] == 't' && + p[1] == 'l' && + p[2] == 'l' && + p[3] == 'i' && + p[4] == 'b' && + is_whitespace(p[5])) + p = parse_mtllib(data, p + 5, callbacks, user_data); + break; + + case 'u': + p++; + if (p[0] == 's' && + p[1] == 'e' && + p[2] == 'm' && + p[3] == 't' && + p[4] == 'l' && + is_whitespace(p[5])) + p = parse_usemtl(data, p + 5); + break; + + case '#': + break; + } + + p = skip_line(p); + + data->line++; + } + if (array_size(data->mesh->colors) > 0) + { + /* Fill the remaining slots in the colors array */ + unsigned int ii; + for (ii = array_size(data->mesh->colors); ii < array_size(data->mesh->positions); ++ii) + { + array_push(data->mesh->colors, 1.0f); + } + } +} + + +void fast_obj_destroy(fastObjMesh* m) +{ + unsigned int ii; + + + for (ii = 0; ii < array_size(m->objects); ii++) + object_clean(&m->objects[ii]); + + for (ii = 0; ii < array_size(m->groups); ii++) + group_clean(&m->groups[ii]); + + for (ii = 0; ii < array_size(m->materials); ii++) + mtl_clean(&m->materials[ii]); + + for (ii = 0; ii < array_size(m->textures); ii++) + map_clean(&m->textures[ii]); + + array_clean(m->positions); + array_clean(m->texcoords); + array_clean(m->normals); + array_clean(m->colors); + array_clean(m->face_vertices); + array_clean(m->face_materials); + array_clean(m->face_lines); + array_clean(m->indices); + array_clean(m->objects); + array_clean(m->groups); + array_clean(m->materials); + array_clean(m->textures); + + memory_dealloc(m); +} + + +fastObjMesh* fast_obj_read(const char* path) +{ + fastObjCallbacks callbacks; + callbacks.file_open = file_open; + callbacks.file_close = file_close; + callbacks.file_read = file_read; + callbacks.file_size = file_size; + + return fast_obj_read_with_callbacks(path, &callbacks, 0); +} + + +fastObjMesh* fast_obj_read_with_callbacks(const char* path, const fastObjCallbacks* callbacks, void* user_data) +{ + fastObjData data; + fastObjMesh* m; + void* file; + char* buffer; + char* start; + char* end; + char* last; + fastObjUInt read; + fastObjUInt bytes; + + /* Check if callbacks are valid */ + if(!callbacks) + return 0; + + + /* Open file */ + file = callbacks->file_open(path, user_data); + if (!file) + return 0; + + + /* Empty mesh */ + m = (fastObjMesh*)(memory_realloc(0, sizeof(fastObjMesh))); + if (!m) + return 0; + + m->positions = 0; + m->texcoords = 0; + m->normals = 0; + m->colors = 0; + m->face_vertices = 0; + m->face_materials = 0; + m->face_lines = 0; + m->indices = 0; + m->materials = 0; + m->textures = 0; + m->objects = 0; + m->groups = 0; + + + /* Add dummy position/texcoord/normal/texture */ + array_push(m->positions, 0.0f); + array_push(m->positions, 0.0f); + array_push(m->positions, 0.0f); + + array_push(m->texcoords, 0.0f); + array_push(m->texcoords, 0.0f); + + array_push(m->normals, 0.0f); + array_push(m->normals, 0.0f); + array_push(m->normals, 1.0f); + + array_push(m->textures, map_default()); + + + /* Data needed during parsing */ + data.mesh = m; + data.object = object_default(); + data.group = group_default(); + data.material = 0; + data.line = 1; + data.base = 0; + + + /* Find base path for materials/textures */ + { + const char* sep1 = strrchr(path, FAST_OBJ_SEPARATOR); + const char* sep2 = strrchr(path, FAST_OBJ_OTHER_SEP); + + /* Use the last separator in the path */ + const char* sep = sep2 && (!sep1 || sep1 < sep2) ? sep2 : sep1; + + if (sep) + data.base = string_substr(path, 0, sep - path + 1); + } + + + /* Create buffer for reading file */ + buffer = (char*)(memory_realloc(0, 2 * BUFFER_SIZE * sizeof(char))); + if (!buffer) + return 0; + + start = buffer; + for (;;) + { + /* Read another buffer's worth from file */ + read = (fastObjUInt)(callbacks->file_read(file, start, BUFFER_SIZE, user_data)); + if (read == 0 && start == buffer) + break; + + + /* Ensure buffer ends in a newline */ + if (read < BUFFER_SIZE) + { + if (read == 0 || start[read - 1] != '\n') + start[read++] = '\n'; + } + + end = start + read; + if (end == buffer) + break; + + + /* Find last new line */ + last = end; + while (last > buffer) + { + last--; + if (*last == '\n') + break; + } + + + /* Check there actually is a new line */ + if (*last != '\n') + break; + + last++; + + + /* Process buffer */ + parse_buffer(&data, buffer, last, callbacks, user_data); + + + /* Copy overflow for next buffer */ + bytes = (fastObjUInt)(end - last); + memmove(buffer, last, bytes); + start = buffer + bytes; + } + + + /* Flush final object/group */ + flush_object(&data); + object_clean(&data.object); + + flush_group(&data); + group_clean(&data.group); + + m->position_count = array_size(m->positions) / 3; + m->texcoord_count = array_size(m->texcoords) / 2; + m->normal_count = array_size(m->normals) / 3; + m->color_count = array_size(m->colors) / 3; + m->face_count = array_size(m->face_vertices); + m->index_count = array_size(m->indices); + m->material_count = array_size(m->materials); + m->texture_count = array_size(m->textures); + m->object_count = array_size(m->objects); + m->group_count = array_size(m->groups); + + + /* Clean up */ + memory_dealloc(buffer); + memory_dealloc(data.base); + + callbacks->file_close(file, user_data); + + return m; +} + +#endif \ No newline at end of file diff --git a/Lib/meshoptimizer/extern/sdefl.h b/Lib/meshoptimizer/extern/sdefl.h new file mode 100644 index 0000000..85ffb8a --- /dev/null +++ b/Lib/meshoptimizer/extern/sdefl.h @@ -0,0 +1,800 @@ +/*# Small Deflate +`sdefl` is a small bare bone lossless compression library in ANSI C (ISO C90) +which implements the Deflate (RFC 1951) compressed data format specification standard. +It is mainly tuned to get as much speed and compression ratio from as little code +as needed to keep the implementation as concise as possible. + +## Features +- Portable single header and source file duo written in ANSI C (ISO C90) +- Dual license with either MIT or public domain +- Small implementation + - Deflate: 525 LoC + - Inflate: 320 LoC +- Webassembly: + - Deflate ~3.7 KB (~2.2KB compressed) + - Inflate ~3.6 KB (~2.2KB compressed) + +## Usage: +This file behaves differently depending on what symbols you define +before including it. + +Header-File mode: +If you do not define `SDEFL_IMPLEMENTATION` before including this file, it +will operate in header only mode. In this mode it declares all used structs +and the API of the library without including the implementation of the library. + +Implementation mode: +If you define `SDEFL_IMPLEMENTATION` before including this file, it will +compile the implementation . Make sure that you only include +this file implementation in *one* C or C++ file to prevent collisions. + +### Benchmark + +| Compressor name | Compression| Decompress.| Compr. size | Ratio | +| ------------------------| -----------| -----------| ----------- | ----- | +| miniz 1.0 -1 | 122 MB/s | 208 MB/s | 48510028 | 48.51 | +| miniz 1.0 -6 | 27 MB/s | 260 MB/s | 36513697 | 36.51 | +| miniz 1.0 -9 | 23 MB/s | 261 MB/s | 36460101 | 36.46 | +| zlib 1.2.11 -1 | 72 MB/s | 307 MB/s | 42298774 | 42.30 | +| zlib 1.2.11 -6 | 24 MB/s | 313 MB/s | 36548921 | 36.55 | +| zlib 1.2.11 -9 | 20 MB/s | 314 MB/s | 36475792 | 36.48 | +| sdefl 1.0 -0 | 127 MB/s | 355 MB/s | 40004116 | 39.88 | +| sdefl 1.0 -1 | 111 MB/s | 413 MB/s | 38940674 | 38.82 | +| sdefl 1.0 -5 | 45 MB/s | 436 MB/s | 36577183 | 36.46 | +| sdefl 1.0 -7 | 38 MB/s | 432 MB/s | 36523781 | 36.41 | +| libdeflate 1.3 -1 | 147 MB/s | 667 MB/s | 39597378 | 39.60 | +| libdeflate 1.3 -6 | 69 MB/s | 689 MB/s | 36648318 | 36.65 | +| libdeflate 1.3 -9 | 13 MB/s | 672 MB/s | 35197141 | 35.20 | +| libdeflate 1.3 -12 | 8.13 MB/s | 670 MB/s | 35100568 | 35.10 | + +### Compression +Results on the [Silesia compression corpus](http://sun.aei.polsl.pl/~sdeor/index.php?page=silesia): + +| File | Original | `sdefl 0` | `sdefl 5` | `sdefl 7` | +| --------| -----------| -------------| ---------- | ------------| +| dickens | 10.192.446 | 4,260,187 | 3,845,261 | 3,833,657 | +| mozilla | 51.220.480 | 20,774,706 | 19,607,009 | 19,565,867 | +| mr | 9.970.564 | 3,860,531 | 3,673,460 | 3,665,627 | +| nci | 33.553.445 | 4,030,283 | 3,094,526 | 3,006,075 | +| ooffice | 6.152.192 | 3,320,063 | 3,186,373 | 3,183,815 | +| osdb | 10.085.684 | 3,919,646 | 3,649,510 | 3,649,477 | +| reymont | 6.627.202 | 2,263,378 | 1,857,588 | 1,827,237 | +| samba | 21.606.400 | 6,121,797 | 5,462,670 | 5,450,762 | +| sao | 7.251.944 | 5,612,421 | 5,485,380 | 5,481,765 | +| webster | 41.458.703 | 13,972,648 | 12,059,432 | 11,991,421 | +| xml | 5.345.280 | 886,620 | 674,009 | 662,141 | +| x-ray | 8.474.240 | 6,304,655 | 6,244,779 | 6,244,779 | + +## License +``` +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2020-2023 Micha Mettke +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +``` +*/ +#ifndef SDEFL_H_INCLUDED +#define SDEFL_H_INCLUDED + +#ifdef __cplusplus +extern "C" { +#endif + +#define SDEFL_MAX_OFF (1 << 15) +#define SDEFL_WIN_SIZ SDEFL_MAX_OFF +#define SDEFL_WIN_MSK (SDEFL_WIN_SIZ-1) + +#define SDEFL_HASH_BITS 15 +#define SDEFL_HASH_SIZ (1 << SDEFL_HASH_BITS) +#define SDEFL_HASH_MSK (SDEFL_HASH_SIZ-1) + +#define SDEFL_MIN_MATCH 4 +#define SDEFL_BLK_MAX (256*1024) +#define SDEFL_SEQ_SIZ ((SDEFL_BLK_MAX+2)/3) + +#define SDEFL_SYM_MAX (288) +#define SDEFL_OFF_MAX (32) +#define SDEFL_PRE_MAX (19) + +#define SDEFL_LVL_MIN 0 +#define SDEFL_LVL_DEF 5 +#define SDEFL_LVL_MAX 8 + +struct sdefl_freq { + unsigned lit[SDEFL_SYM_MAX]; + unsigned off[SDEFL_OFF_MAX]; +}; +struct sdefl_code_words { + unsigned lit[SDEFL_SYM_MAX]; + unsigned off[SDEFL_OFF_MAX]; +}; +struct sdefl_lens { + unsigned char lit[SDEFL_SYM_MAX]; + unsigned char off[SDEFL_OFF_MAX]; +}; +struct sdefl_codes { + struct sdefl_code_words word; + struct sdefl_lens len; +}; +struct sdefl_seqt { + int off, len; +}; +struct sdefl { + int bits, bitcnt; + int tbl[SDEFL_HASH_SIZ]; + int prv[SDEFL_WIN_SIZ]; + + int seq_cnt; + struct sdefl_seqt seq[SDEFL_SEQ_SIZ]; + struct sdefl_freq freq; + struct sdefl_codes cod; +}; +extern int sdefl_bound(int in_len); +extern int sdeflate(struct sdefl *s, void *o, const void *i, int n, int lvl); +extern int zsdeflate(struct sdefl *s, void *o, const void *i, int n, int lvl); + +#ifdef __cplusplus +} +#endif + +#endif /* SDEFL_H_INCLUDED */ + +#ifdef SDEFL_IMPLEMENTATION + +#include /* assert */ +#include /* memcpy */ +#include /* CHAR_BIT */ + +#ifdef __cplusplus +extern "C" { +#endif + +#define SDEFL_NIL (-1) +#define SDEFL_MAX_MATCH 258 +#define SDEFL_MAX_CODE_LEN (15) +#define SDEFL_SYM_BITS (10u) +#define SDEFL_SYM_MSK ((1u << SDEFL_SYM_BITS)-1u) +#define SDEFL_RAW_BLK_SIZE (65535) +#define SDEFL_LIT_LEN_CODES (14) +#define SDEFL_OFF_CODES (15) +#define SDEFL_PRE_CODES (7) +#define SDEFL_CNT_NUM(n) ((((n)+3u/4u)+3u)&~3u) +#define SDEFL_EOB (256) + +#define sdefl_npow2(n) (1 << (sdefl_ilog2((n)-1) + 1)) +#define sdefl_div_round_up(n,d) (((n)+((d)-1))/(d)) + +static int +sdefl_ilog2(int n) { + if (!n) return 0; +#ifdef _MSC_VER + unsigned long msbp = 0; + _BitScanReverse(&msbp, (unsigned long)n); + return (int)msbp; +#elif defined(__GNUC__) || defined(__clang__) + return (int)sizeof(unsigned long) * CHAR_BIT - 1 - __builtin_clzl((unsigned long)n); +#else + #define lt(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n + static const char tbl[256] = { + 0,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,lt(4), lt(5), lt(5), lt(6), lt(6), lt(6), lt(6), + lt(7), lt(7), lt(7), lt(7), lt(7), lt(7), lt(7), lt(7)}; + int tt, t; + if ((tt = (n >> 16))) { + return (t = (tt >> 8)) ? 24 + tbl[t] : 16 + tbl[tt]; + } else { + return (t = (n >> 8)) ? 8 + tbl[t] : tbl[n]; + } + #undef lt +#endif +} +static unsigned +sdefl_uload32(const void *p) { + /* hopefully will be optimized to an unaligned read */ + unsigned n = 0; + memcpy(&n, p, sizeof(n)); + return n; +} +static unsigned +sdefl_hash32(const void *p) { + unsigned n = sdefl_uload32(p); + return (n * 0x9E377989) >> (32 - SDEFL_HASH_BITS); +} +static void +sdefl_put(unsigned char **dst, struct sdefl *s, int code, int bitcnt) { + s->bits |= (code << s->bitcnt); + s->bitcnt += bitcnt; + while (s->bitcnt >= 8) { + unsigned char *tar = *dst; + *tar = (unsigned char)(s->bits & 0xFF); + s->bits >>= 8; + s->bitcnt -= 8; + *dst = *dst + 1; + } +} +static void +sdefl_heap_sub(unsigned A[], unsigned len, unsigned sub) { + unsigned c, p = sub; + unsigned v = A[sub]; + while ((c = p << 1) <= len) { + if (c < len && A[c + 1] > A[c]) c++; + if (v >= A[c]) break; + A[p] = A[c], p = c; + } + A[p] = v; +} +static void +sdefl_heap_array(unsigned *A, unsigned len) { + unsigned sub; + for (sub = len >> 1; sub >= 1; sub--) + sdefl_heap_sub(A, len, sub); +} +static void +sdefl_heap_sort(unsigned *A, unsigned n) { + A--; + sdefl_heap_array(A, n); + while (n >= 2) { + unsigned tmp = A[n]; + A[n--] = A[1]; + A[1] = tmp; + sdefl_heap_sub(A, n, 1); + } +} +static unsigned +sdefl_sort_sym(unsigned sym_cnt, unsigned *freqs, + unsigned char *lens, unsigned *sym_out) { + unsigned cnts[SDEFL_CNT_NUM(SDEFL_SYM_MAX)] = {0}; + unsigned cnt_num = SDEFL_CNT_NUM(sym_cnt); + unsigned used_sym = 0; + unsigned sym, i; + for (sym = 0; sym < sym_cnt; sym++) + cnts[freqs[sym] < cnt_num-1 ? freqs[sym]: cnt_num-1]++; + for (i = 1; i < cnt_num; i++) { + unsigned cnt = cnts[i]; + cnts[i] = used_sym; + used_sym += cnt; + } + for (sym = 0; sym < sym_cnt; sym++) { + unsigned freq = freqs[sym]; + if (freq) { + unsigned idx = freq < cnt_num-1 ? freq : cnt_num-1; + sym_out[cnts[idx]++] = sym | (freq << SDEFL_SYM_BITS); + } else lens[sym] = 0; + } + sdefl_heap_sort(sym_out + cnts[cnt_num-2], cnts[cnt_num-1] - cnts[cnt_num-2]); + return used_sym; +} +static void +sdefl_build_tree(unsigned *A, unsigned sym_cnt) { + unsigned i = 0, b = 0, e = 0; + do { + unsigned m, n, freq_shift; + if (i != sym_cnt && (b == e || (A[i] >> SDEFL_SYM_BITS) <= (A[b] >> SDEFL_SYM_BITS))) + m = i++; + else m = b++; + if (i != sym_cnt && (b == e || (A[i] >> SDEFL_SYM_BITS) <= (A[b] >> SDEFL_SYM_BITS))) + n = i++; + else n = b++; + + freq_shift = (A[m] & ~SDEFL_SYM_MSK) + (A[n] & ~SDEFL_SYM_MSK); + A[m] = (A[m] & SDEFL_SYM_MSK) | (e << SDEFL_SYM_BITS); + A[n] = (A[n] & SDEFL_SYM_MSK) | (e << SDEFL_SYM_BITS); + A[e] = (A[e] & SDEFL_SYM_MSK) | freq_shift; + } while (sym_cnt - ++e > 1); +} +static void +sdefl_gen_len_cnt(unsigned *A, unsigned root, unsigned *len_cnt, + unsigned max_code_len) { + int n; + unsigned i; + for (i = 0; i <= max_code_len; i++) + len_cnt[i] = 0; + len_cnt[1] = 2; + + A[root] &= SDEFL_SYM_MSK; + for (n = (int)root - 1; n >= 0; n--) { + unsigned p = A[n] >> SDEFL_SYM_BITS; + unsigned pdepth = A[p] >> SDEFL_SYM_BITS; + unsigned depth = pdepth + 1; + unsigned len = depth; + + A[n] = (A[n] & SDEFL_SYM_MSK) | (depth << SDEFL_SYM_BITS); + if (len >= max_code_len) { + len = max_code_len; + do len--; while (!len_cnt[len]); + } + len_cnt[len]--; + len_cnt[len+1] += 2; + } +} +static void +sdefl_gen_codes(unsigned *A, unsigned char *lens, const unsigned *len_cnt, + unsigned max_code_word_len, unsigned sym_cnt) { + unsigned i, sym, len, nxt[SDEFL_MAX_CODE_LEN + 1]; + for (i = 0, len = max_code_word_len; len >= 1; len--) { + unsigned cnt = len_cnt[len]; + while (cnt--) lens[A[i++] & SDEFL_SYM_MSK] = (unsigned char)len; + } + nxt[0] = nxt[1] = 0; + for (len = 2; len <= max_code_word_len; len++) + nxt[len] = (nxt[len-1] + len_cnt[len-1]) << 1; + for (sym = 0; sym < sym_cnt; sym++) + A[sym] = nxt[lens[sym]]++; +} +static unsigned +sdefl_rev(unsigned c, unsigned char n) { + c = ((c & 0x5555) << 1) | ((c & 0xAAAA) >> 1); + c = ((c & 0x3333) << 2) | ((c & 0xCCCC) >> 2); + c = ((c & 0x0F0F) << 4) | ((c & 0xF0F0) >> 4); + c = ((c & 0x00FF) << 8) | ((c & 0xFF00) >> 8); + return c >> (16-n); +} +static void +sdefl_huff(unsigned char *lens, unsigned *codes, unsigned *freqs, + unsigned num_syms, unsigned max_code_len) { + unsigned c, *A = codes; + unsigned len_cnt[SDEFL_MAX_CODE_LEN + 1]; + unsigned used_syms = sdefl_sort_sym(num_syms, freqs, lens, A); + if (!used_syms) return; + if (used_syms == 1) { + unsigned s = A[0] & SDEFL_SYM_MSK; + unsigned i = s ? s : 1; + codes[0] = 0, lens[0] = 1; + codes[i] = 1, lens[i] = 1; + return; + } + sdefl_build_tree(A, used_syms); + sdefl_gen_len_cnt(A, used_syms-2, len_cnt, max_code_len); + sdefl_gen_codes(A, lens, len_cnt, max_code_len, num_syms); + for (c = 0; c < num_syms; c++) { + codes[c] = sdefl_rev(codes[c], lens[c]); + } +} +struct sdefl_symcnt { + int items; + int lit; + int off; +}; +static void +sdefl_precode(struct sdefl_symcnt *cnt, unsigned *freqs, unsigned *items, + const unsigned char *litlen, const unsigned char *offlen) { + unsigned *at = items; + unsigned run_start = 0; + + unsigned total = 0; + unsigned char lens[SDEFL_SYM_MAX + SDEFL_OFF_MAX]; + for (cnt->lit = SDEFL_SYM_MAX; cnt->lit > 257; cnt->lit--) + if (litlen[cnt->lit - 1]) break; + for (cnt->off = SDEFL_OFF_MAX; cnt->off > 1; cnt->off--) + if (offlen[cnt->off - 1]) break; + + total = (unsigned)(cnt->lit + cnt->off); + memcpy(lens, litlen, sizeof(unsigned char) * (size_t)cnt->lit); + memcpy(lens + cnt->lit, offlen, sizeof(unsigned char) * (size_t)cnt->off); + do { + unsigned len = lens[run_start]; + unsigned run_end = run_start; + do run_end++; while (run_end != total && len == lens[run_end]); + if (!len) { + while ((run_end - run_start) >= 11) { + unsigned n = (run_end - run_start) - 11; + unsigned xbits = n < 0x7f ? n : 0x7f; + freqs[18]++; + *at++ = 18u | (xbits << 5u); + run_start += 11 + xbits; + } + if ((run_end - run_start) >= 3) { + unsigned n = (run_end - run_start) - 3; + unsigned xbits = n < 0x7 ? n : 0x7; + freqs[17]++; + *at++ = 17u | (xbits << 5u); + run_start += 3 + xbits; + } + } else if ((run_end - run_start) >= 4) { + freqs[len]++; + *at++ = len; + run_start++; + do { + unsigned xbits = (run_end - run_start) - 3; + xbits = xbits < 0x03 ? xbits : 0x03; + *at++ = 16 | (xbits << 5); + run_start += 3 + xbits; + freqs[16]++; + } while ((run_end - run_start) >= 3); + } + while (run_start != run_end) { + freqs[len]++; + *at++ = len; + run_start++; + } + } while (run_start != total); + cnt->items = (int)(at - items); +} +struct sdefl_match_codest { + int ls, lc; + int dc, dx; +}; +static void +sdefl_match_codes(struct sdefl_match_codest *cod, int dist, int len) { + static const short dxmax[] = {0,6,12,24,48,96,192,384,768,1536,3072,6144,12288,24576}; + static const unsigned char lslot[258+1] = { + 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 12, + 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 16, + 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, + 18, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 28 + }; + assert(len <= 258); + assert(dist <= 32768); + cod->ls = lslot[len]; + cod->lc = 257 + cod->ls; + assert(cod->lc <= 285); + + cod->dx = sdefl_ilog2(sdefl_npow2(dist) >> 2); + cod->dc = cod->dx ? ((cod->dx + 1) << 1) + (dist > dxmax[cod->dx]) : dist-1; +} +enum sdefl_blk_type { + SDEFL_BLK_UCOMPR, + SDEFL_BLK_DYN +}; +static enum sdefl_blk_type +sdefl_blk_type(const struct sdefl *s, int blk_len, int pre_item_len, + const unsigned *pre_freq, const unsigned char *pre_len) { + static const unsigned char x_pre_bits[] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7}; + static const unsigned char x_len_bits[] = {0,0,0,0,0,0,0,0, 1,1,1,1,2,2,2,2, + 3,3,3,3,4,4,4,4, 5,5,5,5,0}; + static const unsigned char x_off_bits[] = {0,0,0,0,1,1,2,2, 3,3,4,4,5,5,6,6, + 7,7,8,8,9,9,10,10, 11,11,12,12,13,13}; + + int dyn_cost = 0; + int fix_cost = 0; + int sym = 0; + + dyn_cost += 5 + 5 + 4 + (3 * pre_item_len); + for (sym = 0; sym < SDEFL_PRE_MAX; sym++) + dyn_cost += pre_freq[sym] * (x_pre_bits[sym] + pre_len[sym]); + for (sym = 0; sym < 256; sym++) + dyn_cost += s->freq.lit[sym] * s->cod.len.lit[sym]; + dyn_cost += s->cod.len.lit[SDEFL_EOB]; + for (sym = 257; sym < 286; sym++) + dyn_cost += s->freq.lit[sym] * (x_len_bits[sym - 257] + s->cod.len.lit[sym]); + for (sym = 0; sym < 30; sym++) + dyn_cost += s->freq.off[sym] * (x_off_bits[sym] + s->cod.len.off[sym]); + + fix_cost += 8*(5 * sdefl_div_round_up(blk_len, SDEFL_RAW_BLK_SIZE) + blk_len + 1 + 2); + return (dyn_cost < fix_cost) ? SDEFL_BLK_DYN : SDEFL_BLK_UCOMPR; +} +static void +sdefl_put16(unsigned char **dst, unsigned short x) { + unsigned char *val = *dst; + val[0] = (unsigned char)(x & 0xff); + val[1] = (unsigned char)(x >> 8); + *dst = val + 2; +} +static void +sdefl_match(unsigned char **dst, struct sdefl *s, int dist, int len) { + static const char lxn[] = {0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0}; + static const short lmin[] = {3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43, + 51,59,67,83,99,115,131,163,195,227,258}; + static const short dmin[] = {1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257, + 385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577}; + + struct sdefl_match_codest cod; + sdefl_match_codes(&cod, dist, len); + sdefl_put(dst, s, (int)s->cod.word.lit[cod.lc], s->cod.len.lit[cod.lc]); + sdefl_put(dst, s, len - lmin[cod.ls], lxn[cod.ls]); + sdefl_put(dst, s, (int)s->cod.word.off[cod.dc], s->cod.len.off[cod.dc]); + sdefl_put(dst, s, dist - dmin[cod.dc], cod.dx); +} +static void +sdefl_flush(unsigned char **dst, struct sdefl *s, int is_last, + const unsigned char *in, int blk_begin, int blk_end) { + int blk_len = blk_end - blk_begin; + int j, i = 0, item_cnt = 0; + struct sdefl_symcnt symcnt = {0}; + unsigned codes[SDEFL_PRE_MAX]; + unsigned char lens[SDEFL_PRE_MAX]; + unsigned freqs[SDEFL_PRE_MAX] = {0}; + unsigned items[SDEFL_SYM_MAX + SDEFL_OFF_MAX]; + static const unsigned char perm[SDEFL_PRE_MAX] = {16,17,18,0,8,7,9,6,10,5,11, + 4,12,3,13,2,14,1,15}; + + /* calculate huffman codes */ + s->freq.lit[SDEFL_EOB]++; + sdefl_huff(s->cod.len.lit, s->cod.word.lit, s->freq.lit, SDEFL_SYM_MAX, SDEFL_LIT_LEN_CODES); + sdefl_huff(s->cod.len.off, s->cod.word.off, s->freq.off, SDEFL_OFF_MAX, SDEFL_OFF_CODES); + sdefl_precode(&symcnt, freqs, items, s->cod.len.lit, s->cod.len.off); + sdefl_huff(lens, codes, freqs, SDEFL_PRE_MAX, SDEFL_PRE_CODES); + for (item_cnt = SDEFL_PRE_MAX; item_cnt > 4; item_cnt--) { + if (lens[perm[item_cnt - 1]]){ + break; + } + } + /* write block */ + switch (sdefl_blk_type(s, blk_len, item_cnt, freqs, lens)) { + case SDEFL_BLK_UCOMPR: { + /* uncompressed blocks */ + int n = sdefl_div_round_up(blk_len, SDEFL_RAW_BLK_SIZE); + for (i = 0; i < n; ++i) { + int fin = is_last && (i + 1 == n); + int amount = blk_len < SDEFL_RAW_BLK_SIZE ? blk_len : SDEFL_RAW_BLK_SIZE; + sdefl_put(dst, s, !!fin, 1); /* block */ + sdefl_put(dst, s, 0x00, 2); /* stored block */ + if (s->bitcnt) { + sdefl_put(dst, s, 0x00, 8 - s->bitcnt); + } + assert(s->bitcnt == 0); + sdefl_put16(dst, (unsigned short)amount); + sdefl_put16(dst, ~(unsigned short)amount); + memcpy(*dst, in + blk_begin + i * SDEFL_RAW_BLK_SIZE, amount); + *dst = *dst + amount; + blk_len -= amount; + } + } break; + case SDEFL_BLK_DYN: { + /* dynamic huffman block */ + sdefl_put(dst, s, !!is_last, 1); /* block */ + sdefl_put(dst, s, 0x02, 2); /* dynamic huffman */ + sdefl_put(dst, s, symcnt.lit - 257, 5); + sdefl_put(dst, s, symcnt.off - 1, 5); + sdefl_put(dst, s, item_cnt - 4, 4); + for (i = 0; i < item_cnt; ++i) { + sdefl_put(dst, s, lens[perm[i]], 3); + } + for (i = 0; i < symcnt.items; ++i) { + unsigned sym = items[i] & 0x1F; + sdefl_put(dst, s, (int)codes[sym], lens[sym]); + if (sym < 16) continue; + if (sym == 16) sdefl_put(dst, s, items[i] >> 5, 2); + else if(sym == 17) sdefl_put(dst, s, items[i] >> 5, 3); + else sdefl_put(dst, s, items[i] >> 5, 7); + } + /* block sequences */ + for (i = 0; i < s->seq_cnt; ++i) { + if (s->seq[i].off >= 0) { + for (j = 0; j < s->seq[i].len; ++j) { + int c = in[s->seq[i].off + j]; + sdefl_put(dst, s, (int)s->cod.word.lit[c], s->cod.len.lit[c]); + } + } else { + sdefl_match(dst, s, -s->seq[i].off, s->seq[i].len); + } + } + sdefl_put(dst, s, (int)(s)->cod.word.lit[SDEFL_EOB], (s)->cod.len.lit[SDEFL_EOB]); + } break;} + memset(&s->freq, 0, sizeof(s->freq)); + s->seq_cnt = 0; +} +static void +sdefl_seq(struct sdefl *s, int off, int len) { + assert(s->seq_cnt + 2 < SDEFL_SEQ_SIZ); + s->seq[s->seq_cnt].off = off; + s->seq[s->seq_cnt].len = len; + s->seq_cnt++; +} +static void +sdefl_reg_match(struct sdefl *s, int off, int len) { + struct sdefl_match_codest cod; + sdefl_match_codes(&cod, off, len); + + assert(cod.lc < SDEFL_SYM_MAX); + assert(cod.dc < SDEFL_OFF_MAX); + + s->freq.lit[cod.lc]++; + s->freq.off[cod.dc]++; +} +struct sdefl_match { + int off; + int len; +}; +static void +sdefl_fnd(struct sdefl_match *m, const struct sdefl *s, int chain_len, + int max_match, const unsigned char *in, int p, int e) { + int i = s->tbl[sdefl_hash32(in + p)]; + int limit = ((p - SDEFL_WIN_SIZ) < SDEFL_NIL) ? SDEFL_NIL : (p-SDEFL_WIN_SIZ); + (void)e; /* used in assertions */ + + assert(p < e); + assert(p + max_match <= e); + while (i > limit) { + assert(i + m->len < e); + assert(p + m->len < e); + assert(i + SDEFL_MIN_MATCH < e); + assert(p + SDEFL_MIN_MATCH < e); + + if (in[i + m->len] == in[p + m->len] && + (sdefl_uload32(&in[i]) == sdefl_uload32(&in[p]))) { + int n = SDEFL_MIN_MATCH; + while (n < max_match && in[i + n] == in[p + n]) { + assert(i + n < e); + assert(p + n < e); + n++; + } + if (n > m->len) { + m->len = n, m->off = p - i; + if (n == max_match) + break; + } + } + if (!(--chain_len)) break; + i = s->prv[i & SDEFL_WIN_MSK]; + } +} +static int +sdefl_compr(struct sdefl *s, unsigned char *out, const unsigned char *in, + int in_len, int lvl) { + unsigned char *q = out; + static const unsigned char pref[] = {8,10,14,24,30,48,65,96,130}; + int max_chain = (lvl < 8) ? (1 << (lvl + 1)): (1 << 13); + int n, i = 0, litlen = 0; + for (n = 0; n < SDEFL_HASH_SIZ; ++n) { + s->tbl[n] = SDEFL_NIL; + } + do {int blk_begin = i; + int blk_end = ((i + SDEFL_BLK_MAX) < in_len) ? (i + SDEFL_BLK_MAX) : in_len; + while (i < blk_end) { + struct sdefl_match m = {0}; + int left = blk_end - i; + int max_match = (left > SDEFL_MAX_MATCH) ? SDEFL_MAX_MATCH : left; + int nice_match = pref[lvl] < max_match ? pref[lvl] : max_match; + int run = 1, inc = 1, run_inc = 0; + if (max_match > SDEFL_MIN_MATCH) { + sdefl_fnd(&m, s, max_chain, max_match, in, i, in_len); + } + if (lvl >= 5 && m.len >= SDEFL_MIN_MATCH && m.len + 1 < nice_match){ + struct sdefl_match m2 = {0}; + sdefl_fnd(&m2, s, max_chain, m.len + 1, in, i + 1, in_len); + m.len = (m2.len > m.len) ? 0 : m.len; + } + if (m.len >= SDEFL_MIN_MATCH) { + if (litlen) { + sdefl_seq(s, i - litlen, litlen); + litlen = 0; + } + sdefl_seq(s, -m.off, m.len); + sdefl_reg_match(s, m.off, m.len); + if (lvl < 2 && m.len >= nice_match) { + inc = m.len; + } else { + run = m.len; + } + } else { + s->freq.lit[in[i]]++; + litlen++; + } + run_inc = run * inc; + if (in_len - (i + run_inc) > SDEFL_MIN_MATCH) { + while (run-- > 0) { + unsigned h = sdefl_hash32(&in[i]); + s->prv[i&SDEFL_WIN_MSK] = s->tbl[h]; + s->tbl[h] = i, i += inc; + assert(i <= blk_end); + } + } else { + i += run_inc; + assert(i <= blk_end); + } + } + if (litlen) { + sdefl_seq(s, i - litlen, litlen); + litlen = 0; + } + sdefl_flush(&q, s, blk_end == in_len, in, blk_begin, blk_end); + } while (i < in_len); + if (s->bitcnt) { + sdefl_put(&q, s, 0x00, 8 - s->bitcnt); + } + assert(s->bitcnt == 0); + return (int)(q - out); +} +extern int +sdeflate(struct sdefl *s, void *out, const void *in, int n, int lvl) { + s->bits = s->bitcnt = 0; + return sdefl_compr(s, (unsigned char*)out, (const unsigned char*)in, n, lvl); +} +static unsigned +sdefl_adler32(unsigned adler32, const unsigned char *in, int in_len) { + #define SDEFL_ADLER_INIT (1) + const unsigned ADLER_MOD = 65521; + unsigned s1 = adler32 & 0xffff; + unsigned s2 = adler32 >> 16; + unsigned blk_len, i; + + blk_len = in_len % 5552; + while (in_len) { + for (i = 0; i + 7 < blk_len; i += 8) { + s1 += in[0]; s2 += s1; + s1 += in[1]; s2 += s1; + s1 += in[2]; s2 += s1; + s1 += in[3]; s2 += s1; + s1 += in[4]; s2 += s1; + s1 += in[5]; s2 += s1; + s1 += in[6]; s2 += s1; + s1 += in[7]; s2 += s1; + in += 8; + } + for (; i < blk_len; ++i) { + s1 += *in++, s2 += s1; + } + s1 %= ADLER_MOD; + s2 %= ADLER_MOD; + in_len -= blk_len; + blk_len = 5552; + } + return (unsigned)(s2 << 16) + (unsigned)s1; +} +extern int +zsdeflate(struct sdefl *s, void *out, const void *in, int n, int lvl) { + int p = 0; + unsigned a = 0; + unsigned char *q = (unsigned char*)out; + + s->bits = s->bitcnt = 0; + sdefl_put(&q, s, 0x78, 8); /* deflate, 32k window */ + sdefl_put(&q, s, 0x01, 8); /* fast compression */ + q += sdefl_compr(s, q, (const unsigned char*)in, n, lvl); + + /* append adler checksum */ + a = sdefl_adler32(SDEFL_ADLER_INIT, (const unsigned char*)in, n); + for (p = 0; p < 4; ++p) { + sdefl_put(&q, s, (a >> 24) & 0xFF, 8); + a <<= 8; + } + return (int)(q - (unsigned char*)out); +} +extern int +sdefl_bound(int len) { + int max_blocks = 1 + sdefl_div_round_up(len, SDEFL_RAW_BLK_SIZE); + int bound = 5 * max_blocks + len + 1 + 4 + 8 + 3; + return bound; +} + +#ifdef __cplusplus +} +#endif + +#endif /* SDEFL_IMPLEMENTATION */ diff --git a/Lib/meshoptimizer/gltf/README.md b/Lib/meshoptimizer/gltf/README.md new file mode 100644 index 0000000..fdefbd2 --- /dev/null +++ b/Lib/meshoptimizer/gltf/README.md @@ -0,0 +1,133 @@ +# 📦 gltfpack + +gltfpack is a tool that can automatically optimize glTF files to reduce the download size and improve loading and rendering speed. + +## Installation + +You can download a pre-built binary for gltfpack on [Releases page](https://github.com/zeux/meshoptimizer/releases), or install [npm package](https://www.npmjs.com/package/gltfpack). Native binaries are recommended over npm since they can work with larger files, run faster, and support texture compression. + +## Usage + +To convert a glTF file using gltfpack, run the command-line binary like this on an input `.gltf`/`.glb`/`.obj` file (run it without arguments for a list of options): + +``` +gltfpack -i scene.gltf -o scene.glb +``` + +gltfpack substantially changes the glTF data by optimizing the meshes for vertex fetch and transform cache, quantizing the geometry to reduce the memory consumption and size, merging meshes to reduce the draw call count, quantizing and resampling animations to reduce animation size and simplify playback, and pruning the node tree by removing or collapsing redundant nodes. It will also simplify the meshes when requested to do so. + +By default gltfpack outputs regular `.glb`/`.gltf` files that have been optimized for GPU consumption using various cache optimizers and quantization. These files can be loaded by GLTF loaders that support `KHR_mesh_quantization` extension such as [three.js](https://threejs.org/) (r111+) and [Babylon.js](https://www.babylonjs.com/) (4.1+). + +When using `-c` option, gltfpack outputs compressed `.glb`/`.gltf` files that use meshoptimizer codecs to reduce the download size further. Loading these files requires extending GLTF loaders with support for [EXT_meshopt_compression](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/EXT_meshopt_compression/README.md) extension; three.js supports it in r122+ (requires calling `GLTFLoader.setMeshoptDecoder`), Babylon.js supports it in 5.0+ without further setup. + +For better compression, you can use `-cc` option which applies additional compression; additionally make sure that your content delivery method is configured to use deflate (gzip) - meshoptimizer codecs are designed to produce output that can be compressed further with general purpose compressors. For maximum compression, consider using [KHR_meshopt_compression](https://github.com/KhronosGroup/glTF/pull/2517) extension with a higher compression level (`-cz` option). + +gltfpack can also compress textures using Basis Universal format stored in a KTX2 container (`-tc` flag, requires support for `KHR_texture_basisu`) or using WebP format (`-tw` flag, requires support for `EXT_texture_webp`). + +When working with glTF files that contain point clouds, gltfpack automatically processes the point cloud data to reduce the download size to the extent possible. In addition to aforementioned compression options (either `-c` or `-cc` are recommended), gltfpack can also prune point clouds to provide a more uniform density when `-si` option is used. + +## Decompression + +When using compressed files, [js/meshopt_decoder.mjs](https://github.com/zeux/meshoptimizer/blob/master/js/meshopt_decoder.mjs) needs to be loaded to provide the WebAssembly decoder module like this: + +```js +import { MeshoptDecoder } from './meshopt_decoder.mjs'; + +... + +var loader = new GLTFLoader(); +loader.setMeshoptDecoder(MeshoptDecoder); +loader.load('pirate.glb', function (gltf) { scene.add(gltf.scene); }); +``` + +When using Three.js, this module can be imported from three.js repository from `examples/jsm/libs/meshopt_decoder.module.js`. + +Note that `meshopt_decoder` assumes that WebAssembly is supported. This is the case for all modern browsers; if support for legacy browsers such as Internet Explorer 11 is desired, it's recommended to use `-cf` flag when creating the glTF content. This will create and load fallback uncompressed buffers, but only on browsers that don't support WebAssembly. + +## Options + +By default gltfpack makes certain assumptions when optimizing the scenes, for example meshes that belong to nodes that aren't animated can be merged together, and has some defaults that represent a tradeoff between precision and size that are picked to fit most use cases. However, in some cases the resulting `.gltf` file needs to retain some way for the application to manipulate individual scene elements, and in other cases precision or size are more important to optimize for. gltfpack has a rich set of command line options to control various aspects of its behavior, with the full list available via `gltfpack -h`. + +The following settings are frequently used to reduce the resulting data size: + +* `-cc`: produce compressed gltf/glb files (requires `EXT_meshopt_compression`) +* `-tc`: convert all textures to KTX2 with BasisU supercompression (requires `KHR_texture_basisu` and may require `-tp` flag for compatibility with WebGL 1) +* `-tw`: convert all textures to WebP (requires `EXT_texture_webp`) +* `-mi`: use mesh instancing when serializing references to the same meshes (requires `EXT_mesh_gpu_instancing`) +* `-si R`: simplify meshes targeting triangle/point count ratio R (default: 1; R should be between 0 and 1) + +The following settings are frequently used to restrict some optimizations: + +* `-kn`: keep named nodes and meshes attached to named nodes so that named nodes can be transformed externally +* `-km`: keep named materials and disable named material merging +* `-ke`: keep extras data +* `-vpf`: use floating-point position quantization instead of the default fixed-point (this results in larger position data, but does not insert new nodes with dequantization transforms; when using this option, `-cc` is recommended as well) + +## Extensions + +gltfpack supports most Khronos extensions and some multi-vendor extensions in the input scenes, with newer extensions added regularly. The following extensions are fully supported: + +- KHR_lights_punctual +- KHR_materials_anisotropy +- KHR_materials_clearcoat +- KHR_materials_diffuse_transmission +- KHR_materials_dispersion +- KHR_materials_emissive_strength +- KHR_materials_ior +- KHR_materials_iridescence +- KHR_materials_pbrSpecularGlossiness +- KHR_materials_sheen +- KHR_materials_specular +- KHR_materials_transmission +- KHR_materials_unlit +- KHR_materials_variants +- KHR_materials_volume +- KHR_mesh_quantization +- KHR_meshopt_compression +- KHR_texture_basisu +- KHR_texture_transform +- EXT_mesh_gpu_instancing +- EXT_meshopt_compression +- EXT_texture_webp + +Even if the source file does not use extensions, gltfpack may use some extensions in the output file either by default or when certain options are used: + +- KHR_mesh_quantization (used by default unless disabled via `-noq`) +- KHR_meshopt_compression (used when requested via `-ce khr` or `-cz`) +- KHR_texture_transform (used by default when textures are present, unless disabled via `-noq` or `-vtf`) +- KHR_texture_basisu (used when requested via `-tc` or `-tu`) +- EXT_meshopt_compression (used when requested via `-c` or `-cc`) +- EXT_mesh_gpu_instancing (used when requested via `-mi`) +- EXT_texture_webp (used when requested via `-tw`) + +gltfpack does not support vendor-specific extensions or custom extensions, including ones defined in [Khronos glTF repository](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor). Unknown extension nodes are discarded from the output. + +Some Khronos extensions, notably Draco compression and extensions for interactive scenes (interactivity, animation pointer, node selectability/hoverability/visibility), are not supported. + +## Custom data + +glTF files may contain custom application-specific data stored outside of custom extensions. gltfpack has limited support for preserving this data. + +Unknown vertex or instance attributes are generally discarded. While it would be possible to preserve them in theory, many scene transformations and analyses require a full understanding of the attribute semantics - some attributes need to be transformed with the mesh transformation matrix, some need to be adjusted when meshes are merged, some influence how meshes are rendered, etc. However, gltfpack supports some specific attributes that are commonly used in the ecosystem: + +- Vertex attributes that store integer IDs with names `_ID`, `_BATCHID` or `_FEATURE_ID_n` are preserved as-is. These can be used to identify objects. +- Instance attribute `_COLOR_0` is preserved as-is. This can be used to provide per-instance color tinting. + +Additional application-specific data can be stored via the glTF `extras` property. While extras (outside of `asset`) are discarded by default, using the `-ke` option preserves extras in materials, nodes, primitives and scenes. + +In addition to `asset` extras, gltfpack unconditionally preserves morph target names specified via the `targetNames` property inside `extras` on meshes. + +## Building + +gltfpack can be built from source using CMake or Make. To build a full version of gltfpack that supports texture compression, CMake configuration needs to specify the path to https://github.com/zeux/basis_universal fork (branch gltfpack) via `MESHOPT_GLTFPACK_BASISU_PATH` variable, as well as libwebp path via `MESHOPT_GLTFPACK_LIBWEBP_PATH` variable: + +``` +git clone -b gltfpack https://github.com/zeux/basis_universal +git clone https://github.com/webmproject/libwebp +cmake . -DMESHOPT_BUILD_GLTFPACK=ON -DMESHOPT_GLTFPACK_BASISU_PATH=basis_universal -DMESHOPT_GLTFPACK_LIBWEBP_PATH=libwebp -DCMAKE_BUILD_TYPE=Release +cmake --build . --target gltfpack --config Release +``` + +## License + +gltfpack is available to anybody free of charge, under the terms of MIT License (see LICENSE.md). diff --git a/Lib/meshoptimizer/gltf/animation.cpp b/Lib/meshoptimizer/gltf/animation.cpp new file mode 100644 index 0000000..2f09f61 --- /dev/null +++ b/Lib/meshoptimizer/gltf/animation.cpp @@ -0,0 +1,356 @@ +// This file is part of gltfpack; see gltfpack.h for version/license details +#include "gltfpack.h" + +#include + +#include +#include +#include + +static float getScaleDelta(float l, float r) +{ + float d = fabsf(l - r); + float m = std::max(fabsf(l), fabsf(r)); + return m == 0.f ? 0.f : d / m; +} + +static float getDelta(const Attr& l, const Attr& r, cgltf_animation_path_type type) +{ + switch (type) + { + case cgltf_animation_path_type_translation: + return std::max(std::max(fabsf(l.f[0] - r.f[0]), fabsf(l.f[1] - r.f[1])), fabsf(l.f[2] - r.f[2])); + + case cgltf_animation_path_type_rotation: + return 2 * acosf(std::min(1.f, fabsf(l.f[0] * r.f[0] + l.f[1] * r.f[1] + l.f[2] * r.f[2] + l.f[3] * r.f[3]))); + + case cgltf_animation_path_type_scale: + return std::max(std::max(getScaleDelta(l.f[0], r.f[0]), getScaleDelta(l.f[1], r.f[1])), getScaleDelta(l.f[2], r.f[2])); + + case cgltf_animation_path_type_weights: + return fabsf(l.f[0] - r.f[0]); + + default: + assert(!"Unknown animation path"); + return 0; + } +} + +static float getDeltaTolerance(cgltf_animation_path_type type) +{ + switch (type) + { + case cgltf_animation_path_type_translation: + return 0.0001f; // 0.1mm linear + + case cgltf_animation_path_type_rotation: + return 0.1f * (3.1415926f / 180.f); // 0.1 degrees + + case cgltf_animation_path_type_scale: + return 0.001f; // 0.1% ratio + + case cgltf_animation_path_type_weights: + return 0.001f; // 0.1% linear + + default: + assert(!"Unknown animation path"); + return 0; + } +} + +static Attr interpolateLinear(const Attr& l, const Attr& r, float t, cgltf_animation_path_type type) +{ + if (type == cgltf_animation_path_type_rotation) + { + // Approximating slerp, https://zeux.io/2015/07/23/approximating-slerp/ + // We also handle quaternion double-cover + float ca = l.f[0] * r.f[0] + l.f[1] * r.f[1] + l.f[2] * r.f[2] + l.f[3] * r.f[3]; + + float d = fabsf(ca); + float A = 1.0904f + d * (-3.2452f + d * (3.55645f - d * 1.43519f)); + float B = 0.848013f + d * (-1.06021f + d * 0.215638f); + float k = A * (t - 0.5f) * (t - 0.5f) + B; + float ot = t + t * (t - 0.5f) * (t - 1) * k; + + float t0 = 1 - ot; + float t1 = ca > 0 ? ot : -ot; + + Attr lerp = {{ + l.f[0] * t0 + r.f[0] * t1, + l.f[1] * t0 + r.f[1] * t1, + l.f[2] * t0 + r.f[2] * t1, + l.f[3] * t0 + r.f[3] * t1, + }}; + + float len = sqrtf(lerp.f[0] * lerp.f[0] + lerp.f[1] * lerp.f[1] + lerp.f[2] * lerp.f[2] + lerp.f[3] * lerp.f[3]); + + if (len > 0.f) + { + lerp.f[0] /= len; + lerp.f[1] /= len; + lerp.f[2] /= len; + lerp.f[3] /= len; + } + + return lerp; + } + else + { + Attr lerp = {{ + l.f[0] * (1 - t) + r.f[0] * t, + l.f[1] * (1 - t) + r.f[1] * t, + l.f[2] * (1 - t) + r.f[2] * t, + l.f[3] * (1 - t) + r.f[3] * t, + }}; + + return lerp; + } +} + +static Attr interpolateHermite(const Attr& v0, const Attr& t0, const Attr& v1, const Attr& t1, float t, float dt, cgltf_animation_path_type type) +{ + float s0 = 1 + t * t * (2 * t - 3); + float s1 = t + t * t * (t - 2); + float s2 = 1 - s0; + float s3 = t * t * (t - 1); + + float ts1 = dt * s1; + float ts3 = dt * s3; + + Attr lerp = {{ + s0 * v0.f[0] + ts1 * t0.f[0] + s2 * v1.f[0] + ts3 * t1.f[0], + s0 * v0.f[1] + ts1 * t0.f[1] + s2 * v1.f[1] + ts3 * t1.f[1], + s0 * v0.f[2] + ts1 * t0.f[2] + s2 * v1.f[2] + ts3 * t1.f[2], + s0 * v0.f[3] + ts1 * t0.f[3] + s2 * v1.f[3] + ts3 * t1.f[3], + }}; + + if (type == cgltf_animation_path_type_rotation) + { + float len = sqrtf(lerp.f[0] * lerp.f[0] + lerp.f[1] * lerp.f[1] + lerp.f[2] * lerp.f[2] + lerp.f[3] * lerp.f[3]); + + if (len > 0.f) + { + lerp.f[0] /= len; + lerp.f[1] /= len; + lerp.f[2] /= len; + lerp.f[3] /= len; + } + } + + return lerp; +} + +static void resampleKeyframes(std::vector& data, const std::vector& input, const std::vector& output, cgltf_animation_path_type type, cgltf_interpolation_type interpolation, size_t components, int frames, float mint, int freq) +{ + size_t cursor = 0; + + for (int i = 0; i < frames; ++i) + { + float time = mint + float(i) / freq; + + while (cursor + 1 < input.size()) + { + float next_time = input[cursor + 1]; + + if (next_time > time) + break; + + cursor++; + } + + if (cursor + 1 < input.size()) + { + float cursor_time = input[cursor + 0]; + float next_time = input[cursor + 1]; + + float range = next_time - cursor_time; + float inv_range = (range == 0.f) ? 0.f : 1.f / (next_time - cursor_time); + float t = std::max(0.f, std::min(1.f, (time - cursor_time) * inv_range)); + + for (size_t j = 0; j < components; ++j) + { + switch (interpolation) + { + case cgltf_interpolation_type_linear: + { + const Attr& v0 = output[(cursor + 0) * components + j]; + const Attr& v1 = output[(cursor + 1) * components + j]; + data.push_back(interpolateLinear(v0, v1, t, type)); + } + break; + + case cgltf_interpolation_type_step: + { + const Attr& v = output[cursor * components + j]; + data.push_back(v); + } + break; + + case cgltf_interpolation_type_cubic_spline: + { + const Attr& v0 = output[(cursor * 3 + 1) * components + j]; + const Attr& b0 = output[(cursor * 3 + 2) * components + j]; + const Attr& a1 = output[(cursor * 3 + 3) * components + j]; + const Attr& v1 = output[(cursor * 3 + 4) * components + j]; + data.push_back(interpolateHermite(v0, b0, v1, a1, t, range, type)); + } + break; + + default: + assert(!"Unknown interpolation type"); + } + } + } + else + { + size_t offset = (interpolation == cgltf_interpolation_type_cubic_spline) ? cursor * 3 + 1 : cursor; + + for (size_t j = 0; j < components; ++j) + { + const Attr& v = output[offset * components + j]; + data.push_back(v); + } + } + } +} + +static float getMaxDelta(const std::vector& data, cgltf_animation_path_type type, const Attr* value, size_t components) +{ + assert(data.size() % components == 0); + + float result = 0; + + for (size_t i = 0; i < data.size(); i += components) + { + for (size_t j = 0; j < components; ++j) + { + float delta = getDelta(value[j], data[i + j], type); + + result = (result < delta) ? delta : result; + } + } + + return result; +} + +static void getBaseTransform(Attr* result, size_t components, cgltf_animation_path_type type, cgltf_node* node) +{ + switch (type) + { + case cgltf_animation_path_type_translation: + memcpy(result->f, node->translation, 3 * sizeof(float)); + break; + + case cgltf_animation_path_type_rotation: + memcpy(result->f, node->rotation, 4 * sizeof(float)); + break; + + case cgltf_animation_path_type_scale: + memcpy(result->f, node->scale, 3 * sizeof(float)); + break; + + case cgltf_animation_path_type_weights: + if (node->weights_count) + { + assert(node->weights_count == components); + for (size_t k = 0; k < components; ++k) + result[k].f[0] = node->weights[k]; + } + else if (node->mesh && node->mesh->weights_count) + { + assert(node->mesh->weights_count == components); + for (size_t k = 0; k < components; ++k) + result[k].f[0] = node->mesh->weights[k]; + } + break; + + default: + assert(!"Unknown animation path"); + } +} + +static float getWorldScale(cgltf_node* node) +{ + float transform[16]; + cgltf_node_transform_world(node, transform); + + // 3x3 determinant computes scale^3 + float a0 = transform[5] * transform[10] - transform[6] * transform[9]; + float a1 = transform[4] * transform[10] - transform[6] * transform[8]; + float a2 = transform[4] * transform[9] - transform[5] * transform[8]; + float det = transform[0] * a0 - transform[1] * a1 + transform[2] * a2; + + return powf(fabsf(det), 1.f / 3.f); +} + +void processAnimation(Animation& animation, const Settings& settings) +{ + float mint = FLT_MAX, maxt = 0; + + for (size_t i = 0; i < animation.tracks.size(); ++i) + { + const Track& track = animation.tracks[i]; + assert(!track.time.empty()); + + mint = std::min(mint, track.time.front()); + maxt = std::max(maxt, track.time.back()); + } + + animation.start = mint = std::min(mint, maxt); + + if (settings.anim_freq) + { + // round the number of frames to nearest but favor the "up" direction + // this means that at 100 Hz resampling, we will try to preserve the last frame <10ms + // but if the last frame is <2ms we favor just removing this data + int frames = 1 + int((maxt - mint) * settings.anim_freq + 0.8f); + + animation.frames = frames; + } + + std::vector base; + + for (size_t i = 0; i < animation.tracks.size(); ++i) + { + Track& track = animation.tracks[i]; + + if (settings.anim_freq) + { + std::vector result; + resampleKeyframes(result, track.time, track.data, track.path, track.interpolation, track.components, animation.frames, animation.start, settings.anim_freq); + + track.time.clear(); + track.data.swap(result); + track.interpolation = track.interpolation == cgltf_interpolation_type_cubic_spline ? cgltf_interpolation_type_linear : track.interpolation; + } + + // getMaxDelta assumes linear/step interpolation for now + if (track.interpolation == cgltf_interpolation_type_cubic_spline) + continue; + + float tolerance = getDeltaTolerance(track.path); + + // translation tracks use world space tolerance; in the future, we should compute all errors as linear using hierarchy + if (track.node && track.node->parent && track.path == cgltf_animation_path_type_translation) + { + float scale = getWorldScale(track.node->parent); + tolerance /= scale == 0.f ? 1.f : scale; + } + + float deviation = getMaxDelta(track.data, track.path, &track.data[0], track.components); + + if (deviation <= tolerance) + { + // track is constant (equal to first keyframe), we only need the first keyframe + track.constant = true; + track.time.clear(); + track.data.resize(track.components); + + // track.dummy is true iff track redundantly sets up the value to be equal to default node transform + base.resize(track.components); + getBaseTransform(&base[0], track.components, track.path, track.node); + + track.dummy = getMaxDelta(track.data, track.path, &base[0], track.components) <= tolerance; + } + } +} diff --git a/Lib/meshoptimizer/gltf/cli.js b/Lib/meshoptimizer/gltf/cli.js new file mode 100644 index 0000000..336985b --- /dev/null +++ b/Lib/meshoptimizer/gltf/cli.js @@ -0,0 +1,25 @@ +#!/usr/bin/env node +// This file is part of gltfpack and is distributed under the terms of MIT License. +import { pack } from './library.js'; +import fs from 'fs'; + +var args = process.argv.slice(2); + +var iface = { + read: function (path) { + return fs.readFileSync(path); + }, + write: function (path, data) { + fs.writeFileSync(path, data); + }, +}; + +pack(args, iface) + .then(function (log) { + process.stdout.write(log); + process.exit(0); + }) + .catch(function (err) { + process.stderr.write(err.message); + process.exit(1); + }); diff --git a/Lib/meshoptimizer/gltf/encodebasis.cpp b/Lib/meshoptimizer/gltf/encodebasis.cpp new file mode 100644 index 0000000..720262d --- /dev/null +++ b/Lib/meshoptimizer/gltf/encodebasis.cpp @@ -0,0 +1,214 @@ +// This file is part of gltfpack; see gltfpack.h for version/license details +#ifdef WITH_BASISU +#include "gltfpack.h" + +#define BASISU_NO_ITERATOR_DEBUG_LEVEL + +#ifdef __clang__ +#pragma GCC diagnostic ignored "-Wunknown-warning-option" +#pragma GCC diagnostic ignored "-Warray-bounds" +#pragma GCC diagnostic ignored "-Wc++17-extensions" +#pragma GCC diagnostic ignored "-Wdeprecated-builtins" +#endif + +#ifdef __GNUC__ +#pragma GCC diagnostic ignored "-Wclass-memaccess" +#pragma GCC diagnostic ignored "-Wshadow" +#pragma GCC diagnostic ignored "-Wunused-local-typedefs" +#pragma GCC diagnostic ignored "-Wunused-value" +#endif + +#if defined(__GNUC__) && __GNUC__ >= 12 +#pragma GCC diagnostic ignored "-Wc++17-extensions" +#endif + +#include "encoder/basisu_comp.h" + +struct BasisSettings +{ + int etc1s_l; + int etc1s_q; + int uastc_l; + float uastc_q; +}; + +static const BasisSettings kBasisSettings[10] = { + {1, 1, 0, 4.f}, + {1, 32, 0, 3.f}, + {1, 64, 1, 2.f}, + {1, 96, 1, 1.5f}, + {1, 128, 1, 1.f}, // quality arguments aligned with basisu defaults + {1, 150, 1, 0.8f}, + {1, 170, 1, 0.6f}, + {1, 192, 1, 0.4f}, // gltfpack defaults + {1, 224, 2, 0.2f}, + {1, 255, 2, 0.f}, +}; + +static void fillParams(basisu::basis_compressor_params& params, const char* input, const char* output, bool uastc, int width, int height, const BasisSettings& bs, const ImageInfo& info, const Settings& settings) +{ + if (uastc) + { + static const uint32_t s_level_flags[basisu::TOTAL_PACK_UASTC_LEVELS] = {basisu::cPackUASTCLevelFastest, basisu::cPackUASTCLevelFaster, basisu::cPackUASTCLevelDefault, basisu::cPackUASTCLevelSlower, basisu::cPackUASTCLevelVerySlow}; + + params.m_uastc = true; + +#if BASISU_LIB_VERSION >= 160 + params.m_pack_uastc_ldr_4x4_flags &= ~basisu::cPackUASTCLevelMask; + params.m_pack_uastc_ldr_4x4_flags |= s_level_flags[bs.uastc_l]; + + params.m_rdo_uastc_ldr_4x4 = bs.uastc_q > 0; + params.m_rdo_uastc_ldr_4x4_quality_scalar = bs.uastc_q; + params.m_rdo_uastc_ldr_4x4_dict_size = 1024; +#else + params.m_pack_uastc_flags &= ~basisu::cPackUASTCLevelMask; + params.m_pack_uastc_flags |= s_level_flags[bs.uastc_l]; + + params.m_rdo_uastc = bs.uastc_q > 0; + params.m_rdo_uastc_quality_scalar = bs.uastc_q; + params.m_rdo_uastc_dict_size = 1024; +#endif + } + else + { +#if BASISU_LIB_VERSION >= 200 + params.m_etc1s_compression_level = bs.etc1s_l; + params.m_quality_level = bs.etc1s_q; + params.m_etc1s_max_endpoint_clusters = 0; + params.m_etc1s_max_selector_clusters = 0; +#elif BASISU_LIB_VERSION >= 160 + params.m_compression_level = bs.etc1s_l; + params.m_etc1s_quality_level = bs.etc1s_q; + params.m_etc1s_max_endpoint_clusters = 0; + params.m_etc1s_max_selector_clusters = 0; +#else + params.m_compression_level = bs.etc1s_l; + params.m_quality_level = bs.etc1s_q; + params.m_max_endpoint_clusters = 0; + params.m_max_selector_clusters = 0; +#endif + + params.m_no_selector_rdo = info.normal_map; + params.m_no_endpoint_rdo = info.normal_map; + } + + params.m_perceptual = info.srgb; + + params.m_mip_gen = true; + params.m_mip_srgb = info.srgb; + + params.m_resample_width = width; + params.m_resample_height = height; + + params.m_y_flip = settings.texture_flipy; + + params.m_create_ktx2_file = true; + +#if BASISU_LIB_VERSION >= 200 + params.m_ktx2_and_basis_srgb_transfer_function = info.srgb; +#else + params.m_ktx2_srgb_transfer_func = info.srgb; +#endif + + if (uastc) + { + params.m_ktx2_uastc_supercompression = basist::KTX2_SS_ZSTANDARD; + params.m_ktx2_zstd_supercompression_level = 9; + } + + params.m_read_source_images = true; + +#if BASISU_LIB_VERSION >= 150 + params.m_write_output_basis_or_ktx2_files = true; +#else + params.m_write_output_basis_files = true; +#endif + + params.m_source_filenames.resize(1); + params.m_source_filenames[0] = input; + + params.m_out_filename = output; + + params.m_status_output = false; +} + +static const char* prepareEncode(basisu::basis_compressor_params& params, const cgltf_image& image, const char* input_path, const ImageInfo& info, const Settings& settings, const std::string& temp_prefix, std::string& temp_input, std::string& temp_output) +{ + std::string img_data; + std::string mime_type; + + if (!readImage(image, input_path, img_data, mime_type)) + return "error reading source file"; + + if (mime_type != "image/png" && mime_type != "image/jpeg") + return NULL; + + int width = 0, height = 0; + if (!getDimensions(img_data, mime_type.c_str(), width, height)) + return "error parsing image header"; + + adjustDimensions(width, height, settings.texture_scale[info.kind], settings.texture_limit[info.kind], settings.texture_pow2); + + temp_input = temp_prefix + mimeExtension(mime_type.c_str()); + temp_output = temp_prefix + ".ktx2"; + + if (!writeFile(temp_input.c_str(), img_data)) + return "error writing temporary file"; + + int quality = settings.texture_quality[info.kind]; + bool uastc = settings.texture_mode[info.kind] == TextureMode_UASTC; + + const BasisSettings& bs = kBasisSettings[quality - 1]; + + fillParams(params, temp_input.c_str(), temp_output.c_str(), uastc, width, height, bs, info, settings); + + return NULL; +} + +void encodeImagesBasis(std::string* encoded, const cgltf_data* data, const std::vector& images, const char* input_path, const Settings& settings) +{ + basisu::basisu_encoder_init(); + + basisu::vector params(data->images_count); + basisu::vector results(data->images_count); + + std::string temp_prefix = getTempPrefix(); + + std::vector temp_inputs(data->images_count); + std::vector temp_outputs(data->images_count); + + for (size_t i = 0; i < data->images_count; ++i) + { + const cgltf_image& image = data->images[i]; + ImageInfo info = images[i]; + + if (settings.texture_mode[info.kind] == TextureMode_ETC1S || settings.texture_mode[info.kind] == TextureMode_UASTC) + if (const char* error = prepareEncode(params[i], image, input_path, info, settings, temp_prefix + "-" + std::to_string(i), temp_inputs[i], temp_outputs[i])) + encoded[i] = error; + } + + uint32_t num_threads = settings.texture_jobs == 0 ? std::thread::hardware_concurrency() : settings.texture_jobs; + + basisu::basis_parallel_compress(num_threads, params, results); + + for (size_t i = 0; i < data->images_count; ++i) + { + if (params[i].m_source_filenames.empty()) + ; // encoding was skipped or preparation resulted in an error + else if (results[i].m_error_code == basisu::basis_compressor::cECFailedReadingSourceImages) + encoded[i] = "error decoding source image"; + else if (results[i].m_error_code != basisu::basis_compressor::cECSuccess) + encoded[i] = "error encoding image"; + else if (!readFile(temp_outputs[i].c_str(), encoded[i])) + encoded[i] = "error reading temporary file"; + } + + for (size_t i = 0; i < data->images_count; ++i) + { + if (!temp_inputs[i].empty()) + removeFile(temp_inputs[i].c_str()); + if (!temp_outputs[i].empty()) + removeFile(temp_outputs[i].c_str()); + } +} +#endif diff --git a/Lib/meshoptimizer/gltf/encodewebp.cpp b/Lib/meshoptimizer/gltf/encodewebp.cpp new file mode 100644 index 0000000..5042b37 --- /dev/null +++ b/Lib/meshoptimizer/gltf/encodewebp.cpp @@ -0,0 +1,169 @@ +// This file is part of gltfpack; see gltfpack.h for version/license details +#ifdef WITH_LIBWEBP +#include "gltfpack.h" + +#include "webp/decode.h" +#include "webp/encode.h" + +#ifndef WITH_LIBWEBP_BASIS +#include "imageio/image_dec.h" +#endif + +#include +#include +#include + +static int writeWebP(const uint8_t* data, size_t data_size, const WebPPicture* picture) +{ + std::string* encoded = static_cast(picture->custom_ptr); + encoded->append(reinterpret_cast(data), data_size); + return 1; +} + +// when gltfpack is built with Basis Universal, we have easy access to its jpeg/png decoders +#ifdef WITH_LIBWEBP_BASIS +namespace pv_png +{ +void* load_png(const void* pImage_buf, size_t buf_size, uint32_t desired_chans, uint32_t& width, uint32_t& height, uint32_t& num_chans); +} // namespace pv_png + +namespace jpgd +{ +static const uint32_t cFlagLinearChromaFiltering = 1; +unsigned char* decompress_jpeg_image_from_memory(const unsigned char* pSrc_data, int src_data_size, int* width, int* height, int* actual_comps, int req_comps, uint32_t flags = 0); +} // namespace jpgd + +typedef int (*WebPImageReader)(const uint8_t* const data, size_t data_size, struct WebPPicture* const pic, int keep_alpha, struct Metadata* const metadata); + +static int readPngBasis(const uint8_t* const data, size_t data_size, struct WebPPicture* const pic, int keep_alpha, struct Metadata* const metadata) +{ + (void)keep_alpha; + (void)metadata; + + uint32_t width = 0, height = 0, channels = 0; + void* img = pv_png::load_png(data, data_size, 4, width, height, channels); + if (!img) + return 0; + + pic->width = width; + pic->height = height; + int ok = WebPPictureImportRGBA(pic, static_cast(img), width * 4); + free(img); + return ok; +} + +static int readJpegBasis(const uint8_t* const data, size_t data_size, struct WebPPicture* const pic, int keep_alpha, struct Metadata* const metadata) +{ + (void)keep_alpha; + (void)metadata; + + int width = 0, height = 0, channels = 0; + unsigned char* img = jpgd::decompress_jpeg_image_from_memory(data, int(data_size), &width, &height, &channels, 4, jpgd::cFlagLinearChromaFiltering); + if (!img) + return 0; + + pic->width = width; + pic->height = height; + int ok = WebPPictureImportRGBA(pic, img, width * 4); + free(img); + return ok; +} +#endif + +static const char* encodeWebP(const cgltf_image& image, const char* input_path, const ImageInfo& info, const Settings& settings, std::string& encoded) +{ + WebPConfig config; + if (!WebPConfigInit(&config)) + return "error initializing WebP"; + + std::string img_data; + std::string mime_type; + + if (!readImage(image, input_path, img_data, mime_type)) + return "error reading source file"; + + if (mime_type != "image/png" && mime_type != "image/jpeg") + return NULL; + + int width = 0, height = 0; + if (!getDimensions(img_data, mime_type.c_str(), width, height)) + return "error parsing image header"; + + adjustDimensions(width, height, settings.texture_scale[info.kind], settings.texture_limit[info.kind], settings.texture_pow2); + +#ifdef WITH_LIBWEBP_BASIS + WebPImageReader reader = (mime_type == "image/jpeg") ? readJpegBasis : readPngBasis; +#else + WebPInputFileFormat format = (mime_type == "image/jpeg") ? WEBP_JPEG_FORMAT : WEBP_PNG_FORMAT; + WebPImageReader reader = WebPGetImageReader(format); + if (!reader) + return "unsupported image format"; +#endif + + WebPPicture pic; + if (!WebPPictureInit(&pic)) + return "error initializing picture"; + + std::unique_ptr pic_storage(&pic, WebPPictureFree); + + if (!reader(reinterpret_cast(img_data.data()), img_data.size(), &pic, 1, NULL)) + return "error decoding source image"; + + if ((width != pic.width || height != pic.height) && !WebPPictureRescale(&pic, width, height)) + return "error resizing image"; + + int quality = settings.texture_quality[info.kind]; + + if (info.normal_map) + config.quality = float(50 + quality * 5); // map 1-10 to 55-100 + else + config.quality = float(20 + quality * 8); // map 1-10 to 28-100 + + config.emulate_jpeg_size = 1; // for flatter quality curve + + pic.writer = writeWebP; + pic.custom_ptr = &encoded; + encoded.clear(); + + if (!WebPEncode(&config, &pic)) + return "error encoding image"; + + return NULL; +} + +void encodeImagesWebP(std::string* encoded, const cgltf_data* data, const std::vector& images, const char* input_path, const Settings& settings) +{ + std::atomic next_image{0}; + + auto encode = [&]() + { + for (;;) + { + size_t i = next_image++; + if (i >= data->images_count) + break; + + const cgltf_image& image = data->images[i]; + ImageInfo info = images[i]; + + if (settings.texture_mode[info.kind] == TextureMode_WebP) + if (const char* error = encodeWebP(image, input_path, info, settings, encoded[i])) + encoded[i] = error; + } + }; + + // we use main thread as a worker as well + size_t worker_count = settings.texture_jobs == 0 ? std::thread::hardware_concurrency() : settings.texture_jobs; + size_t thread_count = worker_count > 0 ? worker_count - 1 : 0; + + std::vector threads; + for (size_t i = 0; i < thread_count; ++i) + threads.emplace_back(encode); + + encode(); + + for (size_t i = 0; i < thread_count; ++i) + threads[i].join(); +} + +#endif diff --git a/Lib/meshoptimizer/gltf/fileio.cpp b/Lib/meshoptimizer/gltf/fileio.cpp new file mode 100644 index 0000000..ad0e3d3 --- /dev/null +++ b/Lib/meshoptimizer/gltf/fileio.cpp @@ -0,0 +1,115 @@ +// This file is part of gltfpack; see gltfpack.h for version/license details +#include "gltfpack.h" + +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +std::string getTempPrefix() +{ +#if defined(_WIN32) + const char* temp_dir = getenv("TEMP"); + std::string path = temp_dir ? temp_dir : "."; + path += "\\gltfpack-temp"; + path += std::to_string(_getpid()); + return path; +#elif defined(__wasi__) + return "gltfpack-temp"; +#else + std::string path = "/tmp/gltfpack-temp"; + path += std::to_string(getpid()); + return path; +#endif +} + +std::string getFullPath(const char* path, const char* base_path) +{ + std::string result = base_path; + + std::string::size_type slash = result.find_last_of("/\\"); + result.erase(slash == std::string::npos ? 0 : slash + 1); + + result += path; + + return result; +} + +std::string getFileName(const char* path) +{ + std::string result = path; + + std::string::size_type slash = result.find_last_of("/\\"); + if (slash != std::string::npos) + result.erase(0, slash + 1); + + std::string::size_type dot = result.find_last_of('.'); + if (dot != std::string::npos) + result.erase(dot); + + return result; +} + +std::string getExtension(const char* path) +{ + std::string result = path; + + std::string::size_type slash = result.find_last_of("/\\"); + std::string::size_type dot = result.find_last_of('.'); + + if (slash != std::string::npos && dot != std::string::npos && dot < slash) + dot = std::string::npos; + + result.erase(0, dot); + + for (size_t i = 0; i < result.length(); ++i) + if (unsigned(result[i] - 'A') < 26) + result[i] = (result[i] - 'A') + 'a'; + + return result; +} + +bool readFile(const char* path, std::string& data) +{ + FILE* file = fopen(path, "rb"); + if (!file) + return false; + + fseek(file, 0, SEEK_END); + long length = ftell(file); + fseek(file, 0, SEEK_SET); + + if (length <= 0) + { + fclose(file); + return false; + } + + data.resize(length); + size_t result = fread(&data[0], 1, data.size(), file); + int rc = fclose(file); + + return rc == 0 && result == data.size(); +} + +bool writeFile(const char* path, const std::string& data) +{ + FILE* file = fopen(path, "wb"); + if (!file) + return false; + + size_t result = fwrite(&data[0], 1, data.size(), file); + int rc = fclose(file); + + return rc == 0 && result == data.size(); +} + +void removeFile(const char* path) +{ + remove(path); +} diff --git a/Lib/meshoptimizer/gltf/fuzz.dict b/Lib/meshoptimizer/gltf/fuzz.dict new file mode 100644 index 0000000..64eeb97 --- /dev/null +++ b/Lib/meshoptimizer/gltf/fuzz.dict @@ -0,0 +1,303 @@ +# +# AFL dictionary for JSON +# ----------------------- +# +# Just the very basics. +# +# Inspired by a dictionary by Jakub Wilk +# + +"0" +",0" +":0" +"0:" +"-1.2e+3" + +"true" +"false" +"null" + +"\"\"" +",\"\"" +":\"\"" +"\"\":" + +"{}" +",{}" +":{}" +"{\"\":0}" +"{{}}" + +"[]" +",[]" +":[]" +"[0]" +"[[]]" + +"''" +"\\" +"\\b" +"\\f" +"\\n" +"\\r" +"\\t" +"\\u0000" +"\\x00" +"\\0" +"\\uD800\\uDC00" +"\\uDBFF\\uDFFF" + +"\"\":0" +"//" +"/**/" + +# +# AFL dictionary for GLTF core +# ----------------------- + +"5120" +"5121" +"5122" +"5123" +"5125" +"5126" +"\"BLEND\"" +"\"CUBICSPLINE\"" +"\"LINEAR\"" +"\"MASK\"" +"\"MAT2\"" +"\"MAT3\"" +"\"MAT4\"" +"\"OPAQUE\"" +"\"POSITION\"" +"\"NORMAL\"" +"\"TANGENT\"" +"\"TEXCOORD\"" +"\"JOINTS\"" +"\"WEIGHTS\"" +"\"SCALAR\"" +"\"STEP\"" +"\"VEC2\"" +"\"VEC3\"" +"\"VEC4\"" +"\"accessor\"" +"\"accessors\"" +"\"alphaCutoff\"" +"\"alphaMode\"" +"\"animations\"" +"\"aspectRatio\"" +"\"asset\"" +"\"attributes\"" +"\"baseColorFactor\"" +"\"baseColorTexture\"" +"\"bufferView\"" +"\"bufferViews\"" +"\"buffer\"" +"\"buffers\"" +"\"byteLength\"" +"\"byteOffset\"" +"\"byteStride\"" +"\"camera\"" +"\"cameras\"" +"\"channel\"" +"\"channels\"" +"\"children\"" +"\"componentType\"" +"\"copyright\"" +"\"count\"" +"\"doubleSided\"" +"\"emissiveFactor\"" +"\"emissiveTexture\"" +"\"extensionsRequired\"" +"\"extensionsUsed\"" +"\"extensions\"" +"\"extras\"" +"\"generator\"" +"\"image\"" +"\"images\"" +"\"index\"" +"\"indices\"" +"\"input\"" +"\"interpolation\"" +"\"inverseBindMatrices\"" +"\"joints\"" +"\"magFilter\"" +"\"material\"" +"\"materials\"" +"\"matrix\"" +"\"max\"" +"\"mesh\"" +"\"meshes\"" +"\"metallicFactor\"" +"\"metallicRoughnessTexture\"" +"\"mimeType\"" +"\"minFilter\"" +"\"minVersion\"" +"\"min\"" +"\"mode\"" +"\"name\"" +"\"node\"" +"\"nodes\"" +"\"normalTextureInfo\"" +"\"normalTexture\"" +"\"normalized\"" +"\"occlusionTextureInfo\"" +"\"occlusionTexture\"" +"\"orthographic\"" +"\"output\"" +"\"path\"" +"\"pbrMetallicRoughness\"" +"\"perspective\"" +"\"primitive\"" +"\"primitives\"" +"\"rotation\"" +"\"roughnessFactor\"" +"\"sampler\"" +"\"samplers\"" +"\"scale\"" +"\"scene\"" +"\"scenes\"" +"\"skeleton\"" +"\"skin\"" +"\"skins\"" +"\"source\"" +"\"sparse\"" +"\"strength\"" +"\"target\"" +"\"targets\"" +"\"texCoord\"" +"\"textureInfo\"" +"\"texture\"" +"\"textures\"" +"\"translation\"" +"\"type\"" +"\"uri\"" +"\"values\"" +"\"version\"" +"\"weights\"" +"\"wrapS\"" +"\"wrapT\"" +"\"xmag\"" +"\"yfov\"" +"\"ymag\"" +"\"zfar\"" +"\"znear\"" + +# +# AFL dictionary for GLTF extensions +# ----------------------- +"\"KHR_materials_unlit\"" +"\"KHR_draco_mesh_compression\"" +"\"KHR_texture_basisu\"" + +"\"KHR_materials_pbrSpecularGlossiness\"" +"\"diffuseFactor\"" +"\"diffuseTexture\"" +"\"specularFactor\"" +"\"glossinessFactor\"" +"\"specularGlossinessTexture\"" + +"\"KHR_texture_transform\"" +"\"offset\"" +"\"rotation\"" +"\"scale\"" +"\"texCoord\"" + +"\"KHR_lights_punctual\"" +"\"color\"" +"\"directional\"" +"\"intensity\"" +"\"light\"" +"\"lights\"" +"\"point\"" +"\"spot\"" +"\"type\"" +"\"range\"" +"\"innerConeAngle\"" +"\"outerConeAngle\"" + +"\"KHR_materials_transmission\"" +"\"transmissionFactor\"" +"\"transmissionTexture\"" + +"\"KHR_materials_volume\"" +"\"thicknessFactor\"" +"\"thicknessTexture\"" +"\"attenuationColor\"" +"\"attenuationDistance\"" + +"\"KHR_materials_sheen\"" +"\"sheenColorFactor\"" +"\"sheenColorTexture\"" +"\"sheenRoughnessFactor\"" +"\"sheenRoughnessTexture\"" + +"\"KHR_materials_emissive_strength\"" +"\"emissiveStrength\"" + +"\"KHR_materials_clearcoat\"" +"\"clearcoatFactor\"" +"\"clearcoatTexture\"" +"\"clearcoatRoughnessFactor\"" +"\"clearcoatRoughnessTexture\"" +"\"clearcoatNormalTexture\"" + +"\"KHR_materials_ior\"" +"\"ior\"" + +"\"KHR_materials_specular\"" +"\"specularFactor\"" +"\"specularTexture\"" +"\"specularColorFactor\"" +"\"specularColorTexture\"" + +"\"KHR_materials_iridescence\"" +"\"iridescenceFactor\"" +"\"iridescenceTexture\"" +"\"iridescenceIor\"" +"\"iridescenceThicknessMinimum\"" +"\"iridescenceThicknessMaximum\"" +"\"iridescenceThicknessTexture\"" + +"\"KHR_materials_anisotropy\"" +"\"anisotropyStrength\"" +"\"anisotropyRotation\"" +"\"anisotropyTexture\"" + +"\"KHR_materials_dispersion\"" +"\"dispersion\"" + +"\"KHR_materials_diffuse_transmission\"" +"\"diffuseTransmissionFactor\"" +"\"diffuseTransmissionTexture\"" +"\"diffuseTransmissionColorFactor\"" +"\"diffuseTransmissionColorTexture\"" + +"\"KHR_materials_variants\"" +"\"mappings\"" +"\"targetNames\"" +"\"variants\"" + +"\"EXT_texture_webp\"" + +"\"EXT_mesh_gpu_instancing\"" +"\"TRANSLATION\"" +"\"ROTATION\"" +"\"SCALE\"" +"\"_COLOR_0\"" + +"\"EXT_meshopt_compression\"" +"\"KHR_meshopt_compression\"" +"\"mode\"" +"\"filter\"" +"\"fallback\"" +"\"ATTRIBUTES\"" +"\"TRIANGLES\"" +"\"INDICES\"" +"\"NONE\"" +"\"OCTAHEDRAL\"" +"\"QUATERNION\"" +"\"EXPONENTIAL\"" +"\"COLOR\"" + +"\"KHR_mesh_quantization\"" diff --git a/Lib/meshoptimizer/gltf/fuzz.glb b/Lib/meshoptimizer/gltf/fuzz.glb new file mode 100644 index 0000000000000000000000000000000000000000..fb49bac89ea0ee527e48bb6a43c525ecdfe22604 GIT binary patch literal 6540 zcmbU_c|4SB+xIgYV;v-?5Jp+bzAwdCA_|2fOU5!WVvJqc9#Sf6>Xa6dqf!cKL5eAb zN>LIO8GF*P6d}ue&&c%p&ij7nyua_6xt?pgul>5`kMRv~v_Js>Qda?>hXuge!Ok|C z=ouPH2_qUr6MZQ(N{DAzPzce0XlA!7 zC$n6n?jEZYO$?-j`XPbX9@*q#fM_B;gc?WHuyVArvn3j6C=neuJDb_r*_(T)5e+nvBB(SUsyEvNDI*ZcooJx7m>ngA>KU*U zlja$S$cZ-WEZ8}CdLtI0K_NdY_6py-mlCpzN{L)_;`EXtkn(ePh>M3*=``QPB9?spbc~o=Py>*vhz9z) z8v2XN^h=JoUf$()b%+zl=GFsOvAS z3&$Ih=<>w=&Y7OpUsBW3)YSY}SC%@mtZHp_U7c9>SiKbhm|EE)zowiAHs(+O{2l#w zI*y0sXV|jk{C`E)a6BQl|3Lqfelh+s9~{sBwH@1*4aQ5rp*ff%|G)5G&vAG*=78}s z{~VgFW78bGoaV^bm_u{)9NAwnN6*F_dik0EBxCD2at`LmmUspUIfFcTq(uZ6qy<(y zqRU&t@RFRPXY=r)erYe3^laJUgYayc10;lJKO8@7eo?oCIeLKb?1!Ug^S^v9YJTR$ zk-^44c?h5Y8UYAk02bf?7Xnzo4e)>m@FIW*e1IPa06_%!0RadBVIYEl5Lf|3!Ac;8 zfG9Y+-PV$yXEhHJh~H|9xjjJqv4I~ zFJnX9BQpcTOlDVi*T>F|j*j+@mN)M|KL0RO-TJBVP3?`wfd|h@%4#p3e$=0QulMr3 zh)E{zSi(M7%9MZVgVa%ErK1-H=CR^Dl9Zprd4hh=soVqbV2lLUZZXk~8V z81??ooS{=66bOgX#FDOh+|x62gvqNFE3lu>CrTAN zjMYwxk(R&iKw!?|;pmTzz~RV6M~S-N#`xcPN*|By2bc3&ZYvxsyDqN6^so3_rj$ao z={@?$#YG4RJj&+Uqx>sH$7Xl+0A|2|2-LqgO@wc~x;{jc%i7gQN)HWnlL1}& zOL3tIrpsU}DKGqZ^{Heth}lx@r=>WW$;@4xA4zHN!ap*7Z{sv6NVHJNSX@5ldf|?AGtxD z=MMDkbJo|nWa?Z2ff9eqeJ+dS@L1*}AZ5Q@@m`YKPkDn`JgBGF7;nBLR*V5$!JW4a z?H%f&J%V6hsB6T!SttF{_*3PDy;3b6JcZJw!oX;rT4jirb><>DKYF=`wHDS8Z~_F~ z*;EgLU(YBGKKS5WSMIeI5WfMeVeRRvApm(Yo_V52moUafxLlRPVp%CAcVVTsMM@co zkygivz+q0UCA&z0*7+w6u||ETyrszwD=rw-u@=^<8KaHV;;hIfVElNG7^a5;C51;9 zOh5Uqw}$`(o`0vW_rAXZNUThL#R3f`qvfzjPv#lb0UV^}|9o06}Ke1J8HeHF-eF6%$`VhD?W$cp5 z+jR5i?UE$tfGTX_-Rz#z_fDQNOS~H+NN(CBy}tJ}Cg6nq1g_K-m-ON;p7i|F#LgWy z$_{z>E(#-V3MM%e0NCDNAOEDNODSh}p8Q0Euc?D5?v_}IB+`lXKyUO_{}UlJkPw>% z&8DkX-8EG)L}|jE5?E8$I+h3Y`RyuD@C_tITfswLhx^xtkKenpHH9t!&X0UjBETzO zVQuc2W3k3|4?{J`V*BzWv@eCPw(N1vyS!rKCW1D>&XH}}V{`a?`q$BoT{ zW%gzNjxC8C|3H6PUJhbP9zUGWn_$V=7F2ZJv>j-p&JEIE_DRAge{9OQ@_uv6bK9-6 zX{j}Xq8_VL1|wGy4xs#e#O)5{QQLMpSbW-NIdmDvjF;yrjJaU1u)gg)?zWYXeQM7& z6thMImv{Aifd9pbp(-3VQw6ZtIAns!L z1g0f4i?CNsQL6=tmi=KknD5z?ib4#H!%pwP`Oiic}2O!W=$0+&lr z9&LNQY5+4J`rA8WDa|t(d%+<*Tsd?kgr>JTE%wlx2aq!gE+eiJoQyJ%1VQz!bxqm= zN3%#nK}pSzpJ9g<9Mh6_gE6|>p((Ze8397|C6u1;Yj6q+$aJ?H{;+AovjyPL0h6Bb zqSj#$Wl*d|N(*a+J$RZm%A7MRagy+v#MgZl2V}RM8s!IRcGhSnp8PvRF?2u%6`g}# zt#i2g0dygwPC1XYLd}pi72>9uaBFiAqZ4E{SVn_2>E}|CH1|npwZi-6KF65%y_oei z0`O{ywzZdwaogWL$`U0t)rK{IK_2P-TbObHy$h;%^F|R0o0*Q_9szJ&1GB<{dH{Ws z7jpS@0N5#3nKpC ztb4uBd<2Ex#%V5@lk=clj%hYdy5cXLAAxgraRha7Br*nUmwv9C`7wF)nH8CWZdT}( zemBC|nfe0s)Lh*GJI0vmkX-8sNTbES7iaE*Dv~?!X=XlVHSVf2h&3;?iy( zc2=!mhfroK?6CP*+3^=v#{596z8^n$`KIZ5lx85{+XT$Ci#JK!3GcXHp5Y;v9~}gw z4fLG~0;HWIjTv3R^F;~oD~Xn^YVr%dcBLbSkKE`DVZQX*7V5(@z(eu|wR#&S^+2bj zjA8wY`YRdjw+yP>@=%>uCAKToVmT#R_15f_D zn}A1}fQ`Lh%LzN9^jO>Lv}34NgJNSi-I-M&an_*kkA-8Yhr_?j2P|QR2{6}-tpG#XP{E! z0_*uvik}>G?P!g!4zFE47bEv-G2DpwK@aGtE6ds#y$2QEo8XFqW79UeGBri3qhfvi z7qrCsPua|zTSLm$neg}mn#YdSb|O85Z1ZrLwTi^P+zjvBBCrLe9qDoLu43yqDN2MZ zc~I6tWyNWm7TemZ`wje62HsIz^RbL@i^O}|Z_ElOl@9iJb>K1whV%0;0Y^iLH}Cp; zorUIb4AC6IJ-7VlSQ(o+iRrIiP4zoP@_w@|kQC4bdy`!n!6x{_OnJsWzVzV@-!@sR z)Y7+m4Ow1qas&l^3R;Xa(Wq?I?PvPBPD+hFjRxf>PUiIf7$ik#JY_lhH{#<2N!7T6 zy+5ACIedQR{OA5eQO zo~PJ7hwp2s1G$ZHZ9*71puyi6OM(SUQ6$~!jU1{Ga=_7L z!WUAe+oUt2SAc=q3NjAXwEq+2*g-d1ZlArm(kG{u{#%u|D7eK>43SdeYiq-g9=*9B zv@oiH)&hL}W7;B;nXOvs?h&z7b31z*I!1gjw}9I#D<1dC&eSIDCl*F;G+!sh_tPFr zsIKkM0uvi#ObbLR+D?(+=C;adp7=18JKA7Ek@U5GtYb^QY?Q?ikOSr4 z&Sb7FxyLmX_r1e>u5$j5qOVDAO`l@cS?%hq`dFK?JGWy1#psJ0e0b~ChS_uly$843s?rA2Yg9yZwl3|07H4ySOHssCT|7Vj#4L48=(LQQ~o8?o-~3| z0_3cCK6v~>JDQtjRd3`YoVrTj3q;Q1mx4$u9_nrYuepp9fcToDy){O5S@m4oP_K9* zFa+l3=?xFVR9|y5Lx)Xvpzom>R*_>X2sz|P;5M`H{mI=8uox_URUijdCiM9%RNlwP zf#g1rUIJ9sM6x?CMZ5t`A_-Kje5P_l42C$Lse%QRqy}HSbk2;^tz_Qt+Fb+@)8X!AdNA0xd)?XPQW$yo4}Q4l|}<>2j_aKJ4a z_2mh7_S|p&n*n5X%~OoKZd|2^B?>XcpH>BjVgkMtVR|PadS~K`2bpPRr+KG5#9<{~ zsoKNb?b05sm7qdxR3uQoa_R#giL~WzwaAulZ53U|fSR1u$C3>4iUCWtkHWB9GuSjE+@yLLEHGU zt_{!1yQV|EtCPm<-j)Xe)8D?GQR90)*_50xSiL1yLIMTEWS(l!7<)90Wm0})A=wUa zXWbXKP#IWLl|8-tt46gq6jceyS)JmwTjABH5>|YnYE6bw#Zl$1aj-2tN*3?iyy-^* zp2u{wb3}Bu+4x@7kp-Q~g&h#Lr!6G^j@Pt{cb|_-aJl%81<{yOOpk|B5{w#QfrUrS zleO_bC|_&dOqDdpf2(SO#)M~_tWNds-??*Y_CYUe+_-=DDSC;Bi~3wEgo(0O^UOSp zsyEdbHWp&2b~}~o%8VBojM?EzrRK4OIr_3!6kq}Tp7)a@jk!$ZFAHF`+19++l$?l= F{{!Z{;e7xA literal 0 HcmV?d00001 diff --git a/Lib/meshoptimizer/gltf/gltfpack.cpp b/Lib/meshoptimizer/gltf/gltfpack.cpp new file mode 100644 index 0000000..31dd40b --- /dev/null +++ b/Lib/meshoptimizer/gltf/gltfpack.cpp @@ -0,0 +1,1871 @@ +// This file is part of gltfpack; see gltfpack.h for version/license details +#include "gltfpack.h" + +#include +#include + +#include +#include +#include +#include +#include + +#ifdef __wasi__ +#include +#endif + +#include "../src/meshoptimizer.h" + +std::string getVersion() +{ + char result[32]; + snprintf(result, sizeof(result), "%d.%d", MESHOPTIMIZER_VERSION / 1000, (MESHOPTIMIZER_VERSION % 1000) / 10); + return result; +} + +static void finalizeBufferViews(std::string& json, std::vector& views, std::string& bin, std::string* fallback, size_t& fallback_size, const char* meshopt_ext, int attribute_level) +{ + for (size_t i = 0; i < views.size(); ++i) + { + BufferView& view = views[i]; + + size_t bin_offset = bin.size(); + size_t fallback_offset = fallback_size; + + size_t count = view.data.size() / view.stride; + + if (view.compression == BufferView::Compression_None) + { + bin += view.data; + } + else + { + switch (view.compression) + { + case BufferView::Compression_Attribute: + compressVertexStream(bin, view.data, count, view.stride, attribute_level); + break; + case BufferView::Compression_Index: + compressIndexStream(bin, view.data, count, view.stride); + break; + case BufferView::Compression_IndexSequence: + compressIndexSequence(bin, view.data, count, view.stride); + break; + default: + assert(!"Unknown compression type"); + } + + if (fallback) + *fallback += view.data; + fallback_size += view.data.size(); + } + + size_t raw_offset = (view.compression != BufferView::Compression_None) ? fallback_offset : bin_offset; + + comma(json); + writeBufferView(json, view.kind, view.filter, count, view.stride, raw_offset, view.data.size(), view.compression, bin_offset, bin.size() - bin_offset, meshopt_ext); + + // record written bytes for statistics + view.bytes = bin.size() - bin_offset; + + // align each bufferView by 4 bytes + bin.resize((bin.size() + 3) & ~3); + if (fallback) + fallback->resize((fallback->size() + 3) & ~3); + fallback_size = (fallback_size + 3) & ~3; + } +} + +static void printMeshStats(const std::vector& meshes, const char* name) +{ + size_t mesh_triangles = 0; + size_t mesh_vertices = 0; + size_t total_triangles = 0; + size_t total_instances = 0; + size_t total_draws = 0; + + for (size_t i = 0; i < meshes.size(); ++i) + { + const Mesh& mesh = meshes[i]; + + size_t triangles = mesh.type == cgltf_primitive_type_triangles ? mesh.indices.size() / 3 : 0; + + mesh_triangles += triangles; + mesh_vertices += mesh.streams.empty() ? 0 : mesh.streams[0].data.size(); + + size_t instances = std::max(size_t(1), mesh.nodes.size() + mesh.instances.size()); + + total_triangles += triangles * instances; + total_instances += instances; + total_draws += std::max(size_t(1), mesh.nodes.size()); + } + + printf("%s: %d mesh primitives (%d triangles, %d vertices); %d draw calls (%d instances, %lld triangles)\n", name, + int(meshes.size()), int(mesh_triangles), int(mesh_vertices), + int(total_draws), int(total_instances), (long long)total_triangles); +} + +static void printSceneStats(const std::vector& views, const std::vector& meshes, size_t node_offset, size_t mesh_offset, size_t material_offset, size_t json_size, size_t bin_size) +{ + size_t bytes[BufferView::Kind_Count] = {}; + + for (size_t i = 0; i < views.size(); ++i) + { + const BufferView& view = views[i]; + bytes[view.kind] += view.bytes; + } + + printf("output: %d nodes, %d meshes (%d primitives), %d materials\n", int(node_offset), int(mesh_offset), int(meshes.size()), int(material_offset)); + printf("output: JSON %d bytes, buffers %d bytes\n", int(json_size), int(bin_size)); + printf("output: buffers: vertex %d bytes, index %d bytes, skin %d bytes, time %d bytes, keyframe %d bytes, instance %d bytes, image %d bytes\n", + int(bytes[BufferView::Kind_Vertex]), int(bytes[BufferView::Kind_Index]), int(bytes[BufferView::Kind_Skin]), + int(bytes[BufferView::Kind_Time]), int(bytes[BufferView::Kind_Keyframe]), int(bytes[BufferView::Kind_Instance]), + int(bytes[BufferView::Kind_Image])); +} + +static void printAttributeStats(const std::vector& views, BufferView::Kind kind, const char* name) +{ + for (size_t i = 0; i < views.size(); ++i) + { + const BufferView& view = views[i]; + + if (view.kind != kind) + continue; + + const char* variant = "unknown"; + + switch (kind) + { + case BufferView::Kind_Vertex: + variant = attributeType(cgltf_attribute_type(view.variant)); + break; + + case BufferView::Kind_Index: + variant = "index"; + break; + + case BufferView::Kind_Keyframe: + case BufferView::Kind_Instance: + variant = animationPath(cgltf_animation_path_type(view.variant)); + break; + + default:; + } + + size_t count = view.data.size() / view.stride; + + if (view.compression == BufferView::Compression_None) + printf("stats: %s %s: %d bytes (%.1f bits)\n", + name, variant, int(view.bytes), double(view.bytes) / double(count) * 8); + else + printf("stats: %s %s: compressed %d bytes (%.1f bits), raw %d bytes (%d bits)\n", + name, variant, + int(view.bytes), double(view.bytes) / double(count) * 8, + int(view.data.size()), int(view.stride * 8)); + } +} + +static void printImageStats(const std::vector& views, TextureKind kind, const char* name) +{ + size_t bytes = 0; + size_t count = 0; + + for (size_t i = 0; i < views.size(); ++i) + { + const BufferView& view = views[i]; + + if (view.kind != BufferView::Kind_Image) + continue; + + if (view.variant != -1 - kind) + continue; + + count += 1; + bytes += view.data.size(); + } + + if (count) + printf("stats: image %s: %d bytes in %d images\n", name, int(bytes), int(count)); +} + +static bool printReport(const char* path, const std::vector& views, const std::vector& meshes, size_t node_count, size_t mesh_count, size_t texture_count, size_t material_count, size_t animation_count, size_t json_size, size_t bin_size) +{ + size_t bytes[BufferView::Kind_Count] = {}; + + for (size_t i = 0; i < views.size(); ++i) + { + const BufferView& view = views[i]; + bytes[view.kind] += view.bytes; + } + + size_t total_triangles = 0; + size_t total_instances = 0; + size_t total_draws = 0; + + for (size_t i = 0; i < meshes.size(); ++i) + { + const Mesh& mesh = meshes[i]; + + size_t triangles = mesh.type == cgltf_primitive_type_triangles ? mesh.indices.size() / 3 : 0; + size_t instances = std::max(size_t(1), mesh.nodes.size() + mesh.instances.size()); + + total_triangles += triangles * instances; + total_instances += instances; + total_draws += std::max(size_t(1), mesh.nodes.size()); + } + + FILE* out = fopen(path, "wb"); + if (!out) + return false; + + fprintf(out, "{\n"); + fprintf(out, "\t\"generator\": \"gltfpack %s\",\n", getVersion().c_str()); + fprintf(out, "\t\"scene\": {\n"); + fprintf(out, "\t\t\"nodeCount\": %d,\n", int(node_count)); + fprintf(out, "\t\t\"meshCount\": %d,\n", int(mesh_count)); + fprintf(out, "\t\t\"materialCount\": %d,\n", int(material_count)); + fprintf(out, "\t\t\"textureCount\": %d,\n", int(texture_count)); + fprintf(out, "\t\t\"animationCount\": %d\n", int(animation_count)); + fprintf(out, "\t},\n"); + fprintf(out, "\t\"render\": {\n"); + fprintf(out, "\t\t\"drawCount\": %d,\n", int(total_draws)); + fprintf(out, "\t\t\"instanceCount\": %d,\n", int(total_instances)); + fprintf(out, "\t\t\"triangleCount\": %lld\n", (long long)total_triangles); + fprintf(out, "\t},\n"); + fprintf(out, "\t\"data\": {\n"); + fprintf(out, "\t\t\"json\": %d,\n", int(json_size)); + fprintf(out, "\t\t\"binary\": %d,\n", int(bin_size)); + fprintf(out, "\t\t\"buffers\": {\n"); + fprintf(out, "\t\t\t\"vertex\": %d,\n", int(bytes[BufferView::Kind_Vertex])); + fprintf(out, "\t\t\t\"index\": %d,\n", int(bytes[BufferView::Kind_Index])); + fprintf(out, "\t\t\t\"animation\": %d,\n", int(bytes[BufferView::Kind_Time] + bytes[BufferView::Kind_Keyframe])); + fprintf(out, "\t\t\t\"transform\": %d,\n", int(bytes[BufferView::Kind_Skin] + bytes[BufferView::Kind_Instance])); + fprintf(out, "\t\t\t\"image\": %d\n", int(bytes[BufferView::Kind_Image])); + fprintf(out, "\t\t}\n"); + fprintf(out, "\t}\n"); + fprintf(out, "}\n"); + + int rc = fclose(out); + return rc == 0; +} + +static bool canTransformMesh(const Mesh& mesh) +{ + // volume thickness is specified in mesh coordinate space; to avoid modifying materials we prohibit transforming meshes with volume materials + if (mesh.material && mesh.material->has_volume && mesh.material->volume.thickness_factor > 0.f) + return false; + + return true; +} + +static void detachMesh(Mesh& mesh, cgltf_data* data, const std::vector& nodes, const Settings& settings) +{ + // mesh is already instanced, skip + if (!mesh.instances.empty()) + return; + + // mesh is already world space, skip + if (mesh.nodes.empty()) + return; + + // note: when -kn is specified, we keep mesh-node attachment so that named nodes can be transformed + if (settings.keep_nodes) + return; + + // we keep skinned meshes or meshes with morph targets as is + // in theory we could transform both, but in practice transforming morph target meshes is more involved, + // and reparenting skinned meshes leads to incorrect bounding box generated in three.js + if (mesh.skin || mesh.targets) + return; + + bool any_animated = false; + for (size_t j = 0; j < mesh.nodes.size(); ++j) + any_animated |= nodes[mesh.nodes[j] - data->nodes].animated; + + // animated meshes will be anchored to the same node that they used to be in to retain the animation + if (any_animated) + return; + + int scene = nodes[mesh.nodes[0] - data->nodes].scene; + bool any_other_scene = false; + for (size_t j = 0; j < mesh.nodes.size(); ++j) + any_other_scene |= scene != nodes[mesh.nodes[j] - data->nodes].scene; + + // we only merge instances when all nodes have a single consistent scene + if (scene < 0 || any_other_scene) + return; + + // we only merge multiple instances together if requested + // this often makes the scenes faster to render by reducing the draw call count, but can result in larger files + if (mesh.nodes.size() > 1 && !settings.mesh_merge && !settings.mesh_instancing) + return; + + // mesh has duplicate geometry; detaching it would increase the size due to unique world-space transforms + if (mesh.nodes.size() == 1 && mesh.geometry_duplicate && !settings.mesh_merge) + return; + + // prefer instancing if possible, use merging otherwise + if (mesh.nodes.size() > 1 && settings.mesh_instancing) + { + mesh.instances.resize(mesh.nodes.size()); + + for (size_t j = 0; j < mesh.nodes.size(); ++j) + { + Instance& obj = mesh.instances[j]; + + cgltf_node_transform_world(mesh.nodes[j], obj.transform); + obj.color[0] = obj.color[1] = obj.color[2] = obj.color[3] = 1.0f; + } + + mesh.nodes.clear(); + mesh.scene = scene; + } + else if (canTransformMesh(mesh)) + { + mergeMeshInstances(mesh); + + assert(mesh.nodes.empty()); + mesh.scene = scene; + } +} + +static bool isExtensionSupported(const ExtensionInfo* extensions, size_t count, const char* name) +{ + for (size_t i = 0; i < count; ++i) + if (strcmp(extensions[i].name, name) == 0) + return true; + + return false; +} + +namespace std +{ +template <> +struct hash > +{ + size_t operator()(const std::pair& x) const + { + return std::hash()(x.first ^ x.second); + } +}; +} // namespace std + +struct PrimitiveCacheEntry +{ + size_t offset; + size_t size; + QuantizationTexture qt; +}; + +static bool sameQuantization(const QuantizationTexture& lhs, const QuantizationTexture& rhs, const Settings& settings) +{ + if (!settings.quantize || settings.tex_float) + return true; + + return lhs.offset[0] == rhs.offset[0] && lhs.offset[1] == rhs.offset[1] && + lhs.scale[0] == rhs.scale[0] && lhs.scale[1] == rhs.scale[1] && + lhs.bits == rhs.bits && lhs.normalized == rhs.normalized; +} + +static size_t process(cgltf_data* data, const char* input_path, const char* output_path, const char* report_path, std::vector& meshes, std::vector& animations, const Settings& settings, std::string& json, std::string& bin, std::string& fallback, size_t& fallback_size, const char* meshopt_ext) +{ + if (settings.verbose) + { + printf("input: %d nodes, %d meshes (%d primitives), %d materials, %d skins, %d animations, %d images\n", + int(data->nodes_count), int(data->meshes_count), int(meshes.size()), int(data->materials_count), int(data->skins_count), int(animations.size()), int(data->images_count)); + printMeshStats(meshes, "input"); + } + + for (size_t i = 0; i < animations.size(); ++i) + processAnimation(animations[i], settings); + + std::vector nodes(data->nodes_count); + + markScenes(data, nodes); + markAnimated(data, nodes, animations); + + mergeMeshMaterials(data, meshes, settings); + if (settings.mesh_dedup) + dedupMeshes(meshes, settings); + + for (size_t i = 0; i < meshes.size(); ++i) + detachMesh(meshes[i], data, nodes, settings); + + // material information is required for mesh and image processing + std::vector materials(data->materials_count); + std::vector textures(data->textures_count); + std::vector images(data->images_count); + + // mark materials that need to keep blending due to mesh vertex colors + for (size_t i = 0; i < meshes.size(); ++i) + { + Mesh& mesh = meshes[i]; + + // skip hasAlpha check unless it's required + if ((((mesh.material && mesh.material->alpha_mode != cgltf_alpha_mode_opaque) || mesh.variants.size()) && hasVertexAlpha(mesh)) || hasInstanceAlpha(mesh.instances)) + { + if (mesh.material) + materials[mesh.material - data->materials].mesh_alpha = true; + + for (size_t j = 0; j < mesh.variants.size(); ++j) + materials[mesh.variants[j].material - data->materials].mesh_alpha = true; + } + } + + analyzeMaterials(data, materials, textures, images); + + mergeTextures(data, textures); + + optimizeMaterials(data, materials, images, input_path); + + // streams need to be filtered before mesh merging (or processing) to make sure we can merge meshes with redundant streams + for (size_t i = 0; i < meshes.size(); ++i) + { + Mesh& mesh = meshes[i]; + MaterialInfo mi = mesh.material ? materials[mesh.material - data->materials] : MaterialInfo(); + + // merge material requirements across all variants + for (size_t j = 0; j < mesh.variants.size(); ++j) + { + MaterialInfo vi = materials[mesh.variants[j].material - data->materials]; + + mi.needs_tangents |= vi.needs_tangents; + mi.texture_set_mask |= vi.texture_set_mask; + mi.unlit &= vi.unlit; + } + + if (!settings.keep_attributes) + filterStreams(mesh, mi); + + if (settings.mesh_normals && (!mi.unlit || settings.keep_attributes)) + generateNormals(mesh, settings.normals_crease); + + if (settings.mesh_tangents && (mi.needs_tangents || settings.keep_attributes)) + generateTangents(mesh); + } + + mergeMeshes(meshes, settings); + filterEmptyMeshes(meshes); + + markNeededNodes(data, nodes, meshes, animations, settings); + markNeededMaterials(data, materials, meshes, settings); + + if (settings.simplify_ratio < 1) + computeMeshQuality(meshes); + + for (size_t i = 0; i < meshes.size(); ++i) + { + Mesh& mesh = meshes[i]; + processMesh(mesh, settings); + + if (mesh.geometry_duplicate) + hashMesh(mesh); + } + + filterEmptyMeshes(meshes); // some meshes may become empty after processing + + QuantizationPosition qp = prepareQuantizationPosition(meshes, settings); + + std::vector qt_materials(materials.size()); + std::vector qt_meshes(meshes.size(), size_t(-1)); + prepareQuantizationTexture(data, qt_materials, qt_meshes, meshes, settings); + + QuantizationTexture qt_dummy = {}; + qt_dummy.bits = settings.tex_bits; + + std::string json_images; + std::string json_samplers; + std::string json_textures; + std::string json_materials; + std::string json_accessors; + std::string json_meshes; + std::string json_nodes; + std::string json_skins; + std::vector json_roots(data->scenes_count); + std::string json_animations; + std::string json_cameras; + std::string json_extensions; + + std::vector views; + + bool ext_pbr_specular_glossiness = false; + bool ext_clearcoat = false; + bool ext_transmission = false; + bool ext_ior = false; + bool ext_specular = false; + bool ext_sheen = false; + bool ext_volume = false; + bool ext_emissive_strength = false; + bool ext_iridescence = false; + bool ext_anisotropy = false; + bool ext_dispersion = false; + bool ext_diffuse_transmission = false; + bool ext_unlit = false; + bool ext_instancing = false; + bool ext_texture_transform = false; + bool ext_texture_basisu = false; + bool ext_texture_webp = false; + + size_t accr_offset = 0; + size_t node_offset = 0; + size_t mesh_offset = 0; + size_t texture_offset = 0; + size_t material_offset = 0; + + for (size_t i = 0; i < data->samplers_count; ++i) + { + const cgltf_sampler& sampler = data->samplers[i]; + + comma(json_samplers); + append(json_samplers, "{"); + writeSampler(json_samplers, sampler); + append(json_samplers, "}"); + } + + std::vector encoded_images(data->images_count); + +#ifdef WITH_BASISU + if (data->images_count && settings.texture_ktx2) + encodeImagesBasis(encoded_images.data(), data, images, input_path, settings); +#endif + +#ifdef WITH_LIBWEBP + if (data->images_count && settings.texture_webp) + encodeImagesWebP(encoded_images.data(), data, images, input_path, settings); +#endif + + for (size_t i = 0; i < data->images_count; ++i) + { + const cgltf_image& image = data->images[i]; + + std::string* encoded = !encoded_images[i].empty() ? &encoded_images[i] : NULL; + + comma(json_images); + append(json_images, "{"); + writeImage(json_images, views, image, images[i], encoded, i, input_path, output_path, settings); + append(json_images, "}"); + + if (encoded) + *encoded = std::string(); // reclaim memory early + } + + for (size_t i = 0; i < data->textures_count; ++i) + { + const cgltf_texture& texture = data->textures[i]; + + if (!textures[i].keep) + continue; + + comma(json_textures); + append(json_textures, "{"); + writeTexture(json_textures, texture, texture.image ? &images[texture.image - data->images] : NULL, data, settings); + append(json_textures, "}"); + + assert(textures[i].remap == int(texture_offset)); + texture_offset++; + ext_texture_basisu = ext_texture_basisu || texture.has_basisu; + ext_texture_webp = ext_texture_webp || texture.has_webp; + } + + for (size_t i = 0; i < data->materials_count; ++i) + { + MaterialInfo& mi = materials[i]; + + if (!mi.keep) + continue; + + const cgltf_material& material = data->materials[i]; + + comma(json_materials); + append(json_materials, "{"); + writeMaterial(json_materials, data, material, settings.quantize && !settings.pos_float ? &qp : NULL, settings.quantize && !settings.tex_float ? &qt_materials[i] : NULL, textures); + if (settings.keep_extras) + writeExtras(json_materials, material.extras); + append(json_materials, "}"); + + mi.remap = int(material_offset); + material_offset++; + + ext_pbr_specular_glossiness = ext_pbr_specular_glossiness || material.has_pbr_specular_glossiness; + ext_clearcoat = ext_clearcoat || material.has_clearcoat; + ext_transmission = ext_transmission || material.has_transmission; + ext_ior = ext_ior || material.has_ior; + ext_specular = ext_specular || material.has_specular; + ext_sheen = ext_sheen || material.has_sheen; + ext_volume = ext_volume || material.has_volume; + ext_emissive_strength = ext_emissive_strength || material.has_emissive_strength; + ext_iridescence = ext_iridescence || material.has_iridescence; + ext_diffuse_transmission = ext_diffuse_transmission || material.has_diffuse_transmission; + ext_anisotropy = ext_anisotropy || material.has_anisotropy; + ext_dispersion = ext_dispersion || material.has_dispersion; + ext_unlit = ext_unlit || material.unlit; + ext_texture_transform = ext_texture_transform || mi.uses_texture_transform; + } + + std::unordered_map, PrimitiveCacheEntry> primitive_cache; + + for (size_t i = 0; i < meshes.size(); ++i) + { + const Mesh& mesh = meshes[i]; + + comma(json_meshes); + append(json_meshes, "{\"primitives\":["); + + size_t pi = i; + for (; pi < meshes.size(); ++pi) + { + const Mesh& prim = meshes[pi]; + + if (prim.scene != mesh.scene || prim.skin != mesh.skin || prim.targets != mesh.targets) + break; + + if (pi > i && (mesh.instances.size() || prim.instances.size())) + break; + + if (!compareMeshNodes(mesh, prim)) + break; + + if (!compareMeshTargets(mesh, prim)) + break; + + const QuantizationTexture& qt = qt_meshes[pi] == size_t(-1) ? qt_dummy : qt_materials[qt_meshes[pi]]; + + comma(json_meshes); + + if (prim.geometry_duplicate) + { + PrimitiveCacheEntry& entry = primitive_cache[std::make_pair(prim.geometry_hash[0], prim.geometry_hash[1])]; + + if (entry.size && sameQuantization(entry.qt, qt, settings)) + { + // reuse previously written accessors + json_meshes.append(json_meshes, entry.offset, entry.size); + } + else + { + entry.offset = json_meshes.size(); + writeMeshGeometry(json_meshes, views, json_accessors, accr_offset, prim, qp, qt, settings); + entry.size = json_meshes.size() - entry.offset; + entry.qt = qt; + } + } + else + { + writeMeshGeometry(json_meshes, views, json_accessors, accr_offset, prim, qp, qt, settings); + } + + if (prim.material) + { + MaterialInfo& mi = materials[prim.material - data->materials]; + + assert(mi.keep); + append(json_meshes, ",\"material\":"); + append(json_meshes, size_t(mi.remap)); + } + + if (prim.variants.size()) + { + append(json_meshes, ",\"extensions\":{\"KHR_materials_variants\":{\"mappings\":["); + + for (size_t j = 0; j < prim.variants.size(); ++j) + { + const cgltf_material_mapping& variant = prim.variants[j]; + MaterialInfo& mi = materials[variant.material - data->materials]; + + assert(mi.keep); + comma(json_meshes); + append(json_meshes, "{\"material\":"); + append(json_meshes, size_t(mi.remap)); + append(json_meshes, ",\"variants\":["); + append(json_meshes, size_t(variant.variant)); + append(json_meshes, "]}"); + } + + append(json_meshes, "]}}"); + } + + if (settings.keep_extras) + writeExtras(json_meshes, prim.extras); + + append(json_meshes, "}"); + } + + append(json_meshes, "]"); + + if (mesh.target_weights.size()) + { + append(json_meshes, ",\"weights\":"); + append(json_meshes, mesh.target_weights.data(), mesh.target_weights.size()); + } + + if (mesh.target_names.size()) + { + append(json_meshes, ",\"extras\":{\"targetNames\":["); + for (size_t j = 0; j < mesh.target_names.size(); ++j) + { + comma(json_meshes); + append(json_meshes, "\""); + append(json_meshes, mesh.target_names[j]); + append(json_meshes, "\""); + } + append(json_meshes, "]}"); + } + + append(json_meshes, "}"); + + if (mesh.nodes.size()) + { + for (size_t j = 0; j < mesh.nodes.size(); ++j) + { + NodeInfo& ni = nodes[mesh.nodes[j] - data->nodes]; + assert(ni.keep); + + // if we don't use position quantization, prefer attaching the mesh to its node directly + if (!ni.has_mesh && (!settings.quantize || settings.pos_float || (qp.offset[0] == 0.f && qp.offset[1] == 0.f && qp.offset[2] == 0 && qp.node_scale == 1.f))) + { + ni.has_mesh = true; + ni.mesh_index = mesh_offset; + ni.mesh_skin = mesh.skin; + } + else + { + ni.mesh_nodes.push_back(node_offset); + + writeMeshNode(json_nodes, mesh_offset, mesh.nodes[j], mesh.skin, data, settings.quantize && !settings.pos_float ? &qp : NULL); + + node_offset++; + } + } + } + + if (mesh.instances.size()) + { + assert(mesh.scene >= 0); + comma(json_roots[mesh.scene]); + append(json_roots[mesh.scene], node_offset); + + bool has_color = false; + for (const Instance& instance : mesh.instances) + has_color |= (instance.color[0] != 1.f || instance.color[1] != 1.f || instance.color[2] != 1.f || instance.color[3] != 1.f); + + size_t instance_accr = writeInstances(views, json_accessors, accr_offset, mesh.instances, qp, has_color, settings); + + assert(!mesh.skin); + writeMeshNodeInstanced(json_nodes, mesh_offset, instance_accr, has_color); + + node_offset++; + } + + if (mesh.nodes.empty() && mesh.instances.empty()) + { + assert(mesh.scene >= 0); + comma(json_roots[mesh.scene]); + append(json_roots[mesh.scene], node_offset); + + writeMeshNode(json_nodes, mesh_offset, NULL, mesh.skin, data, settings.quantize && !settings.pos_float ? &qp : NULL); + + node_offset++; + } + + mesh_offset++; + ext_instancing = ext_instancing || !mesh.instances.empty(); + + // skip all meshes that we've written in this iteration + assert(pi > i); + i = pi - 1; + } + + remapNodes(data, nodes, node_offset); + + for (size_t i = 0; i < data->nodes_count; ++i) + { + NodeInfo& ni = nodes[i]; + + if (!ni.keep) + continue; + + const cgltf_node& node = data->nodes[i]; + + comma(json_nodes); + append(json_nodes, "{"); + writeNode(json_nodes, node, nodes, data); + if (settings.keep_extras) + writeExtras(json_nodes, node.extras); + append(json_nodes, "}"); + } + + for (size_t i = 0; i < data->scenes_count; ++i) + { + for (size_t j = 0; j < data->scenes[i].nodes_count; ++j) + { + NodeInfo& ni = nodes[data->scenes[i].nodes[j] - data->nodes]; + + if (ni.keep) + { + comma(json_roots[i]); + append(json_roots[i], size_t(ni.remap)); + } + } + } + + for (size_t i = 0; i < data->skins_count; ++i) + { + const cgltf_skin& skin = data->skins[i]; + + size_t matrix_accr = writeJointBindMatrices(views, json_accessors, accr_offset, skin, qp, settings); + + writeSkin(json_skins, skin, matrix_accr, nodes, data); + } + + for (size_t i = 0; i < animations.size(); ++i) + { + const Animation& animation = animations[i]; + + writeAnimation(json_animations, views, json_accessors, accr_offset, animation, i, data, nodes, settings); + } + + for (size_t i = 0; i < data->cameras_count; ++i) + { + const cgltf_camera& camera = data->cameras[i]; + + writeCamera(json_cameras, camera); + } + + if (data->lights_count > 0) + { + comma(json_extensions); + append(json_extensions, "\"KHR_lights_punctual\":{\"lights\":["); + + for (size_t i = 0; i < data->lights_count; ++i) + { + const cgltf_light& light = data->lights[i]; + + writeLight(json_extensions, light); + } + + append(json_extensions, "]}"); + } + + if (data->variants_count > 0) + { + comma(json_extensions); + append(json_extensions, "\"KHR_materials_variants\":{\"variants\":["); + + for (size_t i = 0; i < data->variants_count; ++i) + { + const cgltf_material_variant& variant = data->variants[i]; + + comma(json_extensions); + append(json_extensions, "{\"name\":\""); + append(json_extensions, variant.name); + append(json_extensions, "\"}"); + } + + append(json_extensions, "]}"); + } + + append(json, "\"asset\":{"); + append(json, "\"version\":\"2.0\",\"generator\":\"gltfpack "); + append(json, getVersion()); + append(json, "\""); + writeExtras(json, data->asset.extras); + append(json, "}"); + + const ExtensionInfo extensions[] = { + {"KHR_mesh_quantization", settings.quantize, true}, + {meshopt_ext, settings.compress, !settings.fallback}, + {"KHR_texture_transform", (settings.quantize && !settings.tex_float && !json_textures.empty()) || ext_texture_transform, false}, + {"KHR_materials_pbrSpecularGlossiness", ext_pbr_specular_glossiness, false}, + {"KHR_materials_clearcoat", ext_clearcoat, false}, + {"KHR_materials_transmission", ext_transmission, false}, + {"KHR_materials_ior", ext_ior, false}, + {"KHR_materials_specular", ext_specular, false}, + {"KHR_materials_sheen", ext_sheen, false}, + {"KHR_materials_volume", ext_volume, false}, + {"KHR_materials_emissive_strength", ext_emissive_strength, false}, + {"KHR_materials_iridescence", ext_iridescence, false}, + {"KHR_materials_anisotropy", ext_anisotropy, false}, + {"KHR_materials_dispersion", ext_dispersion, false}, + {"KHR_materials_diffuse_transmission", ext_diffuse_transmission, false}, + {"KHR_materials_unlit", ext_unlit, false}, + {"KHR_materials_variants", data->variants_count > 0, false}, + {"KHR_lights_punctual", data->lights_count > 0, false}, + {"KHR_texture_basisu", (!json_textures.empty() && settings.texture_ktx2) || ext_texture_basisu, true}, + {"EXT_texture_webp", (!json_textures.empty() && settings.texture_webp) || ext_texture_webp, true}, + {"EXT_mesh_gpu_instancing", ext_instancing, true}, + }; + + for (size_t i = 0; i < data->extensions_required_count; ++i) + { + const char* ext = data->extensions_required[i]; + + if (!isExtensionSupported(extensions, sizeof(extensions) / sizeof(extensions[0]), ext) && strstr(ext, "_meshopt_compression") == NULL) + fprintf(stderr, "Warning: required extension %s is not supported and will be skipped\n", ext); + } + + writeExtensions(json, extensions, sizeof(extensions) / sizeof(extensions[0])); + + // buffers[] array to be inserted by the caller + size_t bufferspec_pos = json.size(); + + std::string json_views; + finalizeBufferViews(json_views, views, bin, settings.fallback ? &fallback : NULL, fallback_size, meshopt_ext, settings.compresskhr ? (settings.compressmore ? 3 : 2) : 0); + + writeArray(json, "bufferViews", json_views); + writeArray(json, "accessors", json_accessors); + writeArray(json, "samplers", json_samplers); + writeArray(json, "images", json_images); + writeArray(json, "textures", json_textures); + writeArray(json, "materials", json_materials); + writeArray(json, "meshes", json_meshes); + writeArray(json, "skins", json_skins); + writeArray(json, "animations", json_animations); + writeArray(json, "nodes", json_nodes); + + if (!json_roots.empty()) + { + append(json, ",\"scenes\":["); + + for (size_t i = 0; i < data->scenes_count; ++i) + writeScene(json, data->scenes[i], json_roots[i], settings); + + append(json, "]"); + } + + writeArray(json, "cameras", json_cameras); + + if (data->scene) + { + append(json, ",\"scene\":"); + append(json, size_t(data->scene - data->scenes)); + } + + if (!json_extensions.empty()) + { + append(json, ",\"extensions\":{"); + append(json, json_extensions); + append(json, "}"); + } + + if (settings.verbose) + { + printMeshStats(meshes, "output"); + printSceneStats(views, meshes, node_offset, mesh_offset, material_offset, json.size(), bin.size()); + } + + if (settings.verbose > 1) + { + printAttributeStats(views, BufferView::Kind_Vertex, "vertex"); + printAttributeStats(views, BufferView::Kind_Index, "index"); + printAttributeStats(views, BufferView::Kind_Keyframe, "keyframe"); + printAttributeStats(views, BufferView::Kind_Instance, "instance"); + + printImageStats(views, TextureKind_Generic, "generic"); + printImageStats(views, TextureKind_Color, "color"); + printImageStats(views, TextureKind_Normal, "normal"); + printImageStats(views, TextureKind_Attrib, "attrib"); + } + + if (report_path) + { + if (!printReport(report_path, views, meshes, node_offset, mesh_offset, texture_offset, material_offset, animations.size(), json.size(), bin.size())) + { + fprintf(stderr, "Warning: cannot save report to %s\n", report_path); + } + } + + return bufferspec_pos; +} + +static void writeU32(FILE* out, uint32_t data) +{ + fwrite(&data, 4, 1, out); +} + +static const char* getBaseName(const char* path) +{ + const char* slash = strrchr(path, '/'); + const char* backslash = strrchr(path, '\\'); + + const char* rs = slash ? slash + 1 : path; + const char* bs = backslash ? backslash + 1 : path; + + return std::max(rs, bs); +} + +static std::string getBufferSpec(const char* bin_path, size_t bin_size, const char* fallback_path, size_t fallback_size, bool fallback_ref, const char* meshopt_ext) +{ + std::string json; + append(json, "\"buffers\":["); + append(json, "{"); + if (bin_path) + { + append(json, "\"uri\":\""); + append(json, bin_path); + append(json, "\""); + } + comma(json); + append(json, "\"byteLength\":"); + append(json, bin_size); + append(json, "}"); + if (fallback_ref) + { + comma(json); + append(json, "{"); + if (fallback_path) + { + append(json, "\"uri\":\""); + append(json, fallback_path); + append(json, "\""); + } + comma(json); + append(json, "\"byteLength\":"); + append(json, fallback_size); + append(json, ",\"extensions\":{"); + append(json, "\""); + append(json, meshopt_ext); + append(json, "\":{"); + append(json, "\"fallback\":true"); + append(json, "}}"); + append(json, "}"); + } + append(json, "]"); + + return json; +} + +int gltfpack(const char* input, const char* output, const char* report, Settings settings) +{ + cgltf_data* data = NULL; + std::vector meshes; + std::vector animations; + + std::string iext = getExtension(input); + std::string oext = output ? getExtension(output) : ""; + + if (output) + { + if (oext != ".gltf" && oext != ".glb") + { + fprintf(stderr, "Error: unsupported output extension '%s' (expected .gltf or .glb)\n", oext.c_str()); + return 4; + } + } + + if (iext == ".gltf" || iext == ".glb") + { + const char* error = NULL; + data = parseGltf(input, meshes, animations, &error); + + if (error) + { + fprintf(stderr, "Error loading %s: %s\n", input, error); + return 2; + } + } + else if (iext == ".obj") + { + const char* error = NULL; + data = parseObj(input, meshes, &error); + + if (!data) + { + fprintf(stderr, "Error loading %s: %s\n", input, error); + return 2; + } + } + else + { + fprintf(stderr, "Error loading %s: unknown extension (expected .gltf or .glb or .obj)\n", input); + return 2; + } + +#ifndef WITH_BASISU + if (data->images_count && settings.texture_ktx2) + { + fprintf(stderr, "Error: gltfpack was built without BasisU support, texture compression is not available\n"); +#ifdef __wasi__ + fprintf(stderr, "Note: node.js builds do not support BasisU due to lack of platform features; download a native build from https://github.com/zeux/meshoptimizer/releases\n"); +#endif + return 3; + } +#endif + +#ifndef WITH_LIBWEBP + if (data->images_count && settings.texture_webp) + { + fprintf(stderr, "Error: gltfpack was built without WebP support, texture compression is not available\n"); +#ifdef __wasi__ + fprintf(stderr, "Note: node.js builds do not support WebP due to lack of platform features; download a native build from https://github.com/zeux/meshoptimizer/releases\n"); +#endif + return 3; + } +#endif + + if (oext == ".glb") + { + settings.texture_embed = true; + } + + if (data->images_count && !settings.texture_ref && !settings.texture_embed) + { + for (size_t i = 0; i < data->images_count; ++i) + { + const char* uri = data->images[i].uri; + if (!uri || strncmp(uri, "data:", 5) == 0) + continue; + + for (size_t j = 0; j < i; ++j) + { + const char* urj = data->images[j].uri; + if (!urj || strncmp(urj, "data:", 5) == 0) + continue; + + if (strcmp(uri, urj) != 0 && strcmp(getBaseName(uri), getBaseName(urj)) == 0) + { + fprintf(stderr, "Warning: images %s and %s share the same base name and will overwrite each other\n", uri, urj); + break; + } + } + } + } + + const char* meshopt_ext = settings.compresskhr ? "KHR_meshopt_compression" : "EXT_meshopt_compression"; + + std::string json, bin, fallback; + size_t fallback_size = 0; + + json += '{'; + size_t bufferspec_pos = process(data, input, output, report, meshes, animations, settings, json, bin, fallback, fallback_size, meshopt_ext); + json += '}'; + + cgltf_free(data); + + if (!output) + { + return 0; + } + + if (oext == ".gltf") + { + std::string binpath = output; + binpath.replace(binpath.size() - 5, 5, ".bin"); + + std::string fbpath = output; + fbpath.replace(fbpath.size() - 5, 5, ".fallback.bin"); + + FILE* outjson = fopen(output, "wb"); + FILE* outbin = fopen(binpath.c_str(), "wb"); + FILE* outfb = settings.fallback ? fopen(fbpath.c_str(), "wb") : NULL; + if (!outjson || !outbin || (!outfb && settings.fallback)) + { + fprintf(stderr, "Error saving %s\n", output); + return 4; + } + + std::string bufferspec = getBufferSpec(getBaseName(binpath.c_str()), bin.size(), settings.fallback ? getBaseName(fbpath.c_str()) : NULL, fallback_size, settings.compress, meshopt_ext); + json.insert(bufferspec_pos, "," + bufferspec); + + fwrite(json.c_str(), json.size(), 1, outjson); + fwrite(bin.c_str(), bin.size(), 1, outbin); + + if (settings.fallback) + fwrite(fallback.c_str(), fallback.size(), 1, outfb); + + int rc = 0; + rc |= fclose(outjson); + rc |= fclose(outbin); + if (outfb) + rc |= fclose(outfb); + + if (rc) + { + fprintf(stderr, "Error saving %s\n", output); + return 4; + } + } + else if (oext == ".glb") + { + std::string fbpath = output; + fbpath.replace(fbpath.size() - 4, 4, ".fallback.bin"); + + std::string bufferspec = getBufferSpec(NULL, bin.size(), settings.fallback ? getBaseName(fbpath.c_str()) : NULL, fallback_size, settings.compress, meshopt_ext); + json.insert(bufferspec_pos, "," + bufferspec); + + while (json.size() % 4) + json.push_back(' '); + + while (bin.size() % 4) + bin.push_back('\0'); + + // GLB header and two chunks with a chunk header + size_t size = 12 + 8 + json.size() + 8 + bin.size(); + + if (size > UINT32_MAX) + { + fprintf(stderr, "Error: GLB output cannot exceed 4 GB in size\n"); + return 4; + } + + FILE* out = fopen(output, "wb"); + FILE* outfb = settings.fallback ? fopen(fbpath.c_str(), "wb") : NULL; + if (!out || (!outfb && settings.fallback)) + { + fprintf(stderr, "Error saving %s\n", output); + return 4; + } + + writeU32(out, 0x46546C67); + writeU32(out, 2); + writeU32(out, uint32_t(size)); + + writeU32(out, uint32_t(json.size())); + writeU32(out, 0x4E4F534A); + fwrite(json.c_str(), json.size(), 1, out); + + writeU32(out, uint32_t(bin.size())); + writeU32(out, 0x004E4942); + fwrite(bin.c_str(), bin.size(), 1, out); + + if (settings.fallback) + fwrite(fallback.c_str(), fallback.size(), 1, outfb); + + int rc = 0; + rc |= fclose(out); + if (outfb) + rc |= fclose(outfb); + + if (rc) + { + fprintf(stderr, "Error saving %s\n", output); + return 4; + } + } + else + { + fprintf(stderr, "Error saving %s: unknown extension (expected .gltf or .glb)\n", output); + return 4; + } + + return 0; +} + +Settings defaults() +{ + Settings settings = {}; + settings.quantize = true; + settings.pos_bits = 14; + settings.tex_bits = 12; + settings.nrm_bits = 8; + settings.col_bits = 8; + settings.trn_bits = 16; + settings.rot_bits = 12; + settings.scl_bits = 16; + settings.anim_freq = 30; + settings.mesh_dedup = true; + settings.simplify_ratio = 1.f; + settings.simplify_error = 1e-2f; + + for (int kind = 0; kind < TextureKind__Count; ++kind) + { + settings.texture_mode[kind] = TextureMode_Raw; + settings.texture_scale[kind] = 1.f; + settings.texture_quality[kind] = 8; + } + + return settings; +} + +template +T clamp(T v, T min, T max) +{ + return v < min ? min : (v > max ? max : v); +} + +unsigned int textureMask(const char* arg) +{ + unsigned int result = 0; + + while (arg) + { + const char* comma = strchr(arg, ','); + size_t seg = comma ? comma - arg : strlen(arg); + + if (seg == 5 && strncmp(arg, "color", seg) == 0) + result |= 1 << TextureKind_Color; + else if (seg == 6 && strncmp(arg, "normal", seg) == 0) + result |= 1 << TextureKind_Normal; + else if (seg == 6 && strncmp(arg, "attrib", seg) == 0) + result |= 1 << TextureKind_Attrib; + else + fprintf(stderr, "Warning: unrecognized texture class %.*s\n", int(seg), arg); + + arg = comma ? comma + 1 : NULL; + } + + return result; +} + +template +void applySetting(T (&data)[TextureKind__Count], T value, unsigned int mask = ~0u) +{ + for (int kind = 0; kind < TextureKind__Count; ++kind) + if (mask & (1 << kind)) + data[kind] = value; +} + +#ifndef GLTFFUZZ +int main(int argc, char** argv) +{ +#ifndef __wasi__ + setlocale(LC_ALL, "C"); // disable locale specific convention for number parsing/printing +#endif + + meshopt_encodeVertexVersion(0); + meshopt_encodeIndexVersion(1); + + Settings settings = defaults(); + + const char* input = NULL; + const char* output = NULL; + const char* report = NULL; + bool help = false; + bool test = false; + bool require_texc = false; + + std::vector testinputs; + + for (int i = 1; i < argc; ++i) + { + const char* arg = argv[i]; + + if (strcmp(arg, "-vp") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) + { + settings.pos_bits = clamp(atoi(argv[++i]), 1, 16); + } + else if (strcmp(arg, "-vt") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) + { + settings.tex_bits = clamp(atoi(argv[++i]), 1, 16); + } + else if (strcmp(arg, "-vn") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) + { + settings.nrm_bits = clamp(atoi(argv[++i]), 1, 16); + } + else if (strcmp(arg, "-vc") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) + { + settings.col_bits = clamp(atoi(argv[++i]), 1, 16); + } + else if (strcmp(arg, "-vpi") == 0) + { + settings.pos_float = false; + settings.pos_normalized = false; + } + else if (strcmp(arg, "-vpn") == 0) + { + settings.pos_float = false; + settings.pos_normalized = true; + } + else if (strcmp(arg, "-vpf") == 0) + { + settings.pos_float = true; + } + else if (strcmp(arg, "-vtf") == 0) + { + settings.tex_float = true; + } + else if (strcmp(arg, "-vnf") == 0) + { + settings.nrm_float = true; + } + else if (strcmp(arg, "-vi") == 0) + { + settings.mesh_interleaved = true; + } + else if (strcmp(arg, "-gt") == 0) + { + settings.mesh_tangents = true; + } + else if (strcmp(arg, "-gn") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) + { + settings.mesh_normals = true; + settings.normals_crease = clamp(float(atof(argv[++i])), 0.f, 180.f); + } + else if (strcmp(arg, "-at") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) + { + settings.trn_bits = clamp(atoi(argv[++i]), 1, 24); + } + else if (strcmp(arg, "-ar") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) + { + settings.rot_bits = clamp(atoi(argv[++i]), 4, 16); + } + else if (strcmp(arg, "-as") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) + { + settings.scl_bits = clamp(atoi(argv[++i]), 1, 24); + } + else if (strcmp(arg, "-af") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) + { + settings.anim_freq = clamp(atoi(argv[++i]), 0, 100); + } + else if (strcmp(arg, "-ac") == 0) + { + settings.anim_const = true; + } + else if (strcmp(arg, "-kn") == 0) + { + settings.keep_nodes = true; + } + else if (strcmp(arg, "-km") == 0) + { + settings.keep_materials = true; + } + else if (strcmp(arg, "-ke") == 0) + { + settings.keep_extras = true; + } + else if (strcmp(arg, "-kv") == 0) + { + settings.keep_attributes = true; + } + else if (strcmp(arg, "-mdd") == 0) + { + fprintf(stderr, "Warning: option -mdd disables mesh deduplication and is temporary; avoid production usage\n"); + settings.mesh_dedup = false; + } + else if (strcmp(arg, "-mm") == 0) + { + settings.mesh_merge = true; + } + else if (strcmp(arg, "-mi") == 0) + { + settings.mesh_instancing = true; + } + else if (strcmp(arg, "-si") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) + { + settings.simplify_ratio = clamp(float(atof(argv[++i])), 0.f, 1.f); + } + else if (strcmp(arg, "-se") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) + { + settings.simplify_error = clamp(float(atof(argv[++i])), 0.f, 1.f); + } + else if (strcmp(arg, "-sa") == 0) + { + settings.simplify_aggressive = true; + } + else if (strcmp(arg, "-slb") == 0) + { + settings.simplify_lock_borders = true; + } + else if (strcmp(arg, "-sv") == 0) + { + settings.simplify_update = true; + } + else if (strcmp(arg, "-sp") == 0) + { + settings.simplify_permissive = true; + } + else if (strcmp(arg, "-tu") == 0) + { + settings.texture_ktx2 = true; + + unsigned int mask = ~0u; + if (i + 1 < argc && isalpha(argv[i + 1][0])) + mask = textureMask(argv[++i]); + + applySetting(settings.texture_mode, TextureMode_UASTC, mask); + } + else if (strcmp(arg, "-tc") == 0) + { + settings.texture_ktx2 = true; + + unsigned int mask = ~0u; + if (i + 1 < argc && isalpha(argv[i + 1][0])) + mask = textureMask(argv[++i]); + + applySetting(settings.texture_mode, TextureMode_ETC1S, mask); + } + else if (strcmp(arg, "-tw") == 0) + { + settings.texture_webp = true; + + unsigned int mask = ~0u; + if (i + 1 < argc && isalpha(argv[i + 1][0])) + mask = textureMask(argv[++i]); + + applySetting(settings.texture_mode, TextureMode_WebP, mask); + } + else if (strcmp(arg, "-tq") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) + { + require_texc = true; + + int quality = clamp(atoi(argv[++i]), 1, 10); + applySetting(settings.texture_quality, quality); + } + else if (strcmp(arg, "-tq") == 0 && i + 2 < argc && isalpha(argv[i + 1][0]) && isdigit(argv[i + 2][0])) + { + require_texc = true; + + unsigned int mask = textureMask(argv[++i]); + int quality = clamp(atoi(argv[++i]), 1, 10); + applySetting(settings.texture_quality, quality, mask); + } + else if (strcmp(arg, "-ts") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) + { + require_texc = true; + + float scale = clamp(float(atof(argv[++i])), 0.f, 1.f); + applySetting(settings.texture_scale, scale); + } + else if (strcmp(arg, "-ts") == 0 && i + 2 < argc && isalpha(argv[i + 1][0]) && isdigit(argv[i + 2][0])) + { + require_texc = true; + + unsigned int mask = textureMask(argv[++i]); + float scale = clamp(float(atof(argv[++i])), 0.f, 1.f); + applySetting(settings.texture_scale, scale, mask); + } + else if (strcmp(arg, "-tl") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) + { + require_texc = true; + + int limit = atoi(argv[++i]); + applySetting(settings.texture_limit, limit); + } + else if (strcmp(arg, "-tl") == 0 && i + 2 < argc && isalpha(argv[i + 1][0]) && isdigit(argv[i + 2][0])) + { + require_texc = true; + + unsigned int mask = textureMask(argv[++i]); + int limit = atoi(argv[++i]); + applySetting(settings.texture_limit, limit, mask); + } + else if (strcmp(arg, "-tp") == 0) + { + require_texc = true; + + settings.texture_pow2 = true; + } + else if (strcmp(arg, "-tfy") == 0) + { + require_texc = true; + + settings.texture_flipy = true; + } + else if (strcmp(arg, "-tr") == 0) + { + settings.texture_ref = true; + } + else if (strcmp(arg, "-tj") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) + { + settings.texture_jobs = clamp(atoi(argv[++i]), 0, 128); + } + else if (strcmp(arg, "-noq") == 0) + { + // TODO: Warn if -noq is used and suggest -vpf instead; use -noqq to silence + settings.quantize = false; + } + else if (strcmp(arg, "-i") == 0 && i + 1 < argc && !input) + { + input = argv[++i]; + } + else if (strcmp(arg, "-o") == 0 && i + 1 < argc && !output) + { + output = argv[++i]; + } + else if (strcmp(arg, "-r") == 0 && i + 1 < argc && !report) + { + report = argv[++i]; + } + else if (strcmp(arg, "-c") == 0) + { + settings.compress = true; + } + else if (strcmp(arg, "-cc") == 0) + { + settings.compress = true; + settings.compressmore = true; + } + else if (strcmp(arg, "-cf") == 0) + { + settings.compress = true; + settings.fallback = true; + } + else if (strcmp(arg, "-cz") == 0) + { + settings.compress = true; + settings.compressmore = true; + settings.compresskhr = true; + } + else if (strcmp(arg, "-ce") == 0 && i + 1 < argc && (strcmp(argv[i + 1], "khr") == 0 || strcmp(argv[i + 1], "ext") == 0)) + { + settings.compress = true; + settings.compresskhr = strcmp(argv[++i], "khr") == 0; + } + else if (strcmp(arg, "-v") == 0) + { + settings.verbose = 1; + } + else if (strcmp(arg, "-vv") == 0) + { + settings.verbose = 2; + } + else if (strcmp(arg, "-h") == 0) + { + help = true; + } + else if (strcmp(arg, "-test") == 0) + { + test = true; + } + else if (arg[0] == '-') + { + fprintf(stderr, "Unrecognized option %s\n", arg); + return 1; + } + else if (test) + { + testinputs.push_back(arg); + } + else + { + fprintf(stderr, "Expected option, got %s instead\n", arg); + return 1; + } + } + + // shortcut for gltfpack -v + if (settings.verbose && argc == 2) + { + printf("gltfpack %s\n", getVersion().c_str()); + return 0; + } + + if (test) + { + for (size_t i = 0; i < testinputs.size(); ++i) + { + const char* path = testinputs[i]; + + printf("%s\n", path); + gltfpack(path, NULL, NULL, settings); + } + + return 0; + } + + if (!input || !output || help) + { + fprintf(stderr, "gltfpack %s\n", getVersion().c_str()); + fprintf(stderr, "Usage: gltfpack [options] -i input -o output\n"); + + if (help) + { + fprintf(stderr, "\nBasics:\n"); + fprintf(stderr, "\t-i file: input file to process, .obj/.gltf/.glb\n"); + fprintf(stderr, "\t-o file: output file path, .gltf/.glb\n"); + fprintf(stderr, "\t-c: produce compressed gltf/glb files (-cc/-cz for higher compression ratio)\n"); + fprintf(stderr, "\nTextures:\n"); + fprintf(stderr, "\t-tc: convert all textures to KTX2 with BasisU supercompression\n"); + fprintf(stderr, "\t-tu: use UASTC when encoding textures (much higher quality and much larger size)\n"); + fprintf(stderr, "\t-tw: convert all textures to WebP\n"); + fprintf(stderr, "\t-tq N: set texture encoding quality (default: 8; N should be between 1 and 10)\n"); + fprintf(stderr, "\t-ts R: scale texture dimensions by the ratio R (default: 1; R should be between 0 and 1)\n"); + fprintf(stderr, "\t-tl N: limit texture dimensions to N pixels (default: 0 = no limit)\n"); + fprintf(stderr, "\t-tp: resize textures to nearest power of 2 to conform to WebGL1 restrictions\n"); + fprintf(stderr, "\t-tfy: flip textures along Y axis during BasisU supercompression\n"); + fprintf(stderr, "\t-tj N: use N threads when compressing textures\n"); + fprintf(stderr, "\t-tr: keep referring to original texture paths instead of copying/embedding images\n"); + fprintf(stderr, "\tTexture classes:\n"); + fprintf(stderr, "\t-tc C: use ETC1S when encoding textures of class C\n"); + fprintf(stderr, "\t-tu C: use UASTC when encoding textures of class C\n"); + fprintf(stderr, "\t-tw C: use WebP when encoding textures of class C\n"); + fprintf(stderr, "\t-tq C N: set texture encoding quality for class C\n"); + fprintf(stderr, "\t-ts C R: scale texture dimensions for class C\n"); + fprintf(stderr, "\t-tl C N: limit texture dimensions for class C\n"); + fprintf(stderr, "\t... where C is a comma-separated list (no spaces) with valid values color,normal,attrib\n"); + fprintf(stderr, "\nSimplification:\n"); + fprintf(stderr, "\t-si R: simplify meshes targeting triangle/point count ratio R (default: 1; R should be between 0 and 1)\n"); + fprintf(stderr, "\t-se E: limit simplification error to E (default: 0.01 = 1%% deviation; E should be between 0 and 1)\n"); + fprintf(stderr, "\t-sv: simplify with vertex position and attribute optimization\n"); + fprintf(stderr, "\t-sp: use permissive simplification mode to allow simplification across attribute discontinuities\n"); + fprintf(stderr, "\t-sa: aggressively simplify to the target ratio disregarding quality\n"); + fprintf(stderr, "\t-slb: lock border vertices during simplification to avoid gaps on connected meshes\n"); + fprintf(stderr, "\nVertex precision:\n"); + fprintf(stderr, "\t-vp N: use N-bit quantization for positions (default: 14; N should be between 1 and 16)\n"); + fprintf(stderr, "\t-vt N: use N-bit quantization for texture coordinates (default: 12; N should be between 1 and 16)\n"); + fprintf(stderr, "\t-vn N: use N-bit quantization for normals (default: 8; N should be between 1 and 16) and tangents (up to 8-bit)\n"); + fprintf(stderr, "\t-vc N: use N-bit quantization for colors (default: 8; N should be between 1 and 16)\n"); + fprintf(stderr, "\nVertex positions:\n"); + fprintf(stderr, "\t-vpi: use integer attributes for positions (default)\n"); + fprintf(stderr, "\t-vpn: use normalized attributes for positions\n"); + fprintf(stderr, "\t-vpf: use floating point attributes for positions\n"); + fprintf(stderr, "\nVertex attributes:\n"); + fprintf(stderr, "\t-vtf: use floating point attributes for texture coordinates\n"); + fprintf(stderr, "\t-vnf: use floating point attributes for normals\n"); + fprintf(stderr, "\t-vi: use interleaved vertex attributes (reduces compression efficiency)\n"); + fprintf(stderr, "\t-gt: generate tangent frames when needed, replacing existing tangents\n"); + fprintf(stderr, "\t-gn A: generate normals when absent, using crease angle A (degrees)\n"); + fprintf(stderr, "\t-kv: keep source vertex attributes even if they aren't used\n"); + fprintf(stderr, "\nAnimations:\n"); + fprintf(stderr, "\t-at N: use N-bit quantization for translations (default: 16; N should be between 1 and 24)\n"); + fprintf(stderr, "\t-ar N: use N-bit quantization for rotations (default: 12; N should be between 4 and 16)\n"); + fprintf(stderr, "\t-as N: use N-bit quantization for scale (default: 16; N should be between 1 and 24)\n"); + fprintf(stderr, "\t-af N: resample animations at N Hz (default: 30; use 0 to disable)\n"); + fprintf(stderr, "\t-ac: keep constant animation tracks even if they don't modify the node transform\n"); + fprintf(stderr, "\nScene:\n"); + fprintf(stderr, "\t-kn: keep named nodes and meshes attached to named nodes so that named nodes can be transformed externally\n"); + fprintf(stderr, "\t-km: keep named materials and disable named material merging\n"); + fprintf(stderr, "\t-ke: keep extras data\n"); + fprintf(stderr, "\t-mm: merge instances of the same mesh together when possible\n"); + fprintf(stderr, "\t-mi: use EXT_mesh_gpu_instancing when serializing multiple mesh instances\n"); + fprintf(stderr, "\nMiscellaneous:\n"); + fprintf(stderr, "\t-cf: produce compressed gltf/glb files with fallback for loaders that don't support compression\n"); + fprintf(stderr, "\t-ce ext|khr: use EXT or KHR version of meshopt compression extension for compression\n"); + fprintf(stderr, "\t-noq: disable quantization; produces much larger glTF files with no extensions\n"); + fprintf(stderr, "\t-v: verbose output (when used with other options)\n"); + fprintf(stderr, "\t-v: print version (when used without other options)\n"); + fprintf(stderr, "\t-r file: output a JSON report to file\n"); + fprintf(stderr, "\t-h: display this help and exit\n"); + } + else + { + fprintf(stderr, "\nBasics:\n"); + fprintf(stderr, "\t-i file: input file to process, .obj/.gltf/.glb\n"); + fprintf(stderr, "\t-o file: output file path, .gltf/.glb\n"); + fprintf(stderr, "\t-c: produce compressed gltf/glb files (-cc/-cz for higher compression ratio)\n"); + fprintf(stderr, "\t-tc: convert all textures to KTX2 with BasisU supercompression\n"); + fprintf(stderr, "\t-tw: convert all textures to WebP\n"); + fprintf(stderr, "\t-si R: simplify meshes targeting triangle/point count ratio R (between 0 and 1)\n"); + fprintf(stderr, "\nRun gltfpack -h to display a full list of options\n"); + } + + return 1; + } + + if (require_texc && !settings.texture_ktx2 && !settings.texture_webp) + { + fprintf(stderr, "Texture processing is only supported when texture compression is enabled via -tc/-tu/-tw\n"); + return 1; + } + + if (settings.texture_ref && (settings.texture_ktx2 || settings.texture_webp)) + { + fprintf(stderr, "Option -tr currently can not be used together with texture compression\n"); + return 1; + } + + if (settings.fallback && settings.compressmore) + { + fprintf(stderr, "Option -cf can not be used together with -cc\n"); + return 1; + } + + if (settings.fallback && (settings.pos_float || settings.tex_float || settings.nrm_float)) + { + fprintf(stderr, "Option -cf can not be used together with -vpf, -vtf or -vnf\n"); + return 1; + } + +#ifdef GLTFPACK_NO_EXPERIMENTAL + if (settings.mesh_normals) + { + fprintf(stderr, "Option -gn is not available in this build\n"); + return 1; + } +#endif + + if (settings.keep_nodes && (settings.mesh_merge || settings.mesh_instancing)) + fprintf(stderr, "Warning: option -kn disables mesh merge (-mm) and mesh instancing (-mi) optimizations\n"); + + return gltfpack(input, output, report, settings); +} +#endif + +#ifdef __wasi__ +extern "C" int pack(int argc, char** argv) +{ + chdir("/gltfpack-$pwd"); + + int result = main(argc, argv); + fflush(NULL); + return result; +} +#endif + +#ifdef GLTFFUZZ +extern "C" size_t LLVMFuzzerMutate(uint8_t* data, size_t size, size_t max_size); + +extern "C" size_t LLVMFuzzerCustomMutator(uint8_t* data, size_t size, size_t max_size, unsigned int seed) +{ + // parse glTF/JSON chunk headers + if (size < 28 || memcmp(data, "glTF", 4) != 0 || memcmp(data + 16, "JSON", 4) != 0) + return LLVMFuzzerMutate(data, size, max_size); + + uint32_t total_size, json_size; + memcpy(&total_size, data + 8, 4); + memcpy(&json_size, data + 12, 4); + + // parse BIN chunk header (note, we assume it's required for simplicity) + if (json_size > size - 28 || memcmp(data + 24 + json_size, "BIN", 4) != 0) + return LLVMFuzzerMutate(data, size, max_size); + + uint32_t bin_size; + memcpy(&bin_size, data + 20 + json_size, 4); + + // final validation; note that all chunks must be 4 byte aligned + if (total_size != size || bin_size > size - 28 - json_size || 28 + json_size + bin_size != size || ((bin_size | json_size) & 3) != 0) + return LLVMFuzzerMutate(data, size, max_size); + + // pick chunk to mutate; we default to mutating JSON but mutate BIN with 10% probability + bool mutate_bin = seed % 10 == 0; + size_t target_offset = mutate_bin ? 28 + json_size : 20; + size_t target_size = mutate_bin ? bin_size : json_size; + + size_t target_capacity = (max_size - (size - target_size)) & ~3; + + // mutate chunk and pad it to 4 byte boundary to maintain GLB alignment + std::vector target(data + target_offset, data + target_offset + target_size); + target.resize(target_capacity); + target.resize(LLVMFuzzerMutate(target.data(), target_size, target_capacity)); + target.resize((target.size() + 3) & ~3, mutate_bin ? 0 : ' '); + + uint32_t new_target_size = uint32_t(target.size()); + uint32_t new_total_size = uint32_t(size - target_size + new_target_size); + + // patch the target chunk and update chunk length as well as total length + memmove(data + target_offset + new_target_size, data + target_offset + target_size, size - target_offset - target_size); + memcpy(data + target_offset, target.data(), new_target_size); + memcpy(data + 8, &new_total_size, 4); + memcpy(data + target_offset - 8, &new_target_size, 4); + + return new_total_size; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* buffer, size_t size) +{ + Settings settings = defaults(); + + settings.texture_embed = true; + + std::vector meshes; + std::vector animations; + + const char* error = NULL; + cgltf_data* data = parseGlb(buffer, size, meshes, animations, &error); + + // this is a difficult tradeoff + // returning 0 on files that fail to parse means that fuzzing is more incremental: files with errors are put into the corpus, + // and the subsequent mutations may lead to discovering more interesting inputs, including valid ones that are difficult to find otherwise. + // however, this leads to most of the corpus being invalid, and we very rarely get useful coverage for actual gltfpack processing. + // for now we just focus on valid files, as we expect cgltf parser itself to be bulletproof as it's fuzzed separately. + if (error) + return -1; + + std::string json, bin, fallback; + size_t fallback_size = 0; + process(data, NULL, NULL, NULL, meshes, animations, settings, json, bin, fallback, fallback_size, "KHR_meshopt_compression"); + + cgltf_free(data); + + return 0; +} +#endif diff --git a/Lib/meshoptimizer/gltf/gltfpack.h b/Lib/meshoptimizer/gltf/gltfpack.h new file mode 100644 index 0000000..fed459e --- /dev/null +++ b/Lib/meshoptimizer/gltf/gltfpack.h @@ -0,0 +1,449 @@ +/** + * gltfpack - version 1.2 + * + * Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) + * Report bugs and download new versions at https://github.com/zeux/meshoptimizer + * + * This application is distributed under the MIT License. See notice at the end of this file. + */ +#pragma once + +#ifndef _CRT_SECURE_NO_WARNINGS +#define _CRT_SECURE_NO_WARNINGS +#endif + +#ifndef _CRT_NONSTDC_NO_WARNINGS +#define _CRT_NONSTDC_NO_WARNINGS +#endif + +#include "../extern/cgltf.h" + +#include + +#include +#include + +struct Attr +{ + float f[4]; +}; + +struct Stream +{ + cgltf_attribute_type type; + int index; + int target; // 0 = base mesh, 1+ = morph target + + const char* custom_name; // only valid for cgltf_attribute_type_custom + + std::vector data; +}; + +struct Instance +{ + float transform[16]; + float color[4]; +}; + +struct Mesh +{ + int scene; + std::vector nodes; + std::vector instances; + + cgltf_material* material; + cgltf_skin* skin; + + cgltf_extras extras; + + cgltf_primitive_type type; + + std::vector streams; + std::vector indices; + + bool geometry_duplicate; + uint64_t geometry_hash[2]; + + size_t targets; + std::vector target_weights; + std::vector target_names; + + std::vector variants; + + float quality; +}; + +struct Track +{ + cgltf_node* node; + cgltf_animation_path_type path; + + bool constant; + bool dummy; + + size_t components; // 1 unless path is cgltf_animation_path_type_weights + + cgltf_interpolation_type interpolation; + + std::vector time; // empty for resampled or constant animations + std::vector data; +}; + +struct Animation +{ + const char* name; + + float start; + int frames; + + std::vector tracks; +}; + +enum TextureKind +{ + TextureKind_Generic, + TextureKind_Color, + TextureKind_Normal, + TextureKind_Attrib, + + TextureKind__Count +}; + +enum TextureMode +{ + TextureMode_Raw, + TextureMode_ETC1S, + TextureMode_UASTC, + TextureMode_WebP, +}; + +struct Settings +{ + int pos_bits; + int tex_bits; + int nrm_bits; + int col_bits; + + bool pos_normalized; + bool pos_float; + bool tex_float; + bool nrm_float; + + int trn_bits; + int rot_bits; + int scl_bits; + + int anim_freq; + bool anim_const; + + bool keep_nodes; + bool keep_materials; + bool keep_extras; + bool keep_attributes; + + bool mesh_dedup; + bool mesh_merge; + bool mesh_instancing; + bool mesh_interleaved; + bool mesh_tangents; + bool mesh_normals; + + float normals_crease; + + float simplify_ratio; + float simplify_error; + bool simplify_aggressive; + bool simplify_lock_borders; + bool simplify_permissive; + bool simplify_update; + + bool texture_ktx2; + bool texture_webp; + bool texture_embed; + bool texture_ref; + + bool texture_pow2; + bool texture_flipy; + float texture_scale[TextureKind__Count]; + int texture_limit[TextureKind__Count]; + + TextureMode texture_mode[TextureKind__Count]; + int texture_quality[TextureKind__Count]; + + int texture_jobs; + + bool quantize; + + bool compress; + bool compressmore; + bool compresskhr; + bool fallback; + + int verbose; +}; + +struct QuantizationPosition +{ + float offset[3]; + float scale; + int bits; + bool normalized; + + float node_scale; // computed from scale/bits/normalized +}; + +struct QuantizationTexture +{ + float offset[2]; + float scale[2]; + int bits; + bool normalized; +}; + +struct StreamFormat +{ + enum Filter + { + Filter_None, + Filter_Oct, + Filter_Quat, + Filter_Exp, + Filter_Color, + }; + + cgltf_type type; + cgltf_component_type component_type; + bool normalized; + size_t stride; + Filter filter; +}; + +struct NodeInfo +{ + int scene; + + bool keep; + bool animated; + + unsigned int animated_path_mask; + + int remap; + + std::vector mesh_nodes; + + bool has_mesh; + size_t mesh_index; + cgltf_skin* mesh_skin; +}; + +struct MaterialInfo +{ + bool keep; + + bool uses_texture_transform; + bool needs_tangents; + bool unlit; + bool mesh_alpha; + + unsigned int texture_set_mask; + + int remap; +}; + +struct TextureInfo +{ + bool keep; + + int remap; +}; + +struct ImageInfo +{ + TextureKind kind; + bool normal_map; + bool srgb; + + int channels; +}; + +struct ExtensionInfo +{ + const char* name; + + bool used; + bool required; +}; + +struct BufferView +{ + enum Kind + { + Kind_Vertex, + Kind_Index, + Kind_Skin, + Kind_Time, + Kind_Keyframe, + Kind_Instance, + Kind_Image, + Kind_Count + }; + + enum Compression + { + Compression_None = -1, + Compression_Attribute, + Compression_Index, + Compression_IndexSequence, + }; + + Kind kind; + StreamFormat::Filter filter; + Compression compression; + size_t stride; + int variant; + + std::string data; + + size_t bytes; +}; + +std::string getTempPrefix(); + +std::string getFullPath(const char* path, const char* base_path); +std::string getFileName(const char* path); +std::string getExtension(const char* path); + +bool readFile(const char* path, std::string& data); +bool writeFile(const char* path, const std::string& data); +void removeFile(const char* path); + +cgltf_data* parseObj(const char* path, std::vector& meshes, const char** error); +cgltf_data* parseGltf(const char* path, std::vector& meshes, std::vector& animations, const char** error); + +cgltf_data* parseGlb(const void* buffer, size_t size, std::vector& meshes, std::vector& animations, const char** error); + +bool areExtrasEqual(const cgltf_extras& lhs, const cgltf_extras& rhs); + +void processAnimation(Animation& animation, const Settings& settings); +void processMesh(Mesh& mesh, const Settings& settings); + +bool compareMeshTargets(const Mesh& lhs, const Mesh& rhs); +bool compareMeshVariants(const Mesh& lhs, const Mesh& rhs); +bool compareMeshNodes(const Mesh& lhs, const Mesh& rhs); + +void hashMesh(Mesh& mesh); +void dedupMeshes(std::vector& meshes, const Settings& settings); +void mergeMeshInstances(Mesh& mesh); +void mergeMeshes(std::vector& meshes, const Settings& settings); +void filterEmptyMeshes(std::vector& meshes); +void filterStreams(Mesh& mesh, const MaterialInfo& mi); +void generateTangents(Mesh& mesh); +void generateNormals(Mesh& mesh, float crease_angle); + +void mergeMeshMaterials(cgltf_data* data, std::vector& meshes, const Settings& settings); +void markNeededMaterials(cgltf_data* data, std::vector& materials, const std::vector& meshes, const Settings& settings); + +void mergeTextures(cgltf_data* data, std::vector& textures); + +bool isValidTransform(const cgltf_texture_transform& transform); + +void analyzeMaterials(cgltf_data* data, std::vector& materials, std::vector& textures, std::vector& images); +void optimizeMaterials(cgltf_data* data, std::vector& materials, std::vector& images, const char* input_path); + +bool readImage(const cgltf_image& image, const char* input_path, std::string& data, std::string& mime_type); +bool hasAlpha(const std::string& data, const char* mime_type); +bool getDimensions(const std::string& data, const char* mime_type, int& width, int& height); +void adjustDimensions(int& width, int& height, float scale, int limit, bool pow2); +const char* mimeExtension(const char* mime_type); + +#ifdef WITH_BASISU +void encodeImagesBasis(std::string* encoded, const cgltf_data* data, const std::vector& images, const char* input_path, const Settings& settings); +#endif + +#ifdef WITH_LIBWEBP +void encodeImagesWebP(std::string* encoded, const cgltf_data* data, const std::vector& images, const char* input_path, const Settings& settings); +#endif + +void markScenes(cgltf_data* data, std::vector& nodes); +void markAnimated(cgltf_data* data, std::vector& nodes, const std::vector& animations); +void markNeededNodes(cgltf_data* data, std::vector& nodes, const std::vector& meshes, const std::vector& animations, const Settings& settings); +void remapNodes(cgltf_data* data, std::vector& nodes, size_t& node_offset); +void decomposeTransform(float translation[3], float rotation[4], float scale[3], const float* transform); + +void computeMeshQuality(std::vector& meshes); + +bool hasVertexAlpha(const Mesh& mesh); +bool hasInstanceAlpha(const std::vector& instances); + +QuantizationPosition prepareQuantizationPosition(const std::vector& meshes, const Settings& settings); +void prepareQuantizationTexture(cgltf_data* data, std::vector& result, std::vector& indices, const std::vector& meshes, const Settings& settings); +void getPositionBounds(float min[3], float max[3], const Stream& stream, const QuantizationPosition& qp, const Settings& settings); + +StreamFormat writeVertexStream(std::string& bin, const Stream& stream, const QuantizationPosition& qp, const QuantizationTexture& qt, const Settings& settings, bool filters = true); +StreamFormat writeIndexStream(std::string& bin, const std::vector& stream); +StreamFormat writeTimeStream(std::string& bin, const std::vector& data); +StreamFormat writeKeyframeStream(std::string& bin, cgltf_animation_path_type type, const std::vector& data, const Settings& settings, bool has_tangents = false); + +void compressVertexStream(std::string& bin, const std::string& data, size_t count, size_t stride, int level); +void compressIndexStream(std::string& bin, const std::string& data, size_t count, size_t stride); +void compressIndexSequence(std::string& bin, const std::string& data, size_t count, size_t stride); + +size_t getBufferView(std::vector& views, BufferView::Kind kind, StreamFormat::Filter filter, BufferView::Compression compression, size_t stride, int variant = 0); + +void comma(std::string& s); +void append(std::string& s, size_t v); +void append(std::string& s, float v); +void append(std::string& s, const char* v); +void append(std::string& s, const std::string& v); +void append(std::string& s, const float* data, size_t count); +void appendJson(std::string& s, const char* data); + +const char* attributeType(cgltf_attribute_type type); +const char* animationPath(cgltf_animation_path_type type); + +void writeMaterial(std::string& json, const cgltf_data* data, const cgltf_material& material, const QuantizationPosition* qp, const QuantizationTexture* qt, std::vector& textures); +void writeBufferView(std::string& json, BufferView::Kind kind, StreamFormat::Filter filter, size_t count, size_t stride, size_t bin_offset, size_t bin_size, BufferView::Compression compression, size_t compressed_offset, size_t compressed_size, const char* meshopt_ext); +void writeSampler(std::string& json, const cgltf_sampler& sampler); +void writeImage(std::string& json, std::vector& views, const cgltf_image& image, const ImageInfo& info, const std::string* encoded, size_t index, const char* input_path, const char* output_path, const Settings& settings); +void writeTexture(std::string& json, const cgltf_texture& texture, const ImageInfo* info, cgltf_data* data, const Settings& settings); +void writeMeshAttributes(std::string& json, std::vector& views, std::string& json_accessors, size_t& accr_offset, const Mesh& mesh, int target, const QuantizationPosition& qp, const QuantizationTexture& qt, const Settings& settings); +size_t writeMeshIndices(std::vector& views, std::string& json_accessors, size_t& accr_offset, const std::vector& indices, cgltf_primitive_type type, const Settings& settings); +void writeMeshGeometry(std::string& json, std::vector& views, std::string& json_accessors, size_t& accr_offset, const Mesh& mesh, const QuantizationPosition& qp, const QuantizationTexture& qt, const Settings& settings); +size_t writeJointBindMatrices(std::vector& views, std::string& json_accessors, size_t& accr_offset, const cgltf_skin& skin, const QuantizationPosition& qp, const Settings& settings); +size_t writeInstances(std::vector& views, std::string& json_accessors, size_t& accr_offset, const std::vector& instances, const QuantizationPosition& qp, bool has_color, const Settings& settings); +void writeMeshNode(std::string& json, size_t mesh_offset, cgltf_node* node, cgltf_skin* skin, cgltf_data* data, const QuantizationPosition* qp); +void writeMeshNodeInstanced(std::string& json, size_t mesh_offset, size_t accr_offset, bool has_color); +void writeSkin(std::string& json, const cgltf_skin& skin, size_t matrix_accr, const std::vector& nodes, cgltf_data* data); +void writeNode(std::string& json, const cgltf_node& node, const std::vector& nodes, cgltf_data* data); +void writeAnimation(std::string& json, std::vector& views, std::string& json_accessors, size_t& accr_offset, const Animation& animation, size_t i, cgltf_data* data, const std::vector& nodes, const Settings& settings); +void writeCamera(std::string& json, const cgltf_camera& camera); +void writeLight(std::string& json, const cgltf_light& light); +void writeArray(std::string& json, const char* name, const std::string& contents); +void writeExtensions(std::string& json, const ExtensionInfo* extensions, size_t count); +void writeExtras(std::string& json, const cgltf_extras& extras); +void writeScene(std::string& json, const cgltf_scene& scene, const std::string& roots, const Settings& settings); + +/** + * Copyright (c) 2016-2026 Arseny Kapoulkine + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ diff --git a/Lib/meshoptimizer/gltf/gltfpack.manifest b/Lib/meshoptimizer/gltf/gltfpack.manifest new file mode 100644 index 0000000..324aa6d --- /dev/null +++ b/Lib/meshoptimizer/gltf/gltfpack.manifest @@ -0,0 +1,9 @@ + + + + + + UTF-8 + + + diff --git a/Lib/meshoptimizer/gltf/image.cpp b/Lib/meshoptimizer/gltf/image.cpp new file mode 100644 index 0000000..0615ff0 --- /dev/null +++ b/Lib/meshoptimizer/gltf/image.cpp @@ -0,0 +1,372 @@ +// This file is part of gltfpack; see gltfpack.h for version/license details +#include "gltfpack.h" + +#include +#include + +static const char* kMimeTypes[][2] = { + {"image/jpeg", ".jpg"}, + {"image/jpeg", ".jpeg"}, + {"image/png", ".png"}, + {"image/ktx2", ".ktx2"}, + {"image/webp", ".webp"}, +}; + +static const char* inferMimeType(const char* path) +{ + std::string ext = getExtension(path); + + for (size_t i = 0; i < sizeof(kMimeTypes) / sizeof(kMimeTypes[0]); ++i) + if (ext == kMimeTypes[i][1]) + return kMimeTypes[i][0]; + + return ""; +} + +static bool parseDataUri(const char* uri, std::string& mime_type, std::string& result) +{ + if (strncmp(uri, "data:", 5) == 0) + { + const char* comma = strchr(uri, ','); + + if (comma && comma - uri >= 7 && strncmp(comma - 7, ";base64", 7) == 0) + { + const char* base64 = comma + 1; + size_t base64_size = strlen(base64); + size_t size = base64_size - base64_size / 4; + + if (base64_size >= 2) + { + size -= base64[base64_size - 2] == '='; + size -= base64[base64_size - 1] == '='; + } + + void* data = NULL; + + cgltf_options options = {}; + cgltf_result res = cgltf_load_buffer_base64(&options, size, base64, &data); + + if (res != cgltf_result_success) + return false; + + mime_type = std::string(uri + 5, comma - 7); + result = std::string(static_cast(data), size); + + free(data); + + return true; + } + } + + return false; +} + +static std::string fixupMimeType(const std::string& data, const std::string& mime_type) +{ + if (mime_type == "image/jpeg" && data.compare(0, 8, "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a") == 0) + return "image/png"; + + if (mime_type == "image/png" && data.compare(0, 3, "\xff\xd8\xff") == 0) + return "image/jpeg"; + + return mime_type; +} + +bool readImage(const cgltf_image& image, const char* input_path, std::string& data, std::string& mime_type) +{ + if (image.uri && parseDataUri(image.uri, mime_type, data)) + { + mime_type = fixupMimeType(data, mime_type); + return true; + } + else if (image.buffer_view && image.buffer_view->buffer->data && image.mime_type) + { + const cgltf_buffer_view* view = image.buffer_view; + + data.assign(static_cast(view->buffer->data) + view->offset, view->size); + + mime_type = image.mime_type; + mime_type = fixupMimeType(data, image.mime_type); + return true; + } + else if (image.uri && *image.uri && input_path) + { + std::string path = image.uri; + + cgltf_decode_uri(&path[0]); + path.resize(strlen(&path[0])); + + if (!readFile(getFullPath(path.c_str(), input_path).c_str(), data)) + return false; + + mime_type = image.mime_type ? image.mime_type : inferMimeType(path.c_str()); + mime_type = fixupMimeType(data, mime_type); + return true; + } + else + { + return false; + } +} + +static int readInt16BE(const std::string& data, size_t offset) +{ + return (unsigned char)data[offset] * 256 + (unsigned char)data[offset + 1]; +} + +static int readInt32BE(const std::string& data, size_t offset) +{ + return (unsigned((unsigned char)data[offset]) << 24) | + (unsigned((unsigned char)data[offset + 1]) << 16) | + (unsigned((unsigned char)data[offset + 2]) << 8) | + unsigned((unsigned char)data[offset + 3]); +} + +static int readInt32LE(const std::string& data, size_t offset) +{ + return unsigned((unsigned char)data[offset]) | + (unsigned((unsigned char)data[offset + 1]) << 8) | + (unsigned((unsigned char)data[offset + 2]) << 16) | + (unsigned((unsigned char)data[offset + 3]) << 24); +} + +// https://en.wikipedia.org/wiki/PNG#File_format +static bool getDimensionsPng(const std::string& data, int& width, int& height) +{ + if (data.size() < 8 + 8 + 13 + 4) + return false; + + const char* signature = "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"; + if (data.compare(0, 8, signature) != 0) + return false; + + if (data.compare(12, 4, "IHDR") != 0) + return false; + + width = readInt32BE(data, 16); + height = readInt32BE(data, 20); + + return true; +} + +// https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format#File_format_structure +static bool getDimensionsJpeg(const std::string& data, int& width, int& height) +{ + size_t offset = 0; + + // note, this can stop parsing before reaching the end but we stop at SOF anyway + while (offset + 4 <= data.size()) + { + if (data[offset] != '\xff') + return false; + + char marker = data[offset + 1]; + + if (marker == '\xff') + { + offset++; + continue; // padding + } + + // d0..d9 correspond to SOI, RSTn, EOI + if (marker == 0 || unsigned(marker - '\xd0') <= 9) + { + offset += 2; + continue; // no payload + } + + // c0/c2 correspond to SOF0/SOF2 + if (marker == '\xc0' || marker == '\xc2') + { + if (offset + 10 > data.size()) + return false; + + width = readInt16BE(data, offset + 7); + height = readInt16BE(data, offset + 5); + + return true; + } + + offset += 2 + readInt16BE(data, offset + 2); + } + + return false; +} + +// https://en.wikipedia.org/wiki/PNG#File_format +static bool hasTransparencyPng(const std::string& data) +{ + if (data.size() < 8 + 8 + 13 + 4) + return false; + + const char* signature = "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"; + if (data.compare(0, 8, signature) != 0) + return false; + + if (data.compare(12, 4, "IHDR") != 0) + return false; + + int ctype = data[25]; + + if (ctype != 3) + return ctype == 4 || ctype == 6; + + size_t offset = 8; // reparse IHDR chunk for simplicity + + while (offset + 12 <= data.size()) + { + int length = readInt32BE(data, offset); + + if (length < 0) + return false; + + if (data.compare(offset + 4, 4, "tRNS") == 0) + return true; + + offset += 12 + length; + } + + return false; +} + +// https://github.khronos.org/KTX-Specification/ktxspec.v2.html +static bool hasTransparencyKtx2(const std::string& data) +{ + if (data.size() < 12 + 17 * 4) + return false; + + const char* signature = "\xabKTX 20\xbb\r\n\x1a\n"; + if (data.compare(0, 12, signature) != 0) + return false; + + int dfdOffset = readInt32LE(data, 48); + int dfdLength = readInt32LE(data, 52); + + if (dfdLength < 4 + 24 + 16 || unsigned(dfdOffset) > data.size() || unsigned(dfdLength) > data.size() - dfdOffset) + return false; + + const int KDF_DF_MODEL_ETC1S = 163; + const int KDF_DF_MODEL_UASTC = 166; + const int KHR_DF_CHANNEL_UASTC_RGBA = 3; + const int KHR_DF_CHANNEL_ETC1S_RGB = 0; + const int KHR_DF_CHANNEL_ETC1S_AAA = 15; + + int colorModel = readInt32LE(data, dfdOffset + 12) & 0xff; + int channelType1 = (readInt32LE(data, dfdOffset + 28) >> 24) & 0xf; + int channelType2 = dfdLength >= 4 + 24 + 16 * 2 ? (readInt32LE(data, dfdOffset + 44) >> 24) & 0xf : channelType1; + + if (colorModel == KDF_DF_MODEL_ETC1S) + { + return channelType1 == KHR_DF_CHANNEL_ETC1S_RGB && channelType2 == KHR_DF_CHANNEL_ETC1S_AAA; + } + else if (colorModel == KDF_DF_MODEL_UASTC) + { + return channelType1 == KHR_DF_CHANNEL_UASTC_RGBA; + } + + return false; +} + +// https://developers.google.com/speed/webp/docs/riff_container +static bool hasTransparencyWebP(const std::string& data) +{ + if (data.size() < 12 + 4 + 12) + return false; + + if (data.compare(0, 4, "RIFF") != 0) + return false; + if (data.compare(8, 4, "WEBP") != 0) + return false; + + // WebP data may use VP8L, VP8X or VP8 format, but VP8 does not support transparency + if (data.compare(12, 4, "VP8L") == 0) + { + if (unsigned(data[20]) != 0x2f) + return false; + + // width (14) | height (14) | alpha_is_used (1) | version_number(3) + unsigned int header = readInt32LE(data, 21); + return (header & (1 << 28)) != 0; + } + else if (data.compare(12, 4, "VP8X") == 0) + { + // zero (2) | icc (1) | alpha (1) | exif (1) | xmp (1) | animation (1) | zero (1) + unsigned char header = data[20]; + return (header & (1 << 4)) != 0; + } + + return false; +} + +bool hasAlpha(const std::string& data, const char* mime_type) +{ + if (strcmp(mime_type, "image/png") == 0) + return hasTransparencyPng(data); + else if (strcmp(mime_type, "image/ktx2") == 0) + return hasTransparencyKtx2(data); + else if (strcmp(mime_type, "image/webp") == 0) + return hasTransparencyWebP(data); + else + return false; +} + +bool getDimensions(const std::string& data, const char* mime_type, int& width, int& height) +{ + if (strcmp(mime_type, "image/png") == 0) + return getDimensionsPng(data, width, height); + if (strcmp(mime_type, "image/jpeg") == 0) + return getDimensionsJpeg(data, width, height); + + return false; +} + +static int roundPow2(int value) +{ + int result = 1; + + while (result < value) + result <<= 1; + + // to prevent odd texture sizes from increasing the size too much, we round to nearest power of 2 above a certain size + if (value > 128 && result * 3 / 4 > value) + result >>= 1; + + return result; +} + +static int roundBlock(int value, bool pow2) +{ + if (value == 0) + return 4; + + if (pow2 && value > 4) + return roundPow2(value); + + return (value + 3) & ~3; +} + +void adjustDimensions(int& width, int& height, float scale, int limit, bool pow2) +{ + width = int(width * scale); + height = int(height * scale); + + if (limit && (width > limit || height > limit)) + { + float limit_scale = float(limit) / float(width > height ? width : height); + + width = int(width * limit_scale); + height = int(height * limit_scale); + } + + width = roundBlock(width, pow2); + height = roundBlock(height, pow2); +} + +const char* mimeExtension(const char* mime_type) +{ + for (size_t i = 0; i < sizeof(kMimeTypes) / sizeof(kMimeTypes[0]); ++i) + if (strcmp(kMimeTypes[i][0], mime_type) == 0) + return kMimeTypes[i][1]; + + return ".raw"; +} diff --git a/Lib/meshoptimizer/gltf/json.cpp b/Lib/meshoptimizer/gltf/json.cpp new file mode 100644 index 0000000..837f47d --- /dev/null +++ b/Lib/meshoptimizer/gltf/json.cpp @@ -0,0 +1,92 @@ +// This file is part of gltfpack; see gltfpack.h for version/license details +#include "gltfpack.h" + +#include +#include +#include + +void comma(std::string& s) +{ + char ch = s.empty() ? 0 : s[s.size() - 1]; + + if (ch != 0 && ch != '[' && ch != '{') + s += ","; +} + +void append(std::string& s, size_t v) +{ + char buf[32]; + snprintf(buf, sizeof(buf), "%zu", v); + s += buf; +} + +void append(std::string& s, float v) +{ + // sanitize +-inf to +-FLT_MAX and NaN to FLT_MAX + // it would be more consistent to use null for NaN but that makes JSON invalid, and 0 makes it hard to distinguish from valid values + float sv = fabsf(v) < FLT_MAX ? v : (v < 0 ? -FLT_MAX : FLT_MAX); + + char buf[64]; + snprintf(buf, sizeof(buf), "%.9g", sv); + s += buf; +} + +void append(std::string& s, const char* v) +{ + s += v; +} + +void append(std::string& s, const std::string& v) +{ + s += v; +} + +void append(std::string& s, const float* data, size_t count) +{ + s += '['; + for (size_t i = 0; i < count; ++i) + { + if (i != 0) + s += ','; + append(s, data[i]); + } + s += ']'; +} + +void appendJson(std::string& s, const char* data) +{ + enum State + { + None, + Escape, + Quoted + } state = None; + + for (const char* it = data; *it; ++it) + { + char ch = *it; + + // whitespace outside of quoted strings can be ignored + if (state != None || !isspace(ch)) + s += ch; + + // the finite automata tracks whether we're inside a quoted string + switch (state) + { + case None: + state = (ch == '"') ? Quoted : None; + break; + + case Quoted: + state = (ch == '"') ? None : (ch == '\\' ? Escape : Quoted); + break; + + case Escape: + state = Quoted; + break; + + default: + assert(!"Unexpected parsing state"); + } + } +} diff --git a/Lib/meshoptimizer/gltf/library.js b/Lib/meshoptimizer/gltf/library.js new file mode 100644 index 0000000..6169626 --- /dev/null +++ b/Lib/meshoptimizer/gltf/library.js @@ -0,0 +1,355 @@ +// This file is part of gltfpack and is distributed under the terms of MIT License. + +/** + * Initialize the library with the Wasm module (library.wasm) + * + * @param wasm Promise with contents of library.wasm + * + * Note: this is called automatically in node.js + */ +function init(wasm) { + if (ready) { + throw new Error('init must be called once'); + } + + ready = Promise.resolve(wasm) + .then(function (buffer) { + return WebAssembly.instantiate(buffer, { wasi_snapshot_preview1: wasi }); + }) + .then(function (result) { + instance = result.instance; + instance.exports.__wasm_call_ctors(); + }); +} + +/** + * Pack the requested glTF data using the requested command line and access interface. + * + * @param args An array of strings with the input arguments; the paths for input and output files are interpreted by the interface + * @param iface An interface to the system that will be used to service file requests and other system calls + * @return Promise that indicates completion of the operation + * + * iface should contain the following methods: + * read(path): Given a path, return a Uint8Array with the contents of that path + * write(path, data): Write the specified Uint8Array to the provided path + */ +function pack(args, iface) { + if (!ready) { + throw new Error('init must be called before pack'); + } + + var argv = args.slice(); + argv.unshift('gltfpack'); + + return ready.then(function () { + var buf = uploadArgv(argv); + + output.position = 0; + output.size = 0; + + fs_interface = iface; + var result = instance.exports.pack(argv.length, buf); + fs_interface = undefined; + + instance.exports.free(buf); + + var log = getString(output.data.buffer, 0, output.size); + + if (result != 0) { + throw new Error(log); + } else { + return log; + } + }); +} + +// Library implementation (here be dragons) +var WASI_EBADF = 8; +var WASI_EINVAL = 28; +var WASI_EIO = 29; +var WASI_ENOSYS = 52; + +var ready; +var instance; +var fs_interface; + +var output = { data: new Uint8Array(), position: 0, size: 0 }; +var fds = { 1: output, 2: output, 3: { mount: '/', path: '/' }, 4: { mount: '/gltfpack-$pwd', path: '' } }; + +var wasi = { + proc_exit: function (rval) {}, + + fd_close: function (fd) { + if (!fds[fd]) { + return WASI_EBADF; + } + + try { + if (fds[fd].close) { + fds[fd].close(); + } + fds[fd] = undefined; + return 0; + } catch (err) { + fds[fd] = undefined; + return WASI_EIO; + } + }, + + fd_fdstat_get: function (fd, stat) { + if (!fds[fd]) { + return WASI_EBADF; + } + + var heap = getHeap(); + heap.setUint8(stat + 0, fds[fd].path !== undefined ? 3 : 4); + heap.setUint16(stat + 2, 0, true); + heap.setUint32(stat + 8, 0, true); + heap.setUint32(stat + 12, 0, true); + heap.setUint32(stat + 16, 0, true); + heap.setUint32(stat + 20, 0, true); + return 0; + }, + + path_open32: function (parent_fd, dirflags, path, path_len, oflags, fs_rights_base, fs_rights_inheriting, fdflags, opened_fd) { + if (!fds[parent_fd] || fds[parent_fd].path === undefined) { + return WASI_EBADF; + } + + var heap = getHeap(); + + var file = {}; + file.name = fds[parent_fd].path + getString(heap.buffer, path, path_len); + file.position = 0; + + if (oflags & 1) { + file.data = new Uint8Array(4096); + file.size = 0; + file.close = function () { + fs_interface.write(file.name, new Uint8Array(file.data.buffer, 0, file.size)); + }; + } else { + try { + file.data = fs_interface.read(file.name); + + if (!file.data) { + return WASI_EIO; + } + + file.size = file.data.length; + } catch (err) { + return WASI_EIO; + } + } + + var fd = nextFd(); + fds[fd] = file; + + heap.setUint32(opened_fd, fd, true); + return 0; + }, + + path_filestat_get: function (parent_fd, flags, path, path_len, buf) { + if (!fds[parent_fd] || fds[parent_fd].path === undefined) { + return WASI_EBADF; + } + + var heap = getHeap(); + var name = getString(heap.buffer, path, path_len); + + for (var i = 0; i < 64; ++i) heap.setUint8(buf + i, 0); + + heap.setUint8(buf + 16, name == '.' ? 3 : 4); + return 0; + }, + + fd_prestat_get: function (fd, buf) { + if (!fds[fd] || fds[fd].path === undefined) { + return WASI_EBADF; + } + + var path_buf = stringBuffer(fds[fd].mount); + + var heap = getHeap(); + heap.setUint8(buf, 0); + heap.setUint32(buf + 4, path_buf.length, true); + return 0; + }, + + fd_prestat_dir_name: function (fd, path, path_len) { + if (!fds[fd] || fds[fd].path === undefined) { + return WASI_EBADF; + } + + var path_buf = stringBuffer(fds[fd].mount); + + if (path_len != path_buf.length) { + return WASI_EINVAL; + } + + var heap = getHeap(); + new Uint8Array(heap.buffer).set(path_buf, path); + return 0; + }, + + path_remove_directory: function (parent_fd, path, path_len) { + return WASI_EINVAL; + }, + + fd_fdstat_set_flags: function (fd, flags) { + return WASI_ENOSYS; + }, + + fd_seek32: function (fd, offset, whence, newoffset) { + if (!fds[fd]) { + return WASI_EBADF; + } + + var newposition; + + switch (whence) { + case 0: + newposition = offset; + break; + + case 1: + newposition = fds[fd].position + offset; + break; + + case 2: + newposition = fds[fd].size + offset; + break; + + default: + return WASI_EINVAL; + } + + if (newposition < 0 || newposition > fds[fd].size) { + return WASI_EINVAL; + } + + fds[fd].position = newposition; + + var heap = getHeap(); + heap.setUint32(newoffset, newposition, true); + return 0; + }, + + fd_read: function (fd, iovs, iovs_len, nread) { + if (!fds[fd]) { + return WASI_EBADF; + } + + var heap = getHeap(); + var read = 0; + + for (var i = 0; i < iovs_len; ++i) { + var buf = heap.getUint32(iovs + 8 * i + 0, true); + var buf_len = heap.getUint32(iovs + 8 * i + 4, true); + + var readi = Math.min(fds[fd].size - fds[fd].position, buf_len); + + new Uint8Array(heap.buffer).set(fds[fd].data.subarray(fds[fd].position, fds[fd].position + readi), buf); + + fds[fd].position += readi; + read += readi; + } + + heap.setUint32(nread, read, true); + return 0; + }, + + fd_write: function (fd, iovs, iovs_len, nwritten) { + if (!fds[fd]) { + return WASI_EBADF; + } + + var heap = getHeap(); + var written = 0; + + for (var i = 0; i < iovs_len; ++i) { + var buf = heap.getUint32(iovs + 8 * i + 0, true); + var buf_len = heap.getUint32(iovs + 8 * i + 4, true); + + if (fds[fd].position + buf_len > fds[fd].data.length) { + fds[fd].data = growArray(fds[fd].data, fds[fd].position + buf_len); + } + + fds[fd].data.set(new Uint8Array(heap.buffer, buf, buf_len), fds[fd].position); + fds[fd].position += buf_len; + fds[fd].size = Math.max(fds[fd].position, fds[fd].size); + + written += buf_len; + } + + heap.setUint32(nwritten, written, true); + return 0; + }, +}; + +function nextFd() { + for (var i = 1; ; ++i) { + if (fds[i] === undefined) { + return i; + } + } +} + +function getHeap() { + return new DataView(instance.exports.memory.buffer); +} + +function getString(buffer, offset, length) { + return new TextDecoder().decode(new Uint8Array(buffer, offset, length)); +} + +function stringBuffer(string) { + return new TextEncoder().encode(string); +} + +function growArray(data, len) { + var new_length = Math.max(1, data.length); + while (new_length < len) { + new_length *= 2; + } + + var new_data = new Uint8Array(new_length); + new_data.set(data); + + return new_data; +} + +function uploadArgv(argv) { + var buf_size = argv.length * 4; + for (var i = 0; i < argv.length; ++i) { + buf_size += stringBuffer(argv[i]).length + 1; + } + + var buf = instance.exports.malloc(buf_size); + var argp = buf + argv.length * 4; + + var heap = getHeap(); + + for (var i = 0; i < argv.length; ++i) { + var item = stringBuffer(argv[i]); + + heap.setUint32(buf + i * 4, argp, true); + new Uint8Array(heap.buffer).set(item, argp); + heap.setUint8(argp + item.length, 0); + + argp += item.length + 1; + } + + return buf; +} + +// Automatic initialization for node.js +if (typeof process !== 'undefined' && process.release && process.release.name === 'node') { + init( + import('node:fs').then(function (fs) { + return fs.readFileSync(new URL('./library.wasm', import.meta.url)); + }) + ); +} + +export { init, pack }; diff --git a/Lib/meshoptimizer/gltf/material.cpp b/Lib/meshoptimizer/gltf/material.cpp new file mode 100644 index 0000000..63aa9f2 --- /dev/null +++ b/Lib/meshoptimizer/gltf/material.cpp @@ -0,0 +1,637 @@ +// This file is part of gltfpack; see gltfpack.h for version/license details +#include "gltfpack.h" + +#include + +static bool areTexturesEqual(const cgltf_texture& lhs, const cgltf_texture& rhs) +{ + if (lhs.image != rhs.image) + return false; + + if (lhs.sampler != rhs.sampler) + return false; + + if (lhs.basisu_image != rhs.basisu_image) + return false; + + if (lhs.webp_image != rhs.webp_image) + return false; + + return true; +} + +static bool areTextureViewsEqual(const cgltf_texture_view& lhs, const cgltf_texture_view& rhs) +{ + if (lhs.has_transform != rhs.has_transform) + return false; + + if (lhs.has_transform) + { + const cgltf_texture_transform& lt = lhs.transform; + const cgltf_texture_transform& rt = rhs.transform; + + if (memcmp(lt.offset, rt.offset, sizeof(cgltf_float) * 2) != 0) + return false; + + if (lt.rotation != rt.rotation) + return false; + + if (memcmp(lt.scale, rt.scale, sizeof(cgltf_float) * 2) != 0) + return false; + + if (lt.texcoord != rt.texcoord) + return false; + } + + if (lhs.texture != rhs.texture && (!lhs.texture || !rhs.texture || !areTexturesEqual(*lhs.texture, *rhs.texture))) + return false; + + if (lhs.texcoord != rhs.texcoord) + return false; + + if (lhs.scale != rhs.scale) + return false; + + return true; +} + +static bool areMaterialComponentsEqual(const cgltf_pbr_metallic_roughness& lhs, const cgltf_pbr_metallic_roughness& rhs) +{ + if (!areTextureViewsEqual(lhs.base_color_texture, rhs.base_color_texture)) + return false; + + if (!areTextureViewsEqual(lhs.metallic_roughness_texture, rhs.metallic_roughness_texture)) + return false; + + if (memcmp(lhs.base_color_factor, rhs.base_color_factor, sizeof(cgltf_float) * 4) != 0) + return false; + + if (lhs.metallic_factor != rhs.metallic_factor) + return false; + + if (lhs.roughness_factor != rhs.roughness_factor) + return false; + + return true; +} + +static bool areMaterialComponentsEqual(const cgltf_pbr_specular_glossiness& lhs, const cgltf_pbr_specular_glossiness& rhs) +{ + if (!areTextureViewsEqual(lhs.diffuse_texture, rhs.diffuse_texture)) + return false; + + if (!areTextureViewsEqual(lhs.specular_glossiness_texture, rhs.specular_glossiness_texture)) + return false; + + if (memcmp(lhs.diffuse_factor, rhs.diffuse_factor, sizeof(cgltf_float) * 4) != 0) + return false; + + if (memcmp(lhs.specular_factor, rhs.specular_factor, sizeof(cgltf_float) * 3) != 0) + return false; + + if (lhs.glossiness_factor != rhs.glossiness_factor) + return false; + + return true; +} + +static bool areMaterialComponentsEqual(const cgltf_clearcoat& lhs, const cgltf_clearcoat& rhs) +{ + if (!areTextureViewsEqual(lhs.clearcoat_texture, rhs.clearcoat_texture)) + return false; + + if (!areTextureViewsEqual(lhs.clearcoat_roughness_texture, rhs.clearcoat_roughness_texture)) + return false; + + if (!areTextureViewsEqual(lhs.clearcoat_normal_texture, rhs.clearcoat_normal_texture)) + return false; + + if (lhs.clearcoat_factor != rhs.clearcoat_factor) + return false; + + if (lhs.clearcoat_roughness_factor != rhs.clearcoat_roughness_factor) + return false; + + return true; +} + +static bool areMaterialComponentsEqual(const cgltf_transmission& lhs, const cgltf_transmission& rhs) +{ + if (!areTextureViewsEqual(lhs.transmission_texture, rhs.transmission_texture)) + return false; + + if (lhs.transmission_factor != rhs.transmission_factor) + return false; + + return true; +} + +static bool areMaterialComponentsEqual(const cgltf_ior& lhs, const cgltf_ior& rhs) +{ + if (lhs.ior != rhs.ior) + return false; + + return true; +} + +static bool areMaterialComponentsEqual(const cgltf_specular& lhs, const cgltf_specular& rhs) +{ + if (!areTextureViewsEqual(lhs.specular_texture, rhs.specular_texture)) + return false; + + if (!areTextureViewsEqual(lhs.specular_color_texture, rhs.specular_color_texture)) + return false; + + if (lhs.specular_factor != rhs.specular_factor) + return false; + + if (memcmp(lhs.specular_color_factor, rhs.specular_color_factor, sizeof(cgltf_float) * 3) != 0) + return false; + + return true; +} + +static bool areMaterialComponentsEqual(const cgltf_sheen& lhs, const cgltf_sheen& rhs) +{ + if (!areTextureViewsEqual(lhs.sheen_color_texture, rhs.sheen_color_texture)) + return false; + + if (memcmp(lhs.sheen_color_factor, rhs.sheen_color_factor, sizeof(cgltf_float) * 3) != 0) + return false; + + if (!areTextureViewsEqual(lhs.sheen_roughness_texture, rhs.sheen_roughness_texture)) + return false; + + if (lhs.sheen_roughness_factor != rhs.sheen_roughness_factor) + return false; + + return true; +} + +static bool areMaterialComponentsEqual(const cgltf_volume& lhs, const cgltf_volume& rhs) +{ + if (!areTextureViewsEqual(lhs.thickness_texture, rhs.thickness_texture)) + return false; + + if (lhs.thickness_factor != rhs.thickness_factor) + return false; + + if (memcmp(lhs.attenuation_color, rhs.attenuation_color, sizeof(cgltf_float) * 3) != 0) + return false; + + if (lhs.attenuation_distance != rhs.attenuation_distance) + return false; + + return true; +} + +static bool areMaterialComponentsEqual(const cgltf_emissive_strength& lhs, const cgltf_emissive_strength& rhs) +{ + if (lhs.emissive_strength != rhs.emissive_strength) + return false; + + return true; +} + +static bool areMaterialComponentsEqual(const cgltf_iridescence& lhs, const cgltf_iridescence& rhs) +{ + if (lhs.iridescence_factor != rhs.iridescence_factor) + return false; + + if (!areTextureViewsEqual(lhs.iridescence_texture, rhs.iridescence_texture)) + return false; + + if (lhs.iridescence_ior != rhs.iridescence_ior) + return false; + + if (lhs.iridescence_thickness_min != rhs.iridescence_thickness_min) + return false; + + if (lhs.iridescence_thickness_max != rhs.iridescence_thickness_max) + return false; + + if (!areTextureViewsEqual(lhs.iridescence_thickness_texture, rhs.iridescence_thickness_texture)) + return false; + + return true; +} + +static bool areMaterialComponentsEqual(const cgltf_anisotropy& lhs, const cgltf_anisotropy& rhs) +{ + if (lhs.anisotropy_strength != rhs.anisotropy_strength) + return false; + + if (lhs.anisotropy_rotation != rhs.anisotropy_rotation) + return false; + + if (!areTextureViewsEqual(lhs.anisotropy_texture, rhs.anisotropy_texture)) + return false; + + return true; +} + +static bool areMaterialComponentsEqual(const cgltf_dispersion& lhs, const cgltf_dispersion& rhs) +{ + if (lhs.dispersion != rhs.dispersion) + return false; + + return true; +} + +static bool areMaterialComponentsEqual(const cgltf_diffuse_transmission& lhs, const cgltf_diffuse_transmission& rhs) +{ + if (lhs.diffuse_transmission_factor != rhs.diffuse_transmission_factor) + return false; + + if (memcmp(lhs.diffuse_transmission_color_factor, rhs.diffuse_transmission_color_factor, sizeof(cgltf_float) * 3) != 0) + return false; + + if (!areTextureViewsEqual(lhs.diffuse_transmission_texture, rhs.diffuse_transmission_texture)) + return false; + + if (!areTextureViewsEqual(lhs.diffuse_transmission_color_texture, rhs.diffuse_transmission_color_texture)) + return false; + + return true; +} + +static bool areMaterialsEqual(const cgltf_material& lhs, const cgltf_material& rhs, const Settings& settings) +{ + if (lhs.has_pbr_metallic_roughness != rhs.has_pbr_metallic_roughness) + return false; + + if (lhs.has_pbr_metallic_roughness && !areMaterialComponentsEqual(lhs.pbr_metallic_roughness, rhs.pbr_metallic_roughness)) + return false; + + if (lhs.has_pbr_specular_glossiness != rhs.has_pbr_specular_glossiness) + return false; + + if (lhs.has_pbr_specular_glossiness && !areMaterialComponentsEqual(lhs.pbr_specular_glossiness, rhs.pbr_specular_glossiness)) + return false; + + if (lhs.has_clearcoat != rhs.has_clearcoat) + return false; + + if (lhs.has_clearcoat && !areMaterialComponentsEqual(lhs.clearcoat, rhs.clearcoat)) + return false; + + if (lhs.has_transmission != rhs.has_transmission) + return false; + + if (lhs.has_transmission && !areMaterialComponentsEqual(lhs.transmission, rhs.transmission)) + return false; + + if (lhs.has_ior != rhs.has_ior) + return false; + + if (lhs.has_ior && !areMaterialComponentsEqual(lhs.ior, rhs.ior)) + return false; + + if (lhs.has_specular != rhs.has_specular) + return false; + + if (lhs.has_specular && !areMaterialComponentsEqual(lhs.specular, rhs.specular)) + return false; + + if (lhs.has_sheen != rhs.has_sheen) + return false; + + if (lhs.has_sheen && !areMaterialComponentsEqual(lhs.sheen, rhs.sheen)) + return false; + + if (lhs.has_volume != rhs.has_volume) + return false; + + if (lhs.has_volume && !areMaterialComponentsEqual(lhs.volume, rhs.volume)) + return false; + + if (lhs.has_emissive_strength != rhs.has_emissive_strength) + return false; + + if (lhs.has_emissive_strength && !areMaterialComponentsEqual(lhs.emissive_strength, rhs.emissive_strength)) + return false; + + if (lhs.has_iridescence != rhs.has_iridescence) + return false; + + if (lhs.has_iridescence && !areMaterialComponentsEqual(lhs.iridescence, rhs.iridescence)) + return false; + + if (lhs.has_anisotropy != rhs.has_anisotropy) + return false; + + if (lhs.has_anisotropy && !areMaterialComponentsEqual(lhs.anisotropy, rhs.anisotropy)) + return false; + + if (lhs.has_dispersion != rhs.has_dispersion) + return false; + + if (lhs.has_dispersion && !areMaterialComponentsEqual(lhs.dispersion, rhs.dispersion)) + return false; + + if (lhs.has_diffuse_transmission != rhs.has_diffuse_transmission) + return false; + + if (lhs.has_diffuse_transmission && !areMaterialComponentsEqual(lhs.diffuse_transmission, rhs.diffuse_transmission)) + return false; + + if (!areTextureViewsEqual(lhs.normal_texture, rhs.normal_texture)) + return false; + + if (!areTextureViewsEqual(lhs.occlusion_texture, rhs.occlusion_texture)) + return false; + + if (!areTextureViewsEqual(lhs.emissive_texture, rhs.emissive_texture)) + return false; + + if (memcmp(lhs.emissive_factor, rhs.emissive_factor, sizeof(cgltf_float) * 3) != 0) + return false; + + if (lhs.alpha_mode != rhs.alpha_mode) + return false; + + if (lhs.alpha_cutoff != rhs.alpha_cutoff) + return false; + + if (lhs.double_sided != rhs.double_sided) + return false; + + if (lhs.unlit != rhs.unlit) + return false; + + if (settings.keep_extras && !areExtrasEqual(lhs.extras, rhs.extras)) + return false; + + return true; +} + +void mergeMeshMaterials(cgltf_data* data, std::vector& meshes, const Settings& settings) +{ + std::vector material_remap(data->materials_count); + + for (size_t i = 0; i < data->materials_count; ++i) + { + material_remap[i] = &data->materials[i]; + + if (settings.keep_materials && data->materials[i].name && *data->materials[i].name) + continue; + + assert(areMaterialsEqual(data->materials[i], data->materials[i], settings)); + + for (size_t j = 0; j < i; ++j) + { + if (settings.keep_materials && data->materials[j].name && *data->materials[j].name) + continue; + + if (areMaterialsEqual(data->materials[i], data->materials[j], settings)) + { + material_remap[i] = &data->materials[j]; + break; + } + } + } + + for (size_t i = 0; i < meshes.size(); ++i) + { + Mesh& mesh = meshes[i]; + + if (mesh.material) + mesh.material = material_remap[mesh.material - data->materials]; + + for (size_t j = 0; j < mesh.variants.size(); ++j) + mesh.variants[j].material = material_remap[mesh.variants[j].material - data->materials]; + } +} + +void markNeededMaterials(cgltf_data* data, std::vector& materials, const std::vector& meshes, const Settings& settings) +{ + // mark all used materials as kept + for (size_t i = 0; i < meshes.size(); ++i) + { + const Mesh& mesh = meshes[i]; + + if (mesh.material) + { + MaterialInfo& mi = materials[mesh.material - data->materials]; + + mi.keep = true; + } + + for (size_t j = 0; j < mesh.variants.size(); ++j) + { + MaterialInfo& mi = materials[mesh.variants[j].material - data->materials]; + + mi.keep = true; + } + } + + // mark all named materials as kept if requested + if (settings.keep_materials) + { + for (size_t i = 0; i < data->materials_count; ++i) + { + cgltf_material& material = data->materials[i]; + + if (material.name && *material.name) + { + materials[i].keep = true; + } + } + } +} + +bool isValidTransform(const cgltf_texture_transform& transform) +{ + return transform.offset[0] != 0.0f || transform.offset[1] != 0.0f || + transform.scale[0] != 1.0f || transform.scale[1] != 1.0f || + transform.rotation != 0.0f; +} + +static const cgltf_image* getTextureImage(const cgltf_texture* texture) +{ + if (texture && texture->image) + return texture->image; + + if (texture && texture->basisu_image) + return texture->basisu_image; + + if (texture && texture->webp_image) + return texture->webp_image; + + return NULL; +} + +static void analyzeMaterialTexture(const cgltf_texture_view& view, TextureKind kind, MaterialInfo& mi, cgltf_data* data, std::vector& textures, std::vector& images) +{ + mi.uses_texture_transform |= view.has_transform && isValidTransform(view.transform); + + if (view.texture) + { + textures[view.texture - data->textures].keep = true; + + mi.texture_set_mask |= 1u << view.texcoord; + mi.needs_tangents |= (kind == TextureKind_Normal); + } + + if (const cgltf_image* image = getTextureImage(view.texture)) + { + ImageInfo& info = images[image - data->images]; + + if (info.kind == TextureKind_Generic) + info.kind = kind; + else if (info.kind > kind) // this is useful to keep color textures that have attrib data in alpha tagged as color + info.kind = kind; + + info.normal_map |= (kind == TextureKind_Normal); + info.srgb |= (kind == TextureKind_Color); + } +} + +static void analyzeMaterial(const cgltf_material& material, MaterialInfo& mi, cgltf_data* data, std::vector& textures, std::vector& images) +{ + if (material.has_pbr_metallic_roughness) + { + analyzeMaterialTexture(material.pbr_metallic_roughness.base_color_texture, TextureKind_Color, mi, data, textures, images); + analyzeMaterialTexture(material.pbr_metallic_roughness.metallic_roughness_texture, TextureKind_Attrib, mi, data, textures, images); + } + + if (material.has_pbr_specular_glossiness) + { + analyzeMaterialTexture(material.pbr_specular_glossiness.diffuse_texture, TextureKind_Color, mi, data, textures, images); + analyzeMaterialTexture(material.pbr_specular_glossiness.specular_glossiness_texture, TextureKind_Attrib, mi, data, textures, images); + } + + if (material.has_clearcoat) + { + analyzeMaterialTexture(material.clearcoat.clearcoat_texture, TextureKind_Attrib, mi, data, textures, images); + analyzeMaterialTexture(material.clearcoat.clearcoat_roughness_texture, TextureKind_Attrib, mi, data, textures, images); + analyzeMaterialTexture(material.clearcoat.clearcoat_normal_texture, TextureKind_Normal, mi, data, textures, images); + } + + if (material.has_transmission) + { + analyzeMaterialTexture(material.transmission.transmission_texture, TextureKind_Attrib, mi, data, textures, images); + } + + if (material.has_specular) + { + analyzeMaterialTexture(material.specular.specular_texture, TextureKind_Attrib, mi, data, textures, images); + analyzeMaterialTexture(material.specular.specular_color_texture, TextureKind_Color, mi, data, textures, images); + } + + if (material.has_sheen) + { + analyzeMaterialTexture(material.sheen.sheen_color_texture, TextureKind_Color, mi, data, textures, images); + analyzeMaterialTexture(material.sheen.sheen_roughness_texture, TextureKind_Attrib, mi, data, textures, images); + } + + if (material.has_volume) + { + analyzeMaterialTexture(material.volume.thickness_texture, TextureKind_Attrib, mi, data, textures, images); + } + + if (material.has_iridescence) + { + analyzeMaterialTexture(material.iridescence.iridescence_texture, TextureKind_Attrib, mi, data, textures, images); + analyzeMaterialTexture(material.iridescence.iridescence_thickness_texture, TextureKind_Attrib, mi, data, textures, images); + } + + if (material.has_anisotropy) + { + analyzeMaterialTexture(material.anisotropy.anisotropy_texture, TextureKind_Normal, mi, data, textures, images); + } + + if (material.has_diffuse_transmission) + { + analyzeMaterialTexture(material.diffuse_transmission.diffuse_transmission_texture, TextureKind_Attrib, mi, data, textures, images); + analyzeMaterialTexture(material.diffuse_transmission.diffuse_transmission_color_texture, TextureKind_Color, mi, data, textures, images); + } + + analyzeMaterialTexture(material.normal_texture, TextureKind_Normal, mi, data, textures, images); + analyzeMaterialTexture(material.occlusion_texture, TextureKind_Attrib, mi, data, textures, images); + analyzeMaterialTexture(material.emissive_texture, TextureKind_Color, mi, data, textures, images); + + if (material.unlit) + mi.unlit = true; +} + +void analyzeMaterials(cgltf_data* data, std::vector& materials, std::vector& textures, std::vector& images) +{ + for (size_t i = 0; i < data->materials_count; ++i) + { + analyzeMaterial(data->materials[i], materials[i], data, textures, images); + } +} + +static int getChannels(const cgltf_image& image, ImageInfo& info, const char* input_path) +{ + if (info.channels) + return info.channels; + + std::string img_data; + std::string mime_type; + if (readImage(image, input_path, img_data, mime_type)) + info.channels = hasAlpha(img_data, mime_type.c_str()) ? 4 : 3; + else + info.channels = -1; + + return info.channels; +} + +static bool shouldKeepAlpha(const cgltf_texture_view& color, float alpha, cgltf_data* data, const char* input_path, std::vector& images) +{ + if (alpha != 1.f) + return true; + + const cgltf_image* image = getTextureImage(color.texture); + + return image && getChannels(*image, images[image - data->images], input_path) == 4; +} + +void optimizeMaterials(cgltf_data* data, std::vector& materials, std::vector& images, const char* input_path) +{ + for (size_t i = 0; i < data->materials_count; ++i) + { + // remove BLEND/MASK from materials that don't have alpha information + cgltf_material& material = data->materials[i]; + + if (material.alpha_mode != cgltf_alpha_mode_opaque && !materials[i].mesh_alpha) + { + if (material.has_pbr_metallic_roughness && shouldKeepAlpha(material.pbr_metallic_roughness.base_color_texture, material.pbr_metallic_roughness.base_color_factor[3], data, input_path, images)) + continue; + + if (material.has_pbr_specular_glossiness && shouldKeepAlpha(material.pbr_specular_glossiness.diffuse_texture, material.pbr_specular_glossiness.diffuse_factor[3], data, input_path, images)) + continue; + + material.alpha_mode = cgltf_alpha_mode_opaque; + material.alpha_cutoff = 0.5f; // reset to default to avoid writing it to output + } + } +} + +void mergeTextures(cgltf_data* data, std::vector& textures) +{ + size_t offset = 0; + + for (size_t i = 0; i < textures.size(); ++i) + { + TextureInfo& info = textures[i]; + + if (!info.keep) + continue; + + for (size_t j = 0; j < i; ++j) + if (textures[j].keep && areTexturesEqual(data->textures[i], data->textures[j])) + { + info.keep = false; + info.remap = textures[j].remap; + break; + } + + if (info.keep) + { + info.remap = int(offset); + offset++; + } + } +} diff --git a/Lib/meshoptimizer/gltf/mesh.cpp b/Lib/meshoptimizer/gltf/mesh.cpp new file mode 100644 index 0000000..0956e87 --- /dev/null +++ b/Lib/meshoptimizer/gltf/mesh.cpp @@ -0,0 +1,1419 @@ +// This file is part of gltfpack; see gltfpack.h for version/license details +#include "gltfpack.h" + +#include +#include +#include + +#include +#include +#include + +#include "../src/meshoptimizer.h" + +static float inverseTranspose(float* result, const float* transform) +{ + float m[4][4] = {}; + memcpy(m, transform, 16 * sizeof(float)); + + float det = + m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2]) - + m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]); + + float invdet = (det == 0.f) ? 0.f : 1.f / det; + + float r[4][4] = {}; + + r[0][0] = (m[1][1] * m[2][2] - m[2][1] * m[1][2]) * invdet; + r[1][0] = (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * invdet; + r[2][0] = (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * invdet; + r[0][1] = (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * invdet; + r[1][1] = (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * invdet; + r[2][1] = (m[1][0] * m[0][2] - m[0][0] * m[1][2]) * invdet; + r[0][2] = (m[1][0] * m[2][1] - m[2][0] * m[1][1]) * invdet; + r[1][2] = (m[2][0] * m[0][1] - m[0][0] * m[2][1]) * invdet; + r[2][2] = (m[0][0] * m[1][1] - m[1][0] * m[0][1]) * invdet; + + r[3][3] = 1.f; + + memcpy(result, r, 16 * sizeof(float)); + + return det; +} + +static void transformPosition(float* res, const float* ptr, const float* transform) +{ + float x = ptr[0] * transform[0] + ptr[1] * transform[4] + ptr[2] * transform[8] + transform[12]; + float y = ptr[0] * transform[1] + ptr[1] * transform[5] + ptr[2] * transform[9] + transform[13]; + float z = ptr[0] * transform[2] + ptr[1] * transform[6] + ptr[2] * transform[10] + transform[14]; + + res[0] = x; + res[1] = y; + res[2] = z; +} + +static void transformNormal(float* res, const float* ptr, const float* transform) +{ + float x = ptr[0] * transform[0] + ptr[1] * transform[4] + ptr[2] * transform[8]; + float y = ptr[0] * transform[1] + ptr[1] * transform[5] + ptr[2] * transform[9]; + float z = ptr[0] * transform[2] + ptr[1] * transform[6] + ptr[2] * transform[10]; + + float l = sqrtf(x * x + y * y + z * z); + float s = (l == 0.f) ? 0.f : 1 / l; + + res[0] = x * s; + res[1] = y * s; + res[2] = z * s; +} + +static Stream* getStream(Mesh& mesh, cgltf_attribute_type type, int index = 0) +{ + for (size_t i = 0; i < mesh.streams.size(); ++i) + if (mesh.streams[i].type == type && mesh.streams[i].index == index) + return &mesh.streams[i]; + + return NULL; +} + +// assumes mesh & target are structurally identical +static void transformMesh(Mesh& target, const Mesh& mesh, const cgltf_node* node) +{ + assert(target.streams.size() == mesh.streams.size()); + assert(target.indices.size() == mesh.indices.size()); + + float transform[16]; + cgltf_node_transform_world(node, transform); + + float transforminvt[16]; + float det = inverseTranspose(transforminvt, transform); + + for (size_t si = 0; si < mesh.streams.size(); ++si) + { + const Stream& source = mesh.streams[si]; + Stream& stream = target.streams[si]; + + assert(source.type == stream.type); + assert(source.data.size() == stream.data.size()); + + if (stream.type == cgltf_attribute_type_position) + { + for (size_t i = 0; i < stream.data.size(); ++i) + transformPosition(stream.data[i].f, source.data[i].f, transform); + } + else if (stream.type == cgltf_attribute_type_normal) + { + for (size_t i = 0; i < stream.data.size(); ++i) + transformNormal(stream.data[i].f, source.data[i].f, transforminvt); + } + else if (stream.type == cgltf_attribute_type_tangent) + { + for (size_t i = 0; i < stream.data.size(); ++i) + transformNormal(stream.data[i].f, source.data[i].f, transform); + + if (det < 0) + { + // negative scale means we need to flip tangent frame handedness + for (size_t i = 0; i < stream.data.size(); ++i) + stream.data[i].f[3] = -stream.data[i].f[3]; + } + } + } + + // copy indices so that we can modify them below + target.indices = mesh.indices; + + if (det < 0 && mesh.type == cgltf_primitive_type_triangles) + { + // negative scale means we need to flip face winding + for (size_t i = 0; i < target.indices.size(); i += 3) + std::swap(target.indices[i + 0], target.indices[i + 1]); + } +} + +bool compareMeshTargets(const Mesh& lhs, const Mesh& rhs) +{ + if (lhs.targets != rhs.targets) + return false; + + if (lhs.target_weights.size() != rhs.target_weights.size()) + return false; + + for (size_t i = 0; i < lhs.target_weights.size(); ++i) + if (lhs.target_weights[i] != rhs.target_weights[i]) + return false; + + if (lhs.target_names.size() != rhs.target_names.size()) + return false; + + for (size_t i = 0; i < lhs.target_names.size(); ++i) + if (strcmp(lhs.target_names[i], rhs.target_names[i]) != 0) + return false; + + return true; +} + +bool compareMeshVariants(const Mesh& lhs, const Mesh& rhs) +{ + if (lhs.variants.size() != rhs.variants.size()) + return false; + + for (size_t i = 0; i < lhs.variants.size(); ++i) + { + if (lhs.variants[i].variant != rhs.variants[i].variant) + return false; + + if (lhs.variants[i].material != rhs.variants[i].material) + return false; + } + + return true; +} + +bool compareMeshNodes(const Mesh& lhs, const Mesh& rhs) +{ + if (lhs.nodes.size() != rhs.nodes.size()) + return false; + + for (size_t i = 0; i < lhs.nodes.size(); ++i) + if (lhs.nodes[i] != rhs.nodes[i]) + return false; + + return true; +} + +static bool compareInstances(const Instance& lhs, const Instance& rhs) +{ + return memcmp(&lhs, &rhs, sizeof(Instance)) == 0; +} + +static bool canMergeMeshNodes(cgltf_node* lhs, cgltf_node* rhs, const Settings& settings) +{ + if (lhs == rhs) + return true; + + if (lhs->parent != rhs->parent) + return false; + + bool lhs_transform = lhs->has_translation | lhs->has_rotation | lhs->has_scale | lhs->has_matrix | (!!lhs->weights); + bool rhs_transform = rhs->has_translation | rhs->has_rotation | rhs->has_scale | rhs->has_matrix | (!!rhs->weights); + + if (lhs_transform || rhs_transform) + return false; + + if (settings.keep_nodes) + { + if (lhs->name && *lhs->name) + return false; + + if (rhs->name && *rhs->name) + return false; + } + + // we can merge nodes that don't have transforms of their own and have the same parent + // this is helpful when instead of splitting mesh into primitives, DCCs split mesh into mesh nodes + return true; +} + +static bool canMergeMeshes(const Mesh& lhs, const Mesh& rhs, const Settings& settings) +{ + if (lhs.scene != rhs.scene) + return false; + + if (lhs.nodes.size() != rhs.nodes.size()) + return false; + + for (size_t i = 0; i < lhs.nodes.size(); ++i) + if (!canMergeMeshNodes(lhs.nodes[i], rhs.nodes[i], settings)) + return false; + + if (lhs.instances.size() != rhs.instances.size()) + return false; + + for (size_t i = 0; i < lhs.instances.size(); ++i) + if (!compareInstances(lhs.instances[i], rhs.instances[i])) + return false; + + if (lhs.material != rhs.material) + return false; + + if (lhs.skin != rhs.skin) + return false; + + if (lhs.type != rhs.type) + return false; + + if (!compareMeshTargets(lhs, rhs)) + return false; + + if (!compareMeshVariants(lhs, rhs)) + return false; + + if (lhs.indices.empty() != rhs.indices.empty()) + return false; + + if (lhs.streams.size() != rhs.streams.size()) + return false; + + if (settings.keep_extras && !areExtrasEqual(lhs.extras, rhs.extras)) + return false; + + for (size_t i = 0; i < lhs.streams.size(); ++i) + if (lhs.streams[i].type != rhs.streams[i].type || lhs.streams[i].index != rhs.streams[i].index || lhs.streams[i].target != rhs.streams[i].target) + return false; + + return true; +} + +static void mergeMeshes(Mesh& target, const Mesh& mesh) +{ + assert(target.streams.size() == mesh.streams.size()); + + size_t vertex_offset = target.streams[0].data.size(); + size_t index_offset = target.indices.size(); + + for (size_t i = 0; i < target.streams.size(); ++i) + target.streams[i].data.insert(target.streams[i].data.end(), mesh.streams[i].data.begin(), mesh.streams[i].data.end()); + + target.indices.resize(target.indices.size() + mesh.indices.size()); + + size_t index_count = mesh.indices.size(); + + for (size_t i = 0; i < index_count; ++i) + target.indices[index_offset + i] = unsigned(vertex_offset + mesh.indices[i]); +} + +static void hashUpdate(uint64_t hash[2], const void* data, size_t size) +{ +#define ROTL64(x, r) (((x) << (r)) | ((x) >> (64 - (r)))) + + // MurMurHash3 128-bit + const uint64_t c1 = 0x87c37b91114253d5ull; + const uint64_t c2 = 0x4cf5ad432745937full; + + uint64_t h1 = hash[0], h2 = hash[1]; + + size_t offset = 0; + + // body + for (; offset + 16 <= size; offset += 16) + { + uint64_t k1, k2; + memcpy(&k1, static_cast(data) + offset + 0, 8); + memcpy(&k2, static_cast(data) + offset + 8, 8); + + k1 *= c1, k1 = ROTL64(k1, 31), k1 *= c2; + h1 ^= k1, h1 = ROTL64(h1, 27), h1 += h2; + h1 = h1 * 5 + 0x52dce729; + k2 *= c2, k2 = ROTL64(k2, 33), k2 *= c1; + h2 ^= k2, h2 = ROTL64(h2, 31), h2 += h1; + h2 = h2 * 5 + 0x38495ab5; + } + + // tail + if (offset < size) + { + uint64_t tail[2] = {}; + memcpy(tail, static_cast(data) + offset, size - offset); + + uint64_t k1 = tail[0], k2 = tail[1]; + + k1 *= c1, k1 = ROTL64(k1, 31), k1 *= c2; + h1 ^= k1; + k2 *= c2, k2 = ROTL64(k2, 33), k2 *= c1; + h2 ^= k2; + } + + h1 ^= size; + h2 ^= size; + + hash[0] = h1; + hash[1] = h2; + +#undef ROTL64 +} + +void hashMesh(Mesh& mesh) +{ + mesh.geometry_hash[0] = mesh.geometry_hash[1] = 41; + + for (size_t i = 0; i < mesh.streams.size(); ++i) + { + const Stream& stream = mesh.streams[i]; + + int meta[3] = {stream.type, stream.index, stream.target}; + hashUpdate(mesh.geometry_hash, meta, sizeof(meta)); + + if (stream.custom_name) + hashUpdate(mesh.geometry_hash, stream.custom_name, strlen(stream.custom_name)); + + hashUpdate(mesh.geometry_hash, stream.data.data(), stream.data.size() * sizeof(Attr)); + } + + if (!mesh.indices.empty()) + hashUpdate(mesh.geometry_hash, mesh.indices.data(), mesh.indices.size() * sizeof(unsigned int)); + + int meta[4] = {int(mesh.streams.size()), mesh.streams.empty() ? 0 : int(mesh.streams[0].data.size()), int(mesh.indices.size()), mesh.type}; + hashUpdate(mesh.geometry_hash, meta, sizeof(meta)); +} + +static bool canDedupMesh(const Mesh& mesh, const Settings& settings) +{ + // empty mesh + if (mesh.streams.empty()) + return false; + + // world-space mesh + if (mesh.nodes.empty() && mesh.instances.empty()) + return false; + + // has extras + if (settings.keep_extras && mesh.extras.data) + return false; + + // to simplify dedup we ignore complex target setups for now + if (!mesh.target_weights.empty() || !mesh.target_names.empty() || !mesh.variants.empty()) + return false; + + return true; +} + +void dedupMeshes(std::vector& meshes, const Settings& settings) +{ + std::unordered_map hashes; + + for (size_t i = 0; i < meshes.size(); ++i) + { + Mesh& mesh = meshes[i]; + + hashMesh(mesh); + hashes[mesh.geometry_hash[0] ^ mesh.geometry_hash[1]]++; + } + + for (size_t i = 0; i < meshes.size(); ++i) + { + Mesh& target = meshes[i]; + + if (!canDedupMesh(target, settings)) + continue; + + if (hashes[target.geometry_hash[0] ^ target.geometry_hash[1]] <= 1) + continue; + + for (size_t j = i + 1; j < meshes.size(); ++j) + { + Mesh& mesh = meshes[j]; + + if (mesh.geometry_hash[0] != target.geometry_hash[0] || mesh.geometry_hash[1] != target.geometry_hash[1]) + continue; + + if (!canDedupMesh(mesh, settings)) + continue; + + if (mesh.scene != target.scene || mesh.material != target.material || mesh.skin != target.skin) + { + // mark both meshes as having duplicate geometry; we don't use this in dedupMeshes but it's useful later in the pipeline + target.geometry_duplicate = true; + mesh.geometry_duplicate = true; + continue; + } + + // basic sanity test; these should be included in geometry hash + assert(mesh.streams.size() == target.streams.size()); + assert(mesh.streams[0].data.size() == target.streams[0].data.size()); + assert(mesh.indices.size() == target.indices.size()); + + target.nodes.insert(target.nodes.end(), mesh.nodes.begin(), mesh.nodes.end()); + target.instances.insert(target.instances.end(), mesh.instances.begin(), mesh.instances.end()); + + mesh.streams.clear(); + mesh.indices.clear(); + mesh.nodes.clear(); + mesh.instances.clear(); + } + } + + for (size_t i = 0; i < meshes.size(); ++i) + { + Mesh& target = meshes[i]; + if (target.nodes.size() <= 1) + continue; + + std::sort(target.nodes.begin(), target.nodes.end()); + target.nodes.erase(std::unique(target.nodes.begin(), target.nodes.end()), target.nodes.end()); + } +} + +void mergeMeshInstances(Mesh& mesh) +{ + if (mesh.nodes.empty()) + return; + + // fast-path: for single instance meshes we transform in-place + if (mesh.nodes.size() == 1) + { + transformMesh(mesh, mesh, mesh.nodes[0]); + mesh.nodes.clear(); + return; + } + + Mesh base = mesh; + Mesh transformed = base; + + for (size_t i = 0; i < mesh.streams.size(); ++i) + { + mesh.streams[i].data.clear(); + mesh.streams[i].data.reserve(base.streams[i].data.size() * mesh.nodes.size()); + } + + mesh.indices.clear(); + mesh.indices.reserve(base.indices.size() * mesh.nodes.size()); + + for (size_t i = 0; i < mesh.nodes.size(); ++i) + { + transformMesh(transformed, base, mesh.nodes[i]); + mergeMeshes(mesh, transformed); + } + + mesh.nodes.clear(); +} + +void mergeMeshes(std::vector& meshes, const Settings& settings) +{ + for (size_t i = 0; i < meshes.size(); ++i) + { + Mesh& target = meshes[i]; + + if (target.streams.empty()) + continue; + + size_t target_vertices = target.streams[0].data.size(); + size_t target_indices = target.indices.size(); + + size_t last_merged = i; + + for (size_t j = i + 1; j < meshes.size(); ++j) + { + Mesh& mesh = meshes[j]; + + if (!mesh.streams.empty() && canMergeMeshes(target, mesh, settings)) + { + target_vertices += mesh.streams[0].data.size(); + target_indices += mesh.indices.size(); + last_merged = j; + } + } + + for (size_t j = 0; j < target.streams.size(); ++j) + target.streams[j].data.reserve(target_vertices); + + target.indices.reserve(target_indices); + + for (size_t j = i + 1; j <= last_merged; ++j) + { + Mesh& mesh = meshes[j]; + + if (!mesh.streams.empty() && canMergeMeshes(target, mesh, settings)) + { + mergeMeshes(target, mesh); + + mesh.streams.clear(); + mesh.indices.clear(); + mesh.nodes.clear(); + mesh.instances.clear(); + } + } + + assert(target.streams[0].data.size() == target_vertices); + assert(target.indices.size() == target_indices); + } +} + +void filterEmptyMeshes(std::vector& meshes) +{ + size_t write = 0; + + for (size_t i = 0; i < meshes.size(); ++i) + { + Mesh& mesh = meshes[i]; + + if (mesh.streams.empty()) + continue; + + if (mesh.streams[0].data.empty()) + continue; + + if (mesh.type != cgltf_primitive_type_points && mesh.indices.empty()) + continue; + + // the following code is roughly equivalent to meshes[write] = std::move(mesh) + std::vector streams; + streams.swap(mesh.streams); + + std::vector indices; + indices.swap(mesh.indices); + + meshes[write] = mesh; + meshes[write].streams.swap(streams); + meshes[write].indices.swap(indices); + + write++; + } + + meshes.resize(write); +} + +static bool isConstant(const std::vector& data, const Attr& value, float tolerance = 0.01f) +{ + for (size_t i = 0; i < data.size(); ++i) + { + const Attr& a = data[i]; + + if (fabsf(a.f[0] - value.f[0]) > tolerance || fabsf(a.f[1] - value.f[1]) > tolerance || fabsf(a.f[2] - value.f[2]) > tolerance || fabsf(a.f[3] - value.f[3]) > tolerance) + return false; + } + + return true; +} + +void filterStreams(Mesh& mesh, const MaterialInfo& mi) +{ + bool morph_normal = false; + bool morph_tangent = false; + int keep_texture_set = -1; + + for (size_t i = 0; i < mesh.streams.size(); ++i) + { + Stream& stream = mesh.streams[i]; + + if (stream.target) + { + morph_normal = morph_normal || (stream.type == cgltf_attribute_type_normal && !isConstant(stream.data, {0, 0, 0, 0})); + morph_tangent = morph_tangent || (stream.type == cgltf_attribute_type_tangent && !isConstant(stream.data, {0, 0, 0, 0})); + } + + if (stream.type == cgltf_attribute_type_texcoord && stream.index < 32 && (mi.texture_set_mask & (1u << stream.index)) != 0) + { + keep_texture_set = std::max(keep_texture_set, stream.index); + } + } + + size_t write = 0; + + for (size_t i = 0; i < mesh.streams.size(); ++i) + { + Stream& stream = mesh.streams[i]; + + if (stream.type == cgltf_attribute_type_texcoord && stream.index > keep_texture_set) + continue; + + if (stream.type == cgltf_attribute_type_normal && mi.unlit) + continue; + + if (stream.type == cgltf_attribute_type_tangent && !mi.needs_tangents) + continue; + + if ((stream.type == cgltf_attribute_type_joints || stream.type == cgltf_attribute_type_weights) && !mesh.skin) + continue; + + if (stream.type == cgltf_attribute_type_color && isConstant(stream.data, {1, 1, 1, 1})) + continue; + + if (stream.target && stream.type == cgltf_attribute_type_normal && !morph_normal) + continue; + + if (stream.target && stream.type == cgltf_attribute_type_tangent && !morph_tangent) + continue; + + if (mesh.type == cgltf_primitive_type_points && stream.type == cgltf_attribute_type_normal && !stream.data.empty() && isConstant(stream.data, stream.data[0])) + continue; + + // the following code is roughly equivalent to streams[write] = std::move(stream) + std::vector data; + data.swap(stream.data); + + mesh.streams[write] = stream; + mesh.streams[write].data.swap(data); + + write++; + } + + mesh.streams.resize(write); +} + +static int getTangentTexcoord(const cgltf_material* material) +{ + if (material->normal_texture.texture) + return material->normal_texture.texcoord; + if (material->has_clearcoat && material->clearcoat.clearcoat_normal_texture.texture) + return material->clearcoat.clearcoat_normal_texture.texcoord; + if (material->has_anisotropy && material->anisotropy.anisotropy_texture.texture) + return material->anisotropy.anisotropy_texture.texcoord; + return 0; +} + +static Stream& prepareTangentStream(Mesh& mesh, size_t vertex_count) +{ + // remove existing tangent streams, if any + // in particular removes morph target streams as we don't support morph tangent generation + for (size_t i = 0; i < mesh.streams.size();) + if (mesh.streams[i].type == cgltf_attribute_type_tangent) + mesh.streams.erase(mesh.streams.begin() + i); + else + ++i; + + mesh.streams.push_back(Stream()); + + Stream& tangent = mesh.streams.back(); + tangent.type = cgltf_attribute_type_tangent; + tangent.data.resize(vertex_count); + + return tangent; +} + +template +static void splitVertices(Mesh& mesh, Stream& target, const float* data) +{ + size_t vertex_count = target.data.size(); + + // seed each vertex with one of its corner values; the loop below fixes any mismatches + for (size_t i = 0; i < mesh.indices.size(); ++i) + memcpy(target.data[mesh.indices[i]].f, &data[i * N], N * sizeof(float)); + + std::vector splits(vertex_count, ~0u); + + for (size_t i = 0; i < mesh.indices.size(); ++i) + { + Attr a = {}; + memcpy(a.f, &data[i * N], N * sizeof(float)); + + unsigned int v = mesh.indices[i]; + unsigned int sv = v; + + // walk the chain of split copies looking for a vertex whose attribute matches + while (sv != ~0u && !(target.data[sv].f[0] == a.f[0] && target.data[sv].f[1] == a.f[1] && target.data[sv].f[2] == a.f[2] && target.data[sv].f[3] == a.f[3])) + sv = splits[sv]; + + // no match in chain: append a new split copy with the target attribute and chain it + if (sv == ~0u) + { + sv = unsigned(target.data.size()); + + for (Stream& stream : mesh.streams) + stream.data.push_back(stream.data[v]); + + target.data[sv] = a; + + splits.push_back(splits[v]); + splits[v] = sv; + } + + mesh.indices[i] = sv; + } +} + +void generateTangents(Mesh& mesh) +{ + if (mesh.type != cgltf_primitive_type_triangles || mesh.indices.empty() || !mesh.material) + return; + + int texcoord = getTangentTexcoord(mesh.material); + Stream* positions = getStream(mesh, cgltf_attribute_type_position); + Stream* normals = getStream(mesh, cgltf_attribute_type_normal); + Stream* uvs = getStream(mesh, cgltf_attribute_type_texcoord, texcoord); + + if (!positions || !normals || !uvs) + return; + + size_t vertex_count = positions->data.size(); + assert(normals->data.size() == vertex_count && uvs->data.size() == vertex_count); + + std::vector tangents(mesh.indices.size() * 4); + meshopt_generateTangents(tangents.data(), mesh.indices.data(), mesh.indices.size(), positions->data[0].f, vertex_count, sizeof(Attr), normals->data[0].f, sizeof(Attr), uvs->data[0].f, sizeof(Attr), 0); + + // note: potentially invalidates positions/normals/uvs but we no longer use these + Stream& tangent = prepareTangentStream(mesh, vertex_count); + + splitVertices<4>(mesh, tangent, tangents.data()); +} + +void generateNormals(Mesh& mesh, float crease_angle) +{ +#ifdef GLTFPACK_NO_EXPERIMENTAL + // disabled until meshopt_generateNormals becomes stable + (void)mesh; + (void)crease_angle; +#else + if (mesh.type != cgltf_primitive_type_triangles || mesh.indices.empty() || mesh.targets || getStream(mesh, cgltf_attribute_type_normal)) + return; + + Stream* positions = getStream(mesh, cgltf_attribute_type_position); + if (!positions) + return; + + size_t vertex_count = positions->data.size(); + + std::vector normals(mesh.indices.size() * 3); + meshopt_generateNormals(normals.data(), mesh.indices.data(), mesh.indices.size(), positions->data[0].f, vertex_count, sizeof(Attr), crease_angle * (3.14159265f / 180.f), 1.5f); + + // note: potentially invalidates positions but we no longer use these + mesh.streams.push_back(Stream()); + + Stream& normal = mesh.streams.back(); + normal.type = cgltf_attribute_type_normal; + normal.data.resize(vertex_count); + + splitVertices<3>(mesh, normal, normals.data()); +#endif +} + +static float reindexTolerance(cgltf_attribute_type type, const Settings& settings) +{ + switch (type) + { + case cgltf_attribute_type_normal: + case cgltf_attribute_type_tangent: + return std::max(2e-4f, settings.quantize ? 1.f / float(1 << settings.nrm_bits) : 0.f); + + case cgltf_attribute_type_texcoord: + return std::max(5e-5f, settings.quantize ? 0.5f / float(1 << settings.tex_bits) : 0.f); + + case cgltf_attribute_type_color: + return 0.5f / float(1 << settings.col_bits); + + case cgltf_attribute_type_weights: + return 0.5f / 256.f; + + default: + return 0.f; + } +} + +static void reindexMesh(Mesh& mesh, const Settings& settings) +{ + size_t total_vertices = mesh.streams[0].data.size(); + size_t total_indices = mesh.indices.size(); + + Stream* positions = getStream(mesh, cgltf_attribute_type_position); + if (!positions) + return; + + std::vector > streams; + streams.reserve(mesh.streams.size()); + + for (size_t i = 0; i < mesh.streams.size(); ++i) + { + const Stream& attr = mesh.streams[i]; + if (attr.type == cgltf_attribute_type_position || attr.target) + continue; + + streams.push_back(std::make_pair(attr.data.data(), reindexTolerance(attr.type, settings))); + } + + std::vector remap(total_vertices); + size_t unique_vertices = meshopt_generateVertexRemapCustom(&remap[0], &mesh.indices[0], total_indices, positions->data[0].f, total_vertices, sizeof(Attr), + [&streams](unsigned int lhs, unsigned int rhs) -> bool + { + for (auto& sp : streams) + { + const Attr* data = sp.first; + float dx = fabsf(data[lhs].f[0] - data[rhs].f[0]), dy = fabsf(data[lhs].f[1] - data[rhs].f[1]); + float dz = fabsf(data[lhs].f[2] - data[rhs].f[2]), dw = fabsf(data[lhs].f[3] - data[rhs].f[3]); + + if (dx > sp.second || dy > sp.second || dz > sp.second || dw > sp.second) + return false; + } + + return true; + }); + assert(unique_vertices <= total_vertices); + + meshopt_remapIndexBuffer(&mesh.indices[0], &mesh.indices[0], total_indices, &remap[0]); + + for (size_t i = 0; i < mesh.streams.size(); ++i) + { + assert(mesh.streams[i].data.size() == total_vertices); + + meshopt_remapVertexBuffer(&mesh.streams[i].data[0], &mesh.streams[i].data[0], total_vertices, sizeof(Attr), &remap[0]); + mesh.streams[i].data.resize(unique_vertices); + } +} + +static void filterTriangles(Mesh& mesh) +{ + assert(mesh.type == cgltf_primitive_type_triangles); + + unsigned int* indices = &mesh.indices[0]; + size_t total_indices = mesh.indices.size(); + + size_t write = 0; + + for (size_t i = 0; i < total_indices; i += 3) + { + unsigned int a = indices[i + 0], b = indices[i + 1], c = indices[i + 2]; + + if (a != b && a != c && b != c) + { + indices[write + 0] = a; + indices[write + 1] = b; + indices[write + 2] = c; + write += 3; + } + } + + mesh.indices.resize(write); +} + +static void simplifyAttributes(std::vector& attrs, float* attrw, size_t stride, Mesh& mesh, float uv_weight) +{ + assert(stride >= 8); // normal + color + UV + + size_t vertex_count = mesh.streams[0].data.size(); + + attrs.resize(vertex_count * stride); + float* data = attrs.data(); + + if (const Stream* attr = getStream(mesh, cgltf_attribute_type_normal)) + { + const Attr* a = attr->data.data(); + + for (size_t i = 0; i < vertex_count; ++i) + { + data[i * stride + 0] = a[i].f[0]; + data[i * stride + 1] = a[i].f[1]; + data[i * stride + 2] = a[i].f[2]; + } + + attrw[0] = attrw[1] = attrw[2] = 0.5f; + } + + if (const Stream* attr = getStream(mesh, cgltf_attribute_type_color)) + { + const Attr* a = attr->data.data(); + + for (size_t i = 0; i < vertex_count; ++i) + { + data[i * stride + 3] = a[i].f[0] * a[i].f[3]; + data[i * stride + 4] = a[i].f[1] * a[i].f[3]; + data[i * stride + 5] = a[i].f[2] * a[i].f[3]; + } + + attrw[3] = attrw[4] = attrw[5] = 1.0f; + } + + if (const Stream* attr = getStream(mesh, cgltf_attribute_type_texcoord)) + { + const Attr* a = attr->data.data(); + for (size_t i = 0; i < vertex_count; ++i) + { + data[i * stride + 6] = a[i].f[0]; + data[i * stride + 7] = a[i].f[1]; + } + attrw[6] = attrw[7] = uv_weight; + } +} + +static void simplifyUpdate(const std::vector& attrs, size_t stride, Mesh& mesh) +{ + size_t vertex_count = mesh.streams[0].data.size(); + const float* data = attrs.data(); + + if (Stream* attr = getStream(mesh, cgltf_attribute_type_normal)) + { + Attr* a = attr->data.data(); + + for (size_t i = 0; i < vertex_count; ++i) + { + float nx = data[i * stride + 0], ny = data[i * stride + 1], nz = data[i * stride + 2]; + float nl = sqrtf(nx * nx + ny * ny + nz * nz); + float ns = nl > 0.f ? 1.f / nl : 0.f; + a[i].f[0] = nx * ns; + a[i].f[1] = ny * ns; + a[i].f[2] = nz * ns; + } + } + + if (Stream* attr = getStream(mesh, cgltf_attribute_type_color)) + { + Attr* a = attr->data.data(); + + for (size_t i = 0; i < vertex_count; ++i) + { + float cs = a[i].f[3] > 0.f ? 1.f / a[i].f[3] : 0.f; + a[i].f[0] = std::max(0.f, std::min(1.f, data[i * stride + 3] * cs)); + a[i].f[1] = std::max(0.f, std::min(1.f, data[i * stride + 4] * cs)); + a[i].f[2] = std::max(0.f, std::min(1.f, data[i * stride + 5] * cs)); + } + } + + if (Stream* attr = getStream(mesh, cgltf_attribute_type_texcoord)) + { + Attr* a = attr->data.data(); + + for (size_t i = 0; i < vertex_count; ++i) + { + a[i].f[0] = data[i * stride + 6]; + a[i].f[1] = data[i * stride + 7]; + } + } +} + +static void simplifyProtect(std::vector& locks, Mesh& mesh, size_t presplit_vertices) +{ + const Stream* positions = getStream(mesh, cgltf_attribute_type_position); + assert(positions); + + size_t vertex_count = positions->data.size(); + + locks.resize(vertex_count); + unsigned char* data = locks.data(); + + std::vector remap(vertex_count); + meshopt_generatePositionRemap(&remap[0], positions->data[0].f, vertex_count, sizeof(Attr)); + + // protect UV discontinuities + if (Stream* attr = getStream(mesh, cgltf_attribute_type_texcoord)) + { + Attr* a = attr->data.data(); + + for (size_t i = 0; i < vertex_count; ++i) + { + unsigned int r = remap[i]; + + if (r != i && (a[i].f[0] != a[r].f[0] || a[i].f[1] != a[r].f[1])) + data[i] |= meshopt_SimplifyVertex_Protect; + } + } + // protect all vertices that were artificially split on the UV mirror edges + for (size_t i = presplit_vertices; i < vertex_count; ++i) + data[i] |= meshopt_SimplifyVertex_Protect; +} + +static void simplifyUvSplit(Mesh& mesh, std::vector& remap) +{ + assert(mesh.type == cgltf_primitive_type_triangles); + assert(!mesh.indices.empty()); + + const Stream* uv = getStream(mesh, cgltf_attribute_type_texcoord); + if (!uv) + return; + + size_t vertex_count = uv->data.size(); + + std::vector uvsign(mesh.indices.size() / 3); + std::vector flipseam(vertex_count); + + for (size_t i = 0; i < mesh.indices.size(); i += 3) + { + unsigned int a = mesh.indices[i + 0]; + unsigned int b = mesh.indices[i + 1]; + unsigned int c = mesh.indices[i + 2]; + + const Attr& va = uv->data[a]; + const Attr& vb = uv->data[b]; + const Attr& vc = uv->data[c]; + + float uvarea = (vb.f[0] - va.f[0]) * (vc.f[1] - va.f[1]) - (vc.f[0] - va.f[0]) * (vb.f[1] - va.f[1]); + unsigned char flag = uvarea > 0 ? 1 : (uvarea < 0 ? 2 : 0); + + uvsign[i / 3] = flag; + flipseam[a] |= flag; + flipseam[b] |= flag; + flipseam[c] |= flag; + } + + std::vector split(vertex_count); + size_t splits = 0; + + remap.resize(vertex_count); + + for (size_t i = 0; i < vertex_count; ++i) + { + remap[i] = unsigned(i); + + if (flipseam[i] == 3) + { + assert(remap.size() == vertex_count + splits); + remap.push_back(unsigned(i)); + + split[i] = unsigned(vertex_count + splits); + splits++; + + for (size_t k = 0; k < mesh.streams.size(); ++k) + mesh.streams[k].data.push_back(mesh.streams[k].data[i]); + } + } + + for (size_t i = 0; i < mesh.indices.size(); i += 3) + { + unsigned int a = mesh.indices[i + 0]; + unsigned int b = mesh.indices[i + 1]; + unsigned int c = mesh.indices[i + 2]; + + unsigned char sign = uvsign[i / 3]; + + if (flipseam[a] == 3 && sign == 2) + mesh.indices[i + 0] = split[a]; + if (flipseam[b] == 3 && sign == 2) + mesh.indices[i + 1] = split[b]; + if (flipseam[c] == 3 && sign == 2) + mesh.indices[i + 2] = split[c]; + } +} + +static void simplifyMesh(Mesh& mesh, float threshold, float error, bool aggressive, bool lock_borders, bool permissive, bool update) +{ + assert(mesh.type == cgltf_primitive_type_triangles); + + if (mesh.indices.empty()) + return; + + Stream* positions = getStream(mesh, cgltf_attribute_type_position); + if (!positions) + return; + + size_t presplit_vertices = positions->data.size(); + + std::vector uvremap; + simplifyUvSplit(mesh, uvremap); + + size_t vertex_count = positions->data.size(); + + size_t target_index_count = size_t(double(size_t(mesh.indices.size() / 3)) * threshold) * 3; + float target_error = error; + float target_error_aggressive = 1e-1f; + + unsigned int options = 0; + if (lock_borders) + options |= meshopt_SimplifyLockBorder; + else + options |= meshopt_SimplifyPrune; + + if (permissive) + options |= meshopt_SimplifyPermissive; + + if (mesh.targets || getStream(mesh, cgltf_attribute_type_weights)) + options |= meshopt_SimplifyRegularize; + + std::vector indices(mesh.indices.size()); + + float attrw[8] = {}; + std::vector attrs; + simplifyAttributes(attrs, attrw, sizeof(attrw) / sizeof(attrw[0]), mesh, update ? 1.f : 0.f); + + std::vector locks; + if (permissive) + simplifyProtect(locks, mesh, presplit_vertices); + + // for now we disable simplify-with-update if the mesh has a second texture coordinate since we don't currently update it and moving vertices may create UV distortion + if (update && !mesh.targets && !getStream(mesh, cgltf_attribute_type_texcoord, 1)) + { + indices = mesh.indices; + indices.resize(meshopt_simplifyWithUpdate(&indices[0], indices.size(), positions->data[0].f, vertex_count, sizeof(Attr), + attrs.data(), sizeof(attrw), attrw, sizeof(attrw) / sizeof(attrw[0]), permissive ? locks.data() : NULL, target_index_count, target_error, options)); + simplifyUpdate(attrs, sizeof(attrw) / sizeof(attrw[0]), mesh); + } + else + indices.resize(meshopt_simplifyWithAttributes(&indices[0], &mesh.indices[0], mesh.indices.size(), positions->data[0].f, vertex_count, sizeof(Attr), + attrs.data(), sizeof(attrw), attrw, sizeof(attrw) / sizeof(attrw[0]), permissive ? locks.data() : NULL, target_index_count, target_error, options)); + + mesh.indices.swap(indices); + + // Note: if the simplifier got stuck, we can try to reindex without normals/tangents and retry + // For now we simply fall back to aggressive simplifier instead + + // if the precise simplifier got "stuck", we'll try to simplify using the sloppy simplifier; this is only used when aggressive simplification is enabled as it breaks attribute discontinuities + if (aggressive && mesh.indices.size() > target_index_count) + { + indices.resize(meshopt_simplifySloppy(&indices[0], &mesh.indices[0], mesh.indices.size(), positions->data[0].f, vertex_count, sizeof(Attr), target_index_count, target_error_aggressive)); + mesh.indices.swap(indices); + } + + if (uvremap.size() && mesh.indices.size()) + meshopt_remapIndexBuffer(&mesh.indices[0], &mesh.indices[0], mesh.indices.size(), &uvremap[0]); +} + +static void optimizeMesh(Mesh& mesh, bool compressmore) +{ + assert(mesh.type == cgltf_primitive_type_triangles); + + if (mesh.indices.empty()) + return; + + size_t vertex_count = mesh.streams[0].data.size(); + + if (compressmore) + meshopt_optimizeVertexCacheStrip(&mesh.indices[0], &mesh.indices[0], mesh.indices.size(), vertex_count); + else + meshopt_optimizeVertexCache(&mesh.indices[0], &mesh.indices[0], mesh.indices.size(), vertex_count); + + std::vector remap(vertex_count); + size_t unique_vertices = meshopt_optimizeVertexFetchRemap(&remap[0], &mesh.indices[0], mesh.indices.size(), vertex_count); + assert(unique_vertices <= vertex_count); + + meshopt_remapIndexBuffer(&mesh.indices[0], &mesh.indices[0], mesh.indices.size(), &remap[0]); + + for (size_t i = 0; i < mesh.streams.size(); ++i) + { + assert(mesh.streams[i].data.size() == vertex_count); + + meshopt_remapVertexBuffer(&mesh.streams[i].data[0], &mesh.streams[i].data[0], vertex_count, sizeof(Attr), &remap[0]); + mesh.streams[i].data.resize(unique_vertices); + } +} + +struct BoneInfluence +{ + float i; + float w; +}; + +struct BoneInfluenceWeightPredicate +{ + bool operator()(const BoneInfluence& lhs, const BoneInfluence& rhs) const + { + return lhs.w > rhs.w; + } +}; + +static void filterBones(Mesh& mesh) +{ + const int kMaxGroups = 8; + + std::pair groups[kMaxGroups]; + int group_count = 0; + + // gather all joint/weight groups; each group contains 4 bone influences + for (int i = 0; i < kMaxGroups; ++i) + { + Stream* jg = getStream(mesh, cgltf_attribute_type_joints, int(i)); + Stream* wg = getStream(mesh, cgltf_attribute_type_weights, int(i)); + + if (!jg || !wg) + break; + + groups[group_count++] = std::make_pair(jg, wg); + } + + if (group_count == 0) + return; + + // weights below cutoff can't be represented in quantized 8-bit storage + const float weight_cutoff = 0.5f / 255.f; + + size_t vertex_count = mesh.streams[0].data.size(); + + BoneInfluence inf[kMaxGroups * 4] = {}; + + for (size_t i = 0; i < vertex_count; ++i) + { + int count = 0; + + // gather all bone influences for this vertex + for (int j = 0; j < group_count; ++j) + { + const Attr& ja = groups[j].first->data[i]; + const Attr& wa = groups[j].second->data[i]; + + for (int k = 0; k < 4; ++k) + if (wa.f[k] > weight_cutoff) + { + inf[count].i = ja.f[k]; + inf[count].w = wa.f[k]; + count++; + } + } + + // pick top 4 influences; this also sorts resulting influences by weight which helps renderers that use influence subset in shader LODs + std::sort(inf, inf + count, BoneInfluenceWeightPredicate()); + + // copy the top 4 influences back into stream 0 - we will remove other streams at the end + Attr& ja = groups[0].first->data[i]; + Attr& wa = groups[0].second->data[i]; + + for (int k = 0; k < 4; ++k) + { + if (k < count) + { + ja.f[k] = inf[k].i; + wa.f[k] = inf[k].w; + } + else + { + ja.f[k] = 0.f; + wa.f[k] = 0.f; + } + } + } + + // remove redundant weight/joint streams + for (size_t i = 0; i < mesh.streams.size();) + { + Stream& s = mesh.streams[i]; + + if ((s.type == cgltf_attribute_type_joints || s.type == cgltf_attribute_type_weights) && s.index > 0) + mesh.streams.erase(mesh.streams.begin() + i); + else + ++i; + } +} + +static void simplifyPointMesh(Mesh& mesh, float threshold) +{ + assert(mesh.type == cgltf_primitive_type_points); + + if (threshold >= 1) + return; + + const Stream* positions = getStream(mesh, cgltf_attribute_type_position); + if (!positions) + return; + + const Stream* colors = getStream(mesh, cgltf_attribute_type_color); + + size_t vertex_count = mesh.streams[0].data.size(); + + size_t target_vertex_count = size_t(double(vertex_count) * threshold); + + const float color_weight = 1; + + std::vector indices(target_vertex_count); + if (target_vertex_count) + indices.resize(meshopt_simplifyPoints(&indices[0], positions->data[0].f, vertex_count, sizeof(Attr), colors ? colors->data[0].f : NULL, sizeof(Attr), color_weight, target_vertex_count)); + + std::vector scratch(indices.size()); + + for (size_t i = 0; i < mesh.streams.size(); ++i) + { + std::vector& data = mesh.streams[i].data; + + assert(data.size() == vertex_count); + + for (size_t j = 0; j < indices.size(); ++j) + scratch[j] = data[indices[j]]; + + data = scratch; + } +} + +static void sortPointMesh(Mesh& mesh) +{ + assert(mesh.type == cgltf_primitive_type_points); + + const Stream* positions = getStream(mesh, cgltf_attribute_type_position); + if (!positions) + return; + + // skip spatial sort in presence of custom attributes, since when they refer to mesh features their order is more important to preserve for compression efficiency + if (getStream(mesh, cgltf_attribute_type_custom)) + return; + + size_t vertex_count = mesh.streams[0].data.size(); + + std::vector remap(vertex_count); + meshopt_spatialSortRemap(&remap[0], positions->data[0].f, vertex_count, sizeof(Attr)); + + for (size_t i = 0; i < mesh.streams.size(); ++i) + { + assert(mesh.streams[i].data.size() == vertex_count); + + meshopt_remapVertexBuffer(&mesh.streams[i].data[0], &mesh.streams[i].data[0], vertex_count, sizeof(Attr), &remap[0]); + } +} + +void processMesh(Mesh& mesh, const Settings& settings) +{ + switch (mesh.type) + { + case cgltf_primitive_type_points: + assert(mesh.indices.empty()); + simplifyPointMesh(mesh, settings.simplify_ratio); + sortPointMesh(mesh); + break; + + case cgltf_primitive_type_lines: + break; + + case cgltf_primitive_type_triangles: + filterBones(mesh); + reindexMesh(mesh, settings); + filterTriangles(mesh); + + if (settings.simplify_ratio < 1) + { + simplifyMesh(mesh, settings.simplify_ratio, settings.simplify_error / mesh.quality, settings.simplify_aggressive, settings.simplify_lock_borders, settings.simplify_permissive, settings.simplify_update); + } + + optimizeMesh(mesh, settings.compressmore); + break; + + default: + assert(!"Unknown primitive type"); + } +} + +static float getScale(const float* transform) +{ + float translation[3], rotation[4], scale[3]; + decomposeTransform(translation, rotation, scale, transform); + + float sx = fabsf(scale[0]), sy = fabsf(scale[1]), sz = fabsf(scale[2]); + return std::max(std::max(sx, sy), sz); +} + +void computeMeshQuality(std::vector& meshes) +{ + std::vector scales(meshes.size(), 1.f); + float maxscale = 0.f; + + for (size_t i = 0; i < meshes.size(); ++i) + { + Mesh& mesh = meshes[i]; + + const Stream* positions = getStream(mesh, cgltf_attribute_type_position); + if (!positions) + continue; + + float geometry_scale = meshopt_simplifyScale(positions->data[0].f, positions->data.size(), sizeof(Attr)); + float node_maxscale = 0.f; + + for (cgltf_node* node : mesh.nodes) + { + float transform[16]; + cgltf_node_transform_world(node, transform); + + node_maxscale = std::max(node_maxscale, getScale(transform)); + } + + for (const Instance& xf : mesh.instances) + node_maxscale = std::max(node_maxscale, getScale(xf.transform)); + + scales[i] = node_maxscale == 0.f ? geometry_scale : node_maxscale * geometry_scale; + maxscale = std::max(maxscale, scales[i]); + } + + for (size_t i = 0; i < meshes.size(); ++i) + meshes[i].quality = (scales[i] == 0.f || maxscale == 0.f) ? 1.f : scales[i] / maxscale; +} + +bool hasVertexAlpha(const Mesh& mesh) +{ + const Stream* color = getStream(const_cast(mesh), cgltf_attribute_type_color); + if (!color) + return false; + + for (size_t i = 0; i < color->data.size(); ++i) + if (color->data[i].f[3] < 1.f) + return true; + + return false; +} + +bool hasInstanceAlpha(const std::vector& instances) +{ + for (const Instance& instance : instances) + if (instance.color[3] < 1.f) + return true; + + return false; +} diff --git a/Lib/meshoptimizer/gltf/node.cpp b/Lib/meshoptimizer/gltf/node.cpp new file mode 100644 index 0000000..c3da288 --- /dev/null +++ b/Lib/meshoptimizer/gltf/node.cpp @@ -0,0 +1,206 @@ +// This file is part of gltfpack; see gltfpack.h for version/license details +#include "gltfpack.h" + +#include +#include + +void markScenes(cgltf_data* data, std::vector& nodes) +{ + for (size_t i = 0; i < nodes.size(); ++i) + nodes[i].scene = -1; + + for (size_t i = 0; i < data->scenes_count; ++i) + for (size_t j = 0; j < data->scenes[i].nodes_count; ++j) + { + NodeInfo& ni = nodes[data->scenes[i].nodes[j] - data->nodes]; + + if (ni.scene >= 0) + ni.scene = -2; // multiple scenes + else + ni.scene = int(i); + } + + for (size_t i = 0; i < data->nodes_count; ++i) + { + cgltf_node* root = &data->nodes[i]; + while (root->parent) + root = root->parent; + + nodes[i].scene = nodes[root - data->nodes].scene; + } +} + +void markAnimated(cgltf_data* data, std::vector& nodes, const std::vector& animations) +{ + for (size_t i = 0; i < animations.size(); ++i) + { + const Animation& animation = animations[i]; + + for (size_t j = 0; j < animation.tracks.size(); ++j) + { + const Track& track = animation.tracks[j]; + + // mark nodes that have animation tracks that change their base transform as animated + if (!track.dummy) + { + NodeInfo& ni = nodes[track.node - data->nodes]; + + ni.animated_path_mask |= (1 << track.path); + } + } + } + + for (size_t i = 0; i < data->nodes_count; ++i) + { + NodeInfo& ni = nodes[i]; + + for (cgltf_node* node = &data->nodes[i]; node; node = node->parent) + ni.animated |= nodes[node - data->nodes].animated_path_mask != 0; + } +} + +void markNeededNodes(cgltf_data* data, std::vector& nodes, const std::vector& meshes, const std::vector& animations, const Settings& settings) +{ + // mark all joints as kept + for (size_t i = 0; i < data->skins_count; ++i) + { + const cgltf_skin& skin = data->skins[i]; + + // for now we keep all joints directly referenced by the skin and the entire ancestry tree; we keep names for joints as well + for (size_t j = 0; j < skin.joints_count; ++j) + { + NodeInfo& ni = nodes[skin.joints[j] - data->nodes]; + + ni.keep = true; + } + } + + // mark all animated nodes as kept + for (size_t i = 0; i < animations.size(); ++i) + { + const Animation& animation = animations[i]; + + for (size_t j = 0; j < animation.tracks.size(); ++j) + { + const Track& track = animation.tracks[j]; + + if (settings.anim_const || !track.dummy) + { + NodeInfo& ni = nodes[track.node - data->nodes]; + + ni.keep = true; + } + } + } + + // mark all mesh nodes as kept + for (size_t i = 0; i < meshes.size(); ++i) + { + const Mesh& mesh = meshes[i]; + + for (size_t j = 0; j < mesh.nodes.size(); ++j) + { + NodeInfo& ni = nodes[mesh.nodes[j] - data->nodes]; + + ni.keep = true; + } + } + + // mark all light/camera nodes as kept + for (size_t i = 0; i < data->nodes_count; ++i) + { + const cgltf_node& node = data->nodes[i]; + + if (node.light || node.camera) + { + nodes[i].keep = true; + } + } + + // mark all named nodes as needed (if -kn is specified) + if (settings.keep_nodes) + { + for (size_t i = 0; i < data->nodes_count; ++i) + { + const cgltf_node& node = data->nodes[i]; + + if (node.name && *node.name) + { + nodes[i].keep = true; + } + } + } +} + +void remapNodes(cgltf_data* data, std::vector& nodes, size_t& node_offset) +{ + // to keep a node, we currently need to keep the entire ancestry chain + for (size_t i = 0; i < data->nodes_count; ++i) + { + if (!nodes[i].keep) + continue; + + for (cgltf_node* node = &data->nodes[i]; node; node = node->parent) + nodes[node - data->nodes].keep = true; + } + + // generate sequential indices for all nodes; they aren't sorted topologically + for (size_t i = 0; i < data->nodes_count; ++i) + { + NodeInfo& ni = nodes[i]; + + if (ni.keep) + { + ni.remap = int(node_offset); + + node_offset++; + } + } +} + +void decomposeTransform(float translation[3], float rotation[4], float scale[3], const float* transform) +{ + float m[4][4] = {}; + memcpy(m, transform, 16 * sizeof(float)); + + // extract translation from last row + translation[0] = m[3][0]; + translation[1] = m[3][1]; + translation[2] = m[3][2]; + + // compute determinant to determine handedness + float det = + m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2]) - + m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]); + + float sign = (det < 0.f) ? -1.f : 1.f; + + // recover scale from axis lengths + scale[0] = sqrtf(m[0][0] * m[0][0] + m[0][1] * m[0][1] + m[0][2] * m[0][2]) * sign; + scale[1] = sqrtf(m[1][0] * m[1][0] + m[1][1] * m[1][1] + m[1][2] * m[1][2]) * sign; + scale[2] = sqrtf(m[2][0] * m[2][0] + m[2][1] * m[2][1] + m[2][2] * m[2][2]) * sign; + + // normalize axes to get a pure rotation matrix + float rsx = (scale[0] == 0.f) ? 0.f : 1.f / scale[0]; + float rsy = (scale[1] == 0.f) ? 0.f : 1.f / scale[1]; + float rsz = (scale[2] == 0.f) ? 0.f : 1.f / scale[2]; + + float r00 = m[0][0] * rsx, r10 = m[1][0] * rsy, r20 = m[2][0] * rsz; + float r01 = m[0][1] * rsx, r11 = m[1][1] * rsy, r21 = m[2][1] * rsz; + float r02 = m[0][2] * rsx, r12 = m[1][2] * rsy, r22 = m[2][2] * rsz; + + // "branchless" version of Mike Day's matrix to quaternion conversion + int qc = r22 < 0 ? (r00 > r11 ? 0 : 1) : (r00 < -r11 ? 2 : 3); + float qs1 = qc & 2 ? -1.f : 1.f; + float qs2 = qc & 1 ? -1.f : 1.f; + float qs3 = (qc - 1) & 2 ? -1.f : 1.f; + + float qt = 1.f - qs3 * r00 - qs2 * r11 - qs1 * r22; + float qs = 0.5f / sqrtf(qt); + + rotation[qc ^ 0] = qs * qt; + rotation[qc ^ 1] = qs * (r01 + qs1 * r10); + rotation[qc ^ 2] = qs * (r20 + qs2 * r02); + rotation[qc ^ 3] = qs * (r12 + qs3 * r21); +} diff --git a/Lib/meshoptimizer/gltf/package.json b/Lib/meshoptimizer/gltf/package.json new file mode 100644 index 0000000..a7900e6 --- /dev/null +++ b/Lib/meshoptimizer/gltf/package.json @@ -0,0 +1,29 @@ +{ + "name": "gltfpack", + "version": "1.2.0", + "description": "A command-line tool that can optimize glTF files for size and speed", + "author": "Arseny Kapoulkine", + "license": "MIT", + "bugs": "https://github.com/zeux/meshoptimizer/issues", + "homepage": "https://github.com/zeux/meshoptimizer", + "keywords": [ + "gltf" + ], + "repository": { + "type": "git", + "url": "https://github.com/zeux/meshoptimizer" + }, + "type": "module", + "bin": "./cli.js", + "main": "./library.js", + "files": [ + "*.js", + "*.wasm" + ], + "scripts": { + "prepublishOnly": "node cli.js -v" + }, + "engines": { + "node": ">=18" + } +} diff --git a/Lib/meshoptimizer/gltf/parsegltf.cpp b/Lib/meshoptimizer/gltf/parsegltf.cpp new file mode 100644 index 0000000..eda4a65 --- /dev/null +++ b/Lib/meshoptimizer/gltf/parsegltf.cpp @@ -0,0 +1,704 @@ +// This file is part of gltfpack; see gltfpack.h for version/license details +#include "gltfpack.h" + +#include +#include +#include + +#include "../src/meshoptimizer.h" + +static const size_t kMaxStreams = 16; + +static const char* getError(cgltf_result result, cgltf_data* data) +{ + switch (result) + { + case cgltf_result_file_not_found: + return data ? "resource not found" : "file not found"; + + case cgltf_result_io_error: + return "I/O error"; + + case cgltf_result_invalid_json: + return "invalid JSON"; + + case cgltf_result_invalid_gltf: + return "invalid GLTF"; + + case cgltf_result_out_of_memory: + return "out of memory"; + + case cgltf_result_legacy_gltf: + return "legacy GLTF"; + + case cgltf_result_data_too_short: + return data ? "buffer too short" : "not a GLTF file"; + + case cgltf_result_unknown_format: + return data ? "unknown resource format" : "not a GLTF file"; + + default: + return "unknown error"; + } +} + +static void readAccessor(std::vector& data, const cgltf_accessor* accessor) +{ + assert(accessor->type == cgltf_type_scalar); + + data.resize(accessor->count); + cgltf_accessor_unpack_floats(accessor, &data[0], data.size()); +} + +static void readAccessor(std::vector& data, const cgltf_accessor* accessor) +{ + size_t components = cgltf_num_components(accessor->type); + + std::vector temp(accessor->count * components); + cgltf_accessor_unpack_floats(accessor, &temp[0], temp.size()); + + data.resize(accessor->count); + + for (size_t i = 0; i < accessor->count; ++i) + { + for (size_t k = 0; k < components && k < 4; ++k) + data[i].f[k] = temp[i * components + k]; + } +} + +static void readAccessor(std::vector& data, const cgltf_accessor* accessor, const std::vector& sparse) +{ + data.resize(sparse.size()); + + for (size_t i = 0; i < sparse.size(); ++i) + cgltf_accessor_read_float(accessor, sparse[i], &data[i].f[0], 4); +} + +static void fixupIndices(std::vector& indices, cgltf_primitive_type& type) +{ + if (type == cgltf_primitive_type_line_loop) + { + std::vector result; + result.reserve(indices.size() * 2 + 2); + + for (size_t i = 1; i <= indices.size(); ++i) + { + result.push_back(indices[i - 1]); + result.push_back(indices[i % indices.size()]); + } + + indices.swap(result); + type = cgltf_primitive_type_lines; + } + else if (type == cgltf_primitive_type_line_strip) + { + std::vector result; + result.reserve(indices.size() * 2); + + for (size_t i = 1; i < indices.size(); ++i) + { + result.push_back(indices[i - 1]); + result.push_back(indices[i]); + } + + indices.swap(result); + type = cgltf_primitive_type_lines; + } + else if (type == cgltf_primitive_type_triangle_strip) + { + std::vector result; + result.reserve(indices.size() * 3); + + for (size_t i = 2; i < indices.size(); ++i) + { + int flip = i & 1; + + result.push_back(indices[i - 2 + flip]); + result.push_back(indices[i - 1 - flip]); + result.push_back(indices[i]); + } + + indices.swap(result); + type = cgltf_primitive_type_triangles; + } + else if (type == cgltf_primitive_type_triangle_fan) + { + std::vector result; + result.reserve(indices.size() * 3); + + for (size_t i = 2; i < indices.size(); ++i) + { + result.push_back(indices[0]); + result.push_back(indices[i - 1]); + result.push_back(indices[i]); + } + + indices.swap(result); + type = cgltf_primitive_type_triangles; + } + else if (type == cgltf_primitive_type_lines) + { + // glTF files don't require that line index count is divisible by 2, but it is obviously critical for scenes to render + indices.resize(indices.size() / 2 * 2); + } + else if (type == cgltf_primitive_type_triangles) + { + // glTF files don't require that triangle index count is divisible by 3, but it is obviously critical for scenes to render + indices.resize(indices.size() / 3 * 3); + } +} + +static bool isIdAttribute(const char* name) +{ + return strcmp(name, "_ID") == 0 || + strcmp(name, "_BATCHID") == 0 || + strncmp(name, "_FEATURE_ID_", 12) == 0; +} + +static void parseMeshesGltf(cgltf_data* data, std::vector& meshes, std::vector >& mesh_remap) +{ + size_t total_primitives = 0; + + for (size_t mi = 0; mi < data->meshes_count; ++mi) + total_primitives += data->meshes[mi].primitives_count; + + meshes.reserve(total_primitives); + mesh_remap.resize(data->meshes_count); + + for (size_t mi = 0; mi < data->meshes_count; ++mi) + { + const cgltf_mesh& mesh = data->meshes[mi]; + + size_t remap_offset = meshes.size(); + + for (size_t pi = 0; pi < mesh.primitives_count; ++pi) + { + const cgltf_primitive& primitive = mesh.primitives[pi]; + + if (primitive.type == cgltf_primitive_type_points && primitive.indices) + { + fprintf(stderr, "Warning: ignoring primitive %d of mesh %d because indexed points are not supported\n", int(pi), int(mi)); + continue; + } + + const cgltf_accessor* positions = cgltf_find_accessor(&primitive, cgltf_attribute_type_position, 0); + + if (!positions) + { + fprintf(stderr, "Warning: ignoring primitive %d of mesh %d because it has no position data\n", int(pi), int(mi)); + continue; + } + + meshes.push_back(Mesh()); + Mesh& result = meshes.back(); + + result.scene = -1; + result.material = primitive.material; + result.extras = primitive.extras; + result.type = primitive.type; + + result.streams.reserve(primitive.attributes_count); + + size_t vertex_count = positions->count; + + if (primitive.indices) + { + result.indices.resize(primitive.indices->count); + if (!result.indices.empty()) + cgltf_accessor_unpack_indices(primitive.indices, &result.indices[0], sizeof(unsigned int), result.indices.size()); + + for (size_t i = 0; i < result.indices.size(); ++i) + assert(result.indices[i] < vertex_count); + } + else if (primitive.type != cgltf_primitive_type_points) + { + // note, while we could generate a good index buffer here, mesh will be reindexed during processing + result.indices.resize(vertex_count); + for (size_t i = 0; i < vertex_count; ++i) + result.indices[i] = unsigned(i); + } + + // convert line loops and line/triangle strips to lists + fixupIndices(result.indices, result.type); + + std::vector sparse; + + // if the index data is very sparse, switch to deindexing on the fly to avoid the excessive cost of reading large accessors + if (!result.indices.empty() && result.indices.size() < vertex_count / 2) + { + sparse = result.indices; + + // mesh will be reindexed during processing + for (size_t i = 0; i < result.indices.size(); ++i) + result.indices[i] = unsigned(i); + } + + for (size_t ai = 0; ai < primitive.attributes_count; ++ai) + { + const cgltf_attribute& attr = primitive.attributes[ai]; + + if (attr.type == cgltf_attribute_type_invalid || (attr.type == cgltf_attribute_type_custom && !isIdAttribute(attr.name))) + { + fprintf(stderr, "Warning: ignoring %s attribute %s in primitive %d of mesh %d\n", attr.type == cgltf_attribute_type_invalid ? "unknown" : "custom", attr.name, int(pi), int(mi)); + continue; + } + + if (result.streams.size() == kMaxStreams) + { + fprintf(stderr, "Warning: ignoring attribute %s in primitive %d of mesh %d (limit %d reached)\n", attr.name, int(pi), int(mi), int(kMaxStreams)); + continue; + } + + result.streams.push_back(Stream()); + Stream& s = result.streams.back(); + + s.type = attr.type; + s.index = attr.index; + + if (attr.type == cgltf_attribute_type_custom) + s.custom_name = attr.name; + + if (sparse.empty()) + readAccessor(s.data, attr.data); + else + readAccessor(s.data, attr.data, sparse); + + if (attr.type == cgltf_attribute_type_color && attr.data->type == cgltf_type_vec3) + { + for (size_t i = 0; i < s.data.size(); ++i) + s.data[i].f[3] = 1.0f; + } + } + + for (size_t ti = 0; ti < primitive.targets_count; ++ti) + { + const cgltf_morph_target& target = primitive.targets[ti]; + + for (size_t ai = 0; ai < target.attributes_count; ++ai) + { + const cgltf_attribute& attr = target.attributes[ai]; + + if (attr.type == cgltf_attribute_type_invalid || attr.type == cgltf_attribute_type_custom) + { + fprintf(stderr, "Warning: ignoring %s attribute %s in morph target %d of primitive %d of mesh %d\n", attr.type == cgltf_attribute_type_invalid ? "unknown" : "custom", attr.name, int(ti), int(pi), int(mi)); + continue; + } + + result.streams.push_back(Stream()); + Stream& s = result.streams.back(); + + s.type = attr.type; + s.index = attr.index; + s.target = int(ti + 1); + + if (sparse.empty()) + readAccessor(s.data, attr.data); + else + readAccessor(s.data, attr.data, sparse); + } + } + + result.targets = primitive.targets_count; + result.target_weights.assign(mesh.weights, mesh.weights + mesh.weights_count); + result.target_names.assign(mesh.target_names, mesh.target_names + mesh.target_names_count); + + result.variants.assign(primitive.mappings, primitive.mappings + primitive.mappings_count); + } + + mesh_remap[mi] = std::make_pair(remap_offset, meshes.size()); + } +} + +static void parseMeshInstancesGltf(std::vector& instances, cgltf_node* node, size_t ni) +{ + cgltf_accessor* translation = NULL; + cgltf_accessor* rotation = NULL; + cgltf_accessor* scale = NULL; + cgltf_accessor* color = NULL; + + for (size_t i = 0; i < node->mesh_gpu_instancing.attributes_count; ++i) + { + const cgltf_attribute& attr = node->mesh_gpu_instancing.attributes[i]; + + if (strcmp(attr.name, "TRANSLATION") == 0 && attr.data->type == cgltf_type_vec3) + translation = attr.data; + else if (strcmp(attr.name, "ROTATION") == 0 && attr.data->type == cgltf_type_vec4) + rotation = attr.data; + else if (strcmp(attr.name, "SCALE") == 0 && attr.data->type == cgltf_type_vec3) + scale = attr.data; + else if (strcmp(attr.name, "_COLOR_0") == 0 && (attr.data->type == cgltf_type_vec3 || attr.data->type == cgltf_type_vec4)) + color = attr.data; + else + fprintf(stderr, "Warning: ignoring %s instance attribute %s in node %d\n", *attr.name == '_' ? "custom" : "unknown", attr.name, int(ni)); + } + + size_t count = node->mesh_gpu_instancing.attributes[0].data->count; + + instances.reserve(instances.size() + count); + + cgltf_node instance = {}; + instance.parent = node; + instance.has_translation = translation != NULL; + instance.has_rotation = rotation != NULL; + instance.has_scale = scale != NULL; + instance.rotation[3] = 1.f; + instance.scale[0] = 1.f; + instance.scale[1] = 1.f; + instance.scale[2] = 1.f; + + for (size_t i = 0; i < count; ++i) + { + if (translation) + cgltf_accessor_read_float(translation, i, instance.translation, 4); + if (rotation) + cgltf_accessor_read_float(rotation, i, instance.rotation, 4); + if (scale) + cgltf_accessor_read_float(scale, i, instance.scale, 4); + + Instance obj = {}; + cgltf_node_transform_world(&instance, obj.transform); + + obj.color[0] = obj.color[1] = obj.color[2] = obj.color[3] = 1.0f; + + if (color) + cgltf_accessor_read_float(color, i, obj.color, 4); + + instances.push_back(obj); + } +} + +static void parseMeshNodesGltf(cgltf_data* data, std::vector& meshes, const std::vector >& mesh_remap) +{ + for (size_t i = 0; i < data->nodes_count; ++i) + { + cgltf_node& node = data->nodes[i]; + if (!node.mesh) + continue; + + std::pair range = mesh_remap[node.mesh - data->meshes]; + + for (size_t mi = range.first; mi < range.second; ++mi) + { + Mesh* mesh = &meshes[mi]; + + if (mesh->skin != node.skin && (!mesh->nodes.empty() || !mesh->instances.empty())) + { + // this should be extremely rare - if the same mesh is used with different skins, we need to duplicate it + // in this case we don't spend any effort on keeping the number of duplicates to the minimum, because this + // should really never happen. + meshes.push_back(*mesh); + mesh = &meshes.back(); + } + + if (node.has_mesh_gpu_instancing) + { + mesh->scene = 0; // we need to assign scene index since instances are attached to a scene; for now we assume 0 + parseMeshInstancesGltf(mesh->instances, &node, i); + } + else + { + mesh->skin = node.skin; + mesh->nodes.push_back(&node); + } + } + } + + for (size_t i = 0; i < meshes.size(); ++i) + { + Mesh& mesh = meshes[i]; + + // because the rest of gltfpack assumes that empty nodes array = world-space mesh, we need to filter unused meshes + if (mesh.nodes.empty() && mesh.instances.empty()) + { + mesh.streams.clear(); + mesh.indices.clear(); + } + } +} + +static void parseAnimationsGltf(cgltf_data* data, std::vector& animations) +{ + animations.reserve(data->animations_count); + + for (size_t i = 0; i < data->animations_count; ++i) + { + const cgltf_animation& animation = data->animations[i]; + + animations.push_back(Animation()); + Animation& result = animations.back(); + + result.name = animation.name; + + result.tracks.reserve(animation.channels_count); + + for (size_t j = 0; j < animation.channels_count; ++j) + { + const cgltf_animation_channel& channel = animation.channels[j]; + + if (!channel.target_node) + { + fprintf(stderr, "Warning: ignoring channel %d of animation %d (%s) because it has no target node\n", int(j), int(i), animation.name ? animation.name : ""); + continue; + } + + result.tracks.push_back(Track()); + Track& track = result.tracks.back(); + + track.node = channel.target_node; + track.path = channel.target_path; + + track.components = (channel.target_path == cgltf_animation_path_type_weights) ? track.node->mesh->primitives[0].targets_count : 1; + + track.interpolation = channel.sampler->interpolation; + + readAccessor(track.time, channel.sampler->input); + readAccessor(track.data, channel.sampler->output); + } + + if (result.tracks.empty()) + { + fprintf(stderr, "Warning: ignoring animation %d (%s) because it has no valid tracks\n", int(i), animation.name ? animation.name : ""); + animations.pop_back(); + } + } +} + +static bool requiresExtension(cgltf_data* data, const char* name) +{ + for (size_t i = 0; i < data->extensions_required_count; ++i) + if (strcmp(data->extensions_required[i], name) == 0) + return true; + + return false; +} + +static bool needsDummyBuffers(cgltf_data* data) +{ + for (size_t i = 0; i < data->accessors_count; ++i) + { + cgltf_accessor* accessor = &data->accessors[i]; + + if (accessor->buffer_view && accessor->buffer_view->data == NULL && accessor->buffer_view->buffer->data == NULL) + return true; + + if (accessor->is_sparse) + { + cgltf_accessor_sparse* sparse = &accessor->sparse; + + if (sparse->indices_buffer_view->buffer->data == NULL) + return true; + if (sparse->values_buffer_view->buffer->data == NULL) + return true; + } + } + + for (size_t i = 0; i < data->images_count; ++i) + { + cgltf_image* image = &data->images[i]; + + if (image->buffer_view && image->buffer_view->buffer->data == NULL) + return true; + } + + return false; +} + +static void freeFile(cgltf_data* data) +{ + data->json = NULL; + data->bin = NULL; + + free(data->file_data); + data->file_data = NULL; +} + +static bool freeUnusedBuffers(cgltf_data* data) +{ + std::vector used(data->buffers_count); + + for (size_t i = 0; i < data->skins_count; ++i) + { + const cgltf_skin& skin = data->skins[i]; + + if (skin.inverse_bind_matrices && skin.inverse_bind_matrices->buffer_view) + { + assert(skin.inverse_bind_matrices->buffer_view->buffer); + used[skin.inverse_bind_matrices->buffer_view->buffer - data->buffers] = 1; + } + } + + for (size_t i = 0; i < data->images_count; ++i) + { + const cgltf_image& image = data->images[i]; + + if (image.buffer_view) + { + assert(image.buffer_view->buffer); + used[image.buffer_view->buffer - data->buffers] = 1; + } + } + + bool free_bin = false; + + for (size_t i = 0; i < data->buffers_count; ++i) + { + cgltf_buffer& buffer = data->buffers[i]; + + if (!used[i] && buffer.data) + { + if (buffer.data != data->bin) + free(buffer.data); + else + free_bin = true; + + buffer.data = NULL; + } + } + + return free_bin; +} + +static cgltf_result decompressMeshopt(cgltf_data* data) +{ + for (size_t i = 0; i < data->buffer_views_count; ++i) + { + if (!data->buffer_views[i].has_meshopt_compression) + continue; + cgltf_meshopt_compression* mc = &data->buffer_views[i].meshopt_compression; + + const unsigned char* source = (const unsigned char*)mc->buffer->data; + if (!source) + return cgltf_result_invalid_gltf; + source += mc->offset; + + void* result = malloc(mc->count * mc->stride); + if (!result) + return cgltf_result_out_of_memory; + + data->buffer_views[i].data = result; + + int rc = -1; + + switch (mc->mode) + { + case cgltf_meshopt_compression_mode_attributes: + rc = meshopt_decodeVertexBuffer(result, mc->count, mc->stride, source, mc->size); + break; + + case cgltf_meshopt_compression_mode_triangles: + rc = meshopt_decodeIndexBuffer(result, mc->count, mc->stride, source, mc->size); + break; + + case cgltf_meshopt_compression_mode_indices: + rc = meshopt_decodeIndexSequence(result, mc->count, mc->stride, source, mc->size); + break; + + default: + return cgltf_result_invalid_gltf; + } + + if (rc != 0) + return cgltf_result_io_error; + + switch (mc->filter) + { + case cgltf_meshopt_compression_filter_octahedral: + meshopt_decodeFilterOct(result, mc->count, mc->stride); + break; + + case cgltf_meshopt_compression_filter_quaternion: + meshopt_decodeFilterQuat(result, mc->count, mc->stride); + break; + + case cgltf_meshopt_compression_filter_exponential: + meshopt_decodeFilterExp(result, mc->count, mc->stride); + break; + + case cgltf_meshopt_compression_filter_color: + meshopt_decodeFilterColor(result, mc->count, mc->stride); + break; + + default: + break; + } + } + + return cgltf_result_success; +} + +static cgltf_data* parseGltf(cgltf_data* data, cgltf_result result, std::vector& meshes, std::vector& animations, const char** error) +{ + *error = NULL; + + if (result != cgltf_result_success) + *error = getError(result, data); + else if (requiresExtension(data, "KHR_draco_mesh_compression")) + *error = "file requires Draco mesh compression support"; + else if (needsDummyBuffers(data)) + *error = "buffer has no data"; + + if (*error) + { + cgltf_free(data); + return NULL; + } + + if (requiresExtension(data, "KHR_mesh_quantization")) + fprintf(stderr, "Warning: file uses quantized geometry; repacking may result in increased quantization error\n"); + if (requiresExtension(data, "EXT_mesh_gpu_instancing") && data->scenes_count > 1) + fprintf(stderr, "Warning: file uses instancing and has more than one scene; results may be incorrect\n"); + + std::vector > mesh_remap; + + parseMeshesGltf(data, meshes, mesh_remap); + parseMeshNodesGltf(data, meshes, mesh_remap); + parseAnimationsGltf(data, animations); + + bool free_bin = freeUnusedBuffers(data); + + if (data->bin && free_bin) + freeFile(data); + + return data; +} + +cgltf_data* parseGltf(const char* path, std::vector& meshes, std::vector& animations, const char** error) +{ + cgltf_data* data = NULL; + + cgltf_options options = {}; + cgltf_result result = cgltf_parse_file(&options, path, &data); + + if (result == cgltf_result_success && !data->bin) + freeFile(data); + + result = (result == cgltf_result_success) ? cgltf_load_buffers(&options, data, path) : result; + result = (result == cgltf_result_success) ? cgltf_validate(data) : result; + result = (result == cgltf_result_success) ? decompressMeshopt(data) : result; + + return parseGltf(data, result, meshes, animations, error); +} + +cgltf_data* parseGlb(const void* buffer, size_t size, std::vector& meshes, std::vector& animations, const char** error) +{ + cgltf_data* data = NULL; + + cgltf_options options = {}; + options.type = cgltf_file_type_glb; + + cgltf_result result = cgltf_parse(&options, buffer, size, &data); + + result = (result == cgltf_result_success) ? cgltf_load_buffers(&options, data, NULL) : result; + result = (result == cgltf_result_success) ? cgltf_validate(data) : result; + result = (result == cgltf_result_success) ? decompressMeshopt(data) : result; + + return parseGltf(data, result, meshes, animations, error); +} + +bool areExtrasEqual(const cgltf_extras& lhs, const cgltf_extras& rhs) +{ + if (lhs.data && rhs.data) + return strcmp(lhs.data, rhs.data) == 0; + else + return lhs.data == rhs.data; +} diff --git a/Lib/meshoptimizer/gltf/parselib.cpp b/Lib/meshoptimizer/gltf/parselib.cpp new file mode 100644 index 0000000..9ae0696 --- /dev/null +++ b/Lib/meshoptimizer/gltf/parselib.cpp @@ -0,0 +1,12 @@ +#ifndef _CRT_SECURE_NO_WARNINGS +#define _CRT_SECURE_NO_WARNINGS +#endif + +#define CGLTF_IMPLEMENTATION +#include "../extern/cgltf.h" + +#define FAST_OBJ_IMPLEMENTATION +#include "../extern/fast_obj.h" + +#undef CGLTF_IMPLEMENTATION +#undef FAST_OBJ_IMPLEMENTATION diff --git a/Lib/meshoptimizer/gltf/parseobj.cpp b/Lib/meshoptimizer/gltf/parseobj.cpp new file mode 100644 index 0000000..cdfa36e --- /dev/null +++ b/Lib/meshoptimizer/gltf/parseobj.cpp @@ -0,0 +1,326 @@ +// This file is part of gltfpack; see gltfpack.h for version/license details +#include "gltfpack.h" + +#include "../extern/fast_obj.h" +#include "../src/meshoptimizer.h" + +#include +#include + +static void defaultFree(void*, void* p) +{ + free(p); +} + +static int textureIndex(const std::vector& textures, unsigned int name) +{ + for (size_t i = 0; i < textures.size(); ++i) + if (textures[i] == name) + return int(i); + + return -1; +} + +static void fixupUri(char* uri) +{ + // Some .obj paths come with back slashes, that are invalid as URI separators and won't open on macOS/Linux when embedding textures + for (char* s = uri; *s; ++s) + if (*s == '\\') + *s = '/'; +} + +static void parseMaterialsObj(fastObjMesh* obj, cgltf_data* data) +{ + // every texture in obj has a unique id (1+); we convert it to a 0-based index + // this effectively extracts only used textures out of the obj, as we don't remove unused textures later in the processing + std::vector textures; + + for (unsigned int mi = 0; mi < obj->material_count; ++mi) + { + fastObjMaterial& om = obj->materials[mi]; + + if (om.map_Kd && textureIndex(textures, om.map_Kd) < 0) + textures.push_back(om.map_Kd); + } + + data->images = (cgltf_image*)calloc(textures.size(), sizeof(cgltf_image)); + data->images_count = textures.size(); + + for (size_t i = 0; i < textures.size(); ++i) + { + unsigned int id = textures[i]; + data->images[i].uri = strdup(obj->textures[id].name); + fixupUri(data->images[i].uri); + } + + data->textures = (cgltf_texture*)calloc(textures.size(), sizeof(cgltf_texture)); + data->textures_count = textures.size(); + + for (size_t i = 0; i < textures.size(); ++i) + { + data->textures[i].image = &data->images[i]; + } + + data->materials = (cgltf_material*)calloc(obj->material_count, sizeof(cgltf_material)); + data->materials_count = obj->material_count; + + for (unsigned int mi = 0; mi < obj->material_count; ++mi) + { + const fastObjMaterial& om = obj->materials[mi]; + cgltf_material& gm = data->materials[mi]; + + if (om.name) + gm.name = strdup(om.name); + + gm.has_pbr_metallic_roughness = true; + + gm.pbr_metallic_roughness.base_color_factor[0] = 1.0f; + gm.pbr_metallic_roughness.base_color_factor[1] = 1.0f; + gm.pbr_metallic_roughness.base_color_factor[2] = 1.0f; + gm.pbr_metallic_roughness.base_color_factor[3] = 1.0f; + + gm.pbr_metallic_roughness.metallic_factor = 0.0f; + gm.pbr_metallic_roughness.roughness_factor = 1.0f; + + gm.alpha_cutoff = 0.5f; + + if (om.map_Kd) + { + gm.pbr_metallic_roughness.base_color_texture.texture = &data->textures[textureIndex(textures, om.map_Kd)]; + gm.pbr_metallic_roughness.base_color_texture.scale = 1.0f; + + gm.alpha_mode = (om.illum == 4 || om.illum == 6 || om.illum == 7 || om.illum == 9) ? cgltf_alpha_mode_mask : cgltf_alpha_mode_opaque; + } + else + { + gm.pbr_metallic_roughness.base_color_factor[0] = om.Kd[0]; + gm.pbr_metallic_roughness.base_color_factor[1] = om.Kd[1]; + gm.pbr_metallic_roughness.base_color_factor[2] = om.Kd[2]; + } + + if (om.map_d) + { + if (om.map_Kd && strcmp(obj->textures[om.map_Kd].name, obj->textures[om.map_d].name) != 0) + fprintf(stderr, "Warning: material has different diffuse and alpha textures (Kd: %s, d: %s) and might not render correctly\n", obj->textures[om.map_Kd].name, obj->textures[om.map_d].name); + + gm.alpha_mode = cgltf_alpha_mode_blend; + } + else if (om.d < 1.0f) + { + gm.pbr_metallic_roughness.base_color_factor[3] = om.d; + + gm.alpha_mode = cgltf_alpha_mode_blend; + } + } +} + +static void parseNodesObj(fastObjMesh* obj, cgltf_data* data) +{ + data->nodes = (cgltf_node*)calloc(obj->object_count, sizeof(cgltf_node)); + data->nodes_count = obj->object_count; + + for (unsigned int oi = 0; oi < obj->object_count; ++oi) + { + const fastObjGroup& og = obj->objects[oi]; + cgltf_node* node = &data->nodes[oi]; + + if (og.name) + node->name = strdup(og.name); + + node->rotation[3] = 1.0f; + node->scale[0] = 1.0f; + node->scale[1] = 1.0f; + node->scale[2] = 1.0f; + } + + data->scenes = (cgltf_scene*)calloc(1, sizeof(cgltf_scene)); + data->scenes_count = 1; + + data->scene = data->scenes; + + data->scenes->nodes = (cgltf_node**)calloc(obj->object_count, sizeof(cgltf_node*)); + data->scenes->nodes_count = obj->object_count; + + for (unsigned int oi = 0; oi < obj->object_count; ++oi) + data->scenes->nodes[oi] = &data->nodes[oi]; +} + +static void parseMeshObj(fastObjMesh* obj, unsigned int face_offset, unsigned int face_vertex_offset, unsigned int face_count, unsigned int face_vertex_count, unsigned int index_count, Mesh& mesh) +{ + std::vector remap(face_vertex_count); + size_t unique_vertices = meshopt_generateVertexRemap(remap.data(), NULL, face_vertex_count, &obj->indices[face_vertex_offset], face_vertex_count, sizeof(fastObjIndex)); + + int pos_stream = 0; + int nrm_stream = obj->normal_count > 1 ? 1 : -1; + int tex_stream = obj->texcoord_count > 1 ? 1 + (nrm_stream >= 0) : -1; + int col_stream = obj->color_count > 1 ? 1 + (nrm_stream >= 0) + (tex_stream >= 0) : -1; + + mesh.streams.resize(1 + (nrm_stream >= 0) + (tex_stream >= 0) + (col_stream >= 0)); + + mesh.streams[pos_stream].type = cgltf_attribute_type_position; + mesh.streams[pos_stream].data.resize(unique_vertices); + + if (nrm_stream >= 0) + { + mesh.streams[nrm_stream].type = cgltf_attribute_type_normal; + mesh.streams[nrm_stream].data.resize(unique_vertices); + } + + if (tex_stream >= 0) + { + mesh.streams[tex_stream].type = cgltf_attribute_type_texcoord; + mesh.streams[tex_stream].data.resize(unique_vertices); + } + + if (col_stream >= 0) + { + mesh.streams[col_stream].type = cgltf_attribute_type_color; + mesh.streams[col_stream].data.resize(unique_vertices); + } + + mesh.indices.resize(index_count); + + for (unsigned int vi = 0; vi < face_vertex_count; ++vi) + { + unsigned int target = remap[vi]; + // TODO: this fills every target vertex multiple times + + fastObjIndex ii = obj->indices[face_vertex_offset + vi]; + + Attr p = {{obj->positions[ii.p * 3 + 0], obj->positions[ii.p * 3 + 1], obj->positions[ii.p * 3 + 2]}}; + mesh.streams[pos_stream].data[target] = p; + + if (nrm_stream >= 0) + { + Attr n = {{obj->normals[ii.n * 3 + 0], obj->normals[ii.n * 3 + 1], obj->normals[ii.n * 3 + 2]}}; + mesh.streams[nrm_stream].data[target] = n; + } + + if (tex_stream >= 0) + { + Attr t = {{obj->texcoords[ii.t * 2 + 0], 1.f - obj->texcoords[ii.t * 2 + 1]}}; + mesh.streams[tex_stream].data[target] = t; + } + + if (col_stream >= 0) + { + Attr c = {{obj->colors[ii.p * 3 + 0], obj->colors[ii.p * 3 + 1], obj->colors[ii.p * 3 + 2]}}; + mesh.streams[col_stream].data[target] = c; + } + } + + unsigned int vertex_offset = 0; + unsigned int index_offset = 0; + + for (unsigned int fi = 0; fi < face_count; ++fi) + { + unsigned int face_vertices = obj->face_vertices[face_offset + fi]; + + if (mesh.type == cgltf_primitive_type_lines) + { + for (unsigned int vi = 1; vi < face_vertices; ++vi) + { + size_t to = index_offset + (vi - 1) * 2; + + mesh.indices[to + 0] = remap[vertex_offset + vi - 1]; + mesh.indices[to + 1] = remap[vertex_offset + vi]; + } + + vertex_offset += face_vertices; + index_offset += (face_vertices - 1) * 2; + } + else + { + for (unsigned int vi = 2; vi < face_vertices; ++vi) + { + size_t to = index_offset + (vi - 2) * 3; + + mesh.indices[to + 0] = remap[vertex_offset]; + mesh.indices[to + 1] = remap[vertex_offset + vi - 1]; + mesh.indices[to + 2] = remap[vertex_offset + vi]; + } + + vertex_offset += face_vertices; + index_offset += (face_vertices - 2) * 3; + } + } + + assert(vertex_offset == face_vertex_count); + assert(index_offset == index_count); +} + +static void parseMeshGroupObj(fastObjMesh* obj, const fastObjGroup& og, cgltf_data* data, cgltf_node* node, std::vector& meshes) +{ + unsigned int face_vertex_offset = og.index_offset; + unsigned int face_end_offset = og.face_offset + og.face_count; + + for (unsigned int face_offset = og.face_offset; face_offset < face_end_offset;) + { + unsigned int mi = obj->face_materials[face_offset]; + unsigned char isl = obj->face_lines ? obj->face_lines[face_offset] : 0; + + unsigned int face_count = 0; + unsigned int face_vertex_count = 0; + unsigned int index_count = 0; + + for (unsigned int fj = face_offset; fj < face_end_offset && obj->face_materials[fj] == mi && (!obj->face_lines || obj->face_lines[fj] == isl); ++fj) + { + face_count += 1; + face_vertex_count += obj->face_vertices[fj]; + index_count += isl ? (obj->face_vertices[fj] - 1) * 2 : (obj->face_vertices[fj] - 2) * 3; + } + + meshes.push_back(Mesh()); + Mesh& mesh = meshes.back(); + + if (data->materials_count) + { + assert(mi < data->materials_count); + mesh.material = &data->materials[mi]; + } + + mesh.type = isl ? cgltf_primitive_type_lines : cgltf_primitive_type_triangles; + mesh.targets = 0; + + if (node) + mesh.nodes.push_back(node); + + parseMeshObj(obj, face_offset, face_vertex_offset, face_count, face_vertex_count, index_count, mesh); + + face_offset += face_count; + face_vertex_offset += face_vertex_count; + } +} + +cgltf_data* parseObj(const char* path, std::vector& meshes, const char** error) +{ + fastObjMesh* obj = fast_obj_read(path); + + if (!obj) + { + *error = "file not found"; + return NULL; + } + + int fallback_materials = 0; + for (unsigned int mi = 0; mi < obj->material_count; ++mi) + fallback_materials += obj->materials[mi].fallback; + + if (fallback_materials) + fprintf(stderr, "Warning: %d/%d materials could not be loaded from mtllib\n", fallback_materials, obj->material_count); + + cgltf_data* data = (cgltf_data*)calloc(1, sizeof(cgltf_data)); + data->memory.free_func = defaultFree; + + parseMaterialsObj(obj, data); + + parseNodesObj(obj, data); + assert(data->nodes_count == obj->object_count); + + for (unsigned int oi = 0; oi < obj->object_count; ++oi) + parseMeshGroupObj(obj, obj->objects[oi], data, &data->nodes[oi], meshes); + + fast_obj_destroy(obj); + + return data; +} diff --git a/Lib/meshoptimizer/gltf/stream.cpp b/Lib/meshoptimizer/gltf/stream.cpp new file mode 100644 index 0000000..ae523c2 --- /dev/null +++ b/Lib/meshoptimizer/gltf/stream.cpp @@ -0,0 +1,913 @@ +// This file is part of gltfpack; see gltfpack.h for version/license details +#include "gltfpack.h" + +#include + +#include +#include +#include +#include + +#include "../src/meshoptimizer.h" + +struct Bounds +{ + Attr min, max; + + Bounds() + { + min.f[0] = min.f[1] = min.f[2] = min.f[3] = +FLT_MAX; + max.f[0] = max.f[1] = max.f[2] = max.f[3] = -FLT_MAX; + } + + bool isValid() const + { + return min.f[0] <= max.f[0] && min.f[1] <= max.f[1] && min.f[2] <= max.f[2] && min.f[3] <= max.f[3]; + } + + float getExtent() const + { + return std::max(max.f[0] - min.f[0], std::max(max.f[1] - min.f[1], max.f[2] - min.f[2])); + } + + void merge(const Bounds& other) + { + for (int k = 0; k < 4; ++k) + { + min.f[k] = std::min(min.f[k], other.min.f[k]); + max.f[k] = std::max(max.f[k], other.max.f[k]); + } + } +}; + +static Bounds computeBounds(const Mesh& mesh, cgltf_attribute_type type) +{ + Bounds b; + Attr pad = {}; + + for (size_t j = 0; j < mesh.streams.size(); ++j) + { + const Stream& s = mesh.streams[j]; + + if (s.type == type) + { + if (s.target == 0) + { + for (size_t k = 0; k < s.data.size(); ++k) + { + const Attr& a = s.data[k]; + + b.min.f[0] = std::min(b.min.f[0], a.f[0]); + b.min.f[1] = std::min(b.min.f[1], a.f[1]); + b.min.f[2] = std::min(b.min.f[2], a.f[2]); + b.min.f[3] = std::min(b.min.f[3], a.f[3]); + + b.max.f[0] = std::max(b.max.f[0], a.f[0]); + b.max.f[1] = std::max(b.max.f[1], a.f[1]); + b.max.f[2] = std::max(b.max.f[2], a.f[2]); + b.max.f[3] = std::max(b.max.f[3], a.f[3]); + } + } + else + { + for (size_t k = 0; k < s.data.size(); ++k) + { + const Attr& a = s.data[k]; + + pad.f[0] = std::max(pad.f[0], fabsf(a.f[0])); + pad.f[1] = std::max(pad.f[1], fabsf(a.f[1])); + pad.f[2] = std::max(pad.f[2], fabsf(a.f[2])); + pad.f[3] = std::max(pad.f[3], fabsf(a.f[3])); + } + } + } + } + + for (int k = 0; k < 4; ++k) + { + b.min.f[k] -= pad.f[k]; + b.max.f[k] += pad.f[k]; + } + + return b; +} + +static float computeUvArea(const Mesh& mesh) +{ + if (mesh.indices.empty() || mesh.type != cgltf_primitive_type_triangles) + return 0.f; + + float result = 0.f; + + for (size_t j = 0; j < mesh.streams.size(); ++j) + { + const Stream& s = mesh.streams[j]; + + if (s.type != cgltf_attribute_type_texcoord) + continue; + + float uvarea = 0.f; + + for (size_t i = 0; i < mesh.indices.size(); i += 3) + { + unsigned int a = mesh.indices[i + 0]; + unsigned int b = mesh.indices[i + 1]; + unsigned int c = mesh.indices[i + 2]; + + const Attr& va = s.data[a]; + const Attr& vb = s.data[b]; + const Attr& vc = s.data[c]; + + uvarea += fabsf((vb.f[0] - va.f[0]) * (vc.f[1] - va.f[1]) - (vc.f[0] - va.f[0]) * (vb.f[1] - va.f[1])); + } + + result = std::max(result, uvarea / float(mesh.indices.size() / 3)); + } + + return result; +} + +QuantizationPosition prepareQuantizationPosition(const std::vector& meshes, const Settings& settings) +{ + QuantizationPosition result = {}; + + result.bits = settings.pos_bits; + result.normalized = settings.pos_normalized; + + std::vector bounds(meshes.size()); + + for (size_t i = 0; i < meshes.size(); ++i) + bounds[i] = computeBounds(meshes[i], cgltf_attribute_type_position); + + Bounds b; + for (size_t i = 0; i < meshes.size(); ++i) + b.merge(bounds[i]); + + if (b.isValid()) + { + result.offset[0] = b.min.f[0]; + result.offset[1] = b.min.f[1]; + result.offset[2] = b.min.f[2]; + result.scale = b.getExtent(); + } + + if (b.isValid() && settings.quantize && !settings.pos_float) + { + float error = result.scale * 0.5f / (1 << (result.bits - 1)); + float max_rel_error = 0; + + for (size_t i = 0; i < meshes.size(); ++i) + if (bounds[i].isValid() && bounds[i].getExtent() > 1e-2f) + max_rel_error = std::max(max_rel_error, error / bounds[i].getExtent()); + + if (max_rel_error > 5e-2f) + fprintf(stderr, "Warning: position data has significant error (%.0f%%); consider using floating-point quantization (-vpf) or more bits (-vp N)\n", max_rel_error * 100); + } + + result.node_scale = result.scale / float((1 << result.bits) - 1) * (result.normalized ? 65535.f : 1.f); + + return result; +} + +static size_t follow(std::vector& parents, size_t index) +{ + while (index != parents[index]) + { + size_t parent = parents[index]; + + parents[index] = parents[parent]; + index = parent; + } + + return index; +} + +void prepareQuantizationTexture(cgltf_data* data, std::vector& result, std::vector& indices, const std::vector& meshes, const Settings& settings) +{ + // use union-find to associate each material with a canonical material + // this is necessary because any set of materials that are used on the same mesh must use the same quantization + std::vector parents(result.size()); + + for (size_t i = 0; i < parents.size(); ++i) + parents[i] = i; + + for (size_t i = 0; i < meshes.size(); ++i) + { + const Mesh& mesh = meshes[i]; + + if (!mesh.material && mesh.variants.empty()) + continue; + + size_t root = follow(parents, (mesh.material ? mesh.material : mesh.variants[0].material) - data->materials); + + for (size_t j = 0; j < mesh.variants.size(); ++j) + { + size_t var = follow(parents, mesh.variants[j].material - data->materials); + + parents[var] = root; + } + + indices[i] = root; + } + + // compute canonical material bounds based on meshes that use them + std::vector bounds(result.size()); + + for (size_t i = 0; i < meshes.size(); ++i) + { + const Mesh& mesh = meshes[i]; + + if (!mesh.material && mesh.variants.empty()) + continue; + + indices[i] = follow(parents, indices[i]); + + Bounds mb = computeBounds(mesh, cgltf_attribute_type_texcoord); + bounds[indices[i]].merge(mb); + } + + // detect potential precision issues and warn about them + if (settings.quantize && !settings.tex_float) + { + float max_rel_error = 0; + + for (size_t i = 0; i < meshes.size(); ++i) + { + const Mesh& mesh = meshes[i]; + + if (!mesh.material && mesh.variants.empty()) + continue; + + const Bounds& b = bounds[indices[i]]; + if (!b.isValid()) + continue; + + float scale = std::max(b.max.f[0] - b.min.f[0], b.max.f[1] - b.min.f[1]); + float error = scale * 0.5f / (1 << (settings.tex_bits - 1)); + + if (error < 1e-3f) + continue; + + float uvarea = computeUvArea(mesh); + float rel_error = uvarea > 0 ? error / sqrtf(uvarea) : 0.f; + + max_rel_error = std::max(max_rel_error, rel_error); + } + + if (max_rel_error > 1e-1f) + fprintf(stderr, "Warning: texture coordinate data has significant error (%.0f%%); consider using floating-point quantization (-vtf) or more bits (-vt N)\n", max_rel_error * 100); + } + + // update all material data using canonical bounds + for (size_t i = 0; i < result.size(); ++i) + { + QuantizationTexture& qt = result[i]; + + qt.bits = settings.tex_bits; + qt.normalized = true; + + const Bounds& b = bounds[follow(parents, i)]; + + if (b.isValid()) + { + qt.offset[0] = b.min.f[0]; + qt.offset[1] = b.min.f[1]; + qt.scale[0] = b.max.f[0] - b.min.f[0]; + qt.scale[1] = b.max.f[1] - b.min.f[1]; + } + } +} + +void getPositionBounds(float min[3], float max[3], const Stream& stream, const QuantizationPosition& qp, const Settings& settings) +{ + assert(stream.type == cgltf_attribute_type_position); + assert(stream.data.size() > 0); + + min[0] = min[1] = min[2] = FLT_MAX; + max[0] = max[1] = max[2] = -FLT_MAX; + + for (size_t i = 0; i < stream.data.size(); ++i) + { + const Attr& a = stream.data[i]; + + for (int k = 0; k < 3; ++k) + { + min[k] = std::min(min[k], a.f[k]); + max[k] = std::max(max[k], a.f[k]); + } + } + + if (settings.quantize) + { + if (settings.pos_float) + { + for (int k = 0; k < 3; ++k) + { + min[k] = meshopt_quantizeFloat(min[k], qp.bits); + max[k] = meshopt_quantizeFloat(max[k], qp.bits); + } + } + else + { + float pos_rscale = qp.scale == 0.f ? 0.f : 1.f / qp.scale * (stream.target > 0 && qp.normalized ? 32767.f / 65535.f : 1.f); + + for (int k = 0; k < 3; ++k) + { + if (stream.target == 0) + { + min[k] = float(meshopt_quantizeUnorm((min[k] - qp.offset[k]) * pos_rscale, qp.bits)); + max[k] = float(meshopt_quantizeUnorm((max[k] - qp.offset[k]) * pos_rscale, qp.bits)); + } + else + { + min[k] = (min[k] >= 0.f ? 1.f : -1.f) * float(meshopt_quantizeUnorm(fabsf(min[k]) * pos_rscale, qp.bits)); + max[k] = (max[k] >= 0.f ? 1.f : -1.f) * float(meshopt_quantizeUnorm(fabsf(max[k]) * pos_rscale, qp.bits)); + } + } + } + } +} + +static void renormalizeWeights(uint8_t (&w)[4]) +{ + int sum = w[0] + w[1] + w[2] + w[3]; + + if (sum == 255) + return; + + // we assume that the total error is limited to 0.5/component = 2 + // this means that it's acceptable to adjust the max. component to compensate for the error + int max = 0; + + for (int k = 1; k < 4; ++k) + if (w[k] > w[max]) + max = k; + + w[max] += uint8_t(255 - sum); +} + +static void encodeSnorm(void* destination, size_t count, size_t stride, int bits, const float* data) +{ + assert(stride == 4 || stride == 8); + assert(bits >= 1 && bits <= 16); + + signed char* d8 = static_cast(destination); + short* d16 = static_cast(destination); + + for (size_t i = 0; i < count; ++i) + { + const float* v = &data[i * 4]; + + int fx = meshopt_quantizeSnorm(v[0], bits); + int fy = meshopt_quantizeSnorm(v[1], bits); + int fz = meshopt_quantizeSnorm(v[2], bits); + int fw = meshopt_quantizeSnorm(v[3], bits); + + if (stride == 4) + { + d8[i * 4 + 0] = (signed char)(fx); + d8[i * 4 + 1] = (signed char)(fy); + d8[i * 4 + 2] = (signed char)(fz); + d8[i * 4 + 3] = (signed char)(fw); + } + else + { + d16[i * 4 + 0] = short(fx); + d16[i * 4 + 1] = short(fy); + d16[i * 4 + 2] = short(fz); + d16[i * 4 + 3] = short(fw); + } + } +} + +static int quantizeColor(float v, int bytebits, int bits) +{ + int result = meshopt_quantizeUnorm(v, bytebits); + + // replicate the top bit into the low significant bits + const int mask = (1 << (bytebits - bits)) - 1; + + return (result & ~mask) | (mask & -(result >> (bytebits - 1))); +} + +static void encodeColor(void* destination, size_t count, size_t stride, int bits, const float* data) +{ + assert(stride == 4 || stride == 8); + assert(bits >= 2 && bits <= 16); + + unsigned char* d8 = static_cast(destination); + unsigned short* d16 = static_cast(destination); + + for (size_t i = 0; i < count; ++i) + { + const float* c = &data[i * 4]; + + if (stride == 4) + { + d8[i * 4 + 0] = uint8_t(quantizeColor(c[0], 8, bits)); + d8[i * 4 + 1] = uint8_t(quantizeColor(c[1], 8, bits)); + d8[i * 4 + 2] = uint8_t(quantizeColor(c[2], 8, bits)); + d8[i * 4 + 3] = uint8_t(quantizeColor(c[3], 8, bits)); + } + else + { + d16[i * 4 + 0] = uint16_t(quantizeColor(c[0], 16, bits)); + d16[i * 4 + 1] = uint16_t(quantizeColor(c[1], 16, bits)); + d16[i * 4 + 2] = uint16_t(quantizeColor(c[2], 16, bits)); + d16[i * 4 + 3] = uint16_t(quantizeColor(c[3], 16, bits)); + } + } +} + +static StreamFormat writeVertexStreamRaw(std::string& bin, const Stream& stream, cgltf_type type, size_t components) +{ + assert(components >= 1 && components <= 4); + + for (size_t i = 0; i < stream.data.size(); ++i) + { + const Attr& a = stream.data[i]; + + bin.append(reinterpret_cast(a.f), sizeof(float) * components); + } + + StreamFormat format = {type, cgltf_component_type_r_32f, false, sizeof(float) * components}; + return format; +} + +static StreamFormat writeVertexStreamFloat(std::string& bin, const Stream& stream, cgltf_type type, int components, bool expf, int bits, meshopt_EncodeExpMode mode) +{ + assert(components >= 1 && components <= 4); + + StreamFormat::Filter filter = expf ? StreamFormat::Filter_Exp : StreamFormat::Filter_None; + + if (filter == StreamFormat::Filter_Exp) + { + size_t offset = bin.size(); + size_t stride = sizeof(float) * components; + for (size_t i = 0; i < stream.data.size(); ++i) + bin.append(reinterpret_cast(stream.data[i].f), stride); + + meshopt_encodeFilterExp(&bin[offset], stream.data.size(), stride, bits + 1, reinterpret_cast(&bin[offset]), mode); + } + else + { + for (size_t i = 0; i < stream.data.size(); ++i) + { + const Attr& a = stream.data[i]; + + float v[4]; + for (int k = 0; k < components; ++k) + v[k] = meshopt_quantizeFloat(a.f[k], bits); + bin.append(reinterpret_cast(v), sizeof(float) * components); + } + } + + StreamFormat format = {type, cgltf_component_type_r_32f, false, sizeof(float) * components, filter}; + return format; +} + +StreamFormat writeVertexStream(std::string& bin, const Stream& stream, const QuantizationPosition& qp, const QuantizationTexture& qt, const Settings& settings, bool filters) +{ + if (stream.type == cgltf_attribute_type_position) + { + if (!settings.quantize) + return writeVertexStreamRaw(bin, stream, cgltf_type_vec3, 3); + + if (settings.pos_float) + return writeVertexStreamFloat(bin, stream, cgltf_type_vec3, 3, settings.compress && filters, qp.bits, + settings.compressmore ? meshopt_EncodeExpSharedComponent : meshopt_EncodeExpSeparate); + + if (stream.target == 0) + { + float pos_rscale = qp.scale == 0.f ? 0.f : 1.f / qp.scale; + + for (size_t i = 0; i < stream.data.size(); ++i) + { + const Attr& a = stream.data[i]; + + uint16_t v[4] = { + uint16_t(meshopt_quantizeUnorm((a.f[0] - qp.offset[0]) * pos_rscale, qp.bits)), + uint16_t(meshopt_quantizeUnorm((a.f[1] - qp.offset[1]) * pos_rscale, qp.bits)), + uint16_t(meshopt_quantizeUnorm((a.f[2] - qp.offset[2]) * pos_rscale, qp.bits)), + 0}; + bin.append(reinterpret_cast(v), sizeof(v)); + } + + StreamFormat format = {cgltf_type_vec3, cgltf_component_type_r_16u, qp.normalized, 8}; + return format; + } + else + { + float pos_rscale = qp.scale == 0.f ? 0.f : 1.f / qp.scale * (qp.normalized ? 32767.f / 65535.f : 1.f); + + int maxv = 0; + + for (size_t i = 0; i < stream.data.size(); ++i) + { + const Attr& a = stream.data[i]; + + maxv = std::max(maxv, meshopt_quantizeUnorm(fabsf(a.f[0]) * pos_rscale, qp.bits)); + maxv = std::max(maxv, meshopt_quantizeUnorm(fabsf(a.f[1]) * pos_rscale, qp.bits)); + maxv = std::max(maxv, meshopt_quantizeUnorm(fabsf(a.f[2]) * pos_rscale, qp.bits)); + } + + if (maxv <= 127 && !qp.normalized) + { + for (size_t i = 0; i < stream.data.size(); ++i) + { + const Attr& a = stream.data[i]; + + int8_t v[4] = { + int8_t((a.f[0] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[0]) * pos_rscale, qp.bits)), + int8_t((a.f[1] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[1]) * pos_rscale, qp.bits)), + int8_t((a.f[2] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[2]) * pos_rscale, qp.bits)), + 0}; + bin.append(reinterpret_cast(v), sizeof(v)); + } + + StreamFormat format = {cgltf_type_vec3, cgltf_component_type_r_8, false, 4}; + return format; + } + else + { + for (size_t i = 0; i < stream.data.size(); ++i) + { + const Attr& a = stream.data[i]; + + int16_t v[4] = { + int16_t((a.f[0] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[0]) * pos_rscale, qp.bits)), + int16_t((a.f[1] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[1]) * pos_rscale, qp.bits)), + int16_t((a.f[2] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[2]) * pos_rscale, qp.bits)), + 0}; + bin.append(reinterpret_cast(v), sizeof(v)); + } + + StreamFormat format = {cgltf_type_vec3, cgltf_component_type_r_16, qp.normalized, 8}; + return format; + } + } + } + else if (stream.type == cgltf_attribute_type_texcoord) + { + if (!settings.quantize) + return writeVertexStreamRaw(bin, stream, cgltf_type_vec2, 2); + + // expand the encoded range to ensure it covers [0..1) interval + // this can slightly reduce precision but we should not need more precision inside 0..1, and this significantly improves compressed size when using encodeExpOne + if (settings.tex_float) + return writeVertexStreamFloat(bin, stream, cgltf_type_vec2, 2, settings.compress && filters, qt.bits, meshopt_EncodeExpClamped); + + float uv_rscale[2] = { + qt.scale[0] == 0.f ? 0.f : 1.f / qt.scale[0], + qt.scale[1] == 0.f ? 0.f : 1.f / qt.scale[1], + }; + + for (size_t i = 0; i < stream.data.size(); ++i) + { + const Attr& a = stream.data[i]; + + uint16_t v[2] = { + uint16_t(meshopt_quantizeUnorm((a.f[0] - qt.offset[0]) * uv_rscale[0], qt.bits)), + uint16_t(meshopt_quantizeUnorm((a.f[1] - qt.offset[1]) * uv_rscale[1], qt.bits)), + }; + bin.append(reinterpret_cast(v), sizeof(v)); + } + + StreamFormat format = {cgltf_type_vec2, cgltf_component_type_r_16u, qt.normalized, 4}; + return format; + } + else if (stream.type == cgltf_attribute_type_normal) + { + if (!settings.quantize) + return writeVertexStreamRaw(bin, stream, cgltf_type_vec3, 3); + + // expand the encoded range to ensure it covers [0..1) interval + // for unit length vectors, SharedComponent results in a slightly smaller encoding vs Clamped + // for morph deltas, we could use SharedComponent but it may preserve noisy small inputs redundantly + if (settings.nrm_float) + return writeVertexStreamFloat(bin, stream, cgltf_type_vec3, 3, settings.compress && filters, settings.nrm_bits, + settings.compressmore && stream.target == 0 ? meshopt_EncodeExpSharedComponent : meshopt_EncodeExpClamped); + + bool oct = filters && settings.compressmore && stream.target == 0; + int bits = settings.nrm_bits; + + StreamFormat::Filter filter = oct ? StreamFormat::Filter_Oct : StreamFormat::Filter_None; + + size_t offset = bin.size(); + size_t stride = bits > 8 ? 8 : 4; + bin.resize(bin.size() + stream.data.size() * stride); + + if (oct) + meshopt_encodeFilterOct(&bin[offset], stream.data.size(), stride, bits, stream.data[0].f); + else + encodeSnorm(&bin[offset], stream.data.size(), stride, bits, stream.data[0].f); + + cgltf_component_type component_type = bits > 8 ? cgltf_component_type_r_16 : cgltf_component_type_r_8; + StreamFormat format = {cgltf_type_vec3, component_type, true, stride, filter}; + return format; + } + else if (stream.type == cgltf_attribute_type_tangent) + { + if (!settings.quantize) + return writeVertexStreamRaw(bin, stream, cgltf_type_vec4, 4); + + bool oct = filters && settings.compressmore && stream.target == 0; + int bits = (settings.nrm_bits > 8) ? 8 : settings.nrm_bits; + + StreamFormat::Filter filter = oct ? StreamFormat::Filter_Oct : StreamFormat::Filter_None; + + size_t offset = bin.size(); + size_t stride = 4; + bin.resize(bin.size() + stream.data.size() * stride); + + if (oct) + meshopt_encodeFilterOct(&bin[offset], stream.data.size(), stride, bits, stream.data[0].f); + else + encodeSnorm(&bin[offset], stream.data.size(), stride, bits, stream.data[0].f); + + cgltf_type type = (stream.target == 0) ? cgltf_type_vec4 : cgltf_type_vec3; + StreamFormat format = {type, cgltf_component_type_r_8, true, 4, filter}; + return format; + } + else if (stream.type == cgltf_attribute_type_color) + { + bool col = filters && settings.compresskhr && settings.compressmore; + int bits = settings.col_bits; + + StreamFormat::Filter filter = col ? StreamFormat::Filter_Color : StreamFormat::Filter_None; + + size_t offset = bin.size(); + size_t stride = bits > 8 ? 8 : 4; + bin.resize(bin.size() + stream.data.size() * stride); + + if (col) + meshopt_encodeFilterColor(&bin[offset], stream.data.size(), stride, bits, stream.data[0].f); + else + encodeColor(&bin[offset], stream.data.size(), stride, bits, stream.data[0].f); + + if (bits > 8) + { + StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_16u, true, 8, filter}; + return format; + } + else + { + StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_8u, true, 4, filter}; + return format; + } + } + else if (stream.type == cgltf_attribute_type_weights) + { + for (size_t i = 0; i < stream.data.size(); ++i) + { + const Attr& a = stream.data[i]; + + float ws = a.f[0] + a.f[1] + a.f[2] + a.f[3]; + float wsi = (ws == 0.f) ? 0.f : 1.f / ws; + + uint8_t v[4] = { + uint8_t(meshopt_quantizeUnorm(a.f[0] * wsi, 8)), + uint8_t(meshopt_quantizeUnorm(a.f[1] * wsi, 8)), + uint8_t(meshopt_quantizeUnorm(a.f[2] * wsi, 8)), + uint8_t(meshopt_quantizeUnorm(a.f[3] * wsi, 8))}; + + if (wsi != 0.f) + renormalizeWeights(v); + + bin.append(reinterpret_cast(v), sizeof(v)); + } + + StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_8u, true, 4}; + return format; + } + else if (stream.type == cgltf_attribute_type_joints) + { + unsigned int maxj = 0; + + for (size_t i = 0; i < stream.data.size(); ++i) + for (int k = 0; k < 4; ++k) + maxj = std::max(maxj, unsigned(int(stream.data[i].f[k]))); + + assert(maxj <= 65535); + + if (maxj <= 255) + { + for (size_t i = 0; i < stream.data.size(); ++i) + { + const Attr& a = stream.data[i]; + + uint8_t v[4] = { + uint8_t(a.f[0]), + uint8_t(a.f[1]), + uint8_t(a.f[2]), + uint8_t(a.f[3])}; + bin.append(reinterpret_cast(v), sizeof(v)); + } + + StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_8u, false, 4}; + return format; + } + else + { + for (size_t i = 0; i < stream.data.size(); ++i) + { + const Attr& a = stream.data[i]; + + uint16_t v[4] = { + uint16_t(a.f[0]), + uint16_t(a.f[1]), + uint16_t(a.f[2]), + uint16_t(a.f[3])}; + bin.append(reinterpret_cast(v), sizeof(v)); + } + + StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_16u, false, 8}; + return format; + } + } + else if (stream.type == cgltf_attribute_type_custom) + { + // note: _custom is equivalent to _ID, as such the data contains scalar integers + if (!settings.compressmore || !filters) + return writeVertexStreamRaw(bin, stream, cgltf_type_scalar, 1); + + unsigned int maxv = 0; + + for (size_t i = 0; i < stream.data.size(); ++i) + maxv = std::max(maxv, unsigned(int(stream.data[i].f[0]))); + + // exp encoding uses a signed mantissa with only 23 significant bits; input glTF encoding may encode indices losslessly up to 2^24 + if (maxv >= (1 << 23)) + return writeVertexStreamRaw(bin, stream, cgltf_type_scalar, 1); + + for (size_t i = 0; i < stream.data.size(); ++i) + { + const Attr& a = stream.data[i]; + + uint32_t id = uint32_t(a.f[0]); + uint32_t v = id; // exp encoding of integers in [0..2^23-1] range is equivalent to the integer itself + + bin.append(reinterpret_cast(&v), sizeof(v)); + } + + StreamFormat format = {cgltf_type_scalar, cgltf_component_type_r_32f, false, 4, StreamFormat::Filter_Exp}; + return format; + } + else + { + return writeVertexStreamRaw(bin, stream, cgltf_type_vec4, 4); + } +} + +StreamFormat writeIndexStream(std::string& bin, const std::vector& stream) +{ + unsigned int maxi = 0; + for (size_t i = 0; i < stream.size(); ++i) + maxi = std::max(maxi, stream[i]); + + // save 16-bit indices if we can; note that we can't use restart index (65535) + if (maxi < 65535) + { + for (size_t i = 0; i < stream.size(); ++i) + { + uint16_t v[1] = {uint16_t(stream[i])}; + bin.append(reinterpret_cast(v), sizeof(v)); + } + + StreamFormat format = {cgltf_type_scalar, cgltf_component_type_r_16u, false, 2}; + return format; + } + else + { + for (size_t i = 0; i < stream.size(); ++i) + { + uint32_t v[1] = {stream[i]}; + bin.append(reinterpret_cast(v), sizeof(v)); + } + + StreamFormat format = {cgltf_type_scalar, cgltf_component_type_r_32u, false, 4}; + return format; + } +} + +StreamFormat writeTimeStream(std::string& bin, const std::vector& data) +{ + for (size_t i = 0; i < data.size(); ++i) + { + float v[1] = {data[i]}; + bin.append(reinterpret_cast(v), sizeof(v)); + } + + StreamFormat format = {cgltf_type_scalar, cgltf_component_type_r_32f, false, 4}; + return format; +} + +StreamFormat writeKeyframeStream(std::string& bin, cgltf_animation_path_type type, const std::vector& data, const Settings& settings, bool has_tangents) +{ + if (type == cgltf_animation_path_type_rotation) + { + StreamFormat::Filter filter = settings.compressmore && !has_tangents ? StreamFormat::Filter_Quat : StreamFormat::Filter_None; + + size_t offset = bin.size(); + size_t stride = 8; + bin.resize(bin.size() + data.size() * stride); + + if (filter == StreamFormat::Filter_Quat) + meshopt_encodeFilterQuat(&bin[offset], data.size(), stride, settings.rot_bits, data[0].f); + else + encodeSnorm(&bin[offset], data.size(), stride, 16, data[0].f); + + StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_16, true, 8, filter}; + return format; + } + else if (type == cgltf_animation_path_type_weights) + { + for (size_t i = 0; i < data.size(); ++i) + { + const Attr& a = data[i]; + + uint8_t v[1] = {uint8_t(meshopt_quantizeUnorm(a.f[0], 8))}; + bin.append(reinterpret_cast(v), sizeof(v)); + } + + StreamFormat format = {cgltf_type_scalar, cgltf_component_type_r_8u, true, 1}; + return format; + } + else if (type == cgltf_animation_path_type_translation || type == cgltf_animation_path_type_scale) + { + StreamFormat::Filter filter = settings.compressmore ? StreamFormat::Filter_Exp : StreamFormat::Filter_None; + int bits = (type == cgltf_animation_path_type_translation) ? settings.trn_bits : settings.scl_bits; + + size_t offset = bin.size(); + + for (size_t i = 0; i < data.size(); ++i) + { + const Attr& a = data[i]; + + float v[3] = {a.f[0], a.f[1], a.f[2]}; + bin.append(reinterpret_cast(v), sizeof(v)); + } + + if (filter == StreamFormat::Filter_Exp) + meshopt_encodeFilterExp(&bin[offset], data.size(), 12, bits, reinterpret_cast(&bin[offset]), meshopt_EncodeExpSharedVector); + + StreamFormat format = {cgltf_type_vec3, cgltf_component_type_r_32f, false, 12, filter}; + return format; + } + else + { + for (size_t i = 0; i < data.size(); ++i) + { + const Attr& a = data[i]; + + float v[4] = {a.f[0], a.f[1], a.f[2], a.f[3]}; + bin.append(reinterpret_cast(v), sizeof(v)); + } + + StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_32f, false, 16}; + return format; + } +} + +void compressVertexStream(std::string& bin, const std::string& data, size_t count, size_t stride, int level) +{ + assert(data.size() == count * stride); + + std::vector compressed(meshopt_encodeVertexBufferBound(count, stride)); + size_t size = meshopt_encodeVertexBufferLevel(&compressed[0], compressed.size(), data.c_str(), count, stride, level, level > 0); + + bin.append(reinterpret_cast(&compressed[0]), size); +} + +void compressIndexStream(std::string& bin, const std::string& data, size_t count, size_t stride) +{ + assert(stride == 2 || stride == 4); + assert(data.size() == count * stride); + assert(count % 3 == 0); + + std::vector compressed(meshopt_encodeIndexBufferBound(count, count)); + size_t size = 0; + + if (stride == 2) + size = meshopt_encodeIndexBuffer(&compressed[0], compressed.size(), reinterpret_cast(data.c_str()), count); + else + size = meshopt_encodeIndexBuffer(&compressed[0], compressed.size(), reinterpret_cast(data.c_str()), count); + + bin.append(reinterpret_cast(&compressed[0]), size); +} + +void compressIndexSequence(std::string& bin, const std::string& data, size_t count, size_t stride) +{ + assert(stride == 2 || stride == 4); + assert(data.size() == count * stride); + + std::vector compressed(meshopt_encodeIndexSequenceBound(count, count)); + size_t size = 0; + + if (stride == 2) + size = meshopt_encodeIndexSequence(&compressed[0], compressed.size(), reinterpret_cast(data.c_str()), count); + else + size = meshopt_encodeIndexSequence(&compressed[0], compressed.size(), reinterpret_cast(data.c_str()), count); + + bin.append(reinterpret_cast(&compressed[0]), size); +} diff --git a/Lib/meshoptimizer/gltf/wasistubs.cpp b/Lib/meshoptimizer/gltf/wasistubs.cpp new file mode 100644 index 0000000..2573b11 --- /dev/null +++ b/Lib/meshoptimizer/gltf/wasistubs.cpp @@ -0,0 +1,45 @@ +#ifdef __wasi__ +#include +#include + +#include + +extern "C" void __cxa_throw(void* ptr, void* type, void* destructor) +{ + abort(); +} + +extern "C" void* __cxa_allocate_exception(size_t thrown_size) +{ + abort(); +} + +extern "C" int32_t __wasi_path_open32(int32_t arg0, int32_t arg1, int32_t arg2, int32_t arg3, int32_t arg4, int32_t arg5, int32_t arg6, int32_t arg7, int32_t arg8) + __attribute__(( + __import_module__("wasi_snapshot_preview1"), + __import_name__("path_open32"), + __warn_unused_result__)); + +extern "C" int32_t __imported_wasi_snapshot_preview1_path_open(int32_t arg0, int32_t arg1, int32_t arg2, int32_t arg3, int32_t arg4, int64_t arg5, int64_t arg6, int32_t arg7, int32_t arg8) +{ + return __wasi_path_open32(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); +} + +extern "C" int32_t __wasi_fd_seek32(int32_t arg0, int32_t arg1, int32_t arg2, int32_t arg3) + __attribute__(( + __import_module__("wasi_snapshot_preview1"), + __import_name__("fd_seek32"), + __warn_unused_result__)); + +extern "C" int32_t __imported_wasi_snapshot_preview1_fd_seek(int32_t arg0, int64_t arg1, int32_t arg2, int32_t arg3) +{ + *(uint64_t*)arg3 = 0; + return __wasi_fd_seek32(arg0, arg1, arg2, arg3); +} + +extern "C" int32_t __imported_wasi_snapshot_preview1_clock_time_get(int32_t arg0, int64_t arg1, int32_t arg2) +{ + return __WASI_ERRNO_NOSYS; +} + +#endif diff --git a/Lib/meshoptimizer/gltf/wasistubs.txt b/Lib/meshoptimizer/gltf/wasistubs.txt new file mode 100644 index 0000000..dc29d46 --- /dev/null +++ b/Lib/meshoptimizer/gltf/wasistubs.txt @@ -0,0 +1,2 @@ +__wasi_path_open32 +__wasi_fd_seek32 diff --git a/Lib/meshoptimizer/gltf/write.cpp b/Lib/meshoptimizer/gltf/write.cpp new file mode 100644 index 0000000..6cb6324 --- /dev/null +++ b/Lib/meshoptimizer/gltf/write.cpp @@ -0,0 +1,1821 @@ +// This file is part of gltfpack; see gltfpack.h for version/license details +#include "gltfpack.h" + +#include +#include +#include +#include +#include + +static const char* componentType(cgltf_component_type type) +{ + switch (type) + { + case cgltf_component_type_r_8: + return "5120"; + case cgltf_component_type_r_8u: + return "5121"; + case cgltf_component_type_r_16: + return "5122"; + case cgltf_component_type_r_16u: + return "5123"; + case cgltf_component_type_r_32u: + return "5125"; + case cgltf_component_type_r_32f: + return "5126"; + default: + return "0"; + } +} + +static const char* shapeType(cgltf_type type) +{ + switch (type) + { + case cgltf_type_scalar: + return "SCALAR"; + case cgltf_type_vec2: + return "VEC2"; + case cgltf_type_vec3: + return "VEC3"; + case cgltf_type_vec4: + return "VEC4"; + case cgltf_type_mat2: + return "MAT2"; + case cgltf_type_mat3: + return "MAT3"; + case cgltf_type_mat4: + return "MAT4"; + default: + return ""; + } +} + +const char* attributeType(cgltf_attribute_type type) +{ + switch (type) + { + case cgltf_attribute_type_position: + return "POSITION"; + case cgltf_attribute_type_normal: + return "NORMAL"; + case cgltf_attribute_type_tangent: + return "TANGENT"; + case cgltf_attribute_type_texcoord: + return "TEXCOORD"; + case cgltf_attribute_type_color: + return "COLOR"; + case cgltf_attribute_type_joints: + return "JOINTS"; + case cgltf_attribute_type_weights: + return "WEIGHTS"; + case cgltf_attribute_type_custom: + return "CUSTOM"; + default: + return "ATTRIBUTE"; + } +} + +const char* animationPath(cgltf_animation_path_type type) +{ + switch (type) + { + case cgltf_animation_path_type_translation: + return "translation"; + case cgltf_animation_path_type_rotation: + return "rotation"; + case cgltf_animation_path_type_scale: + return "scale"; + case cgltf_animation_path_type_weights: + return "weights"; + default: + return ""; + } +} + +static const char* lightType(cgltf_light_type type) +{ + switch (type) + { + case cgltf_light_type_directional: + return "directional"; + case cgltf_light_type_point: + return "point"; + case cgltf_light_type_spot: + return "spot"; + default: + return ""; + } +} + +static const char* alphaMode(cgltf_alpha_mode mode) +{ + switch (mode) + { + case cgltf_alpha_mode_opaque: + return "OPAQUE"; + case cgltf_alpha_mode_mask: + return "MASK"; + case cgltf_alpha_mode_blend: + return "BLEND"; + default: + return ""; + } +} + +static const char* interpolationType(cgltf_interpolation_type type) +{ + switch (type) + { + case cgltf_interpolation_type_linear: + return "LINEAR"; + case cgltf_interpolation_type_step: + return "STEP"; + case cgltf_interpolation_type_cubic_spline: + return "CUBICSPLINE"; + default: + return ""; + } +} + +static const char* compressionMode(BufferView::Compression mode) +{ + switch (mode) + { + case BufferView::Compression_Attribute: + return "ATTRIBUTES"; + + case BufferView::Compression_Index: + return "TRIANGLES"; + + case BufferView::Compression_IndexSequence: + return "INDICES"; + + default: + return ""; + } +} + +static const char* compressionFilter(StreamFormat::Filter filter) +{ + switch (filter) + { + case StreamFormat::Filter_None: + return "NONE"; + + case StreamFormat::Filter_Oct: + return "OCTAHEDRAL"; + + case StreamFormat::Filter_Quat: + return "QUATERNION"; + + case StreamFormat::Filter_Exp: + return "EXPONENTIAL"; + + case StreamFormat::Filter_Color: + return "COLOR"; + + default: + return ""; + } +} + +static void writeTextureInfo(std::string& json, const cgltf_data* data, const cgltf_texture_view& view, const QuantizationTexture* qt, std::vector& textures, const char* scale = NULL) +{ + assert(view.texture); + + bool needs_transform = false; + cgltf_texture_transform transform = {}; + transform.scale[0] = transform.scale[1] = 1.f; + + if (view.has_transform && isValidTransform(view.transform)) + { + transform = view.transform; + needs_transform = true; + } + + if (qt) + { + float rcos = cosf(transform.rotation), rsin = sinf(transform.rotation); + float ox = transform.scale[0] * qt->offset[0], oy = transform.scale[1] * qt->offset[1]; + + transform.offset[0] += rcos * ox + rsin * oy; + transform.offset[1] += -rsin * ox + rcos * oy; + transform.scale[0] *= qt->scale[0] / float((1 << qt->bits) - 1) * (qt->normalized ? 65535.f : 1.f); + transform.scale[1] *= qt->scale[1] / float((1 << qt->bits) - 1) * (qt->normalized ? 65535.f : 1.f); + needs_transform = true; + } + + append(json, "{\"index\":"); + append(json, size_t(textures[view.texture - data->textures].remap)); + if (view.texcoord != 0) + { + append(json, ",\"texCoord\":"); + append(json, size_t(view.texcoord)); + } + if (scale && view.scale != 1) + { + append(json, ",\""); + append(json, scale); + append(json, "\":"); + append(json, view.scale); + } + if (needs_transform) + { + append(json, ",\"extensions\":{\"KHR_texture_transform\":{"); + append(json, "\"offset\":"); + append(json, transform.offset, 2); + append(json, ",\"scale\":"); + append(json, transform.scale, 2); + if (transform.rotation != 0.f) + { + append(json, ",\"rotation\":"); + append(json, transform.rotation); + } + append(json, "}}"); + } + append(json, "}"); +} + +static const float white[4] = {1, 1, 1, 1}; +static const float black[4] = {0, 0, 0, 0}; + +static void writeMaterialComponent(std::string& json, const cgltf_data* data, const cgltf_pbr_metallic_roughness& pbr, const QuantizationTexture* qt, std::vector& textures) +{ + comma(json); + append(json, "\"pbrMetallicRoughness\":{"); + if (memcmp(pbr.base_color_factor, white, 16) != 0) + { + comma(json); + append(json, "\"baseColorFactor\":"); + append(json, pbr.base_color_factor, 4); + } + if (pbr.base_color_texture.texture) + { + comma(json); + append(json, "\"baseColorTexture\":"); + writeTextureInfo(json, data, pbr.base_color_texture, qt, textures); + } + if (pbr.metallic_factor != 1) + { + comma(json); + append(json, "\"metallicFactor\":"); + append(json, pbr.metallic_factor); + } + if (pbr.roughness_factor != 1) + { + comma(json); + append(json, "\"roughnessFactor\":"); + append(json, pbr.roughness_factor); + } + if (pbr.metallic_roughness_texture.texture) + { + comma(json); + append(json, "\"metallicRoughnessTexture\":"); + writeTextureInfo(json, data, pbr.metallic_roughness_texture, qt, textures); + } + append(json, "}"); +} + +static void writeMaterialComponent(std::string& json, const cgltf_data* data, const cgltf_pbr_specular_glossiness& pbr, const QuantizationTexture* qt, std::vector& textures) +{ + comma(json); + append(json, "\"KHR_materials_pbrSpecularGlossiness\":{"); + if (pbr.diffuse_texture.texture) + { + comma(json); + append(json, "\"diffuseTexture\":"); + writeTextureInfo(json, data, pbr.diffuse_texture, qt, textures); + } + if (pbr.specular_glossiness_texture.texture) + { + comma(json); + append(json, "\"specularGlossinessTexture\":"); + writeTextureInfo(json, data, pbr.specular_glossiness_texture, qt, textures); + } + if (memcmp(pbr.diffuse_factor, white, 16) != 0) + { + comma(json); + append(json, "\"diffuseFactor\":"); + append(json, pbr.diffuse_factor, 4); + } + if (memcmp(pbr.specular_factor, white, 12) != 0) + { + comma(json); + append(json, "\"specularFactor\":"); + append(json, pbr.specular_factor, 3); + } + if (pbr.glossiness_factor != 1) + { + comma(json); + append(json, "\"glossinessFactor\":"); + append(json, pbr.glossiness_factor); + } + append(json, "}"); +} + +static void writeMaterialComponent(std::string& json, const cgltf_data* data, const cgltf_clearcoat& cc, const QuantizationTexture* qt, std::vector& textures) +{ + comma(json); + append(json, "\"KHR_materials_clearcoat\":{"); + if (cc.clearcoat_texture.texture) + { + comma(json); + append(json, "\"clearcoatTexture\":"); + writeTextureInfo(json, data, cc.clearcoat_texture, qt, textures); + } + if (cc.clearcoat_roughness_texture.texture) + { + comma(json); + append(json, "\"clearcoatRoughnessTexture\":"); + writeTextureInfo(json, data, cc.clearcoat_roughness_texture, qt, textures); + } + if (cc.clearcoat_normal_texture.texture) + { + comma(json); + append(json, "\"clearcoatNormalTexture\":"); + writeTextureInfo(json, data, cc.clearcoat_normal_texture, qt, textures, "scale"); + } + if (cc.clearcoat_factor != 0) + { + comma(json); + append(json, "\"clearcoatFactor\":"); + append(json, cc.clearcoat_factor); + } + if (cc.clearcoat_roughness_factor != 0) + { + comma(json); + append(json, "\"clearcoatRoughnessFactor\":"); + append(json, cc.clearcoat_roughness_factor); + } + append(json, "}"); +} + +static void writeMaterialComponent(std::string& json, const cgltf_data* data, const cgltf_transmission& tm, const QuantizationTexture* qt, std::vector& textures) +{ + comma(json); + append(json, "\"KHR_materials_transmission\":{"); + if (tm.transmission_texture.texture) + { + comma(json); + append(json, "\"transmissionTexture\":"); + writeTextureInfo(json, data, tm.transmission_texture, qt, textures); + } + if (tm.transmission_factor != 0) + { + comma(json); + append(json, "\"transmissionFactor\":"); + append(json, tm.transmission_factor); + } + append(json, "}"); +} + +static void writeMaterialComponent(std::string& json, const cgltf_data* data, const cgltf_ior& tm) +{ + (void)data; + + comma(json); + append(json, "\"KHR_materials_ior\":{"); + append(json, "\"ior\":"); + append(json, tm.ior); + append(json, "}"); +} + +static void writeMaterialComponent(std::string& json, const cgltf_data* data, const cgltf_specular& tm, const QuantizationTexture* qt, std::vector& textures) +{ + comma(json); + append(json, "\"KHR_materials_specular\":{"); + if (tm.specular_texture.texture) + { + comma(json); + append(json, "\"specularTexture\":"); + writeTextureInfo(json, data, tm.specular_texture, qt, textures); + } + if (tm.specular_color_texture.texture) + { + comma(json); + append(json, "\"specularColorTexture\":"); + writeTextureInfo(json, data, tm.specular_color_texture, qt, textures); + } + if (tm.specular_factor != 1) + { + comma(json); + append(json, "\"specularFactor\":"); + append(json, tm.specular_factor); + } + if (memcmp(tm.specular_color_factor, white, 12) != 0) + { + comma(json); + append(json, "\"specularColorFactor\":"); + append(json, tm.specular_color_factor, 3); + } + append(json, "}"); +} + +static void writeMaterialComponent(std::string& json, const cgltf_data* data, const cgltf_sheen& tm, const QuantizationTexture* qt, std::vector& textures) +{ + comma(json); + append(json, "\"KHR_materials_sheen\":{"); + if (tm.sheen_color_texture.texture) + { + comma(json); + append(json, "\"sheenColorTexture\":"); + writeTextureInfo(json, data, tm.sheen_color_texture, qt, textures); + } + if (tm.sheen_roughness_texture.texture) + { + comma(json); + append(json, "\"sheenRoughnessTexture\":"); + writeTextureInfo(json, data, tm.sheen_roughness_texture, qt, textures); + } + if (memcmp(tm.sheen_color_factor, black, 12) != 0) + { + comma(json); + append(json, "\"sheenColorFactor\":"); + append(json, tm.sheen_color_factor, 3); + } + if (tm.sheen_roughness_factor != 0) + { + comma(json); + append(json, "\"sheenRoughnessFactor\":"); + append(json, tm.sheen_roughness_factor); + } + append(json, "}"); +} + +static void writeMaterialComponent(std::string& json, const cgltf_data* data, const cgltf_volume& tm, const QuantizationPosition* qp, const QuantizationTexture* qt, std::vector& textures) +{ + comma(json); + append(json, "\"KHR_materials_volume\":{"); + if (tm.thickness_texture.texture) + { + comma(json); + append(json, "\"thicknessTexture\":"); + writeTextureInfo(json, data, tm.thickness_texture, qt, textures); + } + if (tm.thickness_factor != 0) + { + // thickness is in mesh coordinate space which is rescaled by quantization + comma(json); + append(json, "\"thicknessFactor\":"); + append(json, tm.thickness_factor / (qp ? qp->node_scale : 1.f)); + } + if (memcmp(tm.attenuation_color, white, 12) != 0) + { + comma(json); + append(json, "\"attenuationColor\":"); + append(json, tm.attenuation_color, 3); + } + if (tm.attenuation_distance != FLT_MAX) + { + comma(json); + append(json, "\"attenuationDistance\":"); + append(json, tm.attenuation_distance); + } + append(json, "}"); +} + +static void writeMaterialComponent(std::string& json, const cgltf_data* data, const cgltf_emissive_strength& tm) +{ + (void)data; + + comma(json); + append(json, "\"KHR_materials_emissive_strength\":{"); + if (tm.emissive_strength != 1) + { + comma(json); + append(json, "\"emissiveStrength\":"); + append(json, tm.emissive_strength); + } + append(json, "}"); +} + +static void writeMaterialComponent(std::string& json, const cgltf_data* data, const cgltf_iridescence& tm, const QuantizationTexture* qt, std::vector& textures) +{ + comma(json); + append(json, "\"KHR_materials_iridescence\":{"); + if (tm.iridescence_factor != 0) + { + comma(json); + append(json, "\"iridescenceFactor\":"); + append(json, tm.iridescence_factor); + } + if (tm.iridescence_texture.texture) + { + comma(json); + append(json, "\"iridescenceTexture\":"); + writeTextureInfo(json, data, tm.iridescence_texture, qt, textures); + } + if (tm.iridescence_ior != 1.3f) + { + comma(json); + append(json, "\"iridescenceIor\":"); + append(json, tm.iridescence_ior); + } + if (tm.iridescence_thickness_min != 100.f) + { + comma(json); + append(json, "\"iridescenceThicknessMinimum\":"); + append(json, tm.iridescence_thickness_min); + } + if (tm.iridescence_thickness_max != 400.f) + { + comma(json); + append(json, "\"iridescenceThicknessMaximum\":"); + append(json, tm.iridescence_thickness_max); + } + if (tm.iridescence_thickness_texture.texture) + { + comma(json); + append(json, "\"iridescenceThicknessTexture\":"); + writeTextureInfo(json, data, tm.iridescence_thickness_texture, qt, textures); + } + append(json, "}"); +} + +static void writeMaterialComponent(std::string& json, const cgltf_data* data, const cgltf_anisotropy& tm, const QuantizationTexture* qt, std::vector& textures) +{ + comma(json); + append(json, "\"KHR_materials_anisotropy\":{"); + if (tm.anisotropy_strength != 0) + { + comma(json); + append(json, "\"anisotropyStrength\":"); + append(json, tm.anisotropy_strength); + } + if (tm.anisotropy_rotation != 0) + { + comma(json); + append(json, "\"anisotropyRotation\":"); + append(json, tm.anisotropy_rotation); + } + if (tm.anisotropy_texture.texture) + { + comma(json); + append(json, "\"anisotropyTexture\":"); + writeTextureInfo(json, data, tm.anisotropy_texture, qt, textures); + } + append(json, "}"); +} + +static void writeMaterialComponent(std::string& json, const cgltf_data* data, const cgltf_dispersion& tm) +{ + (void)data; + + comma(json); + append(json, "\"KHR_materials_dispersion\":{"); + append(json, "\"dispersion\":"); + append(json, tm.dispersion); + append(json, "}"); +} + +static void writeMaterialComponent(std::string& json, const cgltf_data* data, const cgltf_diffuse_transmission& tm, const QuantizationTexture* qt, std::vector& textures) +{ + comma(json); + append(json, "\"KHR_materials_diffuse_transmission\":{"); + if (tm.diffuse_transmission_factor != 0) + { + comma(json); + append(json, "\"diffuseTransmissionFactor\":"); + append(json, tm.diffuse_transmission_factor); + } + if (tm.diffuse_transmission_texture.texture) + { + comma(json); + append(json, "\"diffuseTransmissionTexture\":"); + writeTextureInfo(json, data, tm.diffuse_transmission_texture, qt, textures); + } + if (memcmp(tm.diffuse_transmission_color_factor, white, sizeof(float) * 3) != 0) + { + comma(json); + append(json, "\"diffuseTransmissionColorFactor\":"); + append(json, tm.diffuse_transmission_color_factor, 3); + } + if (tm.diffuse_transmission_color_texture.texture) + { + comma(json); + append(json, "\"diffuseTransmissionColorTexture\":"); + writeTextureInfo(json, data, tm.diffuse_transmission_color_texture, qt, textures); + } + append(json, "}"); +} + +void writeMaterial(std::string& json, const cgltf_data* data, const cgltf_material& material, const QuantizationPosition* qp, const QuantizationTexture* qt, std::vector& textures) +{ + if (material.name && *material.name) + { + comma(json); + append(json, "\"name\":\""); + append(json, material.name); + append(json, "\""); + } + + if (material.has_pbr_metallic_roughness) + { + writeMaterialComponent(json, data, material.pbr_metallic_roughness, qt, textures); + } + + if (material.normal_texture.texture) + { + comma(json); + append(json, "\"normalTexture\":"); + writeTextureInfo(json, data, material.normal_texture, qt, textures, "scale"); + } + + if (material.occlusion_texture.texture) + { + comma(json); + append(json, "\"occlusionTexture\":"); + writeTextureInfo(json, data, material.occlusion_texture, qt, textures, "strength"); + } + + if (material.emissive_texture.texture) + { + comma(json); + append(json, "\"emissiveTexture\":"); + writeTextureInfo(json, data, material.emissive_texture, qt, textures); + } + + if (memcmp(material.emissive_factor, black, 12) != 0) + { + comma(json); + append(json, "\"emissiveFactor\":"); + append(json, material.emissive_factor, 3); + } + + if (material.alpha_mode != cgltf_alpha_mode_opaque) + { + comma(json); + append(json, "\"alphaMode\":\""); + append(json, alphaMode(material.alpha_mode)); + append(json, "\""); + } + + if (material.alpha_cutoff != 0.5f) + { + comma(json); + append(json, "\"alphaCutoff\":"); + append(json, material.alpha_cutoff); + } + + if (material.double_sided) + { + comma(json); + append(json, "\"doubleSided\":true"); + } + + if (material.has_pbr_specular_glossiness || material.has_clearcoat || material.has_transmission || material.has_ior || material.has_specular || + material.has_sheen || material.has_volume || material.has_emissive_strength || material.has_iridescence || material.has_anisotropy || + material.has_dispersion || material.has_diffuse_transmission || material.unlit) + { + comma(json); + append(json, "\"extensions\":{"); + + if (material.has_pbr_specular_glossiness) + writeMaterialComponent(json, data, material.pbr_specular_glossiness, qt, textures); + + if (material.has_clearcoat) + writeMaterialComponent(json, data, material.clearcoat, qt, textures); + + if (material.has_transmission) + writeMaterialComponent(json, data, material.transmission, qt, textures); + + if (material.has_ior) + writeMaterialComponent(json, data, material.ior); + + if (material.has_specular) + writeMaterialComponent(json, data, material.specular, qt, textures); + + if (material.has_sheen) + writeMaterialComponent(json, data, material.sheen, qt, textures); + + if (material.has_volume) + writeMaterialComponent(json, data, material.volume, qp, qt, textures); + + if (material.has_emissive_strength) + writeMaterialComponent(json, data, material.emissive_strength); + + if (material.has_iridescence) + writeMaterialComponent(json, data, material.iridescence, qt, textures); + + if (material.has_anisotropy) + writeMaterialComponent(json, data, material.anisotropy, qt, textures); + + if (material.has_dispersion) + writeMaterialComponent(json, data, material.dispersion); + + if (material.has_diffuse_transmission) + writeMaterialComponent(json, data, material.diffuse_transmission, qt, textures); + + if (material.unlit) + { + comma(json); + append(json, "\"KHR_materials_unlit\":{}"); + } + + append(json, "}"); + } +} + +size_t getBufferView(std::vector& views, BufferView::Kind kind, StreamFormat::Filter filter, BufferView::Compression compression, size_t stride, int variant) +{ + if (variant >= 0) + { + for (size_t i = 0; i < views.size(); ++i) + { + BufferView& v = views[i]; + + if (v.kind == kind && v.filter == filter && v.compression == compression && v.stride == stride && v.variant == variant) + return i; + } + } + + BufferView view = {kind, filter, compression, stride, variant}; + views.push_back(view); + + return views.size() - 1; +} + +void writeBufferView(std::string& json, BufferView::Kind kind, StreamFormat::Filter filter, size_t count, size_t stride, size_t bin_offset, size_t bin_size, BufferView::Compression compression, size_t compressed_offset, size_t compressed_size, const char* meshopt_ext) +{ + assert(bin_size == count * stride); + + // when compression is enabled, we store uncompressed data in buffer 1 and compressed data in buffer 0 + // when compression is disabled, we store uncompressed data in buffer 0 + size_t buffer = compression != BufferView::Compression_None ? 1 : 0; + + append(json, "{\"buffer\":"); + append(json, buffer); + append(json, ",\"byteOffset\":"); + append(json, bin_offset); + append(json, ",\"byteLength\":"); + append(json, bin_size); + if (kind == BufferView::Kind_Vertex) + { + append(json, ",\"byteStride\":"); + append(json, stride); + } + if (kind == BufferView::Kind_Vertex || kind == BufferView::Kind_Index) + { + append(json, ",\"target\":"); + append(json, (kind == BufferView::Kind_Vertex) ? "34962" : "34963"); + } + if (compression != BufferView::Compression_None) + { + append(json, ",\"extensions\":{"); + append(json, "\""); + append(json, meshopt_ext); + append(json, "\":{"); + append(json, "\"buffer\":0"); + append(json, ",\"byteOffset\":"); + append(json, size_t(compressed_offset)); + append(json, ",\"byteLength\":"); + append(json, size_t(compressed_size)); + append(json, ",\"byteStride\":"); + append(json, stride); + append(json, ",\"mode\":\""); + append(json, compressionMode(compression)); + append(json, "\""); + if (filter != StreamFormat::Filter_None) + { + append(json, ",\"filter\":\""); + append(json, compressionFilter(filter)); + append(json, "\""); + } + append(json, ",\"count\":"); + append(json, count); + append(json, "}}"); + } + append(json, "}"); +} + +static void writeAccessor(std::string& json, size_t view, size_t offset, cgltf_type type, cgltf_component_type component_type, bool normalized, size_t count, const float* min = NULL, const float* max = NULL, size_t numminmax = 0) +{ + append(json, "{\"bufferView\":"); + append(json, view); + append(json, ",\"byteOffset\":"); + append(json, offset); + append(json, ",\"componentType\":"); + append(json, componentType(component_type)); + append(json, ",\"count\":"); + append(json, count); + append(json, ",\"type\":\""); + append(json, shapeType(type)); + append(json, "\""); + + if (normalized) + { + append(json, ",\"normalized\":true"); + } + + if (min && max) + { + assert(numminmax); + + append(json, ",\"min\":"); + append(json, min, numminmax); + append(json, ",\"max\":"); + append(json, max, numminmax); + } + + append(json, "}"); +} + +static void writeEmbeddedImage(std::string& json, std::vector& views, const char* data, size_t size, const char* mime_type, TextureKind kind) +{ + size_t view = getBufferView(views, BufferView::Kind_Image, StreamFormat::Filter_None, BufferView::Compression_None, 1, -1 - kind); + + assert(views[view].data.empty()); + views[view].data.assign(data, size); + + append(json, "\"bufferView\":"); + append(json, view); + append(json, ",\"mimeType\":\""); + append(json, mime_type); + append(json, "\""); +} + +static std::string decodeUri(const char* uri) +{ + std::string result = uri; + + if (!result.empty()) + { + cgltf_decode_uri(&result[0]); + result.resize(strlen(result.c_str())); + } + + return result; +} + +void writeSampler(std::string& json, const cgltf_sampler& sampler) +{ + if (sampler.mag_filter != cgltf_filter_type_undefined) + { + comma(json); + append(json, "\"magFilter\":"); + append(json, size_t(sampler.mag_filter)); + } + if (sampler.min_filter != cgltf_filter_type_undefined) + { + comma(json); + append(json, "\"minFilter\":"); + append(json, size_t(sampler.min_filter)); + } + if (sampler.wrap_s != cgltf_wrap_mode_repeat) + { + comma(json); + append(json, "\"wrapS\":"); + append(json, size_t(sampler.wrap_s)); + } + if (sampler.wrap_t != cgltf_wrap_mode_repeat) + { + comma(json); + append(json, "\"wrapT\":"); + append(json, size_t(sampler.wrap_t)); + } +} + +static void writeImageError(std::string& json, const char* action, size_t index, const char* uri, const char* reason) +{ + append(json, "\"uri\":\""); + append(json, "data:image/png;base64,ERR/"); + append(json, "\""); + + fprintf(stderr, "Warning: unable to %s image %d (%s), skipping%s%s%s\n", action, int(index), uri ? uri : "embedded", reason ? " (" : "", reason ? reason : "", reason ? ")" : ""); +} + +static void writeImageData(std::string& json, std::vector& views, size_t index, const char* uri, const char* mime_type, const std::string& contents, const char* output_path, TextureKind kind, bool embed) +{ + bool dataUri = uri && strncmp(uri, "data:", 5) == 0; + + if (!embed && uri && !dataUri && output_path) + { + std::string file_name = getFileName(uri) + mimeExtension(mime_type); + std::string file_path = getFullPath(decodeUri(file_name.c_str()).c_str(), output_path); + + if (writeFile(file_path.c_str(), contents)) + { + append(json, "\"uri\":\""); + append(json, file_name); + append(json, "\""); + } + else + { + writeImageError(json, "save", int(index), uri, file_path.c_str()); + } + } + else + { + writeEmbeddedImage(json, views, contents.c_str(), contents.size(), mime_type, kind); + } +} + +void writeImage(std::string& json, std::vector& views, const cgltf_image& image, const ImageInfo& info, const std::string* encoded, size_t index, const char* input_path, const char* output_path, const Settings& settings) +{ + if (image.name && *image.name) + { + append(json, "\"name\":\""); + append(json, image.name); + append(json, "\","); + } + + if (encoded) + { + const char* mime_type = (settings.texture_mode[info.kind] == TextureMode_WebP) ? "image/webp" : "image/ktx2"; + + // image was pre-encoded via encodeImages (which might have failed!) + if (encoded->compare(0, 5, "error") == 0) + writeImageError(json, "encode", int(index), image.uri, encoded->c_str()); + else + writeImageData(json, views, index, image.uri, mime_type, *encoded, output_path, info.kind, settings.texture_embed); + return; + } + + bool dataUri = image.uri && strncmp(image.uri, "data:", 5) == 0; + + if (image.uri && !dataUri && settings.texture_ref) + { + // fast-path: we don't need to read the image to memory + append(json, "\"uri\":\""); + append(json, image.uri); + append(json, "\""); + return; + } + + std::string img_data; + std::string mime_type; + if (!readImage(image, input_path, img_data, mime_type)) + { + writeImageError(json, "read", index, image.uri, NULL); + return; + } + + writeImageData(json, views, index, image.uri, mime_type.c_str(), img_data, output_path, info.kind, settings.texture_embed); +} + +void writeTexture(std::string& json, const cgltf_texture& texture, const ImageInfo* info, cgltf_data* data, const Settings& settings) +{ + if (texture.sampler) + { + append(json, "\"sampler\":"); + append(json, size_t(texture.sampler - data->samplers)); + } + + if (texture.image) + { + if (info && settings.texture_mode[info->kind] != TextureMode_Raw) + { + const char* texture_ext = (settings.texture_mode[info->kind] == TextureMode_WebP) ? "EXT_texture_webp" : "KHR_texture_basisu"; + + comma(json); + append(json, "\"extensions\":{\""); + append(json, texture_ext); + append(json, "\":{\"source\":"); + append(json, size_t(texture.image - data->images)); + append(json, "}}"); + + return; // skip input basisu image if present, as we replace it with the one we encoded + } + else + { + comma(json); + append(json, "\"source\":"); + append(json, size_t(texture.image - data->images)); + } + } + + if (texture.basisu_image) + { + comma(json); + append(json, "\"extensions\":{\"KHR_texture_basisu\":{\"source\":"); + append(json, size_t(texture.basisu_image - data->images)); + append(json, "}}"); + } + else if (texture.webp_image) + { + comma(json); + append(json, "\"extensions\":{\"EXT_texture_webp\":{\"source\":"); + append(json, size_t(texture.webp_image - data->images)); + append(json, "}}"); + } +} + +static void writePrimitiveAccessor(std::string& json_accessors, const Stream& stream, size_t view, size_t offset, const StreamFormat& format, const QuantizationPosition& qp, const Settings& settings) +{ + comma(json_accessors); + if (stream.type == cgltf_attribute_type_position) + { + float min[3] = {}; + float max[3] = {}; + getPositionBounds(min, max, stream, qp, settings); + + writeAccessor(json_accessors, view, offset, format.type, format.component_type, format.normalized, stream.data.size(), min, max, 3); + } + else + { + writeAccessor(json_accessors, view, offset, format.type, format.component_type, format.normalized, stream.data.size()); + } +} + +static void writePrimitiveAttribute(std::string& json, const Stream& stream, size_t accessor) +{ + comma(json); + append(json, "\""); + if (stream.custom_name) + { + append(json, stream.custom_name); + } + else + { + append(json, attributeType(stream.type)); + if (stream.type != cgltf_attribute_type_position && stream.type != cgltf_attribute_type_normal && stream.type != cgltf_attribute_type_tangent) + { + append(json, "_"); + append(json, size_t(stream.index)); + } + } + append(json, "\":"); + append(json, accessor); +} + +static void writeMeshAttributesInterleaved(std::string& json, std::vector& views, std::string& json_accessors, size_t& accr_offset, const Mesh& mesh, int target, const QuantizationPosition& qp, const QuantizationTexture& qt, const Settings& settings) +{ + struct Attribute + { + const Stream* stream; + StreamFormat format; + std::string data; + }; + + std::vector attributes; + size_t stride = 0; + + for (size_t j = 0; j < mesh.streams.size(); ++j) + { + const Stream& stream = mesh.streams[j]; + + if (stream.target != target) + continue; + + Attribute attr = {&stream}; + attr.format = writeVertexStream(attr.data, stream, qp, qt, settings, /* filters= */ false); + assert(attr.format.filter == StreamFormat::Filter_None); + + stride += attr.format.stride; + attributes.emplace_back(std::move(attr)); + } + + BufferView::Compression compression = settings.compress ? BufferView::Compression_Attribute : BufferView::Compression_None; + size_t view = getBufferView(views, BufferView::Kind_Vertex, StreamFormat::Filter_None, compression, stride, 0); + size_t offset = views[view].data.size(); + + size_t vertex_count = mesh.streams[0].data.size(); + + views[view].data.resize(views[view].data.size() + stride * vertex_count); + + size_t write_offset = offset; + + for (size_t i = 0; i < vertex_count; ++i) + for (Attribute& attr : attributes) + { + memcpy(&views[view].data[write_offset], &attr.data[i * attr.format.stride], attr.format.stride); + write_offset += attr.format.stride; + } + + for (Attribute& attr : attributes) + { + const Stream& stream = *attr.stream; + const StreamFormat& format = attr.format; + + writePrimitiveAccessor(json_accessors, stream, view, offset, format, qp, settings); + + size_t vertex_accr = accr_offset++; + writePrimitiveAttribute(json, stream, vertex_accr); + + offset += attr.format.stride; + } +} + +void writeMeshAttributes(std::string& json, std::vector& views, std::string& json_accessors, size_t& accr_offset, const Mesh& mesh, int target, const QuantizationPosition& qp, const QuantizationTexture& qt, const Settings& settings) +{ + if (settings.mesh_interleaved) + return writeMeshAttributesInterleaved(json, views, json_accessors, accr_offset, mesh, target, qp, qt, settings); + + std::string scratch; + + for (size_t j = 0; j < mesh.streams.size(); ++j) + { + const Stream& stream = mesh.streams[j]; + + if (stream.target != target) + continue; + + scratch.clear(); + StreamFormat format = writeVertexStream(scratch, stream, qp, qt, settings); + BufferView::Compression compression = settings.compress ? BufferView::Compression_Attribute : BufferView::Compression_None; + + size_t view = getBufferView(views, BufferView::Kind_Vertex, format.filter, compression, format.stride, stream.type); + size_t offset = views[view].data.size(); + views[view].data += scratch; + + writePrimitiveAccessor(json_accessors, stream, view, offset, format, qp, settings); + + size_t vertex_accr = accr_offset++; + writePrimitiveAttribute(json, stream, vertex_accr); + } +} + +size_t writeMeshIndices(std::vector& views, std::string& json_accessors, size_t& accr_offset, const std::vector& indices, cgltf_primitive_type type, const Settings& settings) +{ + std::string scratch; + StreamFormat format = writeIndexStream(scratch, indices); + BufferView::Compression compression = settings.compress ? (type == cgltf_primitive_type_triangles ? BufferView::Compression_Index : BufferView::Compression_IndexSequence) : BufferView::Compression_None; + + size_t view = getBufferView(views, BufferView::Kind_Index, StreamFormat::Filter_None, compression, format.stride); + size_t offset = views[view].data.size(); + views[view].data += scratch; + + comma(json_accessors); + writeAccessor(json_accessors, view, offset, format.type, format.component_type, format.normalized, indices.size()); + + size_t index_accr = accr_offset++; + return index_accr; +} + +void writeMeshGeometry(std::string& json, std::vector& views, std::string& json_accessors, size_t& accr_offset, const Mesh& mesh, const QuantizationPosition& qp, const QuantizationTexture& qt, const Settings& settings) +{ + append(json, "{\"attributes\":{"); + writeMeshAttributes(json, views, json_accessors, accr_offset, mesh, 0, qp, qt, settings); + append(json, "}"); + if (mesh.type != cgltf_primitive_type_triangles) + { + append(json, ",\"mode\":"); + append(json, size_t(mesh.type - cgltf_primitive_type_points)); + } + if (mesh.targets) + { + append(json, ",\"targets\":["); + for (size_t j = 0; j < mesh.targets; ++j) + { + comma(json); + append(json, "{"); + writeMeshAttributes(json, views, json_accessors, accr_offset, mesh, int(1 + j), qp, qt, settings); + append(json, "}"); + } + append(json, "]"); + } + + if (!mesh.indices.empty()) + { + size_t index_accr = writeMeshIndices(views, json_accessors, accr_offset, mesh.indices, mesh.type, settings); + + append(json, ",\"indices\":"); + append(json, index_accr); + } +} + +static size_t writeAnimationTime(std::vector& views, std::string& json_accessors, size_t& accr_offset, const std::vector& time, const Settings& settings) +{ + std::string scratch; + StreamFormat format = writeTimeStream(scratch, time); + BufferView::Compression compression = settings.compress ? BufferView::Compression_Attribute : BufferView::Compression_None; + + size_t view = getBufferView(views, BufferView::Kind_Time, StreamFormat::Filter_None, compression, format.stride); + size_t offset = views[view].data.size(); + views[view].data += scratch; + + comma(json_accessors); + writeAccessor(json_accessors, view, offset, cgltf_type_scalar, format.component_type, format.normalized, time.size(), &time.front(), &time.back(), 1); + + size_t time_accr = accr_offset++; + return time_accr; +} + +static size_t writeAnimationTime(std::vector& views, std::string& json_accessors, size_t& accr_offset, float mint, int frames, float period, const Settings& settings) +{ + std::vector time(frames); + + for (int j = 0; j < frames; ++j) + time[j] = mint + float(j) * period; + + return writeAnimationTime(views, json_accessors, accr_offset, time, settings); +} + +size_t writeJointBindMatrices(std::vector& views, std::string& json_accessors, size_t& accr_offset, const cgltf_skin& skin, const QuantizationPosition& qp, const Settings& settings) +{ + std::string scratch; + + for (size_t j = 0; j < skin.joints_count; ++j) + { + float transform[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}; + + if (skin.inverse_bind_matrices) + { + cgltf_accessor_read_float(skin.inverse_bind_matrices, j, transform, 16); + } + + if (settings.quantize && !settings.pos_float) + { + // pos_offset has to be applied first, thus it results in an offset rotated by the bind matrix + transform[12] += qp.offset[0] * transform[0] + qp.offset[1] * transform[4] + qp.offset[2] * transform[8]; + transform[13] += qp.offset[0] * transform[1] + qp.offset[1] * transform[5] + qp.offset[2] * transform[9]; + transform[14] += qp.offset[0] * transform[2] + qp.offset[1] * transform[6] + qp.offset[2] * transform[10]; + + // node_scale will be applied before the rotation/scale from transform + for (int k = 0; k < 12; ++k) + transform[k] *= qp.node_scale; + } + + scratch.append(reinterpret_cast(transform), sizeof(transform)); + } + + BufferView::Compression compression = settings.compress ? BufferView::Compression_Attribute : BufferView::Compression_None; + + size_t view = getBufferView(views, BufferView::Kind_Skin, StreamFormat::Filter_None, compression, 64); + size_t offset = views[view].data.size(); + views[view].data += scratch; + + comma(json_accessors); + writeAccessor(json_accessors, view, offset, cgltf_type_mat4, cgltf_component_type_r_32f, false, skin.joints_count); + + size_t matrix_accr = accr_offset++; + return matrix_accr; +} + +static void writeInstanceData(std::vector& views, std::string& json_accessors, cgltf_animation_path_type type, const std::vector& data, const Settings& settings) +{ + BufferView::Compression compression = settings.compress ? BufferView::Compression_Attribute : BufferView::Compression_None; + + std::string scratch; + StreamFormat format = writeKeyframeStream(scratch, type, data, settings); + + size_t view = getBufferView(views, BufferView::Kind_Instance, format.filter, compression, format.stride, type); + size_t offset = views[view].data.size(); + views[view].data += scratch; + + comma(json_accessors); + writeAccessor(json_accessors, view, offset, format.type, format.component_type, format.normalized, data.size()); +} + +size_t writeInstances(std::vector& views, std::string& json_accessors, size_t& accr_offset, const std::vector& instances, const QuantizationPosition& qp, bool has_color, const Settings& settings) +{ + std::vector position, rotation, scale; + position.resize(instances.size()); + rotation.resize(instances.size()); + scale.resize(instances.size()); + + Stream color = {cgltf_attribute_type_color}; + if (has_color) + color.data.resize(instances.size()); + + for (size_t i = 0; i < instances.size(); ++i) + { + decomposeTransform(position[i].f, rotation[i].f, scale[i].f, instances[i].transform); + + if (settings.quantize && !settings.pos_float) + { + const float* transform = instances[i].transform; + + // pos_offset has to be applied first, thus it results in an offset rotated by the instance matrix + position[i].f[0] += qp.offset[0] * transform[0] + qp.offset[1] * transform[4] + qp.offset[2] * transform[8]; + position[i].f[1] += qp.offset[0] * transform[1] + qp.offset[1] * transform[5] + qp.offset[2] * transform[9]; + position[i].f[2] += qp.offset[0] * transform[2] + qp.offset[1] * transform[6] + qp.offset[2] * transform[10]; + + // node_scale will be applied before the rotation/scale from transform + scale[i].f[0] *= qp.node_scale; + scale[i].f[1] *= qp.node_scale; + scale[i].f[2] *= qp.node_scale; + } + + if (has_color) + memcpy(color.data[i].f, instances[i].color, sizeof(Attr)); + } + + writeInstanceData(views, json_accessors, cgltf_animation_path_type_translation, position, settings); + writeInstanceData(views, json_accessors, cgltf_animation_path_type_rotation, rotation, settings); + writeInstanceData(views, json_accessors, cgltf_animation_path_type_scale, scale, settings); + + size_t result = accr_offset; + accr_offset += 3; + + if (has_color) + { + BufferView::Compression compression = settings.compress ? BufferView::Compression_Attribute : BufferView::Compression_None; + + std::string scratch; + StreamFormat format = writeVertexStream(scratch, color, QuantizationPosition(), QuantizationTexture(), settings); + + size_t view = getBufferView(views, BufferView::Kind_Instance, format.filter, compression, format.stride, 0); + size_t offset = views[view].data.size(); + views[view].data += scratch; + + comma(json_accessors); + writeAccessor(json_accessors, view, offset, format.type, format.component_type, format.normalized, instances.size()); + accr_offset += 1; + } + + return result; +} + +void writeMeshNode(std::string& json, size_t mesh_offset, cgltf_node* node, cgltf_skin* skin, cgltf_data* data, const QuantizationPosition* qp) +{ + comma(json); + append(json, "{\"mesh\":"); + append(json, mesh_offset); + if (skin) + { + append(json, ",\"skin\":"); + append(json, size_t(skin - data->skins)); + } + if (qp) + { + append(json, ",\"translation\":"); + append(json, qp->offset, 3); + append(json, ",\"scale\":["); + append(json, qp->node_scale); + append(json, ","); + append(json, qp->node_scale); + append(json, ","); + append(json, qp->node_scale); + append(json, "]"); + } + if (node && node->weights_count) + { + append(json, ",\"weights\":"); + append(json, node->weights, node->weights_count); + } + append(json, "}"); +} + +void writeMeshNodeInstanced(std::string& json, size_t mesh_offset, size_t accr_offset, bool has_color) +{ + comma(json); + append(json, "{\"mesh\":"); + append(json, mesh_offset); + append(json, ",\"extensions\":{\"EXT_mesh_gpu_instancing\":{\"attributes\":{"); + + comma(json); + append(json, "\"TRANSLATION\":"); + append(json, accr_offset + 0); + + comma(json); + append(json, "\"ROTATION\":"); + append(json, accr_offset + 1); + + comma(json); + append(json, "\"SCALE\":"); + append(json, accr_offset + 2); + + if (has_color) + { + comma(json); + append(json, "\"_COLOR_0\":"); + append(json, accr_offset + 3); + } + + append(json, "}}}"); + append(json, "}"); +} + +void writeSkin(std::string& json, const cgltf_skin& skin, size_t matrix_accr, const std::vector& nodes, cgltf_data* data) +{ + comma(json); + append(json, "{"); + if (skin.name && *skin.name) + { + append(json, "\"name\":\""); + append(json, skin.name); + append(json, "\","); + } + append(json, "\"joints\":["); + for (size_t j = 0; j < skin.joints_count; ++j) + { + comma(json); + append(json, size_t(nodes[skin.joints[j] - data->nodes].remap)); + } + append(json, "]"); + append(json, ",\"inverseBindMatrices\":"); + append(json, matrix_accr); + if (skin.skeleton) + { + comma(json); + append(json, "\"skeleton\":"); + append(json, size_t(nodes[skin.skeleton - data->nodes].remap)); + } + append(json, "}"); +} + +void writeNode(std::string& json, const cgltf_node& node, const std::vector& nodes, cgltf_data* data) +{ + const NodeInfo& ni = nodes[&node - data->nodes]; + + if (node.name && *node.name) + { + comma(json); + append(json, "\"name\":\""); + append(json, node.name); + append(json, "\""); + } + if (node.has_translation) + { + comma(json); + append(json, "\"translation\":"); + append(json, node.translation, 3); + } + if (node.has_rotation) + { + comma(json); + append(json, "\"rotation\":"); + append(json, node.rotation, 4); + } + if (node.has_scale) + { + comma(json); + append(json, "\"scale\":"); + append(json, node.scale, 3); + } + if (node.has_matrix) + { + comma(json); + append(json, "\"matrix\":"); + append(json, node.matrix, 16); + } + + bool has_children = !ni.mesh_nodes.empty(); + for (size_t j = 0; j < node.children_count; ++j) + has_children |= nodes[node.children[j] - data->nodes].keep; + + if (has_children) + { + comma(json); + append(json, "\"children\":["); + for (size_t j = 0; j < node.children_count; ++j) + { + const NodeInfo& ci = nodes[node.children[j] - data->nodes]; + + if (ci.keep) + { + comma(json); + append(json, size_t(ci.remap)); + } + } + for (size_t j = 0; j < ni.mesh_nodes.size(); ++j) + { + comma(json); + append(json, ni.mesh_nodes[j]); + } + append(json, "]"); + } + if (ni.has_mesh) + { + comma(json); + append(json, "\"mesh\":"); + append(json, ni.mesh_index); + if (ni.mesh_skin) + { + append(json, ",\"skin\":"); + append(json, size_t(ni.mesh_skin - data->skins)); + } + if (node.weights_count) + { + append(json, ",\"weights\":"); + append(json, node.weights, node.weights_count); + } + } + if (node.camera) + { + comma(json); + append(json, "\"camera\":"); + append(json, size_t(node.camera - data->cameras)); + } + if (node.light) + { + comma(json); + append(json, "\"extensions\":{\"KHR_lights_punctual\":{\"light\":"); + append(json, size_t(node.light - data->lights)); + append(json, "}}"); + } +} + +void writeAnimation(std::string& json, std::vector& views, std::string& json_accessors, size_t& accr_offset, const Animation& animation, size_t i, cgltf_data* data, const std::vector& nodes, const Settings& settings) +{ + std::vector tracks; + + for (size_t j = 0; j < animation.tracks.size(); ++j) + { + const Track& track = animation.tracks[j]; + + const NodeInfo& ni = nodes[track.node - data->nodes]; + + if (!ni.keep) + continue; + + if (!settings.anim_const && (ni.animated_path_mask & (1 << track.path)) == 0) + continue; + + tracks.push_back(&track); + } + + if (tracks.empty()) + { + fprintf(stderr, "Warning: ignoring animation %d (%s) because it has no tracks with motion; use -ac to override\n", int(i), animation.name ? animation.name : ""); + return; + } + + bool needs_time = false; + bool needs_pose = false; + + for (size_t j = 0; j < tracks.size(); ++j) + { + const Track& track = *tracks[j]; + +#ifndef NDEBUG + size_t keyframe_size = (track.interpolation == cgltf_interpolation_type_cubic_spline) ? 3 : 1; + size_t time_size = track.constant ? 1 : (track.time.empty() ? animation.frames : track.time.size()); + + assert(track.data.size() == keyframe_size * track.components * time_size); +#endif + + needs_time = needs_time || (track.time.empty() && !track.constant); + needs_pose = needs_pose || track.constant; + } + + bool needs_range = needs_pose && !needs_time && animation.frames > 1; + + needs_pose = needs_pose && !(needs_range && tracks.size() == 1); + + assert(int(needs_time) + int(needs_pose) + int(needs_range) <= 2); + + float animation_period = 1.f / float(settings.anim_freq); + float animation_length = float(animation.frames - 1) * animation_period; + + size_t time_accr = needs_time ? writeAnimationTime(views, json_accessors, accr_offset, animation.start, animation.frames, animation_period, settings) : 0; + size_t pose_accr = needs_pose ? writeAnimationTime(views, json_accessors, accr_offset, animation.start, 1, 0.f, settings) : 0; + size_t range_accr = needs_range ? writeAnimationTime(views, json_accessors, accr_offset, animation.start, 2, animation_length, settings) : 0; + + std::string json_samplers; + std::string json_channels; + + size_t track_offset = 0; + + size_t last_track_time_accr = 0; + const Track* last_track_time = NULL; + + for (size_t j = 0; j < tracks.size(); ++j) + { + const Track& track = *tracks[j]; + + bool range = needs_range && j == 0; + int range_size = range ? 2 : 1; + + size_t track_time_accr = time_accr; + if (!track.time.empty()) + { + // reuse time accessors between consecutive tracks if possible + if (last_track_time && track.time == last_track_time->time) + track_time_accr = last_track_time_accr; + else + { + track_time_accr = writeAnimationTime(views, json_accessors, accr_offset, track.time, settings); + last_track_time_accr = track_time_accr; + last_track_time = &track; + } + } + + std::string scratch; + StreamFormat format = writeKeyframeStream(scratch, track.path, track.data, settings, track.interpolation == cgltf_interpolation_type_cubic_spline); + + if (range) + { + assert(range_size == 2); + scratch += scratch; + } + + BufferView::Compression compression = settings.compress && track.path != cgltf_animation_path_type_weights ? BufferView::Compression_Attribute : BufferView::Compression_None; + + size_t view = getBufferView(views, BufferView::Kind_Keyframe, format.filter, compression, format.stride, track.path); + size_t offset = views[view].data.size(); + views[view].data += scratch; + + comma(json_accessors); + writeAccessor(json_accessors, view, offset, format.type, format.component_type, format.normalized, track.data.size() * range_size); + + size_t data_accr = accr_offset++; + + comma(json_samplers); + append(json_samplers, "{\"input\":"); + append(json_samplers, range ? range_accr : (track.constant ? pose_accr : track_time_accr)); + append(json_samplers, ",\"output\":"); + append(json_samplers, data_accr); + if (track.interpolation != cgltf_interpolation_type_linear) + { + append(json_samplers, ",\"interpolation\":\""); + append(json_samplers, interpolationType(track.interpolation)); + append(json_samplers, "\""); + } + append(json_samplers, "}"); + + const NodeInfo& tni = nodes[track.node - data->nodes]; + size_t target_node = size_t(tni.remap); + + // when animating morph weights, quantization may move mesh assignments to a mesh node in which case we need to move the animation output + if (track.path == cgltf_animation_path_type_weights && tni.mesh_nodes.size() == 1) + target_node = tni.mesh_nodes[0]; + + comma(json_channels); + append(json_channels, "{\"sampler\":"); + append(json_channels, track_offset); + append(json_channels, ",\"target\":{\"node\":"); + append(json_channels, target_node); + append(json_channels, ",\"path\":\""); + append(json_channels, animationPath(track.path)); + append(json_channels, "\"}}"); + + track_offset++; + } + + comma(json); + append(json, "{"); + if (animation.name && *animation.name) + { + append(json, "\"name\":\""); + append(json, animation.name); + append(json, "\","); + } + append(json, "\"samplers\":["); + append(json, json_samplers); + append(json, "],\"channels\":["); + append(json, json_channels); + append(json, "]}"); +} + +void writeCamera(std::string& json, const cgltf_camera& camera) +{ + comma(json); + append(json, "{"); + + switch (camera.type) + { + case cgltf_camera_type_perspective: + append(json, "\"type\":\"perspective\",\"perspective\":{"); + append(json, "\"yfov\":"); + append(json, camera.data.perspective.yfov); + append(json, ",\"znear\":"); + append(json, camera.data.perspective.znear); + if (camera.data.perspective.aspect_ratio != 0.f) + { + append(json, ",\"aspectRatio\":"); + append(json, camera.data.perspective.aspect_ratio); + } + if (camera.data.perspective.zfar != 0.f) + { + append(json, ",\"zfar\":"); + append(json, camera.data.perspective.zfar); + } + append(json, "}"); + break; + + case cgltf_camera_type_orthographic: + append(json, "\"type\":\"orthographic\",\"orthographic\":{"); + append(json, "\"xmag\":"); + append(json, camera.data.orthographic.xmag); + append(json, ",\"ymag\":"); + append(json, camera.data.orthographic.ymag); + append(json, ",\"znear\":"); + append(json, camera.data.orthographic.znear); + append(json, ",\"zfar\":"); + append(json, camera.data.orthographic.zfar); + append(json, "}"); + break; + + default: + fprintf(stderr, "Warning: skipping camera of unknown type\n"); + } + + append(json, "}"); +} + +void writeLight(std::string& json, const cgltf_light& light) +{ + comma(json); + append(json, "{\"type\":\""); + append(json, lightType(light.type)); + append(json, "\""); + if (memcmp(light.color, white, 12) != 0) + { + comma(json); + append(json, "\"color\":"); + append(json, light.color, 3); + } + if (light.intensity != 1.f) + { + comma(json); + append(json, "\"intensity\":"); + append(json, light.intensity); + } + if (light.range != 0.f) + { + comma(json); + append(json, "\"range\":"); + append(json, light.range); + } + if (light.type == cgltf_light_type_spot) + { + comma(json); + append(json, "\"spot\":{"); + append(json, "\"innerConeAngle\":"); + append(json, light.spot_inner_cone_angle); + append(json, ",\"outerConeAngle\":"); + append(json, light.spot_outer_cone_angle == 0.f ? 0.78539816339f : light.spot_outer_cone_angle); + append(json, "}"); + } + append(json, "}"); +} + +void writeArray(std::string& json, const char* name, const std::string& contents) +{ + if (contents.empty()) + return; + + comma(json); + append(json, "\""); + append(json, name); + append(json, "\":["); + append(json, contents); + append(json, "]"); +} + +void writeExtensions(std::string& json, const ExtensionInfo* extensions, size_t count) +{ + bool used_extensions = false; + bool required_extensions = false; + + for (size_t i = 0; i < count; ++i) + { + used_extensions |= extensions[i].used; + required_extensions |= extensions[i].used && extensions[i].required; + } + + if (used_extensions) + { + comma(json); + append(json, "\"extensionsUsed\":["); + for (size_t i = 0; i < count; ++i) + if (extensions[i].used) + { + comma(json); + append(json, "\""); + append(json, extensions[i].name); + append(json, "\""); + } + append(json, "]"); + } + + if (required_extensions) + { + comma(json); + append(json, "\"extensionsRequired\":["); + for (size_t i = 0; i < count; ++i) + if (extensions[i].used && extensions[i].required) + { + comma(json); + append(json, "\""); + append(json, extensions[i].name); + append(json, "\""); + } + append(json, "]"); + } +} + +void writeExtras(std::string& json, const cgltf_extras& extras) +{ + if (!extras.data) + return; + + comma(json); + append(json, "\"extras\":"); + appendJson(json, extras.data); +} + +void writeScene(std::string& json, const cgltf_scene& scene, const std::string& roots, const Settings& settings) +{ + comma(json); + append(json, "{"); + if (scene.name && *scene.name) + { + append(json, "\"name\":\""); + append(json, scene.name); + append(json, "\""); + } + if (!roots.empty()) + { + comma(json); + append(json, "\"nodes\":["); + append(json, roots); + append(json, "]"); + } + if (settings.keep_extras) + writeExtras(json, scene.extras); + append(json, "}"); +} diff --git a/Lib/meshoptimizer/js/README.md b/Lib/meshoptimizer/js/README.md new file mode 100644 index 0000000..e0464b3 --- /dev/null +++ b/Lib/meshoptimizer/js/README.md @@ -0,0 +1,314 @@ +# meshoptimizer.js + +This folder contains JavaScript/WebAssembly modules that can be used to access parts of functionality of meshoptimizer library. While normally these would be used internally by glTF loaders, processors and other Web optimization tools, they can also be used directly if needed. The modules are available as an [NPM package](https://www.npmjs.com/package/meshoptimizer) but can also be redistributed individually on a file-by-file basis. + +When using the NPM package, package exports can be used to import individual components (e.g. `meshoptimizer/decoder`) as well as the entire package (`meshoptimizer`). + +## Structure + +Each component comes in a separate file, `meshopt_component.js`, which uses ES6 module exports and can be imported from another ES6 module. The export name is MeshoptComponent and is an object that has two fields: + +- `supported` is a boolean that can be checked to see if the component is supported by the current execution environment; it will generally be `false` when WebAssembly is not supported or enabled. To use these components on browsers without WebAssembly a polyfill library is recommended. +- `ready` is a Promise that is resolved when WebAssembly compilation and initialization finishes; any functions are unsafe to call before that happens. + +In addition to that, each component exposes a set of specific functions documented below. + +## Decoder + +`MeshoptDecoder` (`meshopt_decoder.mjs`) implements high performance decompression of attribute and index buffers encoded using meshopt compression. This can be used to decompress glTF buffers encoded with `KHR_meshopt_compression`/`EXT_meshopt_compression` extensions or for custom geometry compression pipelines. The module contains two implementations, scalar and SIMD, with the best performing implementation selected automatically. When SIMD is available, the decoders run at 1-3 GB/s on modern desktop computers. + +> Note: for maximum compatibility, MeshoptDecoder is also available as CommonJS module via `meshopt_decoder.cjs`; it can be used by a wide variety of JavaScript module loaders, including node.js require(), AMD, Common.JS, and can also be loaded into the web page directly via a `