feat: LOD 网格分级——meshopt 解析期简化 + 按距离/级联选级

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 抓的)。
This commit is contained in:
2026-09-24 20:53:19 +08:00
parent 34b593f734
commit 5bdfb53566
125 changed files with 67608 additions and 29 deletions
+6
View File
@@ -47,4 +47,10 @@ target_compile_definitions(Engine PUBLIC TRACY_ON_DEMAND)
find_package(Threads REQUIRED) find_package(Threads REQUIRED)
target_link_libraries(Engine PUBLIC Threads::Threads) 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) target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1)
+21 -5
View File
@@ -18,13 +18,18 @@ namespace Engine
class Buffer; class Buffer;
class Material; class Material;
// LOD 级数:LOD0 = 原始网格,越高越简(解析期 meshopt_simplify 生成,
// 共享顶点数据,只换索引区间)
inline constexpr u32 kMeshLodCount = 3;
class Mesh class Mesh
{ {
public: public:
// material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份) // material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份)
// lods[0] 必须为原始索引(与 vertices 对应),各级简化链由解析期生成
Mesh(Device& device, MemoryAllocator& allocator, Mesh(Device& device, MemoryAllocator& allocator,
const std::vector<Vertex>& vertices, const std::vector<Vertex>& vertices,
const std::vector<u32>& indices, const std::vector<std::vector<u32>>& lods,
Material* material); Material* material);
~Mesh(); ~Mesh();
@@ -32,11 +37,16 @@ namespace Engine
Mesh& operator=(const Mesh&) = delete; Mesh& operator=(const Mesh&) = delete;
// instanceCount/firstInstance:GPUDriven 按 mesh 分 batch 后, // instanceCount/firstInstance:GPUDriven 按 mesh 分 batch 后,
// 同 mesh 的 N 个实体一次 draw 完成,模型矩阵经实例索引取 // 同 mesh 的 N 个实体一次 draw 完成,模型矩阵经实例索引取。
void Draw(vk::CommandBuffer cmd, u32 instanceCount = 1, u32 firstInstance = 0) const; // lod:索引区间选择(越界取最高级)
void Draw(vk::CommandBuffer cmd, u32 instanceCount = 1,
u32 firstInstance = 0, u32 lod = 0) const;
Material* GetMaterial() const { return m_Material; } 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 { struct Bounds {
@@ -46,10 +56,16 @@ namespace Engine
const Bounds& GetBounds() const { return m_Bounds; } const Bounds& GetBounds() const { return m_Bounds; }
private: private:
// 同一 index buffer 内连续排布的各级索引区间
struct LodRange {
u32 firstIndex = 0;
u32 indexCount = 0;
};
Scope<Buffer> m_VertexBuffer; Scope<Buffer> m_VertexBuffer;
Scope<Buffer> m_IndexBuffer; Scope<Buffer> m_IndexBuffer;
std::array<LodRange, kMeshLodCount> m_Lods{};
Material* m_Material; Material* m_Material;
u32 m_IndexCount = 0;
Bounds m_Bounds; Bounds m_Bounds;
u64 m_VertexBytes = 0; u64 m_VertexBytes = 0;
+3 -1
View File
@@ -34,7 +34,9 @@ namespace Engine
// worker 线程产出(ParseFromDisk),主线程消费(FromParsed)。 // worker 线程产出(ParseFromDisk),主线程消费(FromParsed)。
struct ParsedMesh { struct ParsedMesh {
std::vector<Vertex> vertices; std::vector<Vertex> vertices;
std::vector<u32> indices; // LOD 链([0] = 原始索引,越往后越简):解析期 meshopt_simplify
// 生成(worker 线程),消费端共享顶点只换索引
std::vector<std::vector<u32>> lods;
std::array<std::string, Material::kSlotCount> texPaths; std::array<std::string, Material::kSlotCount> texPaths;
MaterialParams params; MaterialParams params;
}; };
@@ -73,7 +73,9 @@ namespace Engine {
// 时实例分散各处),扁平化成连续数组推迟到 appendView // 时实例分散各处),扁平化成连续数组推迟到 appendView
std::vector<std::vector<glm::mat4>> batchWorlds; std::vector<std::vector<glm::mat4>> batchWorlds;
std::vector<std::vector<glm::mat4>> batchPrevWorlds; std::vector<std::vector<glm::mat4>> batchPrevWorlds;
std::unordered_map<Mesh*, u32> lookup; // key = (Mesh*, LOD):同 mesh 不同 LOD 索引区间不同,
// 必须分开成 batch(低 8 位 LOD,高位指针,指针 8 字节对齐安全)
std::unordered_map<u64, u32> lookup;
void Clear() void Clear()
{ {
+3 -1
View File
@@ -19,11 +19,13 @@ namespace Engine {
class Mesh; class Mesh;
// GPUDriven:按 mesh 聚合的绘制批 // GPUDriven:按 (mesh, LOD) 聚合的绘制批——同 mesh 不同 LOD 的实例
// 索引区间不同,不能并成一次 draw
struct InstanceBatch { struct InstanceBatch {
Mesh* mesh; Mesh* mesh;
u32 first; // 在实例矩阵数组中的起始下标 u32 first; // 在实例矩阵数组中的起始下标
u32 count; // 实例数 u32 count; // 实例数
u32 lod = 0; // 绘制用索引区间(Mesh::Draw)
}; };
// 一个渲染视图的剔除结果(相机视图 / 光源视图各一份) // 一个渲染视图的剔除结果(相机视图 / 光源视图各一份)
+32 -10
View File
@@ -17,13 +17,13 @@ namespace Engine
{ {
Mesh::Mesh(Device& device, MemoryAllocator& allocator, Mesh::Mesh(Device& device, MemoryAllocator& allocator,
const std::vector<Vertex>& vertices, const std::vector<Vertex>& vertices,
const std::vector<u32>& indices, const std::vector<std::vector<u32>>& lods,
Material* material) Material* material)
: m_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; return;
} }
@@ -35,15 +35,31 @@ namespace Engine
vk::MemoryPropertyFlagBits::eHostCoherent); vk::MemoryPropertyFlagBits::eHostCoherent);
m_VertexBuffer->Upload(vertices.data(), vbSize); m_VertexBuffer->Upload(vertices.data(), vbSize);
u64 ibSize = sizeof(u32) * indices.size(); // 各级索引连续拼进同一 index buffer(共享顶点,只换索引区间)
std::vector<u32> allIndices;
for (const auto& idx : lods) {
allIndices.insert(allIndices.end(), idx.begin(), idx.end());
}
u64 ibSize = sizeof(u32) * allIndices.size();
m_IndexBuffer = CreateScope<Buffer>( m_IndexBuffer = CreateScope<Buffer>(
device, allocator, ibSize, device, allocator, ibSize,
vk::BufferUsageFlagBits::eIndexBuffer, vk::BufferUsageFlagBits::eIndexBuffer,
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent); vk::MemoryPropertyFlagBits::eHostCoherent);
m_IndexBuffer->Upload(indices.data(), ibSize); m_IndexBuffer->Upload(allIndices.data(), ibSize);
m_IndexCount = static_cast<u32>(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<u32>(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_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); STATS.AddMesh(m_VertexBytes, m_IndexBytes);
} }
@@ -73,10 +91,14 @@ namespace Engine
STATS.RemoveMesh(m_VertexBytes, m_IndexBytes); 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.bindVertexBuffers(0, m_VertexBuffer->Get(), {0});
cmd.bindIndexBuffer(m_IndexBuffer->Get(), 0, vk::IndexType::eUint32); 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 } // namespace Engine
+33 -4
View File
@@ -14,6 +14,8 @@
#include <assimp/scene.h> #include <assimp/scene.h>
#include <assimp/postprocess.h> #include <assimp/postprocess.h>
#include <meshoptimizer.h>
#include <filesystem> #include <filesystem>
#include <algorithm> #include <algorithm>
@@ -81,13 +83,14 @@ Model::Model(Device& device, MemoryAllocator& allocator,
{{ h, 0.0f, -h}, {0, 1, 0}, {0.0f, 0.0f}}, {{ h, 0.0f, -h}, {0, 1, 0}, {0.0f, 0.0f}},
}; };
const std::vector<u32> indices = {0, 1, 2, 0, 2, 3}; const std::vector<u32> indices = {0, 1, 2, 0, 2, 3};
const std::vector<std::vector<u32>> lodIndices = {indices};
// 材质/纹理生命周期随模型(缓存持有) // 材质/纹理生命周期随模型(缓存持有)
std::string paths[Material::kSlotCount] = {"", "", "", ""}; std::string paths[Material::kSlotCount] = {"", "", "", ""};
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}); // 全默认材质 Material* mat = GetOrCreateMaterial(paths, MaterialParams{}); // 全默认材质
m_Meshes.push_back(CreateScope<Mesh>( m_Meshes.push_back(CreateScope<Mesh>(
m_Device, m_Allocator, vertices, indices, mat)); m_Device, m_Allocator, vertices, lodIndices, mat));
} }
Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator, Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator,
@@ -158,7 +161,7 @@ Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
for (auto& pm : parsed.meshes) { for (auto& pm : parsed.meshes) {
Material* mat = model->GetOrCreateMaterial(pm.texPaths.data(), pm.params); Material* mat = model->GetOrCreateMaterial(pm.texPaths.data(), pm.params);
model->m_Meshes.push_back(CreateScope<Mesh>( model->m_Meshes.push_back(CreateScope<Mesh>(
device, allocator, std::move(pm.vertices), std::move(pm.indices), mat)); device, allocator, pm.vertices, pm.lods, mat));
} }
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)", ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
parsed.path, model->m_Meshes.size(), model->m_TextureCache.size()); 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) { for (auto& pm : parsed.meshes) {
Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params); Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params);
m_Meshes.push_back(CreateScope<Mesh>( m_Meshes.push_back(CreateScope<Mesh>(
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)", ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
path, m_Meshes.size(), m_TextureCache.size()); path, m_Meshes.size(), m_TextureCache.size());
@@ -256,7 +259,7 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
{ {
ParsedMesh out; ParsedMesh out;
std::vector<Vertex>& vertices = out.vertices; std::vector<Vertex>& vertices = out.vertices;
std::vector<u32>& indices = out.indices; std::vector<u32> indices;
vertices.reserve(mesh->mNumVertices); 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.texPaths[slot] = std::move(texPaths[slot]);
} }
out.params = params; 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<u32> 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; return out;
} }
+1 -1
View File
@@ -139,7 +139,7 @@ namespace Engine::Passes
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 1, m_Gpu.forwardLayout, 1,
batch.mesh->GetMaterial()->GetDescriptorSet(), {}); batch.mesh->GetMaterial()->GetDescriptorSet(), {});
batch.mesh->Draw(cmd, batch.count, batch.first); batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
} }
cmd.endRendering(); cmd.endRendering();
+1 -1
View File
@@ -101,7 +101,7 @@ namespace Engine::Passes
&m_Frame.ubo.lightSpace[c]); &m_Frame.ubo.lightSpace[c]);
for (const auto& batch : m_Frame.shadowCascades[c].batches) { 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(); cmd.endRendering();
+22 -5
View File
@@ -297,7 +297,19 @@ namespace Engine
glm::vec3(t.World() * glm::vec4(bounds.center, 1.0f)); glm::vec3(t.World() * glm::vec4(bounds.center, 1.0f));
const f32 worldRadius = bounds.radius * worldScale; 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)) { if (!frustum.IntersectsSphere(worldCenter, worldRadius)) {
return; return;
} }
@@ -309,10 +321,12 @@ namespace Engine
// 单实体时两序号恰好恒等,多实体共享 mesh 后分叉 // 单实体时两序号恰好恒等,多实体共享 mesh 后分叉
// → batches[worldsIdx] 越界)。batch.first(GPU // → batches[worldsIdx] 越界)。batch.first(GPU
// firstInstance 基址)才是 worlds 索引,语义别混 // firstInstance 基址)才是 worlds 索引,语义别混
const u64 key =
(reinterpret_cast<u64>(mesh.get()) << 8) | lod;
const auto [it, inserted] = s.lookup.emplace( const auto [it, inserted] = s.lookup.emplace(
mesh.get(), static_cast<u32>(s.result.batches.size())); key, static_cast<u32>(s.result.batches.size()));
if (inserted) { 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.batchWorlds.emplace_back();
s.batchPrevWorlds.emplace_back(); s.batchPrevWorlds.emplace_back();
} }
@@ -326,9 +340,12 @@ namespace Engine
++totalInstances; ++totalInstances;
}; };
tryAppend(m_MainScratch, mainFrustum); tryAppend(m_MainScratch, mainFrustum, mainLod);
for (u32 c = 0; c < kMaxCascades; ++c) { 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));
} }
} }
}); });
+15
View File
@@ -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=&_&]
+11
View File
@@ -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
+17
View File
@@ -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
@@ -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: ''
---
+5
View File
@@ -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.
@@ -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: ''
---
+11
View File
@@ -0,0 +1,11 @@
comment: false
coverage:
status:
project: off
patch: off
ignore:
- demo
- extern
- tools
+240
View File
@@ -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$<$<CONFIG:Debug>: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
+33
View File
@@ -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
+77
View File
@@ -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
+19
View File
@@ -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*
+16
View File
@@ -0,0 +1,16 @@
{
"useTabs": true,
"tabWidth": 4,
"semi": true,
"singleQuote": true,
"printWidth": 150,
"trailingComma": "es5",
"overrides": [
{
"files": "*.yml",
"options": {
"tabWidth": 2
}
},
]
}
+227
View File
@@ -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 "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>")
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 $<TARGET_PDB_FILE:${TARGET}> 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()
+63
View File
@@ -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.
+21
View File
@@ -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.
+271
View File
@@ -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 <build/decoder_base.wasm
python3 tools/wasmpack.py patch $@ simd <build/decoder_simd.wasm
js/meshopt_encoder.js: build/encoder.wasm tools/wasmpack.py
js/meshopt_simplifier.js: build/simplifier.wasm tools/wasmpack.py
js/meshopt_clusterizer.js: build/clusterizer.wasm tools/wasmpack.py
js/meshopt_tangents.js: build/tangents.wasm tools/wasmpack.py
js/meshopt_encoder.js js/meshopt_simplifier.js js/meshopt_clusterizer.js js/meshopt_tangents.js:
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 $@ wasm <$<
js/meshopt_decoder.cjs: js/meshopt_decoder.mjs
sed '/export {/d' <$< >$@
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
+967
View File
@@ -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<unsigned int> 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<unsigned int> 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<meshopt_Meshlet> meshlets(max_meshlets);
std::vector<unsigned int> meshlet_vertices(indices.size());
std::vector<unsigned char> 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<uint>`) 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<uint16_t2>(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<meshopt_Meshlet> meshlets(max_meshlets);
std::vector<unsigned int> meshlet_vertices(indices.size());
std::vector<unsigned char> 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<unsigned int> 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<unsigned int> 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<unsigned char> 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<unsigned char> 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<unsigned char> 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<unsigned int> 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<unsigned int> 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<unsigned int> 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<unsigned int> 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<unsigned int> remap(vertices.size());
meshopt_generatePositionRemap(&remap[0], &vertices[0].px, vertices.size(), sizeof(Vertex));
std::vector<unsigned char> 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<unsigned int> 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<unsigned int> 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<unsigned int> 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<unsigned int> 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<unsigned int> 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<unsigned int> provoke(indices.size());
std::vector<unsigned int> 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<unsigned char> levels(indices.size() / 3);
std::vector<unsigned int> sources(indices.size() / 3);
std::vector<int> 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<unsigned int> 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<vec4> 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<unsigned int> 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<vec3> 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
+2
View File
@@ -0,0 +1,2 @@
/* This file makes sure the library can be used by C89 code */
#include "../src/meshoptimizer.h"
+962
View File
@@ -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 <stddef.h>
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<<K) corresponds to attribute K
unsigned int attribute_protect_mask;
};
// To compute 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 simplicity, we ignore perspective distortion and use rotationally invariant projection size estimation
// - return: bounds.error / max(distance(bounds.center, camera_position) - bounds.radius, camera_znear) * (camera_proj * 0.5f)
struct clodBounds
{
// sphere bounds, in mesh coordinate space
float center[3];
float radius;
// combined simplification error, in mesh coordinate space
float error;
};
struct clodCluster
{
// index of more refined group (with more triangles) that produced this cluster during simplification, or -1 for original geometry
int refined;
// cluster bounds; should only be used for culling, as bounds.error is not monotonic across DAG
clodBounds bounds;
// cluster indices; refer to the original mesh vertex buffer
const unsigned int* indices;
size_t index_count;
// cluster vertex count; indices[] has vertex_count unique entries
size_t vertex_count;
};
struct clodGroup
{
// DAG level the group was generated at
int depth;
// simplified group bounds (reflects error for clusters with clodCluster::refined == group id; error is FLT_MAX for terminal groups)
// cluster should be rendered if:
// 1. clodGroup::simplified for the group it's in is over error threshold
// 2. cluster.refined is -1 *or* clodGroup::simplified for groups[cluster.refined].simplified is at or under error threshold
clodBounds simplified;
};
// gets called for each group in sequence
// returned value gets saved for clusters emitted from this group (clodCluster::refined)
typedef int (*clodOutput)(void* output_context, clodGroup group, const clodCluster* clusters, size_t cluster_count);
// when using BVH to determine cluster visibility, traverse the tree into nodes while bounds is over error threshold; for leaves,
// group clusters should be rendered if cluster.refined is -1 *or* clodGroup::simplified for groups[cluster.refined].simplified is at or under error threshold
struct clodNode
{
// BVH node bounds, reflecting worst case error for all groups in the subtree
clodBounds bounds;
// leaf node: index of the group this node represents (or -1 for internal nodes)
int group;
// internal node: children are at nodes[child_offset..child_offset+child_count)
unsigned int child_offset;
unsigned int child_count;
};
#ifdef __cplusplus
extern "C"
{
#endif
// default configuration optimized for rasterization / raytracing
clodConfig clodDefaultConfig(size_t max_triangles);
clodConfig clodDefaultConfigRT(size_t max_triangles);
// build cluster LOD hierarchy, calling output callbacks as new clusters and groups are generated
void clodBuild(clodConfig config, clodMesh mesh, void* output_context, clodOutput output_callback);
// extract meshlet-local indices from cluster indices produced by clodBuild
// fills triangles[] and vertices[] such that vertices[triangles[i]] == indices[i]
// returns number of unique vertices (which will be equal to clodCluster::vertex_count)
size_t clodLocalIndices(unsigned int* vertices, unsigned char* triangles, const unsigned int* indices, size_t index_count);
// upper bound on the number of nodes clodBuildHierarchy can produce
// level_count is the number of levels in the group DAG (max(clodGroup::depth) + 1)
size_t clodBuildHierarchyBound(size_t group_count, size_t node_width, size_t level_count);
// build a spatial hierarchy over the groups produced by clodBuild for accelerating cluster visibility
// level_count is the number of levels in the group DAG (max(clodGroup::depth) + 1)
// the result is a forest with one tree per DAG level:
// - nodes 0..level_count-1 are the roots of per-level trees, each covering groups with clodGroup::depth == level
// - remaining nodes are the tree nodes; each internal node has up to node_width children, and each leaf node refers to a single group
size_t clodBuildHierarchy(clodNode* nodes, const clodGroup* groups, size_t group_count, size_t node_width, size_t level_count);
#ifdef __cplusplus
} // extern "C"
template <typename Output>
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*>(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 <float.h>
#include <math.h>
#include <string.h>
#include <algorithm>
#include <vector>
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<const clodBounds*>(reinterpret_cast<const char*>(bounds) + j * stride)->error);
return {{merged.center[0], merged.center[1], merged.center[2]}, merged.radius, error};
}
static void clusterize(std::vector<Cluster>& clusters, std::vector<unsigned int>& 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<char> buffer_heap;
char* buffer = (buffer_size <= sizeof(buffer_stack)) ? buffer_stack : (buffer_heap.resize(buffer_size), buffer_heap.data());
meshopt_Meshlet* meshlets = reinterpret_cast<meshopt_Meshlet*>(buffer);
unsigned int* meshlet_vertices = reinterpret_cast<unsigned int*>(meshlets + max_meshlets);
unsigned char* meshlet_triangles = reinterpret_cast<unsigned char*>(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<unsigned int> partition(std::vector<Cluster>& clusters, const clodConfig& config, const clodMesh& mesh, const std::vector<unsigned int>& indices, const std::vector<unsigned int>& 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<unsigned int> cluster_counts(clusters.size());
std::vector<unsigned int> 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<unsigned int> 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<float> 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<unsigned int> 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<unsigned int> 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<unsigned int> write = partition_offsets;
std::vector<Cluster> 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<unsigned char>& locks, const Cluster* clusters, const unsigned int* group_offsets, size_t group_count, const std::vector<unsigned int>& cluster_indices, const std::vector<unsigned int>& 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<unsigned int>& lod, const clodMesh& mesh, const std::vector<unsigned int>& indices, const std::vector<unsigned char>& locks, size_t target_count, float* error)
{
std::vector<SloppyVertex> subset(indices.size());
std::vector<unsigned char> 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<unsigned int>& lod, const clodConfig& config, const clodMesh& mesh, const std::vector<unsigned int>& indices, const std::vector<unsigned char>& 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<unsigned int>& 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<clodCluster> 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<unsigned long long>& 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<unsigned int>& indices, const std::vector<unsigned char>& locks, const std::vector<unsigned int>& remap, std::vector<unsigned long long>& 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<unsigned int>& merged, const std::vector<unsigned int>& simplified, const std::vector<unsigned char>& locks, const std::vector<unsigned int>& remap, clodBounds& bounds, std::vector<float>& dilate_offsets, std::vector<unsigned long long>& 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<float*>(&mesh.vertex_positions[simplified[i] * (mesh.vertex_positions_stride / sizeof(float))]);
float* vr = const_cast<float*>(&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<unsigned char> 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<unsigned int> 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<float> dilate_offsets(config.simplify_dilate_borders ? mesh.vertex_count * 4 : 0);
std::vector<unsigned long long> dilate_table;
// initial clusterization splits the original mesh
std::vector<unsigned int> cluster_indices;
std::vector<Cluster> 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<Cluster> pending;
std::vector<unsigned int> pending_indices;
pending.reserve(clusters.size());
pending_indices.reserve(size_t(cluster_indices.size() * config.simplify_threshold));
std::vector<unsigned int> 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<unsigned int> 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<clodNode> row(group_count);
std::vector<unsigned int> 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.
*/
+148
View File
@@ -0,0 +1,148 @@
<!doctype html>
<html lang="en">
<head>
<title>meshoptimizer - demo</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
<style>
body {
font-family: Monospace;
background-color: #000;
color: #fff;
margin: 0px;
overflow: hidden;
}
#info {
color: #fff;
position: absolute;
top: 10px;
width: 100%;
text-align: center;
z-index: 100;
display: block;
}
#info a,
.button {
color: #f00;
font-weight: bold;
text-decoration: underline;
cursor: pointer;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.183.0/build/three.module.js",
"three-examples/": "https://cdn.jsdelivr.net/npm/three@0.183.0/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="info">
<a href="https://github.com/zeux/meshoptimizer" target="_blank" rel="noopener">meshoptimizer</a>
</div>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three-examples/loaders/GLTFLoader.js';
import { MeshoptDecoder } from '../js/meshopt_decoder.mjs';
var container;
var camera, scene, renderer, mixer, timer;
var windowHalfX = window.innerWidth / 2;
var windowHalfY = window.innerHeight / 2;
init();
animate();
function init() {
container = document.createElement('div');
document.body.appendChild(container);
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 100);
camera.position.y = 1.0;
camera.position.z = 3.0;
scene = new THREE.Scene();
scene.background = new THREE.Color(0x300a24);
var ambientLight = new THREE.AmbientLight(0xcccccc, 1);
scene.add(ambientLight);
var pointLight = new THREE.PointLight(0xffffff, 4);
pointLight.position.set(3, 3, 0);
pointLight.decay = 0.5;
camera.add(pointLight);
scene.add(camera);
var onProgress = function (xhr) {};
var onError = function (e) {
console.log(e);
};
var loader = new GLTFLoader();
loader.setMeshoptDecoder(MeshoptDecoder);
loader.load(
'pirate.glb',
function (gltf) {
var bbox = new THREE.Box3().setFromObject(gltf.scene);
var scale = 2 / (bbox.max.y - bbox.min.y);
gltf.scene.scale.set(scale, scale, scale);
gltf.scene.position.set(0, 0, 0);
scene.add(gltf.scene);
mixer = new THREE.AnimationMixer(gltf.scene);
if (gltf.animations.length) {
mixer.clipAction(gltf.animations[gltf.animations.length - 1]).play();
}
},
onProgress,
onError
);
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
window.addEventListener('resize', onWindowResize, false);
timer = new THREE.Timer();
timer.connect(document);
}
function onWindowResize() {
windowHalfX = window.innerWidth / 2;
windowHalfY = window.innerHeight / 2;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate(timestamp) {
timer.update(timestamp);
if (mixer) {
mixer.update(timer.getDelta());
}
requestAnimationFrame(animate);
render();
}
function render() {
renderer.render(scene, camera);
}
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+209
View File
@@ -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<uint8_t> gStream : register(t0);
[[vk::binding(1)]]
StructuredBuffer<MeshletDesc> gMeshlets : register(t1);
[[vk::binding(2)]]
RWStructuredBuffer<uint> 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.
*/
+523
View File
@@ -0,0 +1,523 @@
// For reference, see the original Nanite paper:
// Brian Karis. Nanite: A Deep Dive. 2021
#include "../src/meshoptimizer.h"
#include <float.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <vector>
#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<int>& parents, const std::vector<unsigned int>& indices, const std::vector<unsigned int>& remap);
static size_t measureBoundary(std::vector<int>& used, const std::vector<std::vector<unsigned int> >& clusters, const std::vector<unsigned int>& remap);
static float sahOverhead(const std::vector<std::vector<unsigned int> >& clusters, const std::vector<Vertex>& vertices);
void dumpObj(const std::vector<Vertex>& vertices, const std::vector<unsigned int>& indices, bool recomputeNormals = false);
void dumpObj(const char* section, const std::vector<unsigned int>& indices);
// simulate BVH based LOD cut and return triangle count
static size_t countTriangles(const std::vector<clodNode>& nodes, unsigned int index, const std::vector<clodGroup>& groups, const std::vector<std::pair<unsigned int, unsigned int> >& group_clusters, const std::vector<std::pair<int, unsigned int> >& 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<unsigned int, unsigned int> 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<Vertex>& vertices, const std::vector<unsigned int>& 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<std::vector<unsigned int> > indices; // for detailed connectivity analysis
};
std::vector<Stats> stats;
std::vector<clodGroup> groups;
std::vector<std::pair<unsigned int, unsigned int> > group_clusters; // per group: (offset, count) into cluster_data
std::vector<std::pair<int, unsigned int> > cluster_data; // per cluster: (clodCluster::refined, triangle count)
std::vector<std::vector<unsigned int> > 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<unsigned int>(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<unsigned int>(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<unsigned int>(cluster.indices, cluster.indices + cluster.index_count));
}
level.indices.push_back(std::vector<unsigned int>()); // 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<unsigned int> remap(vertices.size());
meshopt_generatePositionRemap(&remap[0], &vertices[0].px, vertices.size(), sizeof(Vertex));
std::vector<int> 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<clodNode> 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<unsigned int>());
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<int>& parents, int index)
{
while (index != parents[index])
{
int parent = parents[index];
parents[index] = parents[parent];
index = parent;
}
return index;
}
static int measureComponents(std::vector<int>& parents, const std::vector<unsigned int>& indices, const std::vector<unsigned int>& 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<int>& used, const std::vector<std::vector<unsigned int> >& clusters, const std::vector<unsigned int>& 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<unsigned int> order(count);
for (size_t i = 0; i < count; ++i)
order[i] = unsigned(i);
std::vector<unsigned int> temp(count);
return sahCost(boxes, &order[0], &temp[0], count);
}
static float sahOverhead(const std::vector<std::vector<unsigned int> >& clusters, const std::vector<Vertex>& vertices)
{
std::vector<Box> 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;
}
Binary file not shown.
File diff suppressed because it is too large Load Diff
+815
View File
@@ -0,0 +1,815 @@
<!doctype html>
<html lang="en">
<head>
<title>meshoptimizer - demo</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
<style>
body {
font-family: Monospace;
background-color: #300a24;
color: #fff;
margin: 0px;
overflow: hidden;
display: flex;
flex-direction: column;
height: 100vh;
}
#container {
width: 100%;
flex: 1;
position: relative;
}
.renderer-container {
position: relative;
width: 100%;
height: 100%;
}
.renderer-container canvas {
width: 100% !important;
height: 100% !important;
display: block;
user-select: none;
}
.renderer-label {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 5px;
pointer-events: none;
font-size: 14px;
}
.stats-label {
position: absolute;
top: 45px;
left: 10px;
background-color: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 5px;
pointer-events: none;
font-size: 12px;
line-height: 1.4;
}
#progress-label {
left: 50%;
transform: translateX(-50%);
z-index: 1;
}
#texture-view,
#normal-view,
#material-view {
position: fixed;
left: 10px;
bottom: 10px;
width: 256px;
height: 256px;
display: none;
}
#normal-view {
left: 276px;
}
#material-view {
left: 542px;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.module.js",
"three-examples/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/",
"three-mesh-bvh": "https://cdn.jsdelivr.net/npm/three-mesh-bvh@0.9.10/build/index.module.js",
"watlas": "https://cdn.jsdelivr.net/npm/watlas@1.0.1/dist/watlas.js",
"lil-gui": "https://cdn.jsdelivr.net/npm/lil-gui@0.20.0/dist/lil-gui.esm.js"
}
}
</script>
</head>
<body>
<div id="progress-label" class="renderer-label" hidden></div>
<div id="container"></div>
<canvas id="texture-view"></canvas>
<canvas id="normal-view"></canvas>
<canvas id="material-view"></canvas>
<input type="file" id="fileInput" style="display: none" accept=".obj,.glb" />
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three-examples/loaders/GLTFLoader.js';
import { OBJLoader } from 'three-examples/loaders/OBJLoader.js';
import { GLTFExporter } from 'three-examples/exporters/GLTFExporter.js';
import { OrbitControls } from 'three-examples/controls/OrbitControls.js';
import { mergeGeometries, mergeVertices } from 'three-examples/utils/BufferGeometryUtils.js';
import { MeshoptDecoder } from '../js/meshopt_decoder.mjs';
import { MeshoptSimplifier } from '../js/meshopt_simplifier.js';
import { MeshoptTangents } from '../js/meshopt_tangents.js';
import * as repaint from './repaint.js';
import { Initialize as initializeWatlas, Atlas } from 'watlas';
import { GUI } from 'lil-gui';
var container;
var camera, renderer, scene, controls, model, merged, remeshed, remeshMaterial, wireframeMaterial, stats;
var paintCache = null;
var progressLabel = document.getElementById('progress-label');
var settings = {
wireframe: false,
wireframeOverlay: false,
resolution: 100,
thicken: false,
shell: false,
solve: true,
debug: false,
normalCrease: 60,
normalSmoothing: 1.5,
simplify: {
enabled: true,
regularize: true,
solve: true,
errorAuto: false,
errorThresholdLog10: 2.5,
forceTriangleCount: 0,
},
paint: {
mode: 'texture',
resolution: 1024,
gutter: 1,
atlasMode: 'balanced',
preserveNormalSeams: true,
infill: true,
normals: false,
materials: false,
},
loadFile: function () {
var input = document.getElementById('fileInput');
input.onchange = function () {
var file = input.files[0];
var uri = URL.createObjectURL(file);
var ext = file.name.split('.').pop().toLowerCase();
container.querySelector('.renderer-label').textContent = file.name;
load(uri, ext);
};
input.click();
},
remesh: function () {
remesh();
},
revert: function () {
revert();
},
exportFile: function () {
if (!remeshed) return;
exportGlb('remesh.glb');
},
updateModule: function () {
reload();
remesh();
},
autoUpdate: false,
autoUpdateStatus: '',
};
var gui = new GUI({ width: 300 });
gui.add(settings, 'remesh');
gui.add(settings, 'revert');
gui.add(settings, 'loadFile');
gui.add(settings, 'exportFile');
var guiDisplay = gui.addFolder('Display');
guiDisplay.add(settings, 'wireframe').onChange(update);
guiDisplay.add(settings, 'wireframeOverlay').onChange(update);
var guiRemesh = gui.addFolder('Remesh');
guiRemesh.add(settings, 'resolution', 4, 256, 1);
guiRemesh.add(settings, 'thicken').disable();
guiRemesh.add(settings, 'shell');
guiRemesh.add(settings, 'solve');
guiRemesh.add(settings, 'debug');
guiRemesh.add(settings, 'normalCrease', 0, 180, 1).name('normal crease');
guiRemesh.add(settings, 'normalSmoothing', 0, 10, 0.1).name('normal smoothing');
var guiSimplify = gui.addFolder('Simplify');
guiSimplify.add(settings.simplify, 'enabled');
guiSimplify.add(settings.simplify, 'regularize');
guiSimplify.add(settings.simplify, 'solve');
guiSimplify.add(settings.simplify, 'errorAuto');
guiSimplify.add(settings.simplify, 'errorThresholdLog10', 1.5, 3, 0.1);
guiSimplify.add(settings.simplify, 'forceTriangleCount');
var guiPaint = gui.addFolder('Paint');
guiPaint.add(settings.paint, 'mode', ['none', 'vertex', 'facets', 'texture']);
guiPaint.add(settings.paint, 'resolution', [256, 512, 1024, 2048]);
guiPaint.add(settings.paint, 'gutter', 0, 8, 1);
guiPaint.add(settings.paint, 'atlasMode', ['fast', 'balanced', 'quality']);
guiPaint.add(settings.paint, 'preserveNormalSeams');
guiPaint.add(settings.paint, 'infill');
guiPaint.add(settings.paint, 'normals').name('normal map');
guiPaint.add(settings.paint, 'materials').name('material map');
var guiUpdate = gui.addFolder('Update');
guiUpdate.add(settings, 'updateModule');
guiUpdate.add(settings, 'autoUpdate').onChange(autoReload);
guiUpdate.add(settings, 'autoUpdateStatus').listen();
init();
loadDefault();
render();
function createView(name) {
// Create renderer container
var rendererContainer = document.createElement('div');
rendererContainer.className = 'renderer-container';
container.appendChild(rendererContainer);
// Add label with file name
var label = document.createElement('div');
label.className = 'renderer-label';
label.textContent = name;
rendererContainer.appendChild(label);
// Add stats label
var statsLabel = document.createElement('div');
statsLabel.className = 'stats-label';
rendererContainer.appendChild(statsLabel);
// Initialize stats
stats = {
triangles: 0,
vertices: 0,
ratio: 0,
label: statsLabel,
};
// Create renderer
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
rendererContainer.appendChild(renderer.domElement);
// Create scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x300a24);
// Add lighting
var ambientLight = new THREE.AmbientLight(0xcccccc, 1);
scene.add(ambientLight);
var pointLightR = new THREE.PointLight(0xffffff, 4);
pointLightR.position.set(3, 3, 0);
pointLightR.decay = 0.5;
scene.add(pointLightR);
var pointLightL = new THREE.PointLight(0xffffff, 1);
pointLightL.position.set(-3, 5, 0);
pointLightL.decay = 0.5;
scene.add(pointLightL);
remeshMaterial = new THREE.MeshStandardMaterial({ color: 0xdddddd, polygonOffsetFactor: 0.5, polygonOffsetUnits: 16 });
wireframeMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true, visible: false });
// Create controls
controls = new OrbitControls(camera, renderer.domElement);
// Adjust renderer size
updateRendererSizes();
}
function copyAttribute(attribute, count, itemSize, def) {
var result = new Float32Array(count * itemSize).fill(def);
if (attribute) {
var copy = Math.min(itemSize, attribute.itemSize);
for (var i = 0; i < count; ++i) {
for (var k = 0; k < copy; ++k) {
result[i * itemSize + k] = attribute.getComponent(i, k);
}
}
}
return new THREE.BufferAttribute(result, itemSize);
}
function skinAttribute(object, attribute, w) {
var v = new THREE.Vector4();
for (var i = 0; i < attribute.count; ++i) {
v.set(attribute.getX(i), attribute.getY(i), attribute.getZ(i), w);
object.applyBoneTransform(i, v);
if (w == 0) v.normalize();
attribute.setXYZ(i, v.x, v.y, v.z);
}
}
function mergeMesh(model) {
var geometries = [];
var materials = [];
model.updateMatrixWorld(true);
model.traverse(function (object) {
if (object.isMesh && object.geometry.attributes.position) {
var source = object.geometry.attributes.position;
var geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', copyAttribute(source, source.count, 3, 0));
// we need original normal/uv/colors for gpu repaint
// for mergeGeometries to agree to merge everything, we maintain consistent attribute sets
geometry.setAttribute('normal', copyAttribute(object.geometry.attributes.normal, source.count, 3, 0));
geometry.setAttribute('uv', copyAttribute(object.geometry.attributes.uv, source.count, 2, 0));
geometry.setAttribute('color', copyAttribute(object.geometry.attributes.color, source.count, 4, 1));
if (object.isSkinnedMesh) {
skinAttribute(object, geometry.attributes.position, 1);
skinAttribute(object, geometry.attributes.normal, 0);
}
var material = Array.isArray(object.material) ? object.material[0] : object.material;
if (materials.indexOf(material) < 0) materials.push(material);
var materialIndex = new Uint32Array(source.count).fill(materials.indexOf(material));
geometry.setAttribute('material', new THREE.BufferAttribute(materialIndex, 1));
if (object.geometry.index) {
geometry.setIndex(object.geometry.index);
} else {
var indices = new Uint32Array(source.count);
for (var i = 0; i < source.count; ++i) indices[i] = i;
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
}
// we prefer to use the original tangents, but we need to generate them if not present so that normal baking works
if (object.geometry.attributes.tangent) {
geometry.setAttribute('tangent', copyAttribute(object.geometry.attributes.tangent, source.count, 4, 0));
if (object.isSkinnedMesh) skinAttribute(object, geometry.attributes.tangent, 0);
} else {
geometry = generateTangents(geometry, -1);
geometry = mergeVertices(geometry);
}
geometry.applyMatrix4(object.matrixWorld);
geometries.push(geometry);
}
});
var result = mergeGeometries(geometries);
result.computeBoundingBox();
result.computeBoundingSphere();
result.materials = materials;
result.thickness = result.boundingBox.getSize(new THREE.Vector3()).length() * 0.01;
return result;
}
function unwrapMesh(geometry, resolution, gutter, preserveNormalSeams, maxIterations, maxChartSize) {
var positions = geometry.attributes.position.array;
var indices = new Uint32Array(geometry.index.array);
var triangle = new THREE.Triangle();
var avgTriangleArea = 0;
for (var i = 0; i < indices.length; i += 3) {
triangle.a.fromBufferAttribute(geometry.attributes.position, indices[i + 0]);
triangle.b.fromBufferAttribute(geometry.attributes.position, indices[i + 1]);
triangle.c.fromBufferAttribute(geometry.attributes.position, indices[i + 2]);
avgTriangleArea += triangle.getArea() / (indices.length / 3);
}
var atlas = new Atlas();
atlas.addMesh({
vertexPositionData: positions,
vertexCount: positions.length / 3,
vertexPositionStride: 12,
vertexNormalData: geometry.attributes.normal.array,
vertexNormalStride: 12,
indexData: indices,
indexCount: indices.length,
});
atlas.generate(
{ normalSeamWeight: preserveNormalSeams ? 1000 : 0, maxIterations: maxIterations, maxChartArea: maxChartSize * avgTriangleArea },
{ resolution: resolution, padding: gutter }
);
var mesh = atlas.getMesh(0);
var atlasIndices = new Uint32Array(mesh.indexCount);
var uv = new Float32Array(mesh.indexCount * 2);
mesh.getIndexArray(atlasIndices);
for (var i = 0; i < mesh.indexCount; ++i) {
var v = mesh.getVertex(atlasIndices[i]);
uv[i * 2 + 0] = v.uv[0] / atlas.width;
uv[i * 2 + 1] = v.uv[1] / atlas.height;
}
atlas.delete();
// uvs are computed per corner, for simplicity we deindex the geometry and expect caller to reindex
geometry = geometry.toNonIndexed();
geometry.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
return geometry;
}
function generateTangents(geometry, sign = 1) {
var attr = geometry.attributes;
var indices = geometry.index ? new Uint32Array(geometry.index.array) : null;
var tangents = MeshoptTangents.generateTangents(indices, attr.position.array, 3, attr.normal.array, 3, attr.uv.array, 2);
for (var i = 3; i < tangents.length; i += 4) tangents[i] *= sign;
// tangents are computed per corner, for simplicity we deindex the geometry and expect caller to reindex
if (indices) geometry = geometry.toNonIndexed();
geometry.setAttribute('tangent', new THREE.BufferAttribute(tangents, 4));
return geometry;
}
function generateNormals(geometry, creaseAngle, smoothing) {
var attr = geometry.attributes;
var indices = geometry.index ? new Uint32Array(geometry.index.array) : null;
var normals = MeshoptTangents.generateNormals(indices, attr.position.array, 3, creaseAngle, smoothing);
// normals are computed per corner, for simplicity we deindex the geometry and expect caller to reindex
geometry = indices ? geometry.toNonIndexed() : geometry.clone();
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
return geometry;
}
function showTexture(id, texture) {
var canvas = document.getElementById(id);
canvas.style.display = texture ? 'block' : 'none';
if (!texture) return;
canvas.width = texture.image.width;
canvas.height = texture.image.height;
canvas.getContext('2d').putImageData(new ImageData(new Uint8ClampedArray(texture.image.data), canvas.width, canvas.height), 0, 0);
}
function clearRemeshed() {
if (remeshed) {
scene.remove(remeshed);
remeshed.geometry.dispose();
}
remeshed = undefined;
if (remeshMaterial.map) remeshMaterial.map.dispose();
remeshMaterial.map = null;
if (remeshMaterial.normalMap) remeshMaterial.normalMap.dispose();
remeshMaterial.normalMap = null;
// roughness and metalness share one texture, so we only dispose once
if (remeshMaterial.metalnessMap) remeshMaterial.metalnessMap.dispose();
remeshMaterial.metalnessMap = null;
remeshMaterial.roughnessMap = null;
showTexture('texture-view', null);
showTexture('normal-view', null);
showTexture('material-view', null);
}
function simplifyMesh(geo, target, threshold, flags, solve) {
var positions = solve ? new Float32Array(geo.attributes.position.array) : geo.attributes.position.array;
var indices = solve ? new Uint32Array(geo.index.array) : geo.index.array;
var res;
if (solve) {
res = MeshoptSimplifier.simplifyWithUpdate(indices, positions, 3, new Float32Array(), 0, [], null, target * 3, threshold, flags);
res[0] = indices.slice(0, res[0]);
} else {
res = MeshoptSimplifier.simplify(indices, positions, 3, target * 3, threshold, flags);
}
var [remap, unique] = MeshoptSimplifier.compactMesh(res[0]);
var compact = new Float32Array(unique * 3);
for (var i = 0; i < remap.length; ++i) {
if (remap[i] < unique) compact.set(positions.subarray(i * 3, i * 3 + 3), remap[i] * 3);
}
var rgeo = geo.clone();
rgeo.setAttribute('position', new THREE.BufferAttribute(compact, 3));
rgeo.setIndex(new THREE.BufferAttribute(res[0], 1));
return rgeo;
}
function showProgress(stage) {
progressLabel.textContent = stage;
progressLabel.hidden = false;
return new Promise(requestAnimationFrame);
}
async function remesh() {
if (!merged) return;
await Promise.all([MeshoptSimplifier.ready, MeshoptTangents.ready, initializeWatlas()]);
await showProgress('remesh');
var flags = [];
if (settings.thicken) flags.push('Thicken');
if (settings.shell) flags.push('Shell');
if (settings.solve) flags.push('Solve');
if (settings.debug) flags.push('_InternalDebug');
var result = MeshoptSimplifier.remesh(merged.index.array, merged.attributes.position.array, 3, settings.resolution, flags);
var geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(result, 3));
geometry.setIndex(new THREE.BufferAttribute(MeshoptSimplifier.generatePositionRemap(result, 3), 1));
if (settings.simplify.enabled) {
await showProgress('simplify');
var sflags = ['PreserveFolds', 'Sparse']; // Sparse is needed because we skip position compaction above to be able to use quick remap to generate indices
if (settings.simplify.regularize) sflags.push('RegularizeLight');
var error = settings.simplify.forceTriangleCount ? 1 : Math.pow(10, -settings.simplify.errorThresholdLog10);
if (settings.simplify.errorAuto) {
sflags.push('ErrorAbsolute'); // merged mesh is in world space, so we don't need to adjust error by mesh scale and can simply use world space units
var distance = Math.max(camera.position.distanceTo(merged.boundingSphere.center) - merged.boundingSphere.radius, 0);
error = distance * 1e-3 * (2 * Math.tan(THREE.MathUtils.degToRad(camera.fov) * 0.5)); // ~1 pixel at 1k x 1k
}
geometry = simplifyMesh(geometry, settings.simplify.forceTriangleCount, error, sflags, settings.simplify.solve);
}
await showProgress('normals');
geometry = mergeVertices(
generateNormals(geometry, THREE.MathUtils.degToRad(settings.debug ? 30 : settings.normalCrease), settings.normalSmoothing)
);
var material = remeshMaterial.clone();
material.map = material.normalMap = material.metalnessMap = material.roughnessMap = null;
material.vertexColors = false;
material.color.set(0xffffff);
material.metalness = 0;
material.roughness = 1;
if (settings.paint.mode != 'none' && !paintCache) {
await showProgress('paint cache');
paintCache = repaint.buildCache(merged, merged.materials);
}
if (settings.paint.mode == 'vertex') {
await showProgress('repaint');
repaint.transferColors(renderer, geometry, paintCache, merged.thickness);
material.vertexColors = true;
} else if (settings.paint.mode == 'facets') {
await showProgress('repaint');
geometry = geometry.toNonIndexed();
geometry.computeVertexNormals();
var indices = new Uint32Array(geometry.attributes.position.count);
for (var i = 0; i < indices.length; ++i) indices[i] = i;
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
repaint.transferColors(renderer, geometry, paintCache, merged.thickness, true);
material.vertexColors = true;
} else if (settings.paint.mode == 'texture') {
await showProgress('unwrap');
var maxIterations = settings.paint.atlasMode == 'quality' ? 1 : 0;
var maxChartSize = settings.paint.atlasMode == 'fast' ? 256 : settings.paint.atlasMode == 'balanced' ? 512 : 1024;
var preserveSeams = settings.paint.preserveNormalSeams;
geometry = unwrapMesh(geometry, settings.paint.resolution, settings.paint.gutter, preserveSeams, maxIterations, maxChartSize);
if (settings.paint.normals) {
await showProgress('tangents');
geometry = generateTangents(geometry);
}
geometry = mergeVertices(geometry);
await showProgress('repaint');
var textures = repaint.transferTexture(
renderer,
geometry,
paintCache,
merged.thickness,
settings.paint.resolution,
settings.paint.gutter,
settings.paint.infill,
settings.paint.normals,
settings.paint.materials
);
material.map = textures.map;
material.normalMap = textures.normalMap;
if (settings.paint.materials) {
material.metalnessMap = textures.materialMap;
material.roughnessMap = textures.materialMap;
material.metalness = 1;
} else if (settings.paint.normals) {
// we need this to have a specular response
material.roughness = 0.5;
}
} else {
material.color.set(0xdddddd);
}
material.needsUpdate = true;
// remesh material + wireframe overlay material
geometry.addGroup(0, geometry.index.count, 0);
geometry.addGroup(0, geometry.index.count, 1);
clearRemeshed();
remeshMaterial.dispose();
remeshMaterial = material;
remeshed = new THREE.Mesh(geometry, [remeshMaterial, wireframeMaterial]);
model.visible = false;
scene.add(remeshed);
showTexture('texture-view', remeshMaterial.map);
showTexture('normal-view', remeshMaterial.normalMap);
showTexture('material-view', remeshMaterial.metalnessMap);
update();
var triangles = geometry.index.count / 3;
stats.triangles = triangles;
stats.vertices = geometry.attributes.position.count;
stats.ratio = (triangles / (merged.index.count / 3)) * 100;
updateStatsDisplay();
progressLabel.hidden = true;
}
function exportGlb(name) {
// can't export remeshed directly as it uses debug materials for wireframe
var mesh = new THREE.Mesh(remeshed.geometry, remeshMaterial);
new GLTFExporter().parse(
mesh,
function (glb) {
var link = document.createElement('a');
link.href = URL.createObjectURL(new Blob([glb], { type: 'model/gltf-binary' }));
link.download = name;
link.click();
// may resolve rare race conditions
setTimeout(function () {
URL.revokeObjectURL(link.href);
}, 0);
},
function (e) {
console.log(e);
},
{ binary: true }
);
}
function revert() {
if (!model) return;
model.visible = true;
clearRemeshed();
stats.triangles = 0;
stats.vertices = 0;
stats.ratio = 100;
model.traverse(function (object) {
if (object.isMesh) {
stats.triangles += (object.geometry.index ? object.geometry.index.count : object.geometry.attributes.position.count) / 3;
stats.vertices += object.geometry.attributes.position.count;
}
});
updateStatsDisplay();
}
function updateStatsDisplay() {
stats.label.innerHTML =
'Triangles: ' + stats.triangles + '<br>' + 'Vertices: ' + stats.vertices + '<br>' + 'Ratio: ' + stats.ratio.toFixed(1) + '%';
}
function reload() {
var simp = import('/js/meshopt_simplifier.js?x=' + Date.now());
MeshoptSimplifier.ready = simp.then(function (s) {
return s.MeshoptSimplifier.ready.then(function () {
for (var prop in s.MeshoptSimplifier) {
MeshoptSimplifier[prop] = s.MeshoptSimplifier[prop];
}
});
});
}
var moduleLastModified = 0;
function autoReload() {
if (!settings.autoUpdate) return;
fetch('/js/meshopt_simplifier.js?x=' + Date.now(), { method: 'HEAD' })
.then(function (r) {
var last = r.headers.get('Last-Modified');
if (last != moduleLastModified) {
moduleLastModified = last;
reload();
remesh();
}
settings.autoUpdateStatus = new Date(last).toLocaleTimeString();
setTimeout(autoReload, 1000);
})
.catch(function (e) {
settings.autoUpdateStatus = 'error';
setTimeout(autoReload, 5000);
});
}
function update() {
remeshMaterial.polygonOffset = settings.wireframeOverlay;
wireframeMaterial.visible = settings.wireframeOverlay;
scene.traverse(function (child) {
if (child.isMesh) {
if (Array.isArray(child.material)) {
child.material[0].wireframe = settings.wireframe;
} else {
child.material.wireframe = settings.wireframe;
}
}
});
}
function load(path, ext) {
if (model) scene.remove(model);
clearRemeshed();
if (paintCache) paintCache.dispose();
paintCache = null;
model = undefined;
merged = undefined;
var onProgress = function (xhr) {};
var onError = function (e) {
console.log(e);
};
function center(model) {
var bbox = new THREE.Box3().setFromObject(model);
var scale = 2 / Math.max(bbox.max.x - bbox.min.x, bbox.max.y - bbox.min.y, bbox.max.z - bbox.min.z);
var offset = new THREE.Vector3().addVectors(bbox.max, bbox.min).multiplyScalar(scale / 2);
model.scale.set(scale, scale, scale);
model.position.set(-offset.x, -offset.y, -offset.z);
}
function finish(object) {
model = object;
center(model);
scene.add(model);
merged = mergeMesh(model);
revert();
controls.target.set(0, 0, 0);
}
if (ext == 'gltf' || ext == 'glb') {
var loader = new GLTFLoader();
loader.setMeshoptDecoder(MeshoptDecoder);
loader.load(
path,
function (gltf) {
finish(gltf.scene);
},
onProgress,
onError
);
} else if (ext == 'obj') {
var loader = new OBJLoader();
loader.load(
path,
function (obj) {
finish(obj);
},
onProgress,
onError
);
} else {
console.error('Unsupported file format');
}
}
function loadDefault() {
createView('pirate.glb');
load('pirate.glb', 'glb');
}
function init() {
container = document.getElementById('container');
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.y = 1.0;
camera.position.z = 3.0;
window.addEventListener('resize', updateRendererSizes, false);
}
function updateRendererSizes() {
var rendererContainer = renderer.domElement.parentElement;
var aspectRatio = rendererContainer.clientWidth / rendererContainer.clientHeight;
camera.aspect = aspectRatio;
camera.updateProjectionMatrix();
renderer.setSize(rendererContainer.clientWidth, rendererContainer.clientHeight);
}
function render() {
requestAnimationFrame(render);
controls.update();
renderer.render(scene, camera);
}
</script>
</body>
</html>
+593
View File
@@ -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));
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
DisableFormat: true
+7240
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+800
View File
@@ -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.h> /* assert */
#include <string.h> /* memcpy */
#include <limits.h> /* 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 */
+133
View File
@@ -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).
+356
View File
@@ -0,0 +1,356 @@
// This file is part of gltfpack; see gltfpack.h for version/license details
#include "gltfpack.h"
#include <algorithm>
#include <float.h>
#include <math.h>
#include <string.h>
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<Attr>& data, const std::vector<float>& input, const std::vector<Attr>& 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<Attr>& 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<Attr> base;
for (size_t i = 0; i < animation.tracks.size(); ++i)
{
Track& track = animation.tracks[i];
if (settings.anim_freq)
{
std::vector<Attr> 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;
}
}
}
+25
View File
@@ -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);
});
+214
View File
@@ -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<ImageInfo>& images, const char* input_path, const Settings& settings)
{
basisu::basisu_encoder_init();
basisu::vector<basisu::basis_compressor_params> params(data->images_count);
basisu::vector<basisu::parallel_results> results(data->images_count);
std::string temp_prefix = getTempPrefix();
std::vector<std::string> temp_inputs(data->images_count);
std::vector<std::string> 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
+169
View File
@@ -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 <atomic>
#include <memory>
#include <thread>
static int writeWebP(const uint8_t* data, size_t data_size, const WebPPicture* picture)
{
std::string* encoded = static_cast<std::string*>(picture->custom_ptr);
encoded->append(reinterpret_cast<const char*>(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<uint8_t*>(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<WebPPicture, void (*)(WebPPicture*)> pic_storage(&pic, WebPPictureFree);
if (!reader(reinterpret_cast<const uint8_t*>(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<ImageInfo>& images, const char* input_path, const Settings& settings)
{
std::atomic<size_t> 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<std::thread> 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
+115
View File
@@ -0,0 +1,115 @@
// This file is part of gltfpack; see gltfpack.h for version/license details
#include "gltfpack.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <process.h>
#else
#include <unistd.h>
#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);
}
+303
View File
@@ -0,0 +1,303 @@
#
# AFL dictionary for JSON
# -----------------------
#
# Just the very basics.
#
# Inspired by a dictionary by Jakub Wilk <jwilk@jwilk.net>
#
"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\""
Binary file not shown.
File diff suppressed because it is too large Load Diff
+449
View File
@@ -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 <assert.h>
#include <string>
#include <vector>
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<Attr> data;
};
struct Instance
{
float transform[16];
float color[4];
};
struct Mesh
{
int scene;
std::vector<cgltf_node*> nodes;
std::vector<Instance> instances;
cgltf_material* material;
cgltf_skin* skin;
cgltf_extras extras;
cgltf_primitive_type type;
std::vector<Stream> streams;
std::vector<unsigned int> indices;
bool geometry_duplicate;
uint64_t geometry_hash[2];
size_t targets;
std::vector<float> target_weights;
std::vector<const char*> target_names;
std::vector<cgltf_material_mapping> 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<float> time; // empty for resampled or constant animations
std::vector<Attr> data;
};
struct Animation
{
const char* name;
float start;
int frames;
std::vector<Track> 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<size_t> 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<Mesh>& meshes, const char** error);
cgltf_data* parseGltf(const char* path, std::vector<Mesh>& meshes, std::vector<Animation>& animations, const char** error);
cgltf_data* parseGlb(const void* buffer, size_t size, std::vector<Mesh>& meshes, std::vector<Animation>& 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<Mesh>& meshes, const Settings& settings);
void mergeMeshInstances(Mesh& mesh);
void mergeMeshes(std::vector<Mesh>& meshes, const Settings& settings);
void filterEmptyMeshes(std::vector<Mesh>& 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<Mesh>& meshes, const Settings& settings);
void markNeededMaterials(cgltf_data* data, std::vector<MaterialInfo>& materials, const std::vector<Mesh>& meshes, const Settings& settings);
void mergeTextures(cgltf_data* data, std::vector<TextureInfo>& textures);
bool isValidTransform(const cgltf_texture_transform& transform);
void analyzeMaterials(cgltf_data* data, std::vector<MaterialInfo>& materials, std::vector<TextureInfo>& textures, std::vector<ImageInfo>& images);
void optimizeMaterials(cgltf_data* data, std::vector<MaterialInfo>& materials, std::vector<ImageInfo>& 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<ImageInfo>& images, const char* input_path, const Settings& settings);
#endif
#ifdef WITH_LIBWEBP
void encodeImagesWebP(std::string* encoded, const cgltf_data* data, const std::vector<ImageInfo>& images, const char* input_path, const Settings& settings);
#endif
void markScenes(cgltf_data* data, std::vector<NodeInfo>& nodes);
void markAnimated(cgltf_data* data, std::vector<NodeInfo>& nodes, const std::vector<Animation>& animations);
void markNeededNodes(cgltf_data* data, std::vector<NodeInfo>& nodes, const std::vector<Mesh>& meshes, const std::vector<Animation>& animations, const Settings& settings);
void remapNodes(cgltf_data* data, std::vector<NodeInfo>& nodes, size_t& node_offset);
void decomposeTransform(float translation[3], float rotation[4], float scale[3], const float* transform);
void computeMeshQuality(std::vector<Mesh>& meshes);
bool hasVertexAlpha(const Mesh& mesh);
bool hasInstanceAlpha(const std::vector<Instance>& instances);
QuantizationPosition prepareQuantizationPosition(const std::vector<Mesh>& meshes, const Settings& settings);
void prepareQuantizationTexture(cgltf_data* data, std::vector<QuantizationTexture>& result, std::vector<size_t>& indices, const std::vector<Mesh>& 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<unsigned int>& stream);
StreamFormat writeTimeStream(std::string& bin, const std::vector<float>& data);
StreamFormat writeKeyframeStream(std::string& bin, cgltf_animation_path_type type, const std::vector<Attr>& 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<BufferView>& 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<TextureInfo>& 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<BufferView>& 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<BufferView>& 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<BufferView>& views, std::string& json_accessors, size_t& accr_offset, const std::vector<unsigned int>& indices, cgltf_primitive_type type, const Settings& settings);
void writeMeshGeometry(std::string& json, std::vector<BufferView>& 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<BufferView>& views, std::string& json_accessors, size_t& accr_offset, const cgltf_skin& skin, const QuantizationPosition& qp, const Settings& settings);
size_t writeInstances(std::vector<BufferView>& views, std::string& json_accessors, size_t& accr_offset, const std::vector<Instance>& 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<NodeInfo>& nodes, cgltf_data* data);
void writeNode(std::string& json, const cgltf_node& node, const std::vector<NodeInfo>& nodes, cgltf_data* data);
void writeAnimation(std::string& json, std::vector<BufferView>& views, std::string& json_accessors, size_t& accr_offset, const Animation& animation, size_t i, cgltf_data* data, const std::vector<NodeInfo>& 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.
*/
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity type="win32" name="..." version="6.0.0.0"/>
<application>
<windowsSettings>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
</windowsSettings>
</application>
</assembly>
+372
View File
@@ -0,0 +1,372 @@
// This file is part of gltfpack; see gltfpack.h for version/license details
#include "gltfpack.h"
#include <stdlib.h>
#include <string.h>
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<const char*>(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<const char*>(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";
}
+92
View File
@@ -0,0 +1,92 @@
// This file is part of gltfpack; see gltfpack.h for version/license details
#include "gltfpack.h"
#include <float.h>
#include <math.h>
#include <stdio.h>
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");
}
}
}
+355
View File
@@ -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 };
+637
View File
@@ -0,0 +1,637 @@
// This file is part of gltfpack; see gltfpack.h for version/license details
#include "gltfpack.h"
#include <string.h>
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<Mesh>& meshes, const Settings& settings)
{
std::vector<cgltf_material*> 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<MaterialInfo>& materials, const std::vector<Mesh>& 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<TextureInfo>& textures, std::vector<ImageInfo>& 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<TextureInfo>& textures, std::vector<ImageInfo>& 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<MaterialInfo>& materials, std::vector<TextureInfo>& textures, std::vector<ImageInfo>& 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<ImageInfo>& 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<MaterialInfo>& materials, std::vector<ImageInfo>& 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<TextureInfo>& 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++;
}
}
}
File diff suppressed because it is too large Load Diff
+206
View File
@@ -0,0 +1,206 @@
// This file is part of gltfpack; see gltfpack.h for version/license details
#include "gltfpack.h"
#include <math.h>
#include <string.h>
void markScenes(cgltf_data* data, std::vector<NodeInfo>& 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<NodeInfo>& nodes, const std::vector<Animation>& 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<NodeInfo>& nodes, const std::vector<Mesh>& meshes, const std::vector<Animation>& 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<NodeInfo>& 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);
}
+29
View File
@@ -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"
}
}
+704
View File
@@ -0,0 +1,704 @@
// This file is part of gltfpack; see gltfpack.h for version/license details
#include "gltfpack.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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<float>& 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<Attr>& data, const cgltf_accessor* accessor)
{
size_t components = cgltf_num_components(accessor->type);
std::vector<float> 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<Attr>& data, const cgltf_accessor* accessor, const std::vector<unsigned int>& 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<unsigned int>& indices, cgltf_primitive_type& type)
{
if (type == cgltf_primitive_type_line_loop)
{
std::vector<unsigned int> 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<unsigned int> 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<unsigned int> 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<unsigned int> 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<Mesh>& meshes, std::vector<std::pair<size_t, size_t> >& 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<unsigned int> 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<Instance>& 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<Mesh>& meshes, const std::vector<std::pair<size_t, size_t> >& mesh_remap)
{
for (size_t i = 0; i < data->nodes_count; ++i)
{
cgltf_node& node = data->nodes[i];
if (!node.mesh)
continue;
std::pair<size_t, size_t> 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<Animation>& 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<char> 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<Mesh>& meshes, std::vector<Animation>& 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<std::pair<size_t, size_t> > 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<Mesh>& meshes, std::vector<Animation>& 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<Mesh>& meshes, std::vector<Animation>& 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;
}
+12
View File
@@ -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
+326
View File
@@ -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 <stdlib.h>
#include <string.h>
static void defaultFree(void*, void* p)
{
free(p);
}
static int textureIndex(const std::vector<unsigned int>& 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<unsigned int> 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<unsigned int> 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<Mesh>& 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<Mesh>& 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;
}
+913
View File
@@ -0,0 +1,913 @@
// This file is part of gltfpack; see gltfpack.h for version/license details
#include "gltfpack.h"
#include <algorithm>
#include <float.h>
#include <limits.h>
#include <math.h>
#include <stdint.h>
#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<Mesh>& meshes, const Settings& settings)
{
QuantizationPosition result = {};
result.bits = settings.pos_bits;
result.normalized = settings.pos_normalized;
std::vector<Bounds> 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<size_t>& 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<QuantizationTexture>& result, std::vector<size_t>& indices, const std::vector<Mesh>& 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<size_t> 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> 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<signed char*>(destination);
short* d16 = static_cast<short*>(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<unsigned char*>(destination);
unsigned short* d16 = static_cast<unsigned short*>(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<const char*>(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<const char*>(stream.data[i].f), stride);
meshopt_encodeFilterExp(&bin[offset], stream.data.size(), stride, bits + 1, reinterpret_cast<const float*>(&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<const char*>(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<const char*>(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<const char*>(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<const char*>(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<const char*>(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<const char*>(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<const char*>(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<const char*>(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<const char*>(&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<unsigned int>& 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<const char*>(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<const char*>(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<float>& data)
{
for (size_t i = 0; i < data.size(); ++i)
{
float v[1] = {data[i]};
bin.append(reinterpret_cast<const char*>(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<Attr>& 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<const char*>(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<const char*>(v), sizeof(v));
}
if (filter == StreamFormat::Filter_Exp)
meshopt_encodeFilterExp(&bin[offset], data.size(), 12, bits, reinterpret_cast<const float*>(&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<const char*>(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<unsigned char> 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<const char*>(&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<unsigned char> compressed(meshopt_encodeIndexBufferBound(count, count));
size_t size = 0;
if (stride == 2)
size = meshopt_encodeIndexBuffer(&compressed[0], compressed.size(), reinterpret_cast<const uint16_t*>(data.c_str()), count);
else
size = meshopt_encodeIndexBuffer(&compressed[0], compressed.size(), reinterpret_cast<const uint32_t*>(data.c_str()), count);
bin.append(reinterpret_cast<const char*>(&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<unsigned char> compressed(meshopt_encodeIndexSequenceBound(count, count));
size_t size = 0;
if (stride == 2)
size = meshopt_encodeIndexSequence(&compressed[0], compressed.size(), reinterpret_cast<const uint16_t*>(data.c_str()), count);
else
size = meshopt_encodeIndexSequence(&compressed[0], compressed.size(), reinterpret_cast<const uint32_t*>(data.c_str()), count);
bin.append(reinterpret_cast<const char*>(&compressed[0]), size);
}
+45
View File
@@ -0,0 +1,45 @@
#ifdef __wasi__
#include <stdlib.h>
#include <string.h>
#include <wasi/api.h>
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
+2
View File
@@ -0,0 +1,2 @@
__wasi_path_open32
__wasi_fd_seek32
File diff suppressed because it is too large Load Diff
+314
View File
@@ -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 `<script>` tag which exposes the module as a global variable `MeshoptDecoder`. The ESM version uses `.mjs` file extension unlike other components, to avoid compatibility issues with prior versions.
To decode a buffer, one of the decoding functions should be called:
```ts
decodeVertexBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array, filter?: string) => void;
decodeIndexBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array) => void;
decodeIndexSequence: (target: Uint8Array, count: number, size: number, source: Uint8Array) => void;
```
The `source` should contain the data encoded using meshopt codecs; `count` represents the number of elements (attributes or indices); `size` represents the size of each element and should be divisible by 4 for `decodeVertexBuffer` and equal to 2 or 4 for the index decoders. `target` must be `count * size` bytes.
Given a valid encoded buffer and the correct input parameters, these functions always succeed; they fail if the input data is malformed.
When decoding attribute (vertex) data, additionally one of the decoding filters can be applied to further post-process the decoded data. `filter` must be equal to `"OCTAHEDRAL"`, `"QUATERNION"`, `"EXPONENTIAL"` or `"COLOR"` to activate this extra step. The description of filters can be found in [the specification for KHR_meshopt_compression](https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_meshopt_compression/README.md).
To simplify the decoding further, a wrapper function is provided that automatically calls the correct version of the decoding based on `mode` - which should be `"ATTRIBUTES"`, `"TRIANGLES"` or `"INDICES"`. The difference in terminology is due to the fact that the JavaScript API uses the terms established in the glTF extension, whereas the function names match that of the meshoptimizer C++ API.
```ts
decodeGltfBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array, mode: string, filter?: string) => void;
```
Note that all functions above run synchronously; sometimes decoding large buffers takes time, so this library provides support for asynchronous decoding
using WebWorkers via the following API; `useWorkers` must be called once at startup to create the desired number of workers:
```ts
useWorkers: (count: number) => void;
decodeGltfBufferAsync: (count: number, size: number, source: Uint8Array, mode: string, filter?: string) => Promise<Uint8Array>;
```
## Encoder
`MeshoptEncoder` (`meshopt_encoder.js`) implements data preprocessing and compression of attribute and index buffers. It can be used to compress data that can be decompressed using the decoder module - note that the encoding process is more complicated and nuanced. It is typically split into three steps:
1. Pre-process the mesh to improve index and vertex locality which increases compression ratio
2. Quantize the data, either manually using integer or normalized integer format as a target, or using filter encoders
3. Encode the data
Step 1 is optional but highly recommended for triangle meshes; it can be omitted when compressing data with a predefined order such as animation keyframes.
Step 2 is the only lossy step in this process; without step 2, encoding will retain all semantics of the input exactly which can result in compressed data that is too large.
To reverse the process, decoder is used to reverse step 3 and (optionally) 2; the resulting data can typically be fed directly to the GPU. Note that the output of step 3 can also be further compressed in transport using a general-purpose compression algorithm such as Deflate.
To pre-process the mesh, the following function should be called with the input index buffer:
```ts
reorderMesh: (indices: Uint32Array, triangles: boolean, optsize: boolean) => [Uint32Array, number];
```
The function optimizes the input array for locality of reference (make sure to pass `triangles=true` for triangle lists, and `false` otherwise). `optsize` can choose whether the order should be optimal for transmission size (recommended for Web) or for GPU rendering performance. The function changes the `indices` array in place and returns an additional remap array and the total number of unique vertices.
After this function returns, to maintain correct rendering the application should reorder all vertex streams - including morph targets if applicable - according to the remap array. For each original index, remap array contains the new location for that index (or `0xffffffff` if the value is unused), so the remapping pseudocode looks like this:
```ts
let newvertices = new VertexArray(unique); // unique is returned by reorderMesh
for (let i = 0; i < oldvertices.length; ++i)
if (remap[i] != 0xffffffff)
newvertices[remap[i]] = oldvertices[i];
```
When the input is a point cloud and not a triangle mesh, it is recommended to reorder the points using a specialized function that performs spatial sorting that can result in significant improvements in compression ratio by the subsequent processing:
```ts
reorderPoints: (positions: Float32Array, positions_stride: number) => Uint32Array;
```
This function returns a remap array just like `reorderMesh`, so the vertices need to be reordered accordingly for every vertex stream - the `positions` input is not modified. Note that it assumes no index buffer is provided, as it is redundant for point clouds.
To quantize the attribute data (whether it represents a mesh component or something else like a rotation quaternion for a bone), typically some data-specific analysis should be performed to determine the optimal quantization strategy. For linear data such as positions or texture coordinates remapping the input range to 0..1 and quantizing the resulting integer using fixed-point encoding with a given number of bits stored in a 16-bit or 8-bit integer is recommended; however, this is not always best for compression ratio for data with complex cross-component dependencies.
To that end, three filter encoders are provided: octahedral (optimal for normal or tangent data), quaternion (optimal for unit-length quaternions) and exponential (optimal for compressing floating-point vectors). The last two are recommended for use for animation data, and exponential filter can additionally be used to quantize any floating-point vertex attribute for which integer quantization is not sufficiently precise.
```ts
encodeFilterOct: (source: Float32Array, count: number, stride: number, bits: number) => Uint8Array;
encodeFilterQuat: (source: Float32Array, count: number, stride: number, bits: number) => Uint8Array;
encodeFilterExp: (source: Float32Array, count: number, stride: number, bits: number, mode?: string) => Uint8Array;
encodeFilterColor: (source: Float32Array, count: number, stride: number, bits: number) => Uint8Array;
```
All these functions take a source floating point buffer as an input, and perform a complex transformation that, when reversed by a decoder, results in an optimally quantized decompressed output. Because of this these functions assume specific configuration of input and output data:
- `encodeFilterOct` takes each 4 floats from the source array (for a total of `count` 4-vectors), treats them as a unit vector (XYZ) and fourth component from -1..1 (W), and encodes them into `stride` bytes in a way that, when decoded, the result is stored as a normalized signed 4-vector. `stride` must be 4 (in which case the round-trip result is 4 8-bit normalized values) or 8 (in which case the round-trip result is 4 16-bit normalized values). This encoding is recommended for normals (with stride=4 for medium quality and 8 for high quality output) and tangents (with stride=4 providing enough quality in all cases; note that 4-th component is preserved in case it stores tangent frame winding). `bits` represents the desired precision of each component and must be in `[2..8]` range if `stride=4` and `[2..16]` range if `stride=8`.
- `encodeFilterQuat` takes each 4 floats from the source array (for a total of `count` 4-vectors), treats them as a unit quaternion, and encodes them into `stride` bytes in a way that, when decoded, the result is stored as a normalized signed 4-vector representing the same rotation as the source quaternion. `stride` must be 8 (the round-trip result is 4 16-bit normalized values). `bits` represents the desired precision of each component and must be in `[4..16]` range, although using less than 9-10 bits is likely going to lead to significant deviation in rotations.
- `encodeFilterExp` takes each K floats from the source array (where `K=stride/4`, for a total of `count` K-vectors), and encodes them into `stride` bytes in a way that, when decoded, the result is stored as K single-precision floating point values. This may seem redundant but it allows to trade some precision for a higher compression ratio due to reduced precision of stored components, controlled by `bits` which must be in `[1..24]` range, and a shared exponent encoding used by the function.
The `mode` parameter can be used to influence the exponent sharing and provides a tradeoff between compressed size and quality for various use cases, and can be one of 'Separate', 'SharedVector', 'SharedComponent' and 'Clamped' (defaulting to 'SharedVector').
- `encodeFilterColor` takes each 4 floats from the source array (for a total of `count` 4-vectors), treats them as an RGBA color with each component from 0..1, and encodes them into `stride` bytes in a way that, when decoded, the result is stored as a normalized unsigned 4-vector. `stride` must be 4 (in which case the round-trip result is 4 8-bit normalized values) or 8 (in which case the round-trip result is 4 16-bit normalized values). This encoding is recommended for colors (with stride=4 for medium quality and 8 for high quality output). `bits` represents the desired precision of each component and must be in `[2..8]` range if `stride=4` and `[2..16]` range if `stride=8`.
Note that in all cases using the highest `bits` value allowed by the output `stride` won't change the size of the output array (which is always going to be `count * stride` bytes), but it *will* reduce compression efficiency, as such the lowest acceptable `bits` value is recommended to use. When multiple parts of the data require different levels of precision, encode filters can be called multiple times and the output of the same filter called with the same `stride` can be concatenated even if `bits` are different.
After data is quantized using filter encoding or manual quantization, the result should be compressed using one of the following functions that mirror the interface of the decoding functions described above:
```ts
encodeVertexBuffer: (source: Uint8Array, count: number, size: number) => Uint8Array;
encodeVertexBufferLevel: (source: Uint8Array, count: number, size: number, level: number, version?: number) => Uint8Array;
encodeIndexBuffer: (source: Uint8Array, count: number, size: number) => Uint8Array;
encodeIndexSequence: (source: Uint8Array, count: number, size: number) => Uint8Array;
encodeGltfBuffer: (source: Uint8Array, count: number, size: number, mode: string, version?: number) => Uint8Array;
```
`size` is the size of each component in bytes; it must be divisible by 4 for attribute/vertex encoding and must be equal to 2 or 4 for index encoding; additionally, index buffer encoding assumes triangle lists as an input and as such count must be divisible by 3.
Note that the source is specified as byte arrays; for example, to quantize a position stream encoded using 16-bit integers with 5 vertices, `source` must have length of `5 * 8 = 40` bytes (8 bytes for each position - 3\*2 bytes of data and 2 bytes of padding to conform to alignment requirements), `count` must be 5 and `size` must be 8. When padding data to the alignment boundary make sure to use 0 as padding bytes for optimal compression.
When interleaved vertex data is compressed, `encodeVertexBuffer` can be called with the full size of a single interleaved vertex; however, when compressing deinterleaved data, note that `encodeVertexBuffer` should be called on each component individually if the strides of different streams are different.
By default, `encodeVertexBuffer` uses v1 version of the encoding; this encoding is compatible with `KHR_meshopt_compression` glTF extension but *not* with `EXT_meshopt_compression`. To encode data compatible with `EXT_meshopt_compression`, use `encodeVertexBufferLevel` with version=0, or - preferably - `encodeGltfBuffer`, which defaults to v0 (but can also be used to encode v1 content by passing `version=1`).
## Simplifier
`MeshoptSimplifier` (`meshopt_simplifier.js`) implements mesh simplification, producing a mesh with fewer triangles/points that resembles the original mesh in its appearance. The simplification algorithms are lossy and may result in significant change in appearance, but can often be used without visible visual degradation on high poly input meshes or for level of detail variants far away.
To simplify the mesh, the following function needs to be called first:
```ts
simplify(indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, target_index_count: number, target_error: number, flags?: SimplifierFlags[]) => [Uint32Array, number];
```
Given an input triangle mesh represented by an index buffer and a position buffer, the algorithm tries to simplify the mesh down to the target index count while maintaining the 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 possible to preprocess the index buffer to discard any vertex attributes that aren't critical and can be rebuilt later.
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.
The algorithm uses position data stored in a strided array; `vertex_positions_stride` represents the distance between subsequent positions in `Float32` units and should typically be set to 3. If the input position data is quantized, it's necessary to dequantize it so that the algorithm can estimate the position error correctly. While the algorithm doesn't use other attributes like normals/texture coordinates, it automatically recognizes and preserves attribute discontinuities based on index data. Because of this, for the algorithm to function well, the mesh vertices should be unique (de-duplicated).
Upon completion, the function returns the new index buffer as well as the resulting appearance error. The index buffer can be used to render the simplified mesh with the same vertex buffer(s) as the original one, including non-positional attributes. For example, `simplify` can be called multiple times with different target counts/errors, and the application can select the appropriate index buffer to render for the mesh at runtime to implement level of detail.
To control behavior of the algorithm more precisely, `flags` may specify an array of strings that enable various additional options:
- `'LockBorder'` locks the vertices that lie on the topological border of the mesh in place such that they don't move during simplification. This can be valuable to simplify independent chunks of a mesh, for example terrain, to ensure that individual levels of detail can be stitched together later without gaps.
- `'ErrorAbsolute'` 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 `getScale`.
- `'Sparse'` improves simplification performance assuming input indices are a sparse subset of the mesh. This can be useful when simplifying small mesh subsets independently. For consistency, it is recommended to use absolute errors when sparse simplification is desired.
- `'Prune'` allows removal of 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.
- `'Regularize'` 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. `'RegularizeLight'` can be used instead for a smaller regularization factor, reducing the impact on geometric quality.
- `'Permissive'` allows collapses across attribute discontinuities, except for vertices that are tagged with `2` via `vertex_lock`.
In addition to the `Prune` flag, you can explicitly prune isolated components under a target threshold by calling the `simplifyPrune` function:
```ts
simplifyPrune: (indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, target_error: number) => Uint32Array;
```
This can be done before regular simplification or as the only step, which is useful for scenarios like isosurface cleanup.
While `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, `simplifyWithAttributes`:
```ts
simplifyWithAttributes: (indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, vertex_attributes: Float32Array, vertex_attributes_stride: number, attribute_weights: number[], vertex_lock: Uint8Array | null, target_index_count: number, target_error: number, flags?: SimplifierFlags[]) => [Uint32Array, number];
```
This function takes an additional `vertex_attributes` buffer that contains all the attributes to be used. The `attribute_weights` array contains a weight for each attribute, which is used to balance the importance of each attribute during simplification. 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).
The optional `vertex_lock` parameter can be used to lock some vertices in place, preventing them from being moved during simplification. This is an array of the same length as the number of vertices, where `1` means that the vertex is locked and `0` means that it is free to move. This can be used to preserve seams or other important features of the mesh. In permissive mode, `2` can be used to protect seams while allowing vertices to collapse along them.
All simplification functions described so far reuse the original vertex buffer and only produce a new index buffer; 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, `simplifyWithUpdate`:
```ts
simplifyWithUpdate: (indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, vertex_attributes: Float32Array, vertex_attributes_stride: number, attribute_weights: number[], vertex_lock: Uint8Array | null, target_index_count: number, target_error: number, flags?: SimplifierFlags[]) => [number, number];
```
Unlike `simplify`/`simplifyWithAttributes`, this function updates the index buffer as well as vertex positions and attributes in place, and returns the resulting index count (which the `indices` array should be truncated to) and appearance error. 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 vertex data is no longer valid for rendering the original mesh, a new compact vertex buffer should be generated using `compactMesh`. If the original data was important, it should be copied before calling this function.
The library also provides another simplification algorithm, `simplifySloppy`, which doesn't follow the topology of the original mesh:
```ts
simplifySloppy: (indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, vertex_lock: Uint8Array | null, target_index_count: number, target_error: number) => [Uint32Array, number];
```
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 `simplify`, but may simplify further. The optional `vertex_lock` array can be used to lock some vertices in place, with `1` marking locked vertices, or can be `null`.
When the resulting mesh is stored, it might be desirable to remove the redundant vertices from the attribute buffers instead of simply using the original vertex data with the smaller index buffer. For that purpose, the simplifier module provides the `compactMesh` function, which is similar to `reorderMesh` function that the encoder provides, but doesn't perform extra optimizations and merely prepares a new vertex order that can be used to create new, smaller, vertex buffers:
```ts
compactMesh: (indices: Uint32Array) => [Uint32Array, number];
```
The simplification algorithm uses relative errors for input and output; to convert these errors to absolute units, they need to be multiplied by the scaling factor which depends on the mesh geometry and can be computed by calling the following function with the position data:
```ts
getScale: (vertex_positions: Float32Array, vertex_positions_stride: number) => number;
```
The algorithms `simplify` and `simplifyWithAttributes` work on triangle meshes. `MeshoptSimplifier` additionally provides an algorithm to simplify point clouds, with optional per-point color support:
```ts
simplifyPoints: (vertex_positions: Float32Array, vertex_positions_stride: number, target_vertex_count: number, vertex_colors?: Float32Array, vertex_colors_stride?: number, color_weight?: number) => Uint32Array;
```
`vertex_colors` is an optional buffer containing RGB colors, with 3 values per point; `color_weight` can be used to balance the importance of color preservation with position preservation, and can be set to `1.0` if the input colors are in `[0..1]` range.
The resulting indices can be used to render the simplified point cloud; similarly to triangle simplification, to reduce the memory footprint, the point cloud can be reindexed using the remap table returned by `compactMesh`.
## Clusterizer
`MeshoptClusterizer` (`meshopt_clusterizer.js`) implements meshlet generation and optimization.
To split a triangle mesh into clusters, call `buildMeshlets`, which tries to balance topological efficiency (by maximizing vertex reuse inside meshlets) with culling efficiency.
```ts
buildMeshlets(indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, max_vertices: number, max_triangles: number, cone_weight?: number) => MeshletBuffers;
```
The algorithm uses position data stored in a strided array; `vertex_positions_stride` represents the distance between subsequent positions in `Float32` units.
The maximum number of triangles and number of vertices per meshlet can be controlled via `max_triangles` and `max_vertices` parameters. However, `max_vertices` must not be greater than 256 and `max_triangles` must not be greater than 512.
Additionally, if cluster cone culling is to be used, `buildMeshlets` allows specifying a `cone_weight` as a value between 0 and 1 to balance culling efficiency with other forms of culling. By default, `cone_weight` is set to 0.
For finer control over triangle counts, use `buildMeshletsFlex`, which accepts minimum and maximum triangle limits and an optional `split_factor` to nudge large clusters to split sooner.
```ts
buildMeshletsFlex(indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, max_vertices: number, min_triangles: number, max_triangles: number, cone_weight?: number, split_factor?: number) => MeshletBuffers;
```
To favor spatial splits for ray tracing, `buildMeshletsSpatial` keeps the same controls but replaces cone weighting with `fill_weight` to trade off cluster fullness against SAH cost.
```ts
buildMeshletsSpatial(indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, max_vertices: number, min_triangles: number, max_triangles: number, fill_weight?: number) => MeshletBuffers;
```
All meshlets produced by these builders are implicitly optimized for better triangle and vertex locality.
The algorithm returns the meshlet data as packed buffers:
```ts
const buffers = MeshoptClusterizer.buildMeshlets(indices, positions, stride, /* args */);
console.log(buffers.meshlets); // prints the raw packed Uint32Array containing the meshlet data, i.e., the indices into the vertices and triangles array
console.log(buffers.vertices); // prints the raw packed Uint32Array containing the indices into the original meshes vertices
console.log(buffers.triangles); // prints the raw packed Uint8Array containing the indices into the vertices array.
console.log(buffers.meshletCount); // prints the number of meshlets - this is not the same as buffers.meshlets.length because each meshlet consists of 4 unsigned 32-bit integers
```
Individual meshlets can be extracted from the packed buffers using `extractMeshlet`. The memory of the returned `Meshlet` object's `vertices` and `triangles` arrays is backed by the `MeshletBuffers` object.
```ts
const buffers = MeshoptClusterizer.buildMeshlets(indices, positions, stride, /* args */);
const meshlet = MeshoptClusterizer.extractMeshlet(buffers, 0);
console.log(meshlet.vertices); // prints the packed Uint32Array of the first meshlet's vertex indices, i.e., indices into the original meshes vertex buffer
console.log(meshlet.triangles); // prints the packed Uint8Array of the first meshlet's indices into its own vertices array
console.log(MeshoptClusterizer.extractMeshlet(buffers, 0).triangles[0] === meshlet.triangles[0]) // prints true
meshlet.triangles.set([123], 0);
console.log(MeshoptClusterizer.extractMeshlet(buffers, 0).triangles[0] === meshlet.triangles[0]) // still prints true
```
After generating the meshlet data, it's also possible to generate extra culling data for one or more meshlets, producing an array of objects with per-meshlet bounding sphere and normal cone:
```ts
computeMeshletBounds(buffers: MeshletBuffers, vertex_positions: Float32Array, vertex_positions_stride: number) => Bounds[];
```
It is also possible to compute bounds of a vertex cluster that is not generated by `MeshoptClusterizer` using `computeClusterBounds`. Like `buildMeshlets`, this algorithm takes vertex indices and a strided vertex positions array with a vertex stride in `Float32` units as input.
```ts
computeClusterBounds(indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number) => Bounds;
```
Finally, it is possible to compute spherical bounds of an arbitrary set of points, which can be useful to compute bounds for arbitrary mesh subsets. Each point can have an optional radius; this can be used to merge the spherical bounds of multiple clusters. The inputs are provided as strided arrays with the stride in `Float32` units.
```ts
computeSphereBounds: (positions: Float32Array, positions_stride: number, radii?: Float32Array, radii_stride?: number) => Bounds;
```
## Tangents
`MeshoptTangents` (`meshopt_tangents.js`) implements tangent space generation for meshes that use tangent space normal maps. These meshes often require per-vertex tangent vectors in addition to normals; these could be exported alongside mesh data, but this module can generate them from positions, normals and texture coordinates:
```ts
generateTangents: (indices: Uint32Array | null, vertex_positions: Float32Array, vertex_positions_stride: number, vertex_normals: Float32Array, vertex_normals_stride: number, vertex_uvs: Float32Array, vertex_uvs_stride: number, flags?: TangentsFlags[]) => Float32Array;
```
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, 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, or using indexed data and copying tangents to existing vertex data while duplicating vertices with different tangents. 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.
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 `'Compatible'` option in `flags`.
Additionally, this module provides a function to generate vertex normals from positions alone:
```ts
generateNormals: (indices: Uint32Array | null, vertex_positions: Float32Array, vertex_positions_stride: number, crease_angle: number, smoothing?: number) => Float32Array;
```
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 `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]`).
Similarly to `generateTangents`, the function returns an array of 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.
## License
This library is available to anybody free of charge, under the terms of MIT License (see LICENSE.md).
+150
View File
@@ -0,0 +1,150 @@
import { MeshoptEncoder as encoder } from './meshopt_encoder.js';
import { MeshoptDecoder as decoder } from './meshopt_decoder.mjs';
import { performance } from 'perf_hooks';
process.on('unhandledRejection', (error) => {
console.log('unhandledRejection', error);
process.exit(1);
});
function bytes(view) {
return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
}
var tests = {
roundtripVertexBuffer: function () {
var N = 1024 * 1024;
var data = new Uint8Array(N * 16);
var lcg = 1;
for (var i = 0; i < N * 16; ++i) {
// mindstd_rand
lcg = (lcg * 48271) % 2147483647;
var k = i % 16;
if (k <= 8) data[i] = lcg & ((1 << k) - 1);
else data[i] = i & ((1 << (k - 8)) - 1);
}
var decoded = new Uint8Array(N * 16);
var t0 = performance.now();
var encoded = encoder.encodeVertexBuffer(data, N, 16);
var t1 = performance.now();
decoder.decodeVertexBuffer(decoded, N, 16, encoded);
var t2 = performance.now();
return { encodeVertex: t1 - t0, decodeVertex: t2 - t1, bytes: N * 16 };
},
roundtripIndexBuffer: function () {
var N = 1024 * 1024;
var data = new Uint32Array(N * 3);
for (var i = 0; i < N * 3; i += 6) {
var v = i / 6;
data[i + 0] = v;
data[i + 1] = v + 1;
data[i + 2] = v + 2;
data[i + 3] = v + 2;
data[i + 4] = v + 1;
data[i + 5] = v + 3;
}
var decoded = new Uint32Array(data.length);
var t0 = performance.now();
var encoded = encoder.encodeIndexBuffer(bytes(data), data.length, 4);
var t1 = performance.now();
decoder.decodeIndexBuffer(bytes(decoded), data.length, 4, encoded);
var t2 = performance.now();
return { encodeIndex: t1 - t0, decodeIndex: t2 - t1, bytes: N * 12 };
},
decodeGltf: function () {
var N = 1024 * 1024;
var data = new Uint8Array(N * 16);
for (var i = 0; i < N * 16; i += 4) {
data[i + 0] = 0;
data[i + 1] = (i % 16) * 1;
data[i + 2] = (i % 16) * 2;
data[i + 3] = (i % 16) * 8;
}
var decoded = new Uint8Array(N * 16);
var filters = [
{ name: 'none', filter: 'NONE', stride: 16 },
{ name: 'oct8', filter: 'OCTAHEDRAL', stride: 4 },
{ name: 'oct12', filter: 'OCTAHEDRAL', stride: 8 },
{ name: 'quat12', filter: 'QUATERNION', stride: 8 },
{ name: 'col8', filter: 'COLOR', stride: 4 },
{ name: 'col12', filter: 'COLOR', stride: 8 },
{ name: 'exp', filter: 'EXPONENTIAL', stride: 16 },
];
var results = { bytes: N * 16 };
for (var i = 0; i < filters.length; ++i) {
var f = filters[i];
var encoded = encoder.encodeVertexBuffer(data, (N * 16) / f.stride, f.stride);
var t0 = performance.now();
decoder.decodeGltfBuffer(decoded, (N * 16) / f.stride, f.stride, encoded, 'ATTRIBUTES', f.filter);
var t1 = performance.now();
results[f.name] = t1 - t0;
}
return results;
},
};
Promise.all([encoder.ready, decoder.ready]).then(() => {
var reps = 10;
var data = {};
for (var key in tests) {
data[key] = tests[key]();
}
for (var i = 1; i < reps; ++i) {
for (var key in tests) {
var nd = tests[key]();
var od = data[key];
for (var idx in nd) {
od[idx] = Math.min(od[idx], nd[idx]);
}
}
}
for (var key in tests) {
var rep = key;
rep += ':\n';
for (var idx in data[key]) {
if (idx != 'bytes') {
rep += idx;
rep += ' ';
rep += data[key][idx].toFixed(3);
rep += ' ms (';
rep += ((data[key].bytes / 1e9 / data[key][idx]) * 1000).toFixed(3);
rep += ' GB/s)';
if (key == 'decodeGltf' && idx != 'none') {
rep += '; filter ';
rep += ((data[key].bytes / 1e9 / (data[key][idx] - data[key]['none'])) * 1000).toFixed(3);
rep += ' GB/s';
}
rep += '\n';
}
}
console.log(rep);
}
});
+5
View File
@@ -0,0 +1,5 @@
export * from './meshopt_encoder.js';
export * from './meshopt_decoder.js';
export * from './meshopt_simplifier.js';
export * from './meshopt_clusterizer.js';
export * from './meshopt_tangents.js';
+5
View File
@@ -0,0 +1,5 @@
export * from './meshopt_encoder.js';
export * from './meshopt_decoder.mjs';
export * from './meshopt_simplifier.js';
export * from './meshopt_clusterizer.js';
export * from './meshopt_tangents.js';
+69
View File
@@ -0,0 +1,69 @@
// This file is part of meshoptimizer library and is distributed under the terms of MIT License.
// Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
export class Bounds {
centerX: number;
centerY: number;
centerZ: number;
radius: number;
coneApexX: number;
coneApexY: number;
coneApexZ: number;
coneAxisX: number;
coneAxisY: number;
coneAxisZ: number;
coneCutoff: number;
}
export class MeshletBuffers {
meshlets: Uint32Array;
vertices: Uint32Array;
triangles: Uint8Array;
meshletCount: number;
}
export class Meshlet {
vertices: Uint32Array;
triangles: Uint8Array;
}
export const MeshoptClusterizer: {
supported: boolean;
ready: Promise<void>;
buildMeshlets: (
indices: Uint32Array,
vertex_positions: Float32Array,
vertex_positions_stride: number,
max_vertices: number,
max_triangles: number,
cone_weight?: number
) => MeshletBuffers;
buildMeshletsFlex: (
indices: Uint32Array,
vertex_positions: Float32Array,
vertex_positions_stride: number,
max_vertices: number,
min_triangles: number,
max_triangles: number,
cone_weight?: number,
split_factor?: number
) => MeshletBuffers;
buildMeshletsSpatial: (
indices: Uint32Array,
vertex_positions: Float32Array,
vertex_positions_stride: number,
max_vertices: number,
min_triangles: number,
max_triangles: number,
fill_weight?: number
) => MeshletBuffers;
extractMeshlet: (buffers: MeshletBuffers, index: number) => Meshlet;
computeClusterBounds: (indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number) => Bounds;
computeMeshletBounds: (buffers: MeshletBuffers, vertex_positions: Float32Array, vertex_positions_stride: number) => Bounds[];
computeSphereBounds: (positions: Float32Array, positions_stride: number, radii?: Float32Array, radii_stride?: number) => Bounds;
};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,135 @@
import assert from 'assert/strict';
import { MeshoptClusterizer as clusterizer } from './meshopt_clusterizer.js';
process.on('unhandledRejection', (error) => {
console.log('unhandledRejection', error);
process.exit(1);
});
const cubeWithNormals = {
vertices: new Float32Array([
// n = (0, 0, 1)
-1.0, -1.0, 1.0, 0.0, 0.0, 1.0, 1.0, -1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, -1.0, 1.0, 1.0, 0.0, 0.0, 1.0,
// n = (0, 0, -1)
-1.0, 1.0, -1.0, 0.0, 0.0, -1.0, 1.0, 1.0, -1.0, 0.0, 0.0, -1.0, 1.0, -1.0, -1.0, 0.0, 0.0, -1.0, -1.0, -1.0, -1.0, 0.0, 0.0, -1.0,
// n = (1, 0, 0)
1.0, -1.0, -1.0, 1.0, 0.0, 0.0, 1.0, 1.0, -1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, -1.0, 1.0, 1.0, 0.0, 0.0,
// n = (-1, 0, 0)
-1.0, -1.0, 1.0, -1.0, 0.0, 0.0, -1.0, 1.0, 1.0, -1.0, 0.0, 0.0, -1.0, 1.0, -1.0, -1.0, 0.0, 0.0, -1.0, -1.0, -1.0, -1.0, 0.0, 0.0,
// n = (0, 1, 0)
1.0, 1.0, -1.0, 0.0, 1.0, 0.0, -1.0, 1.0, -1.0, 0.0, 1.0, 0.0, -1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0,
// n = (0, -1, 0)
1.0, -1.0, 1.0, 0.0, -1.0, 0.0, -1.0, -1.0, 1.0, 0.0, -1.0, 0.0, -1.0, -1.0, -1.0, 0.0, -1.0, 0.0, 1.0, -1.0, -1.0, 0.0, -1.0, 0.0,
]),
indices: new Uint32Array([
// n = (0, 0, 1)
0, 1, 2, 2, 3, 0,
// n = (0, 0, -1)
4, 5, 6, 6, 7, 4,
// n = (1, 0, 0)
8, 9, 10, 10, 11, 8,
// n = (-1, 0, 0)
12, 13, 14, 14, 15, 12,
// n = (0, 1, 0)
16, 17, 18, 18, 19, 16,
// n = (0, -1, 0)
20, 21, 22, 22, 23, 20,
]),
vertexStride: 6, // in floats
};
const tests = {
buildMeshlets: function () {
const maxVertices = 4;
const buffers = clusterizer.buildMeshlets(cubeWithNormals.indices, cubeWithNormals.vertices, cubeWithNormals.vertexStride, maxVertices, 512);
const expectedVertices = [
new Uint32Array([6, 7, 4, 5]),
new Uint32Array([14, 15, 12, 13]),
new Uint32Array([2, 3, 0, 1]),
new Uint32Array([20, 21, 22, 23]),
new Uint32Array([10, 11, 8, 9]),
new Uint32Array([18, 19, 16, 17]),
];
const expectedTriangles = new Uint8Array([0, 1, 2, 2, 3, 0]);
assert.equal(buffers.meshletCount, 6);
for (let i = 0; i < buffers.meshletCount; ++i) {
const m = clusterizer.extractMeshlet(buffers, i);
assert.deepStrictEqual(m.vertices, expectedVertices[i]);
assert.deepStrictEqual(m.triangles, expectedTriangles);
}
},
computeClusterBounds: function () {
for (let i = 0; i < 6; ++i) {
const indexOffset = i * 6;
const normalOffset = i * 4 * cubeWithNormals.vertexStride;
const bounds = clusterizer.computeClusterBounds(
cubeWithNormals.indices.subarray(indexOffset, 6 + indexOffset),
cubeWithNormals.vertices,
cubeWithNormals.vertexStride
);
assert.deepStrictEqual(
new Int32Array([bounds.coneAxisX, bounds.coneAxisY, bounds.coneAxisZ]),
new Int32Array(cubeWithNormals.vertices.subarray(3 + normalOffset, 6 + normalOffset))
);
}
},
computeMeshletBounds: function () {
const maxVertices = 4;
const buffers = clusterizer.buildMeshlets(cubeWithNormals.indices, cubeWithNormals.vertices, cubeWithNormals.vertexStride, maxVertices, 512);
const expectedNormals = [
new Int32Array([0, 0, -1]),
new Int32Array([-1, 0, 0]),
new Int32Array([0, 0, 1]),
new Int32Array([0, -1, 0]),
new Int32Array([1, 0, 0]),
new Int32Array([0, 1, 0]),
];
const bounds = clusterizer.computeMeshletBounds(buffers, cubeWithNormals.vertices, cubeWithNormals.vertexStride);
assert(bounds.length === 6);
assert(bounds.length === buffers.meshletCount);
bounds.forEach((b, i) => {
const normal = new Int32Array([b.coneAxisX, b.coneAxisY, b.coneAxisZ]);
assert.deepStrictEqual(normal, expectedNormals[i]);
});
},
computeSphereBounds: function () {
// positions without per-point radii (tetrahedron)
const positions = new Float32Array([0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1]);
const bp = clusterizer.computeSphereBounds(positions, 3);
assert(Math.abs(bp.centerX - 0.5) < 1e-3);
assert(Math.abs(bp.centerY - 0.5) < 1e-3);
assert(Math.abs(bp.centerZ - 0.5) < 1e-3);
assert(bp.radius < 0.87);
// use 4th float of each element as radius (last point has radius=3 enveloping others)
const radii = new Float32Array([0, 1, 2, 3]);
const br = clusterizer.computeSphereBounds(positions, 3, radii, 1);
assert(Math.abs(br.centerX - 1.0) < 1e-3);
assert(Math.abs(br.centerY - 0.0) < 1e-3);
assert(Math.abs(br.centerZ - 1.0) < 1e-3);
assert(Math.abs(br.radius - 3.0) < 1e-3);
},
};
clusterizer.ready.then(() => {
var count = 0;
for (var key in tests) {
tests[key]();
count++;
}
console.log(count, 'tests passed');
});
File diff suppressed because one or more lines are too long
+15
View File
@@ -0,0 +1,15 @@
// This file is part of meshoptimizer library and is distributed under the terms of MIT License.
// Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
export const MeshoptDecoder: {
supported: boolean;
ready: Promise<void>;
decodeVertexBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array, filter?: string) => void;
decodeIndexBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array) => void;
decodeIndexSequence: (target: Uint8Array, count: number, size: number, source: Uint8Array) => void;
decodeGltfBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array, mode: string, filter?: string) => void;
useWorkers: (count: number) => void;
decodeGltfBufferAsync: (count: number, size: number, source: Uint8Array, mode: string, filter?: string) => Promise<Uint8Array>;
};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,356 @@
import assert from 'assert/strict';
import { MeshoptDecoder as decoder } from './meshopt_decoder.mjs';
process.on('unhandledRejection', (error) => {
console.log('unhandledRejection', error);
process.exit(1);
});
var tests = {
decodeVertexBuffer: function () {
var encoded = new Uint8Array([
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,
]);
var expected = new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 1, 0, 0, 0, 0, 0, 0, 244, 1, 0, 0, 0, 0, 44, 1, 0, 0, 0, 0, 0, 0, 244, 1, 44, 1, 44, 1, 0, 0, 0,
0, 244, 1, 244, 1,
]);
var result = new Uint8Array(expected.length);
decoder.decodeVertexBuffer(result, 4, 12, encoded);
assert.deepStrictEqual(result, expected);
},
decodeVertexBuffer_More: function () {
var encoded = new Uint8Array([
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,
]);
var expected = new Uint8Array([
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,
]);
var result = new Uint8Array(expected.length);
decoder.decodeVertexBuffer(result, 16, 4, encoded);
assert.deepStrictEqual(result, expected);
},
decodeVertexBuffer_Mode2: function () {
var encoded = new Uint8Array([
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,
]);
var expected = new Uint8Array([
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,
]);
var result = new Uint8Array(expected.length);
decoder.decodeVertexBuffer(result, 16, 4, encoded);
assert.deepStrictEqual(result, expected);
},
decodeVertexBufferV1: function () {
var encoded = new Uint8Array([
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,
]);
var expected = new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 1, 0, 0, 0, 0, 0, 0, 244, 1, 0, 0, 0, 0, 44, 1, 0, 0, 0, 0, 0, 0, 244, 1, 44, 1, 44, 1, 0, 0, 0,
0, 244, 1, 244, 1,
]);
var result = new Uint8Array(expected.length);
decoder.decodeVertexBuffer(result, 4, 12, encoded);
assert.deepStrictEqual(result, expected);
},
decodeVertexBufferV1_Custom: function () {
var encoded = new Uint8Array([
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,
]);
var expected = new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 1, 0, 0, 0, 0, 0, 0, 244, 1, 0, 0, 0, 0, 44, 1, 0, 0, 0, 0, 0, 0, 244, 1, 44, 1, 44, 1, 0, 0, 0,
0, 244, 1, 244, 1,
]);
var result = new Uint8Array(expected.length);
decoder.decodeVertexBuffer(result, 4, 12, encoded);
assert.deepStrictEqual(result, expected);
},
decodeVertexBufferV1_Deltas: function () {
var encoded = new Uint8Array([
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,
]);
var expected = new Uint16Array([
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,
]);
var result = new Uint16Array(expected.length);
decoder.decodeVertexBuffer(new Uint8Array(result.buffer), 16, 8, encoded);
assert.deepStrictEqual(result, expected);
},
decodeVertexBufferV1_BitXorRotate: function () {
var encoded = new Uint8Array([
0xa1, 0xab, 0xab, 0xfa, 0xff, 0x00, 0x02, 0x02, 0x02, 0x00, 0xa5, 0xc2, 0x3a, 0x00, 0xab, 0xf9, 0x57, 0x00, 0x95, 0x42, 0x85, 0x00, 0xdd,
0xca, 0x6d, 0x00, 0xac, 0x1a, 0x50, 0x00, 0x5c, 0x99, 0xfe, 0x00, 0x4d, 0x0c, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70,
0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x0c, 0x22, 0x26, 0x3b, 0x86, 0x00, 0x00, 0x12, 0x00,
]);
var expected = new Uint32Array([
0, 112, 201818112, 2252023330, 1, 29, 1188167680, 1600748723, 2, 126, 1739489280, 1696368920, 3, 155, 621084672, 1218163169,
]);
var result = new Uint32Array(expected.length);
decoder.decodeVertexBuffer(new Uint8Array(result.buffer), 4, 16, encoded);
assert.deepStrictEqual(result, expected);
},
decodeIndexBuffer16: function () {
var encoded = new Uint8Array([
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,
]);
var expected = new Uint16Array([0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9]);
var result = new Uint16Array(expected.length);
decoder.decodeIndexBuffer(new Uint8Array(result.buffer), 12, 2, encoded);
assert.deepEqual(result, expected);
},
decodeIndexBuffer32: function () {
var encoded = new Uint8Array([
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,
]);
var expected = new Uint32Array([0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9]);
var result = new Uint32Array(expected.length);
decoder.decodeIndexBuffer(new Uint8Array(result.buffer), 12, 4, encoded);
assert.deepStrictEqual(result, expected);
},
decodeIndexBufferV1: function () {
var encoded = new Uint8Array([
0xe1, 0xf0, 0x10, 0xfe, 0x1f, 0x3d, 0x00, 0x0a, 0x00, 0x76, 0x87, 0x56, 0x67, 0x78, 0xa9, 0x86, 0x65, 0x89, 0x68, 0x98, 0x01, 0x69, 0x00,
0x00,
]);
var expected = new Uint32Array([0, 1, 2, 2, 1, 3, 0, 1, 2, 2, 1, 5, 2, 1, 4]);
var result = new Uint32Array(expected.length);
decoder.decodeIndexBuffer(new Uint8Array(result.buffer), 15, 4, encoded);
assert.deepStrictEqual(result, expected);
},
decodeIndexBufferV1_More: function () {
var encoded = new Uint8Array([
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,
]);
var expected = new Uint32Array([0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9]);
var result = new Uint32Array(expected.length);
decoder.decodeIndexBuffer(new Uint8Array(result.buffer), 12, 4, encoded);
assert.deepStrictEqual(result, expected);
},
decodeIndexBufferV1_3Edges: function () {
var encoded = new Uint8Array([
0xe1, 0xf0, 0x20, 0x30, 0x40, 0x00, 0x76, 0x87, 0x56, 0x67, 0x78, 0xa9, 0x86, 0x65, 0x89, 0x68, 0x98, 0x01, 0x69, 0x00, 0x00,
]);
var expected = new Uint32Array([0, 1, 2, 1, 0, 3, 2, 1, 4, 0, 2, 5]);
var result = new Uint32Array(expected.length);
decoder.decodeIndexBuffer(new Uint8Array(result.buffer), 12, 4, encoded);
assert.deepStrictEqual(result, expected);
},
decodeIndexSequence: function () {
var encoded = new Uint8Array([0xd1, 0x00, 0x04, 0xcd, 0x01, 0x04, 0x07, 0x98, 0x1f, 0x00, 0x00, 0x00, 0x00]);
var expected = new Uint32Array([0, 1, 51, 2, 49, 1000]);
var result = new Uint32Array(expected.length);
decoder.decodeIndexSequence(new Uint8Array(result.buffer), 6, 4, encoded);
assert.deepStrictEqual(result, expected);
},
decodeFilterOct8: function () {
var encoded = new Uint8Array([
0xa0, 0x01, 0x07, 0x00, 0x00, 0x00, 0x1e, 0x01, 0x3f, 0x00, 0x00, 0x00, 0x8b, 0x8c, 0xfd, 0x00, 0x01, 0x26, 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, 0x01, 0x7f, 0x00,
]);
var expected = new Uint8Array([0, 1, 127, 0, 0, 159, 82, 1, 255, 1, 127, 0, 1, 130, 241, 1]);
var result = new Uint8Array(expected.length);
decoder.decodeVertexBuffer(new Uint8Array(result.buffer), 4, 4, encoded, /* filter= */ 'OCTAHEDRAL');
assert.deepStrictEqual(result, expected);
},
decodeFilterOct12: function () {
var encoded = new Uint8Array([
0xa0, 0x01, 0x0f, 0x00, 0x00, 0x00, 0x3d, 0x5a, 0x01, 0x0f, 0x00, 0x00, 0x00, 0x0e, 0x0d, 0x01, 0x3f, 0x00, 0x00, 0x00, 0x9a, 0x99, 0x26,
0x01, 0x3f, 0x00, 0x00, 0x00, 0x0e, 0x0d, 0x0a, 0x00, 0x00, 0x01, 0x26, 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, 0x01, 0x00, 0xff, 0x07,
0x00, 0x00,
]);
var expected = new Uint16Array([0, 16, 32767, 0, 0, 32621, 3088, 1, 32764, 16, 471, 0, 307, 28541, 16093, 1]);
var result = new Uint16Array(expected.length);
decoder.decodeVertexBuffer(new Uint8Array(result.buffer), 4, 8, encoded, /* filter= */ 'OCTAHEDRAL');
assert.deepStrictEqual(result, expected);
},
decodeFilterQuat12: function () {
var encoded = new Uint8Array([
0xa0, 0x01, 0x0f, 0x00, 0x00, 0x00, 0x3d, 0x5a, 0x01, 0x0f, 0x00, 0x00, 0x00, 0x0e, 0x0d, 0x01, 0x3f, 0x00, 0x00, 0x00, 0x9a, 0x99, 0x26,
0x01, 0x3f, 0x00, 0x00, 0x00, 0x0e, 0x0d, 0x0a, 0x00, 0x00, 0x01, 0x2a, 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, 0x01, 0x00, 0x00, 0x00,
0xfc, 0x07,
]);
var expected = new Uint16Array([32767, 0, 11, 0, 0, 25013, 0, 21166, 11, 0, 23504, 22830, 158, 14715, 0, 29277]);
var result = new Uint16Array(expected.length);
decoder.decodeVertexBuffer(new Uint8Array(result.buffer), 4, 8, encoded, /* filter= */ 'QUATERNION');
assert.deepStrictEqual(result, expected);
},
decodeFilterExp: function () {
var encoded = new Uint8Array([
0xa0, 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, 0x00, 0x03, 0x00, 0x00, 0xff, 0xf7, 0xff, 0xff, 0x02, 0xff,
0xff, 0x7f, 0xfe,
]);
var expected = new Uint32Array([0, 0x3fc00000, 0xc2100000, 0x49fffffe]);
var result = new Uint32Array(expected.length);
decoder.decodeVertexBuffer(new Uint8Array(result.buffer), 1, 16, encoded, /* filter= */ 'EXPONENTIAL');
assert.deepStrictEqual(result, expected);
},
decodeFilterColor8: function () {
var encoded = new Uint8Array([
0xa0, 0x01, 0x3f, 0x00, 0x00, 0x00, 0x7e, 0x7d, 0x4c, 0x01, 0x3f, 0x00, 0x00, 0x00, 0xfd, 0xfd, 0xfe, 0x01, 0x3f, 0x00, 0x00, 0x00, 0x83,
0x82, 0x80, 0x01, 0x3f, 0x00, 0x00, 0x00, 0x7d, 0x3f, 0x7e, 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, 0x40, 0x7f, 0xc1, 0xff,
]);
var expected = new Uint8Array([254, 1, 0, 255, 0, 254, 0, 128, 1, 0, 255, 64, 102, 102, 102, 191]);
var result = new Uint8Array(expected.length);
decoder.decodeVertexBuffer(new Uint8Array(result.buffer), 4, 4, encoded, /* filter= */ 'COLOR');
assert.deepStrictEqual(result, expected);
},
decodeFilterColor12: function () {
var encoded = new Uint8Array([
0xa0, 0x01, 0x1b, 0x00, 0x00, 0x00, 0xcc, 0x01, 0x3f, 0x00, 0x00, 0x00, 0x06, 0x05, 0x04, 0x01, 0x29, 0x00, 0x00, 0x00, 0x01, 0x3f, 0x00,
0x00, 0x00, 0x0d, 0x0f, 0x10, 0x01, 0x38, 0x00, 0x00, 0x00, 0x03, 0x01, 0x3f, 0x00, 0x00, 0x00, 0x16, 0x15, 0x08, 0x01, 0x21, 0x00, 0x00,
0x00, 0x01, 0x3f, 0x00, 0x00, 0x00, 0x05, 0x03, 0x06, 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, 0x04, 0xff, 0x07, 0x01, 0xfc, 0xff, 0x0f,
]);
var expected = new Uint16Array([65519, 16, 0, 65535, 0, 65519, 0, 32776, 16, 0, 65535, 16388, 26214, 26214, 26214, 49147]);
var result = new Uint16Array(expected.length);
decoder.decodeVertexBuffer(new Uint8Array(result.buffer), 4, 8, encoded, /* filter= */ 'COLOR');
assert.deepStrictEqual(result, expected);
},
decodeGltfBuffer: function () {
var encoded = new Uint8Array([
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,
]);
var expected = new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 1, 0, 0, 0, 0, 0, 0, 244, 1, 0, 0, 0, 0, 44, 1, 0, 0, 0, 0, 0, 0, 244, 1, 44, 1, 44, 1, 0, 0, 0,
0, 244, 1, 244, 1,
]);
var result = new Uint8Array(expected.length);
decoder.decodeGltfBuffer(result, 4, 12, encoded, /* mode= */ 'ATTRIBUTES');
assert.deepStrictEqual(result, expected);
},
decodeGltfBufferAsync: function () {
var encoded = new Uint8Array([
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,
]);
var expected = new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 1, 0, 0, 0, 0, 0, 0, 244, 1, 0, 0, 0, 0, 44, 1, 0, 0, 0, 0, 0, 0, 244, 1, 44, 1, 44, 1, 0, 0, 0,
0, 244, 1, 244, 1,
]);
decoder.decodeGltfBufferAsync(4, 12, encoded, /* mode= */ 'ATTRIBUTES').then(function (result) {
assert.deepStrictEqual(result, expected);
});
},
};
decoder.ready.then(() => {
var count = 0;
for (var key in tests) {
tests[key]();
count++;
}
console.log(count, 'tests passed');
});
@@ -0,0 +1,472 @@
// This file is part of meshoptimizer library and is distributed under the terms of MIT License.
// Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
// This is the reference decoder implementation by Jasper St. Pierre.
// It follows the decoder interface and should be a drop-in replacement for the actual decoder from meshopt_decoder module
// It is provided for educational value and is not recommended for use in production because it's not performance-optimized.
const MeshoptDecoder = {};
MeshoptDecoder.supported = true;
MeshoptDecoder.ready = Promise.resolve();
function assert(cond) {
if (!cond) {
throw new Error('Assertion failed');
}
}
function dezig(v) {
// Equivalent to (v & 1) !== 0 ? ~(v >>> 1) : v >>> 1
return (v >>> 1) ^ -(v & 1);
}
MeshoptDecoder.decodeVertexBuffer = (target, elementCount, byteStride, source, filter) => {
assert(source[0] === 0xa0 || source[0] === 0xa1);
const version = source[0] & 0x0f;
const maxBlockElements = Math.min((0x2000 / byteStride) & ~0x000f, 0x100);
const deltas = new Uint8Array(maxBlockElements * byteStride);
const tailSize = version === 0 ? byteStride : byteStride + byteStride / 4;
const tailDataOffs = source.length - tailSize;
// What deltas are stored relative to
const tempData = source.slice(tailDataOffs, tailDataOffs + byteStride);
// Channel modes for v1
const channels = version === 0 ? null : source.slice(tailDataOffs + byteStride, tailDataOffs + tailSize);
let srcOffs = 1; // Skip header byte
const headerModes = [
[0, 2, 4, 8], // v0
[0, 1, 2, 4], // v1, when control is 0
[1, 2, 4, 8], // v1, when control is 1
];
// Attribute blocks
for (let dstElemBase = 0; dstElemBase < elementCount; dstElemBase += maxBlockElements) {
const attrBlockElementCount = Math.min(elementCount - dstElemBase, maxBlockElements);
const groupCount = ((attrBlockElementCount + 0x0f) & ~0x0f) >>> 4;
const headerByteCount = ((groupCount + 0x03) & ~0x03) >>> 2;
// Control modes for v1
const controlBitsOffs = srcOffs;
srcOffs += version === 0 ? 0 : byteStride / 4;
// Zero out deltas to simplify logic
deltas.fill(0x00);
// Data blocks
for (let byte = 0; byte < byteStride; byte++) {
const deltaBase = byte * attrBlockElementCount;
// Control mode for current byte for v1
const controlMode = version === 0 ? 0 : (source[controlBitsOffs + (byte >>> 2)] >>> ((byte & 0x03) << 1)) & 0x03;
if (controlMode === 2) {
// All byte deltas are 0; no data is stored for this byte
continue;
} else if (controlMode === 3) {
// Byte deltas are stored uncompressed with no header bits
deltas.set(source.subarray(srcOffs, srcOffs + attrBlockElementCount), deltaBase);
srcOffs += attrBlockElementCount;
continue;
}
// Header bits are omitted for v1 when using control modes 2/3
const headerBitsOffs = srcOffs;
srcOffs += headerByteCount;
for (let group = 0; group < groupCount; group++) {
const mode = (source[headerBitsOffs + (group >>> 2)] >>> ((group & 0x03) << 1)) & 0x03;
const modeBits = headerModes[version === 0 ? 0 : controlMode + 1][mode];
const deltaOffs = deltaBase + (group << 4);
if (modeBits === 0) {
// All 16 byte deltas are 0; the size of the encoded block is 0 bytes
} else if (modeBits === 1) {
// Deltas are using 1-bit sentinel encoding; the size of the encoded block is [2..18] bytes
const srcBase = srcOffs;
srcOffs += 0x02;
for (let m = 0; m < 0x10; m++) {
// Bits are stored from least significant to most significant for 1-bit encoding
const shift = m & 0x07;
let delta = (source[srcBase + (m >>> 3)] >>> shift) & 0x01;
if (delta === 1) delta = source[srcOffs++];
deltas[deltaOffs + m] = delta;
}
} else if (modeBits === 2) {
// Deltas are using 2-bit sentinel encoding; the size of the encoded block is [4..20] bytes
const srcBase = srcOffs;
srcOffs += 0x04;
for (let m = 0; m < 0x10; m++) {
// 0 = >>> 6, 1 = >>> 4, 2 = >>> 2, 3 = >>> 0
const shift = 6 - ((m & 0x03) << 1);
let delta = (source[srcBase + (m >>> 2)] >>> shift) & 0x03;
if (delta === 3) delta = source[srcOffs++];
deltas[deltaOffs + m] = delta;
}
} else if (modeBits === 4) {
// Deltas are using 4-bit sentinel encoding; the size of the encoded block is [8..24] bytes
const srcBase = srcOffs;
srcOffs += 0x08;
for (let m = 0; m < 0x10; m++) {
// 0 = >>> 6, 1 = >>> 4, 2 = >>> 2, 3 = >>> 0
const shift = 4 - ((m & 0x01) << 2);
let delta = (source[srcBase + (m >>> 1)] >>> shift) & 0x0f;
if (delta === 0xf) delta = source[srcOffs++];
deltas[deltaOffs + m] = delta;
}
} else {
// All 16 byte deltas are stored verbatim; the size of the encoded block is 16 bytes
deltas.set(source.subarray(srcOffs, srcOffs + 0x10), deltaOffs);
srcOffs += 0x10;
}
}
}
// Go through and apply deltas to data
for (let elem = 0; elem < attrBlockElementCount; elem++) {
const dstElem = dstElemBase + elem;
for (let byteGroup = 0; byteGroup < byteStride; byteGroup += 4) {
let channelMode = version === 0 ? 0 : channels[byteGroup >>> 2] & 0x03;
assert(channelMode !== 0x03);
if (channelMode === 0) {
// Channel 0 (byte deltas): Byte deltas are stored as zigzag-encoded differences between the byte values of the element and the byte values of the previous element in the same position.
for (let byte = byteGroup; byte < byteGroup + 4; byte++) {
const delta = dezig(deltas[byte * attrBlockElementCount + elem]);
const temp = (tempData[byte] + delta) & 0xff; // wrap around
const dstOffs = dstElem * byteStride + byte;
target[dstOffs] = tempData[byte] = temp;
}
} else if (channelMode === 1) {
// Channel 1 (2-byte deltas): 2-byte deltas are computed as zigzag-encoded differences between 16-bit values of the element and the previous element in the same position.
for (let byte = byteGroup; byte < byteGroup + 4; byte += 2) {
const delta = dezig(deltas[byte * attrBlockElementCount + elem] + (deltas[(byte + 1) * attrBlockElementCount + elem] << 8));
let temp = tempData[byte] + (tempData[byte + 1] << 8);
temp = (temp + delta) & 0xffff; // wrap around
const dstOffs = dstElem * byteStride + byte;
target[dstOffs] = tempData[byte] = temp & 0xff;
target[dstOffs + 1] = tempData[byte + 1] = temp >>> 8;
}
} else if (channelMode === 2) {
// Channel 2 (4-byte XOR deltas): 4-byte deltas are computed as XOR between 32-bit values of the element and the previous element in the same position, with an additional rotation applied based on the high 4 bits of the channel mode byte.
const byte = byteGroup;
const delta =
deltas[byte * attrBlockElementCount + elem] +
(deltas[(byte + 1) * attrBlockElementCount + elem] << 8) +
(deltas[(byte + 2) * attrBlockElementCount + elem] << 16) +
(deltas[(byte + 3) * attrBlockElementCount + elem] << 24);
let temp = tempData[byte] + (tempData[byte + 1] << 8) + (tempData[byte + 2] << 16) + (tempData[byte + 3] << 24);
const rot = channels[byteGroup >>> 2] >>> 4;
temp = temp ^ ((delta >>> rot) | (delta << (32 - rot))); // rotate and XOR
const dstOffs = dstElem * byteStride + byte;
target[dstOffs] = tempData[byte] = temp & 0xff;
target[dstOffs + 1] = tempData[byte + 1] = (temp >>> 8) & 0xff;
target[dstOffs + 2] = tempData[byte + 2] = (temp >>> 16) & 0xff;
target[dstOffs + 3] = tempData[byte + 3] = temp >>> 24;
}
}
}
}
const tailSizePadded = Math.max(tailSize, version === 0 ? 32 : 24);
assert(srcOffs == source.length - tailSizePadded);
// Filters - only applied if filter isn't undefined or NONE
if (filter === 'OCTAHEDRAL') {
assert(byteStride === 4 || byteStride === 8);
const dst = new (byteStride === 4 ? Int8Array : Int16Array)(target.buffer, target.byteOffset, elementCount * 4);
const maxInt = byteStride === 4 ? 127 : 32767;
for (let i = 0; i < elementCount * 4; i += 4) {
let x = dst[i + 0],
y = dst[i + 1],
one = dst[i + 2];
x /= one;
y /= one;
const z = 1.0 - Math.abs(x) - Math.abs(y);
const t = Math.max(-z, 0.0);
x -= x >= 0 ? t : -t;
y -= y >= 0 ? t : -t;
const h = maxInt / Math.sqrt(x * x + y * y + z * z);
dst[i + 0] = Math.round(x * h);
dst[i + 1] = Math.round(y * h);
dst[i + 2] = Math.round(z * h);
// keep dst[i + 3] as is
}
} else if (filter === 'QUATERNION') {
assert(byteStride === 8);
const dst = new Int16Array(target.buffer, target.byteOffset, elementCount * 4);
for (let i = 0; i < elementCount * 4; i += 4) {
const inputW = dst[i + 3];
const maxComponent = inputW & 0x03;
const s = Math.SQRT1_2 / (inputW | 0x03);
let x = dst[i + 0] * s;
let y = dst[i + 1] * s;
let z = dst[i + 2] * s;
let w = Math.sqrt(Math.max(0.0, 1.0 - x * x - y * y - z * z));
dst[i + ((maxComponent + 1) % 4)] = Math.round(x * 32767);
dst[i + ((maxComponent + 2) % 4)] = Math.round(y * 32767);
dst[i + ((maxComponent + 3) % 4)] = Math.round(z * 32767);
dst[i + ((maxComponent + 0) % 4)] = Math.round(w * 32767);
}
} else if (filter === 'EXPONENTIAL') {
assert((byteStride & 0x03) === 0x00);
const expBits = new Uint32Array(1);
const expFloat = new Float32Array(expBits.buffer);
const src = new Int32Array(target.buffer, target.byteOffset, elementCount * (byteStride / 4));
const dst = new Float32Array(target.buffer, target.byteOffset, elementCount * (byteStride / 4));
for (let i = 0; i < elementCount * (byteStride / 4); i++) {
const v = src[i];
const exp = v >> 24,
mantissa = (v << 8) >> 8;
expBits[0] = (exp + 127) << 23; // Equivalent to 2**exp
dst[i] = expFloat[0] * mantissa;
}
} else if (filter === 'COLOR') {
assert(byteStride === 4 || byteStride === 8);
const maxInt = (1 << (byteStride * 2)) - 1;
const data = new (byteStride === 4 ? Uint8Array : Uint16Array)(target.buffer, target.byteOffset, elementCount * 4);
const dataSigned = new (byteStride === 4 ? Int8Array : Int16Array)(target.buffer, target.byteOffset, elementCount * 4);
for (let i = 0; i < elementCount * 4; i += 4) {
const y = data[i + 0];
const co = dataSigned[i + 1];
const cg = dataSigned[i + 2];
const alphaInput = data[i + 3];
// Recover scale from alpha high bit - find highest bit set
const alphaBit = 31 - Math.clz32(alphaInput);
const as = (1 << (alphaBit + 1)) - 1;
// YCoCg to RGB conversion
const r = y + co - cg;
const g = y + cg;
const b = y - co - cg;
// Expand alpha by one bit, replicating last bit
let a = alphaInput & (as >> 1);
a = (a << 1) | (a & 1);
// Scale to full range
const ss = maxInt / as;
// Store result
data[i + 0] = Math.round(r * ss);
data[i + 1] = Math.round(g * ss);
data[i + 2] = Math.round(b * ss);
data[i + 3] = Math.round(a * ss);
}
}
};
function readfifo(fifo, n) {
return fifo[(fifo.offset - 1 - n) & (fifo.length - 1)];
}
function pushfifo(fifo, n) {
const offset = fifo.offset;
fifo[offset] = n;
fifo.offset = (offset + 1) & (fifo.length - 1);
}
MeshoptDecoder.decodeIndexBuffer = (target, count, byteStride, source) => {
assert(source[0] === 0xe1);
assert(count % 3 === 0);
assert(byteStride === 2 || byteStride === 4);
let dst;
if (byteStride === 2) dst = new Uint16Array(target.buffer);
else dst = new Uint32Array(target.buffer);
const triCount = count / 3;
let codeOffs = 0x01;
let dataOffs = codeOffs + triCount;
let codeauxOffs = source.length - 0x10;
function readLEB128() {
let n = 0;
for (let i = 0; ; i += 7) {
const b = source[dataOffs++];
n |= (b & 0x7f) << i;
if (b < 0x80) return n;
}
}
let next = 0,
last = 0;
const edgefifo = new Uint32Array(32);
const vertexfifo = new Uint32Array(16);
edgefifo.offset = 0;
vertexfifo.offset = 0;
function decodeIndex(v) {
return (last += dezig(v));
}
let dstOffs = 0;
for (let i = 0; i < triCount; i++) {
const code = source[codeOffs++];
const b0 = code >>> 4,
b1 = code & 0x0f;
if (b0 < 0x0f) {
const a = readfifo(edgefifo, (b0 << 1) + 0),
b = readfifo(edgefifo, (b0 << 1) + 1);
let c = -1;
if (b1 === 0x00) {
c = next++;
pushfifo(vertexfifo, c);
} else if (b1 < 0x0d) {
c = readfifo(vertexfifo, b1);
} else if (b1 === 0x0d) {
c = --last;
pushfifo(vertexfifo, c);
} else if (b1 === 0x0e) {
c = ++last;
pushfifo(vertexfifo, c);
} else if (b1 === 0x0f) {
const v = readLEB128();
c = decodeIndex(v);
pushfifo(vertexfifo, c);
}
// fifo pushes happen backwards
pushfifo(edgefifo, b);
pushfifo(edgefifo, c);
pushfifo(edgefifo, c);
pushfifo(edgefifo, a);
dst[dstOffs++] = a;
dst[dstOffs++] = b;
dst[dstOffs++] = c;
} else {
// b0 === 0x0F
let a = -1,
b = -1,
c = -1;
if (b1 < 0x0e) {
const e = source[codeauxOffs + b1];
const z = e >>> 4,
w = e & 0x0f;
a = next++;
if (z === 0x00) b = next++;
else b = readfifo(vertexfifo, z - 1);
if (w === 0x00) c = next++;
else c = readfifo(vertexfifo, w - 1);
pushfifo(vertexfifo, a);
if (z === 0x00) pushfifo(vertexfifo, b);
if (w === 0x00) pushfifo(vertexfifo, c);
} else {
const e = source[dataOffs++];
if (e === 0x00) next = 0;
const z = e >>> 4,
w = e & 0x0f;
if (b1 === 0x0e) a = next++;
else a = decodeIndex(readLEB128());
if (z === 0x00) b = next++;
else if (z === 0x0f) b = decodeIndex(readLEB128());
else b = readfifo(vertexfifo, z - 1);
if (w === 0x00) c = next++;
else if (w === 0x0f) c = decodeIndex(readLEB128());
else c = readfifo(vertexfifo, w - 1);
pushfifo(vertexfifo, a);
if (z === 0x00 || z === 0x0f) pushfifo(vertexfifo, b);
if (w === 0x00 || w === 0x0f) pushfifo(vertexfifo, c);
}
pushfifo(edgefifo, a);
pushfifo(edgefifo, b);
pushfifo(edgefifo, b);
pushfifo(edgefifo, c);
pushfifo(edgefifo, c);
pushfifo(edgefifo, a);
dst[dstOffs++] = a;
dst[dstOffs++] = b;
dst[dstOffs++] = c;
}
}
};
MeshoptDecoder.decodeIndexSequence = (target, count, byteStride, source) => {
assert(source[0] === 0xd1);
assert(byteStride === 2 || byteStride === 4);
let dst;
if (byteStride === 2) dst = new Uint16Array(target.buffer);
else dst = new Uint32Array(target.buffer);
let dataOffs = 0x01;
function readLEB128() {
let n = 0;
for (let i = 0; ; i += 7) {
const b = source[dataOffs++];
n |= (b & 0x7f) << i;
if (b < 0x80) return n;
}
}
const last = new Uint32Array(2);
for (let i = 0; i < count; i++) {
const v = readLEB128();
const b = v & 0x01;
const delta = dezig(v >>> 1);
dst[i] = last[b] += delta;
}
};
MeshoptDecoder.decodeGltfBuffer = (target, count, size, source, mode, filter) => {
const table = {
ATTRIBUTES: MeshoptDecoder.decodeVertexBuffer,
TRIANGLES: MeshoptDecoder.decodeIndexBuffer,
INDICES: MeshoptDecoder.decodeIndexSequence,
};
assert(table[mode] !== undefined);
table[mode](target, count, size, source, filter);
};
MeshoptDecoder.decodeGltfBufferAsync = (count, size, source, mode, filter) => {
const target = new Uint8Array(count * size);
MeshoptDecoder.decodeGltfBuffer(target, count, size, source, mode, filter);
return Promise.resolve(target);
};
// node.js interface:
// for (let k in MeshoptDecoder) exports[k] = MeshoptDecoder[k];
export { MeshoptDecoder };
+23
View File
@@ -0,0 +1,23 @@
// This file is part of meshoptimizer library and is distributed under the terms of MIT License.
// Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
export type ExpMode = 'Separate' | 'SharedVector' | 'SharedComponent' | 'Clamped';
export const MeshoptEncoder: {
supported: boolean;
ready: Promise<void>;
reorderMesh: (indices: Uint32Array, triangles: boolean, optsize: boolean) => [Uint32Array, number];
reorderPoints: (positions: Float32Array, positions_stride: number) => Uint32Array;
encodeVertexBuffer: (source: Uint8Array, count: number, size: number) => Uint8Array;
encodeVertexBufferLevel: (source: Uint8Array, count: number, size: number, level: number, version?: number) => Uint8Array;
encodeIndexBuffer: (source: Uint8Array, count: number, size: number) => Uint8Array;
encodeIndexSequence: (source: Uint8Array, count: number, size: number) => Uint8Array;
encodeGltfBuffer: (source: Uint8Array, count: number, size: number, mode: string, version?: number) => Uint8Array;
encodeFilterOct: (source: Float32Array, count: number, stride: number, bits: number) => Uint8Array;
encodeFilterQuat: (source: Float32Array, count: number, stride: number, bits: number) => Uint8Array;
encodeFilterExp: (source: Float32Array, count: number, stride: number, bits: number, mode?: ExpMode) => Uint8Array;
encodeFilterColor: (source: Float32Array, count: number, stride: number, bits: number) => Uint8Array;
};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,243 @@
import assert from 'assert/strict';
import { MeshoptEncoder as encoder } from './meshopt_encoder.js';
import { MeshoptDecoder as decoder } from './meshopt_decoder.mjs';
process.on('unhandledRejection', (error) => {
console.log('unhandledRejection', error);
process.exit(1);
});
function bytes(view) {
return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
}
var tests = {
reorderMesh: function () {
var indices = new Uint32Array([4, 2, 5, 3, 1, 4, 0, 1, 3, 1, 2, 4]);
var expected = new Uint32Array([0, 1, 2, 3, 1, 0, 4, 3, 0, 5, 3, 4]);
var remap = new Uint32Array([5, 3, 1, 4, 0, 2]);
var res = encoder.reorderMesh(indices, /* triangles= */ true, /* optsize= */ true);
assert.deepEqual(indices, expected);
assert.deepEqual(res[0], remap);
assert.equal(res[1], 6); // unique
},
reorderPoints: function () {
var points = new Float32Array([1, 1, 1, 11, 11, 11, 2, 2, 2, 12, 12, 12]);
var expected = new Uint32Array([0, 2, 1, 3]);
var remap = encoder.reorderPoints(points, 3);
assert.deepEqual(remap, expected);
},
roundtripVertexBuffer: function () {
var data = new Uint8Array(16 * 4);
// this tests 0/2/4/8 bit groups in one stream
for (var i = 0; i < 16; ++i) {
data[i * 4 + 0] = 0;
data[i * 4 + 1] = i * 1;
data[i * 4 + 2] = i * 2;
data[i * 4 + 3] = i * 8;
}
var encoded = encoder.encodeVertexBuffer(data, 16, 4);
assert.equal(encoded[0], 0xa1);
var decoded = new Uint8Array(16 * 4);
decoder.decodeVertexBuffer(decoded, 16, 4, encoded);
assert.deepEqual(decoded, data);
},
roundtripVertexBufferV1: function () {
var data = new Uint8Array(16 * 4);
// this tests 0/2/4/8 bit groups in one stream
for (var i = 0; i < 16; ++i) {
data[i * 4 + 0] = 0;
data[i * 4 + 1] = i * 1;
data[i * 4 + 2] = i * 2;
data[i * 4 + 3] = i * 8;
}
var encoded = encoder.encodeVertexBufferLevel(data, 16, 4, 3, /* version= */ 1);
assert.equal(encoded[0], 0xa1);
var decoded = new Uint8Array(16 * 4);
decoder.decodeVertexBuffer(decoded, 16, 4, encoded);
assert.deepEqual(decoded, data);
},
roundtripIndexBuffer: function () {
var data = new Uint32Array([0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9]);
var encoded = encoder.encodeIndexBuffer(bytes(data), data.length, 4);
var decoded = new Uint32Array(data.length);
decoder.decodeIndexBuffer(bytes(decoded), data.length, 4, encoded);
assert.deepEqual(decoded, data);
},
roundtripIndexBuffer16: function () {
var data = new Uint16Array([0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9]);
var encoded = encoder.encodeIndexBuffer(bytes(data), data.length, 2);
var decoded = new Uint16Array(data.length);
decoder.decodeIndexBuffer(bytes(decoded), data.length, 2, encoded);
assert.deepEqual(decoded, data);
},
roundtripIndexSequence: function () {
var data = new Uint32Array([0, 1, 51, 2, 49, 1000]);
var encoded = encoder.encodeIndexSequence(bytes(data), data.length, 4);
var decoded = new Uint32Array(data.length);
decoder.decodeIndexSequence(bytes(decoded), data.length, 4, encoded);
assert.deepEqual(decoded, data);
},
roundtripIndexSequence16: function () {
var data = new Uint16Array([0, 1, 51, 2, 49, 1000]);
var encoded = encoder.encodeIndexSequence(bytes(data), data.length, 2);
var decoded = new Uint16Array(data.length);
decoder.decodeIndexSequence(bytes(decoded), data.length, 2, encoded);
assert.deepEqual(decoded, data);
},
encodeFilterOct8: function () {
var data = new Float32Array([1, 0, 0, 0, 0, -1, 0, 0, 0.7071068, 0, 0.707168, 1, -0.7071068, 0, -0.707168, 1]);
var expected = new Uint8Array([0x7f, 0, 0x7f, 0, 0, 0x81, 0x7f, 0, 0x3f, 0, 0x7f, 0x7f, 0x81, 0x40, 0x7f, 0x7f]);
// 4 vectors, encode each vector into 4 bytes with 8 bits of precision/component
var encoded = encoder.encodeFilterOct(data, 4, 4, 8);
assert.deepEqual(encoded, expected);
},
encodeFilterOct12: function () {
var data = new Float32Array([1, 0, 0, 0, 0, -1, 0, 0, 0.7071068, 0, 0.707168, 1, -0.7071068, 0, -0.707168, 1]);
var expected = new Uint16Array([0x7ff, 0, 0x7ff, 0, 0x0, 0xf801, 0x7ff, 0, 0x3ff, 0, 0x7ff, 0x7fff, 0xf801, 0x400, 0x7ff, 0x7fff]);
// 4 vectors, encode each vector into 8 bytes with 12 bits of precision/component
var encoded = encoder.encodeFilterOct(data, 4, 8, 12);
assert.deepEqual(encoded, bytes(expected));
},
encodeFilterQuat12: function () {
var data = new Float32Array([1, 0, 0, 0, 0, -1, 0, 0, 0.7071068, 0, 0, 0.707168, -0.7071068, 0, 0, -0.707168]);
var expected = new Uint16Array([0, 0, 0, 0x7fc, 0, 0, 0, 0x7fd, 0x7ff, 0, 0, 0x7ff, 0x7ff, 0, 0, 0x7ff]);
// 4 quaternions, encode each quaternion into 8 bytes with 12 bits of precision/component
var encoded = encoder.encodeFilterQuat(data, 4, 8, 12);
assert.deepEqual(encoded, bytes(expected));
},
encodeFilterExp: function () {
var data = new Float32Array([1, -23.4, -0.1]);
var expected = new Uint32Array([0xf7000200, 0xf7ffd133, 0xf7ffffcd]);
// 1 vector with 3 components (12 bytes), encode each vector into 12 bytes with 15 bits of precision/component
var encoded = encoder.encodeFilterExp(data, 1, 12, 15);
assert.deepEqual(encoded, bytes(expected));
},
encodeFilterExpMode: function () {
var data = new Float32Array([1, -23.4, -0.1, 11.0]);
var expected = new Uint32Array([0xf3002000, 0xf7ffd133, 0xf3fffccd, 0xf7001600]);
// 2 vectors with 2 components (8 bytes), encode each vector into 8 bytes with 15 bits of precision/component
var encoded = encoder.encodeFilterExp(data, 2, 8, 15, 'SharedComponent');
assert.deepEqual(encoded, bytes(expected));
},
encodeFilterExpClamp: function () {
var data = new Float32Array([1, -23.4, -0.1]);
var expected = new Uint32Array([0xf3002000, 0xf7ffd133, 0xf2fff99a]);
// 1 vector with 3 components (12 bytes), encode each vector into 12 bytes with 15 bits of precision/component
// exponents are separate but clamped to 0
var encoded = encoder.encodeFilterExp(data, 1, 12, 15, 'Clamped');
assert.deepEqual(encoded, bytes(expected));
},
encodeFilterColor8: function () {
var data = new Float32Array([1, 0, 0, 1, 0, 1, 0, 0.5, 0, 0, 1, 0.25, 0.4, 0.4, 0.4, 0.75]);
var expected = new Uint8Array([0x40, 0x7f, 0xc1, 0xff, 0x7f, 0x00, 0x7f, 0xc0, 0x40, 0x81, 0xc0, 0xa0, 0x66, 0x00, 0x00, 0xdf]);
// 4 vectors, encode each vector into 4 bytes with 8 bits of precision/component
var encoded = encoder.encodeFilterColor(data, 4, 4, 8);
assert.deepEqual(encoded, expected);
},
encodeFilterColor12: function () {
var data = new Float32Array([1, 0, 0, 1, 0, 1, 0, 0.5, 0, 0, 1, 0.25, 0.4, 0.4, 0.4, 0.75]);
var expected = new Uint16Array([
0x0400, 0x07ff, 0xfc01, 0x0fff, 0x07ff, 0x0000, 0x07ff, 0x0c00, 0x0400, 0xf801, 0xfc00, 0x0a00, 0x0666, 0x0000, 0x0000, 0x0dff,
]);
// 4 vectors, encode each vector into 8 bytes with 12 bits of precision/component
var encoded = encoder.encodeFilterColor(data, 4, 8, 12);
assert.deepEqual(encoded, bytes(expected));
},
encodeGltfBuffer: function () {
var data = new Uint32Array([0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9]);
var encoded = encoder.encodeGltfBuffer(bytes(data), data.length, 4, 'TRIANGLES');
var decoded = new Uint32Array(data.length);
decoder.decodeGltfBuffer(bytes(decoded), data.length, 4, encoded, 'TRIANGLES');
assert.equal(encoded[0], 0xe1);
assert.deepEqual(decoded, data);
},
encodeGltfBufferAttribute: function () {
var data = new Uint32Array([0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9]);
var encoded = encoder.encodeGltfBuffer(bytes(data), data.length, 4, 'ATTRIBUTES');
var decoded = new Uint32Array(data.length);
decoder.decodeGltfBuffer(bytes(decoded), data.length, 4, encoded, 'ATTRIBUTES');
assert.equal(encoded[0], 0xa0);
assert.deepEqual(decoded, data);
},
encodeGltfBufferAttributeV1: function () {
var data = new Uint32Array([0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9]);
var encoded = encoder.encodeGltfBuffer(bytes(data), data.length, 4, 'ATTRIBUTES', 1);
var decoded = new Uint32Array(data.length);
decoder.decodeGltfBuffer(bytes(decoded), data.length, 4, encoded, 'ATTRIBUTES');
assert.equal(encoded[0], 0xa1);
assert.deepEqual(decoded, data);
},
};
Promise.all([encoder.ready, decoder.ready]).then(() => {
var count = 0;
for (var key in tests) {
tests[key]();
count++;
}
console.log(count, 'tests passed');
});
+80
View File
@@ -0,0 +1,80 @@
// This file is part of meshoptimizer library and is distributed under the terms of MIT License.
// Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
export type SimplifierFlags = 'LockBorder' | 'Sparse' | 'ErrorAbsolute' | 'Prune' | 'Regularize' | 'Permissive' | 'RegularizeLight' | 'PreserveFolds' | 'ErrorClamped';
export type RemesherFlags = 'Thicken' | 'Shell' | 'Solve';
/** @deprecated Use SimplifierFlags instead. */
export type Flags = SimplifierFlags;
export const MeshoptSimplifier: {
supported: boolean;
ready: Promise<void>;
compactMesh: (indices: Uint32Array) => [Uint32Array, number];
generatePositionRemap: (vertex_positions: Float32Array, vertex_positions_stride: number) => Uint32Array;
simplify: (
indices: Uint32Array,
vertex_positions: Float32Array,
vertex_positions_stride: number,
target_index_count: number,
target_error: number,
flags?: SimplifierFlags[]
) => [Uint32Array, number];
simplifyWithAttributes: (
indices: Uint32Array,
vertex_positions: Float32Array,
vertex_positions_stride: number,
vertex_attributes: Float32Array,
vertex_attributes_stride: number,
attribute_weights: number[],
vertex_lock: Uint8Array | null,
target_index_count: number,
target_error: number,
flags?: SimplifierFlags[]
) => [Uint32Array, number];
simplifyWithUpdate: (
indices: Uint32Array,
vertex_positions: Float32Array,
vertex_positions_stride: number,
vertex_attributes: Float32Array,
vertex_attributes_stride: number,
attribute_weights: number[],
vertex_lock: Uint8Array | null,
target_index_count: number,
target_error: number,
flags?: SimplifierFlags[]
) => [number, number];
simplifySloppy: (
indices: Uint32Array,
vertex_positions: Float32Array,
vertex_positions_stride: number,
vertex_lock: Uint8Array | null,
target_index_count: number,
target_error: number
) => [Uint32Array, number];
getScale: (vertex_positions: Float32Array, vertex_positions_stride: number) => number;
simplifyPoints: (
vertex_positions: Float32Array,
vertex_positions_stride: number,
target_vertex_count: number,
vertex_colors?: Float32Array,
vertex_colors_stride?: number,
color_weight?: number
) => Uint32Array;
simplifyPrune: (indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, target_error: number) => Uint32Array;
remesh: (
indices: Uint32Array,
vertex_positions: Float32Array,
vertex_positions_stride: number,
resolution: number,
flags?: RemesherFlags[]
) => Float32Array;
};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,203 @@
import assert from 'assert/strict';
import { MeshoptSimplifier as simplifier } from './meshopt_simplifier.js';
process.on('unhandledRejection', (error) => {
console.log('unhandledRejection', error);
process.exit(1);
});
var tests = {
compactMesh: function () {
var indices = new Uint32Array([0, 1, 3, 3, 1, 5]);
var expected = new Uint32Array([0, 1, 2, 2, 1, 3]);
var missing = 2 ** 32 - 1;
var remap = new Uint32Array([0, 1, missing, 2, missing, 3]);
var res = simplifier.compactMesh(indices);
assert.deepEqual(indices, expected);
assert.deepEqual(res[0], remap);
assert.equal(res[1], 4); // unique
},
simplify: function () {
// 0
// 1 2
// 3 4 5
var indices = new Uint32Array([0, 2, 1, 1, 2, 3, 3, 2, 4, 2, 5, 4]);
var positions = new Float32Array([0, 4, 0, 0, 1, 0, 2, 2, 0, 0, 0, 0, 1, 0, 0, 4, 0, 0]);
var res = simplifier.simplify(indices, positions, 3, /* target indices */ 3, /* target error */ 0.01);
var expected = new Uint32Array([0, 5, 3]);
assert.deepEqual(res[0], expected);
assert(res[1] < 1e-4); // error
},
simplify16: function () {
// 0
// 1 2
// 3 4 5
var indices = new Uint16Array([0, 2, 1, 1, 2, 3, 3, 2, 4, 2, 5, 4]);
var positions = new Float32Array([0, 4, 0, 0, 1, 0, 2, 2, 0, 0, 0, 0, 1, 0, 0, 4, 0, 0]);
var res = simplifier.simplify(indices, positions, 3, /* target indices */ 3, /* target error */ 0.01);
var expected = new Uint16Array([0, 5, 3]);
assert.deepEqual(res[0], expected);
assert(res[1] < 1e-4); // error
},
simplifyLockBorder: function () {
// 0
// 1 2
// 3 4 5
var indices = new Uint32Array([0, 2, 1, 1, 2, 3, 3, 2, 4, 2, 5, 4]);
var positions = new Float32Array([0, 2, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0]);
var res = simplifier.simplify(indices, positions, 3, /* target indices */ 3, /* target error */ 0.01, ['LockBorder']);
var expected = new Uint32Array([0, 2, 1, 1, 2, 3, 3, 2, 4, 2, 5, 4]);
assert.deepEqual(res[0], expected);
assert(res[1] < 1e-4); // error
},
simplifyAttr: function () {
var vb_pos = new Float32Array(8 * 3 * 3);
var vb_att = new Float32Array(8 * 3 * 3);
for (var y = 0; y < 8; ++y) {
// first four rows are a blue gradient, next four rows are a yellow gradient
var r = y < 4 ? 0.8 + y * 0.05 : 0;
var g = y < 4 ? 0.8 + y * 0.05 : 0;
var b = y < 4 ? 0 : 0.8 + (7 - y) * 0.05;
for (var x = 0; x < 3; ++x) {
vb_pos[(y * 3 + x) * 3 + 0] = x;
vb_pos[(y * 3 + x) * 3 + 1] = y;
vb_pos[(y * 3 + x) * 3 + 2] = 0.03 * x + 0.028 * (y % 2) + (x == 2 && y == 7 ? 1 : 0) * 0.03;
vb_att[(y * 3 + x) * 3 + 0] = r;
vb_att[(y * 3 + x) * 3 + 1] = g;
vb_att[(y * 3 + x) * 3 + 2] = b;
}
}
var ib = new Uint32Array(7 * 2 * 6);
for (var y = 0; y < 7; ++y) {
for (var x = 0; x < 2; ++x) {
ib[(y * 2 + x) * 6 + 0] = (y + 0) * 3 + (x + 0);
ib[(y * 2 + x) * 6 + 1] = (y + 0) * 3 + (x + 1);
ib[(y * 2 + x) * 6 + 2] = (y + 1) * 3 + (x + 0);
ib[(y * 2 + x) * 6 + 3] = (y + 1) * 3 + (x + 0);
ib[(y * 2 + x) * 6 + 4] = (y + 0) * 3 + (x + 1);
ib[(y * 2 + x) * 6 + 5] = (y + 1) * 3 + (x + 1);
}
}
var attr_weights = [0.5, 0.5, 0.5];
var res = simplifier.simplifyWithAttributes(ib, vb_pos, 3, vb_att, 3, attr_weights, null, 6 * 3, 1e-2);
var expected = new Uint32Array([0, 2, 11, 0, 11, 9, 9, 11, 12, 12, 11, 14, 12, 14, 23, 12, 23, 21]);
assert.deepEqual(res[0], expected);
},
simplifyUpdate: function () {
var indices = new Uint32Array([0, 1, 3, 3, 1, 4, 4, 1, 2, 0, 3, 2, 3, 4, 2]);
var positions = new Float32Array([0, 0, 0, 1, 1, 0, 2, 0, 0, 0.9, 0.2, 0.1, 1.1, 0.2, 0.1]);
var attributes = new Float32Array([0, 0, 0, 0.2, 0.1]);
var res = simplifier.simplifyWithUpdate(indices, positions, 3, attributes, 1, [1], null, 9, 1);
var expected = new Uint32Array([0, 1, 3, 3, 1, 2, 0, 3, 2]);
assert.equal(res[0], expected.length);
assert.deepEqual(indices.subarray(0, expected.length), expected);
// border vertices haven't moved but may have small floating point drift
for (var i = 0; i < 3; ++i) {
assert(Math.abs(attributes[i]) < 1e-6);
}
// center vertex got updated
assert(Math.abs(positions[3 * 3 + 0] - 0.88) < 1e-2);
assert(Math.abs(positions[3 * 3 + 1] - 0.19) < 1e-2);
assert(Math.abs(positions[3 * 3 + 2] - 0.11) < 1e-2);
assert(Math.abs(attributes[3] - 0.18) < 1e-2);
},
simplifyLockFlags: function () {
// 0
// 1 2
// 3 4 5
var indices = new Uint32Array([0, 2, 1, 1, 2, 3, 3, 2, 4, 2, 5, 4]);
var positions = new Float32Array([0, 2, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0]);
var locks = new Uint8Array([1, 1, 1, 1, 0, 1]); // only vertex 4 can move
var res = simplifier.simplifyWithAttributes(indices, positions, 3, new Float32Array(), 0, [], locks, 3, 0.01);
var expected = new Uint32Array([0, 2, 1, 1, 2, 3, 2, 5, 3]);
assert.deepEqual(res[0], expected);
assert(res[1] < 1e-4); // error
},
getScale: function () {
var positions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3]);
assert(simplifier.getScale(positions, 3) == 3.0);
},
simplifyPoints: function () {
var positions = new Float32Array([0, 0, 0, 100, 0, 0, 100, 1, 1, 110, 0, 0]);
var colors = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]);
var expected = new Uint32Array([0, 1]);
var expectedC = new Uint32Array([0, 2]);
var res = simplifier.simplifyPoints(positions, 3, 2);
assert.deepEqual(res, expected);
// note: recommended value for color_weight is 1e-2 but here we push color weight to be very high to bias candidate selection for testing
var resC1 = simplifier.simplifyPoints(positions, 3, 2, colors, 3, 1e-1);
assert.deepEqual(resC1, expectedC);
var resC2 = simplifier.simplifyPoints(positions, 3, 2, colors, 3, 1e-2);
assert.deepEqual(resC2, expected);
},
simplifyPrune: function () {
var indices = new Uint32Array([0, 1, 2, 3, 4, 5, 6, 7, 8]);
var positions = new Float32Array([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]);
var expected = new Uint32Array([6, 7, 8]);
var res = simplifier.simplifyPrune(indices, positions, 3, 0.5);
assert.deepEqual(res, expected);
},
};
Promise.all([simplifier.ready]).then(() => {
var count = 0;
for (var key in tests) {
tests[key]();
count++;
}
console.log(count, 'tests passed');
});
+28
View File
@@ -0,0 +1,28 @@
// This file is part of meshoptimizer library and is distributed under the terms of MIT License.
// Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
export type TangentsFlags = 'Compatible' | 'ZeroFallback';
export const MeshoptTangents: {
supported: boolean;
ready: Promise<void>;
generateTangents: (
indices: Uint32Array | null,
vertex_positions: Float32Array,
vertex_positions_stride: number,
vertex_normals: Float32Array,
vertex_normals_stride: number,
vertex_uvs: Float32Array,
vertex_uvs_stride: number,
flags?: TangentsFlags[]
) => Float32Array;
generateNormals: (
indices: Uint32Array | null,
vertex_positions: Float32Array,
vertex_positions_stride: number,
crease_angle: number,
smoothing?: number
) => Float32Array;
};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,124 @@
import assert from 'assert/strict';
import { MeshoptTangents as tangents } from './meshopt_tangents.js';
process.on('unhandledRejection', (error) => {
console.log('unhandledRejection', error);
process.exit(1);
});
var tests = {
tangentsBasic: function () {
// unindexed quad with matching corner data for shared diagonal
var positions = new Float32Array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0]);
var normals = new Float32Array([-0.28, 0, 0.96, 0.28, 0, 0.96, 0.28, 0, 0.96, -0.28, 0, 0.96, 0.28, 0, 0.96, -0.28, 0, 0.96]);
var uvs = new Float32Array([0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1]);
// (1, 0, 0) reprojected onto tilted normals
var left = [0.96, 0, 0.28, 1];
var right = [0.96, 0, -0.28, 1];
var expected = [left, right, right, left, right, left].flat();
// unindexed input: indices = null
var result = tangents.generateTangents(null, positions, 3, normals, 3, uvs, 2, []);
assert.equal(result.length, expected.length);
for (var i = 0; i < result.length; ++i) {
assert(Math.abs(result[i] - expected[i]) < 1e-3);
}
// shared vertices get the same tangent vector
for (var k = 0; k < 4; ++k) {
assert(result[0 * 4 + k] === result[3 * 4 + k]); // diag 1
assert(result[2 * 4 + k] === result[4 * 4 + k]); // diag 2
}
},
tangentDegenerate: function () {
var positions = new Float32Array([0, 0, 0, 1, 1, 0, 2, 2, 0, -1, -2, 1, 0, 1, 1, -1, 0, 0]);
var normals = new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]);
var uvs = new Float32Array([0, 0, 1, 1, 2, 2, 0, -2, 1, 0, 1, 0]);
var indices = new Uint32Array([
// outer, positive
0, 3, 1,
// inner, degenerate UVs
0, 1, 2,
// outer, positive
1, 4, 2,
// outer, negative
2, 5, 0,
]);
var vx = 0.0836;
var vy = 0.9965;
var expected = [
// outer, positive
[1, 0, 0, 1],
[1, 0, 0, 1],
[vx, vy, 0, 1],
// inner, degenerate UVs
[1, 0, 0, 1],
[vx, vy, 0, 1],
[0, 1, 0, 1],
// outer, positive
[vx, vy, 0, 1],
[0, 1, 0, 1],
[0, 1, 0, 1],
// outer, negative
[-1, 0, 0, -1],
[-1, 0, 0, -1],
[-1, 0, 0, -1],
].flat();
var result = tangents.generateTangents(indices, positions, 3, normals, 3, uvs, 2, ['Compatible']);
assert.equal(result.length, expected.length);
for (var i = 0; i < result.length; ++i) {
assert(Math.abs(result[i] - expected[i]) < 1e-3);
}
},
normalsBasic: function () {
var positions = new Float32Array([-1, -0.57735, 0, 1, -0.57735, 0, 0, 1.1547, 0, 0, 0, 0.38]);
// flattened tetrahedron (apex is closer to the base so that we can get soft side edges)
var indices = new Uint32Array([0, 2, 1, 0, 1, 3, 1, 2, 3, 2, 0, 3]);
var base = [0, 0, -1];
var side0 = [-0.2707, -0.1563, 0.9499];
var side1 = [0.2707, -0.1563, 0.9499];
var side2 = [0, 0.3126, 0.9499];
var apex = [0, 0, 1];
var expected = [base, base, base, side0, side1, apex, side1, side2, apex, side2, side0, apex].flat();
var result = tangents.generateNormals(indices, positions, 3, Math.PI / 3);
assert.equal(result.length, expected.length);
for (var i = 0; i < result.length; ++i) {
assert(Math.abs(result[i] - expected[i]) < 1e-3);
}
// shared vertices around soft side edges get the same normal vector
for (var k = 0; k < 3; ++k) {
assert(result[3 * 3 + k] === result[10 * 3 + k]); // side0
assert(result[4 * 3 + k] === result[6 * 3 + k]); // side1
assert(result[7 * 3 + k] === result[9 * 3 + k]); // side2
assert(result[5 * 3 + k] === result[8 * 3 + k]); // apex
assert(result[5 * 3 + k] === result[11 * 3 + k]); // apex
}
},
};
tangents.ready.then(() => {
var count = 0;
for (var key in tests) {
tests[key]();
count++;
}
console.log(count, 'tests passed');
});
+59
View File
@@ -0,0 +1,59 @@
{
"name": "meshoptimizer",
"version": "1.2.0",
"description": "Mesh optimization library that makes meshes smaller and faster to render",
"author": "Arseny Kapoulkine",
"license": "MIT",
"bugs": "https://github.com/zeux/meshoptimizer/issues",
"homepage": "https://github.com/zeux/meshoptimizer",
"keywords": [
"compression",
"mesh"
],
"repository": {
"type": "git",
"url": "https://github.com/zeux/meshoptimizer"
},
"files": [
"*.cjs",
"*.mjs",
"*.js",
"*.ts"
],
"type": "module",
"main": "index.js",
"types": "index.d.ts",
"exports": {
".": {
"types": "./index.d.ts",
"default": "./index.js"
},
"./encoder": {
"types": "./meshopt_encoder.d.ts",
"default": "./meshopt_encoder.js"
},
"./decoder": {
"types": "./meshopt_decoder.d.ts",
"default": "./meshopt_decoder.mjs"
},
"./simplifier": {
"types": "./meshopt_simplifier.d.ts",
"default": "./meshopt_simplifier.js"
},
"./clusterizer": {
"types": "./meshopt_clusterizer.d.ts",
"default": "./meshopt_clusterizer.js"
},
"./tangents": {
"types": "./meshopt_tangents.d.ts",
"default": "./meshopt_tangents.js"
},
"./decoder.cjs": {
"require": "./meshopt_decoder.cjs"
}
},
"scripts": {
"test": "node meshopt_encoder.test.js && node meshopt_decoder.test.js && node meshopt_simplifier.test.js && node meshopt_clusterizer.test.js && node meshopt_tangents.test.js",
"prepublishOnly": "npm test"
}
}
+54
View File
@@ -0,0 +1,54 @@
// Usage:
// 1. import { wasi_trace } from './wasi_trace.js';
// 2. Pass wasi_trace as an import object to WebAssembly.instantiate
// 3. Call wasi_trace.init(instance) after instantiation
var instance;
var wasi_snapshot_preview1 = {
fd_close: function () {
return 8;
},
fd_seek: function () {
return 8;
},
fd_fdstat_get: function (fd, stat) {
// needed for isatty() to enable line buffering for stdout
var heap = new DataView(instance.exports.memory.buffer);
heap.setUint8(stat, 2);
for (var i = 1; i < 24; ++i) heap.setUint8(stat + i, 0);
return 0;
},
fd_write: function (fd, iovs, iovs_len, nwritten) {
var heap = new DataView(instance.exports.memory.buffer);
var written = 0;
var str = '';
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 buf_data = new Uint8Array(heap.buffer, buf, buf_len);
for (var j = 0; j < buf_data.length; ++j) {
str += String.fromCharCode(buf_data[j]);
}
written += buf_len;
}
console.log(str);
heap.setUint32(nwritten, written, true);
return 0;
},
};
var wasi_trace = {
wasi_snapshot_preview1,
init: function (inst) {
instance = inst;
},
};
export { wasi_trace };
+21
View File
@@ -0,0 +1,21 @@
// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details
#include "meshoptimizer.h"
#include <assert.h>
#ifdef MESHOPTIMIZER_ALLOC_EXPORT
meshopt_Allocator::Storage& meshopt_Allocator::storage()
{
static Storage s = {::operator new, ::operator delete };
return s;
}
#endif
void meshopt_setAllocator(void* (MESHOPTIMIZER_ALLOC_CALLCONV* allocate)(size_t), void (MESHOPTIMIZER_ALLOC_CALLCONV* deallocate)(void*))
{
assert(allocate && deallocate);
meshopt_Allocator::Storage& s = meshopt_Allocator::storage();
s.allocate = allocate;
s.deallocate = deallocate;
}
File diff suppressed because it is too large Load Diff
+125
View File
@@ -0,0 +1,125 @@
// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details
#include "meshoptimizer.h"
#include <assert.h>
#include <string.h>
meshopt_VertexCacheStatistics meshopt_analyzeVertexCache(const unsigned int* indices, size_t index_count, size_t vertex_count, unsigned int cache_size, unsigned int warp_size, unsigned int primgroup_size)
{
assert(index_count % 3 == 0);
assert(cache_size >= 3);
assert(warp_size == 0 || warp_size >= 3);
meshopt_Allocator allocator;
meshopt_VertexCacheStatistics result = {};
unsigned int warp_offset = 0;
unsigned int primgroup_offset = 0;
unsigned int* cache_timestamps = allocator.allocate<unsigned int>(vertex_count);
memset(cache_timestamps, 0, vertex_count * sizeof(unsigned int));
unsigned int timestamp = cache_size + 1;
for (size_t i = 0; i < index_count; i += 3)
{
unsigned int a = indices[i + 0], b = indices[i + 1], c = indices[i + 2];
assert(a < vertex_count && b < vertex_count && c < vertex_count);
bool ac = (timestamp - cache_timestamps[a]) > cache_size;
bool bc = (timestamp - cache_timestamps[b]) > cache_size;
bool cc = (timestamp - cache_timestamps[c]) > cache_size;
// flush cache if triangle doesn't fit into warp or into the primitive buffer
if ((primgroup_size && primgroup_offset == primgroup_size) || (warp_size && warp_offset + ac + bc + cc > warp_size))
{
result.warps_executed += warp_offset > 0;
warp_offset = 0;
primgroup_offset = 0;
// reset cache
timestamp += cache_size + 1;
}
// update cache and add vertices to warp
for (int j = 0; j < 3; ++j)
{
unsigned int index = indices[i + j];
if (timestamp - cache_timestamps[index] > cache_size)
{
cache_timestamps[index] = timestamp++;
result.vertices_transformed++;
warp_offset++;
}
}
primgroup_offset++;
}
size_t unique_vertex_count = 0;
for (size_t i = 0; i < vertex_count; ++i)
unique_vertex_count += cache_timestamps[i] > 0;
result.warps_executed += warp_offset > 0;
result.acmr = index_count == 0 ? 0 : float(result.vertices_transformed) / float(index_count / 3);
result.atvr = unique_vertex_count == 0 ? 0 : float(result.vertices_transformed) / float(unique_vertex_count);
return result;
}
meshopt_VertexFetchStatistics meshopt_analyzeVertexFetch(const unsigned int* indices, size_t index_count, size_t vertex_count, size_t vertex_size)
{
assert(vertex_size > 0 && vertex_size <= 256);
meshopt_Allocator allocator;
meshopt_VertexFetchStatistics result = {};
unsigned char* vertex_visited = allocator.allocate<unsigned char>(vertex_count);
memset(vertex_visited, 0, vertex_count);
const size_t kCacheLine = 64;
const size_t kCacheSize = 128 * 1024;
// simple direct mapped cache; on typical mesh data this is close to 4-way cache, and this model is a gross approximation anyway
size_t cache[kCacheSize / kCacheLine] = {};
for (size_t i = 0; i < index_count; ++i)
{
unsigned int index = indices[i];
assert(index < vertex_count);
vertex_visited[index] = 1;
size_t start_address = index * vertex_size;
size_t end_address = start_address + vertex_size;
size_t start_tag = start_address / kCacheLine;
size_t end_tag = (end_address + kCacheLine - 1) / kCacheLine;
assert(start_tag < end_tag);
for (size_t tag = start_tag; tag < end_tag; ++tag)
{
size_t line = tag % (sizeof(cache) / sizeof(cache[0]));
// we store +1 since cache is filled with 0 by default
result.bytes_fetched += (cache[line] != tag + 1) * kCacheLine;
cache[line] = tag + 1;
}
}
size_t unique_vertex_count = 0;
for (size_t i = 0; i < vertex_count; ++i)
unique_vertex_count += vertex_visited[i];
result.overfetch = unique_vertex_count == 0 ? 0 : float(result.bytes_fetched) / float(unique_vertex_count * vertex_size);
return result;
}
+703
View File
@@ -0,0 +1,703 @@
// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details
#include "meshoptimizer.h"
#include <assert.h>
#include <string.h>
// This work is based on:
// Fabian Giesen. Simple lossless index buffer compression & follow-up. 2013
// Conor Stokes. Vertex Cache Optimised Index Buffer Compression. 2014
namespace meshopt
{
const unsigned char kIndexHeader = 0xe0;
const unsigned char kSequenceHeader = 0xd0;
static int gEncodeIndexVersion = 1;
const int kDecodeIndexVersion = 1;
typedef unsigned int VertexFifo[16];
typedef unsigned int EdgeFifo[16][2];
static const unsigned char kCodeAuxEncodingTable[16] = {
0x00, 0x76, 0x87, 0x56, 0x67, 0x78, 0xa9, 0x86, 0x65, 0x89, 0x68, 0x98, 0x01, 0x69,
0, 0 // last two entries aren't used for encoding
};
static int rotateTriangle(unsigned int a, unsigned int b, unsigned int c, unsigned int next)
{
(void)a;
return (b == next) ? 1 : (c == next ? 2 : 0);
}
static int getEdgeFifo(EdgeFifo fifo, unsigned int a, unsigned int b, unsigned int c, size_t offset)
{
for (int i = 0; i < 16; ++i)
{
size_t index = (offset - 1 - i) & 15;
unsigned int e0 = fifo[index][0];
unsigned int e1 = fifo[index][1];
if (e0 == a && e1 == b)
return (i << 2) | 0;
if (e0 == b && e1 == c)
return (i << 2) | 1;
if (e0 == c && e1 == a)
return (i << 2) | 2;
}
return -1;
}
static void pushEdgeFifo(EdgeFifo fifo, unsigned int a, unsigned int b, size_t& offset)
{
fifo[offset][0] = a;
fifo[offset][1] = b;
offset = (offset + 1) & 15;
}
static int getVertexFifo(VertexFifo fifo, unsigned int v, size_t offset)
{
for (int i = 0; i < 16; ++i)
{
size_t index = (offset - 1 - i) & 15;
if (fifo[index] == v)
return i;
}
return -1;
}
static void pushVertexFifo(VertexFifo fifo, unsigned int v, size_t& offset, int cond = 1)
{
fifo[offset] = v;
offset = (offset + cond) & 15;
}
static void encodeVByte(unsigned char*& data, unsigned int v)
{
// encode 32-bit value in up to 5 7-bit groups
do
{
*data++ = (v & 127) | (v > 127 ? 128 : 0);
v >>= 7;
} while (v);
}
static unsigned int decodeVByte(const unsigned char*& data)
{
unsigned char lead = *data++;
// fast path: single byte
if (lead < 128)
return lead;
// slow path: up to 4 extra bytes
// note that this loop always terminates, which is important for malformed data
unsigned int result = lead & 127;
unsigned int shift = 7;
for (int i = 0; i < 4; ++i)
{
unsigned char group = *data++;
result |= unsigned(group & 127) << shift;
shift += 7;
if (group < 128)
break;
}
return result;
}
static void encodeIndex(unsigned char*& data, unsigned int index, unsigned int last)
{
unsigned int d = index - last;
unsigned int v = (d << 1) ^ (int(d) >> 31);
encodeVByte(data, v);
}
static unsigned int decodeIndex(const unsigned char*& data, unsigned int last)
{
unsigned int v = decodeVByte(data);
unsigned int d = (v >> 1) ^ -int(v & 1);
return last + d;
}
static int getCodeAuxIndex(unsigned char v, const unsigned char* table)
{
for (int i = 0; i < 16; ++i)
if (table[i] == v)
return i;
return -1;
}
static void* writeTriangle(void* destination, size_t index_size, unsigned int a, unsigned int b, unsigned int c)
{
if (index_size == 2)
{
unsigned short* tri = static_cast<unsigned short*>(destination);
tri[0] = (unsigned short)(a);
tri[1] = (unsigned short)(b);
tri[2] = (unsigned short)(c);
return tri + 3;
}
else
{
unsigned int* tri = static_cast<unsigned int*>(destination);
tri[0] = a;
tri[1] = b;
tri[2] = c;
return tri + 3;
}
}
} // namespace meshopt
size_t meshopt_encodeIndexBuffer(unsigned char* buffer, size_t buffer_size, const unsigned int* indices, size_t index_count)
{
using namespace meshopt;
assert(index_count % 3 == 0);
// the minimum valid encoding is header, 1 byte per triangle and a 16-byte codeaux table
if (buffer_size < 1 + index_count / 3 + 16)
return 0;
int version = gEncodeIndexVersion;
buffer[0] = (unsigned char)(kIndexHeader | version);
EdgeFifo edgefifo;
memset(edgefifo, -1, sizeof(edgefifo));
VertexFifo vertexfifo;
memset(vertexfifo, -1, sizeof(vertexfifo));
size_t edgefifooffset = 0;
size_t vertexfifooffset = 0;
unsigned int next = 0;
unsigned int last = 0;
unsigned char* code = buffer + 1;
unsigned char* data = code + index_count / 3;
unsigned char* data_safe_end = buffer + buffer_size - 16;
int fecmax = version >= 1 ? 13 : 15;
static const int rotations[] = {0, 1, 2, 0, 1};
// use static encoding table; it's possible to pack the result and then build an optimal table and repack
// for now we keep it simple and use the table that has been generated based on symbol frequency on a training mesh set
const unsigned char* codeaux_table = kCodeAuxEncodingTable;
for (size_t i = 0; i < index_count; i += 3)
{
// make sure we have enough space to write a triangle
// each triangle writes at most 16 bytes: 1b for codeaux and 5b for each free index
// after this we can be sure we can write without extra bounds checks
if (data > data_safe_end)
return 0;
int fer = getEdgeFifo(edgefifo, indices[i + 0], indices[i + 1], indices[i + 2], edgefifooffset);
if (fer >= 0 && (fer >> 2) < 15)
{
// note: getEdgeFifo implicitly rotates triangles by matching a/b to existing edge
const int* order = rotations + (fer & 3);
unsigned int a = indices[i + order[0]], b = indices[i + order[1]], c = indices[i + order[2]];
// encode edge index and vertex fifo index, next or free index
int fe = fer >> 2;
int fc = getVertexFifo(vertexfifo, c, vertexfifooffset);
int fec = (fc >= 1 && fc < fecmax) ? fc : (c == next ? (next++, 0) : 15);
if (fec == 15 && version >= 1)
{
// encode last-1 and last+1 to optimize strip-like sequences
if (c + 1 == last)
fec = 13, last = c;
if (c == last + 1)
fec = 14, last = c;
}
*code++ = (unsigned char)((fe << 4) | fec);
// note that we need to update the last index since free indices are delta-encoded
if (fec == 15)
encodeIndex(data, c, last), last = c;
// we only need to push third vertex since first two are likely already in the vertex fifo
if (fec == 0 || fec >= fecmax)
pushVertexFifo(vertexfifo, c, vertexfifooffset);
// we only need to push two new edges to edge fifo since the third one is already there
pushEdgeFifo(edgefifo, c, b, edgefifooffset);
pushEdgeFifo(edgefifo, a, c, edgefifooffset);
}
else
{
int rotation = rotateTriangle(indices[i + 0], indices[i + 1], indices[i + 2], next);
const int* order = rotations + rotation;
unsigned int a = indices[i + order[0]], b = indices[i + order[1]], c = indices[i + order[2]];
// if a/b/c are 0/1/2, we emit a reset code
bool reset = false;
if (a == 0 && b == 1 && c == 2 && next > 0 && version >= 1)
{
reset = true;
next = 0;
// reset vertex fifo to make sure we don't accidentally reference vertices from that in the future
// this makes sure next continues to get incremented instead of being stuck
memset(vertexfifo, -1, sizeof(vertexfifo));
}
int fb = getVertexFifo(vertexfifo, b, vertexfifooffset);
int fc = getVertexFifo(vertexfifo, c, vertexfifooffset);
// after rotation, a is almost always equal to next, so we don't waste bits on FIFO encoding for a
// note: decoder implicitly assumes that if feb=fec=0, then fea=0 (reset code); this is enforced by rotation
int fea = (a == next) ? (next++, 0) : 15;
int feb = (fb >= 0 && fb < 14) ? fb + 1 : (b == next ? (next++, 0) : 15);
int fec = (fc >= 0 && fc < 14) ? fc + 1 : (c == next ? (next++, 0) : 15);
// we encode feb & fec in 4 bits using a table if possible, and as a full byte otherwise
unsigned char codeaux = (unsigned char)((feb << 4) | fec);
int codeauxindex = getCodeAuxIndex(codeaux, codeaux_table);
// <14 encodes an index into codeaux table, 14 encodes fea=0, 15 encodes fea=15
if (fea == 0 && codeauxindex >= 0 && codeauxindex < 14 && !reset)
{
*code++ = (unsigned char)((15 << 4) | codeauxindex);
}
else
{
*code++ = (unsigned char)((15 << 4) | 14 | fea);
*data++ = codeaux;
}
// note that we need to update the last index since free indices are delta-encoded
if (fea == 15)
encodeIndex(data, a, last), last = a;
if (feb == 15)
encodeIndex(data, b, last), last = b;
if (fec == 15)
encodeIndex(data, c, last), last = c;
// only push vertices that weren't already in fifo
if (fea == 0 || fea == 15)
pushVertexFifo(vertexfifo, a, vertexfifooffset);
if (feb == 0 || feb == 15)
pushVertexFifo(vertexfifo, b, vertexfifooffset);
if (fec == 0 || fec == 15)
pushVertexFifo(vertexfifo, c, vertexfifooffset);
// all three edges aren't in the fifo; pushing all of them is important so that we can match them for later triangles
pushEdgeFifo(edgefifo, b, a, edgefifooffset);
pushEdgeFifo(edgefifo, c, b, edgefifooffset);
pushEdgeFifo(edgefifo, a, c, edgefifooffset);
}
}
// make sure we have enough space to write codeaux table
if (data > data_safe_end)
return 0;
// add codeaux encoding table to the end of the stream; this is used for decoding codeaux *and* as padding
// we need padding for decoding to be able to assume that each triangle is encoded as <= 16 bytes of extra data
// this is enough space for aux byte + 5 bytes per varint index which is the absolute worst case for any input
for (size_t i = 0; i < 16; ++i)
{
// decoder assumes that table entries never refer to separately encoded indices
assert((codeaux_table[i] & 0xf) != 0xf && (codeaux_table[i] >> 4) != 0xf);
*data++ = codeaux_table[i];
}
// since we encode restarts as codeaux without a table reference, we need to make sure 00 is encoded as a table reference
assert(codeaux_table[0] == 0);
assert(data >= buffer + index_count / 3 + 16);
assert(data <= buffer + buffer_size);
return data - buffer;
}
size_t meshopt_encodeIndexBufferBound(size_t index_count, size_t vertex_count)
{
assert(index_count % 3 == 0);
// compute number of bits required for each index
unsigned int vertex_bits = 1;
while (vertex_bits < 32 && vertex_count > size_t(1) << vertex_bits)
vertex_bits++;
// worst-case encoding is 2 header bytes + 3 varint-7 encoded index deltas
unsigned int vertex_groups = (vertex_bits + 1 + 6) / 7;
return 1 + (index_count / 3) * (2 + 3 * vertex_groups) + 16;
}
void meshopt_encodeIndexVersion(int version)
{
assert(unsigned(version) <= unsigned(meshopt::kDecodeIndexVersion));
meshopt::gEncodeIndexVersion = version;
}
int meshopt_decodeIndexVersion(const unsigned char* buffer, size_t buffer_size)
{
if (buffer_size < 1)
return -1;
unsigned char header = buffer[0];
if ((header & 0xf0) != meshopt::kIndexHeader && (header & 0xf0) != meshopt::kSequenceHeader)
return -1;
int version = header & 0x0f;
if (version > meshopt::kDecodeIndexVersion)
return -1;
return version;
}
int meshopt_decodeIndexBuffer(void* destination, size_t index_count, size_t index_size, const unsigned char* buffer, size_t buffer_size)
{
using namespace meshopt;
assert(index_count % 3 == 0);
assert(index_size == 2 || index_size == 4);
// the minimum valid encoding is header, 1 byte per triangle and a 16-byte codeaux table
if (buffer_size < 1 + index_count / 3 + 16)
return -2;
if ((buffer[0] & 0xf0) != kIndexHeader)
return -1;
int version = buffer[0] & 0x0f;
if (version > kDecodeIndexVersion)
return -1;
EdgeFifo edgefifo;
memset(edgefifo, -1, sizeof(edgefifo));
VertexFifo vertexfifo;
memset(vertexfifo, -1, sizeof(vertexfifo));
size_t edgefifooffset = 0;
size_t vertexfifooffset = 0;
unsigned int next = 0;
unsigned int last = 0;
int fecmax = version >= 1 ? 13 : 15;
// since we store 16-byte codeaux table at the end, triangle data has to begin before data_safe_end
const unsigned char* code = buffer + 1;
const unsigned char* code_end = code + index_count / 3;
const unsigned char* data = code_end;
// each triangle reads at most 16 bytes of data: 1b for codeaux and 5b for each free index
const unsigned char* data_safe_end = buffer + buffer_size - 16;
const unsigned char* codeaux_table = data_safe_end;
while (code < code_end)
{
unsigned char codetri = *code++;
if (codetri < 0xf0)
{
int fe = codetri >> 4;
// fifo reads are wrapped around 16 entry buffer
unsigned int a = edgefifo[(edgefifooffset - 1 - fe) & 15][0];
unsigned int b = edgefifo[(edgefifooffset - 1 - fe) & 15][1];
unsigned int c = 0;
int fec = codetri & 15;
// note: this is the most common path in the entire decoder
// inside this if we try to stay branchless (by using cmov/etc.) since these aren't predictable
if (fec < fecmax)
{
// fifo reads are wrapped around 16 entry buffer
unsigned int cf = vertexfifo[(vertexfifooffset - 1 - fec) & 15];
#if (defined(__GNUC__) || defined(__clang__)) && (defined(__x86_64__) || defined(__aarch64__))
// on clang, x86-cmov-conversion pass emits a branch since cf is a load from memory; asm barrier defeats this
// this can be fixed with __builtin_unpredictable, but gcc doesn't support it and has a similar problem due to if-conversion
// additionally, gcc can fuse a/b into one 64-bit load but use 32-bit edgefifo[] stores, which breaks load store forwarding
__asm__("" : "+r"(a) : "r"(cf));
#endif
c = (fec == 0) ? next : cf;
int fec0 = fec == 0;
next += fec0;
// push vertex fifo must match the encoding step *exactly* otherwise the data will not be decoded correctly
pushVertexFifo(vertexfifo, c, vertexfifooffset, fec0);
}
else
{
// make sure we have enough data to read for a triangle; this check covers worst case advance
if (data > data_safe_end)
return -2;
// fec * 2 - 27 decodes 13, 14 into -1, 1
// note that we need to update the last index since free indices are delta-encoded
last = c = (fec != 15) ? last + (fec * 2 - 27) : decodeIndex(data, last);
// push vertex/edge fifo must match the encoding step *exactly* otherwise the data will not be decoded correctly
pushVertexFifo(vertexfifo, c, vertexfifooffset);
}
// push edge fifo must match the encoding step *exactly* otherwise the data will not be decoded correctly
pushEdgeFifo(edgefifo, c, b, edgefifooffset);
pushEdgeFifo(edgefifo, a, c, edgefifooffset);
// output triangle
destination = writeTriangle(destination, index_size, a, b, c);
}
else
{
// fast path: read codeaux from the table
if (codetri < 0xfe)
{
unsigned char codeaux = codeaux_table[codetri & 15];
// note: table can't contain feb/fec=15
int feb = codeaux >> 4;
int fec = codeaux & 15;
// fifo reads are wrapped around 16 entry buffer
// also note that we increment next for all three vertices before decoding indices - this matches encoder behavior
unsigned int a = next++;
unsigned int bf = vertexfifo[(vertexfifooffset - feb) & 15];
unsigned int b = (feb == 0) ? next : bf;
int feb0 = feb == 0;
next += feb0;
unsigned int cf = vertexfifo[(vertexfifooffset - fec) & 15];
unsigned int c = (fec == 0) ? next : cf;
int fec0 = fec == 0;
next += fec0;
// output triangle
destination = writeTriangle(destination, index_size, a, b, c);
// push vertex/edge fifo must match the encoding step *exactly* otherwise the data will not be decoded correctly
pushVertexFifo(vertexfifo, a, vertexfifooffset);
pushVertexFifo(vertexfifo, b, vertexfifooffset, feb0);
pushVertexFifo(vertexfifo, c, vertexfifooffset, fec0);
pushEdgeFifo(edgefifo, b, a, edgefifooffset);
pushEdgeFifo(edgefifo, c, b, edgefifooffset);
pushEdgeFifo(edgefifo, a, c, edgefifooffset);
}
else
{
// make sure we have enough data to read for a triangle; this check covers worst case advance
if (data > data_safe_end)
return -2;
// slow path: read a full byte for codeaux instead of using a table lookup
unsigned char codeaux = *data++;
int fea = codetri == 0xfe ? 0 : 15;
int feb = codeaux >> 4;
int fec = codeaux & 15;
// reset: codeaux is 0 but encoded as not-a-table
if (codeaux == 0)
next = 0;
// fifo reads are wrapped around 16 entry buffer
// also note that we increment next for all three vertices before decoding indices - this matches encoder behavior
unsigned int a = (fea == 0) ? next++ : 0;
unsigned int b = (feb == 0) ? next++ : vertexfifo[(vertexfifooffset - feb) & 15];
unsigned int c = (fec == 0) ? next++ : vertexfifo[(vertexfifooffset - fec) & 15];
// note that we need to update the last index since free indices are delta-encoded
if (fea == 15)
last = a = decodeIndex(data, last);
if (feb == 15)
last = b = decodeIndex(data, last);
if (fec == 15)
last = c = decodeIndex(data, last);
// output triangle
destination = writeTriangle(destination, index_size, a, b, c);
// push vertex/edge fifo must match the encoding step *exactly* otherwise the data will not be decoded correctly
pushVertexFifo(vertexfifo, a, vertexfifooffset);
pushVertexFifo(vertexfifo, b, vertexfifooffset, (feb == 0) | (feb == 15));
pushVertexFifo(vertexfifo, c, vertexfifooffset, (fec == 0) | (fec == 15));
pushEdgeFifo(edgefifo, b, a, edgefifooffset);
pushEdgeFifo(edgefifo, c, b, edgefifooffset);
pushEdgeFifo(edgefifo, a, c, edgefifooffset);
}
}
}
// we should've read all data bytes and stopped at the boundary between data and codeaux table
if (data != data_safe_end)
return -3;
return 0;
}
size_t meshopt_encodeIndexSequence(unsigned char* buffer, size_t buffer_size, const unsigned int* indices, size_t index_count)
{
using namespace meshopt;
// the minimum valid encoding is header, 1 byte per index and a 4-byte tail
if (buffer_size < 1 + index_count + 4)
return 0;
int version = gEncodeIndexVersion;
buffer[0] = (unsigned char)(kSequenceHeader | version);
unsigned int last[2] = {};
unsigned int current = 0;
unsigned char* data = buffer + 1;
unsigned char* data_safe_end = buffer + buffer_size - 4;
for (size_t i = 0; i < index_count; ++i)
{
// make sure we have enough data to write
// each index writes at most 5 bytes of data; there's a 4 byte tail after data_safe_end
// after this we can be sure we can write without extra bounds checks
if (data >= data_safe_end)
return 0;
unsigned int index = indices[i];
// this is a heuristic that switches between baselines when the delta grows too large
// we want the encoded delta to fit into one byte (7 bits), but 2 bits are used for sign and baseline index
// for now we immediately switch the baseline when delta grows too large - this can be adjusted arbitrarily
int cd = int(index - last[current]);
current ^= ((cd < 0 ? -cd : cd) >= 30);
// encode delta from the last index
unsigned int d = index - last[current];
unsigned int v = (d << 1) ^ (int(d) >> 31);
// note: low bit encodes the index of the last baseline which will be used for reconstruction
encodeVByte(data, (v << 1) | current);
// update last for the next iteration that uses it
last[current] = index;
}
// make sure we have enough space to write tail
if (data > data_safe_end)
return 0;
for (int k = 0; k < 4; ++k)
*data++ = 0;
return data - buffer;
}
size_t meshopt_encodeIndexSequenceBound(size_t index_count, size_t vertex_count)
{
// compute number of bits required for each index
unsigned int vertex_bits = 1;
while (vertex_bits < 32 && vertex_count > size_t(1) << vertex_bits)
vertex_bits++;
// worst-case encoding is 1 varint-7 encoded index delta for a K bit value and an extra bit
unsigned int vertex_groups = (vertex_bits + 1 + 1 + 6) / 7;
return 1 + index_count * vertex_groups + 4;
}
int meshopt_decodeIndexSequence(void* destination, size_t index_count, size_t index_size, const unsigned char* buffer, size_t buffer_size)
{
using namespace meshopt;
// the minimum valid encoding is header, 1 byte per index and a 4-byte tail
if (buffer_size < 1 + index_count + 4)
return -2;
if ((buffer[0] & 0xf0) != kSequenceHeader)
return -1;
int version = buffer[0] & 0x0f;
if (version > kDecodeIndexVersion)
return -1;
const unsigned char* data = buffer + 1;
const unsigned char* data_safe_end = buffer + buffer_size - 4;
unsigned int last[2] = {};
for (size_t i = 0; i < index_count; ++i)
{
// make sure we have enough data to read
// each index reads at most 5 bytes of data; there's a 4 byte tail after data_safe_end
// after this we can be sure we can read without extra bounds checks
if (data >= data_safe_end)
return -2;
unsigned int v = decodeVByte(data);
// decode the index of the last baseline
unsigned int current = v & 1;
v >>= 1;
// reconstruct index as a delta
unsigned int d = (v >> 1) ^ -int(v & 1);
unsigned int index = last[current] + d;
// update last for the next iteration that uses it
last[current] = index;
if (index_size == 2)
{
static_cast<unsigned short*>(destination)[i] = (unsigned short)(index);
}
else
{
static_cast<unsigned int*>(destination)[i] = index;
}
}
// we should've read all data bytes and stopped at the boundary between data and tail
if (data != data_safe_end)
return -3;
return 0;
}
+825
View File
@@ -0,0 +1,825 @@
// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details
#include "meshoptimizer.h"
#include <assert.h>
#include <string.h>
// This work is based on:
// Matthias Teschner, Bruno Heidelberger, Matthias Mueller, Danat Pomeranets, Markus Gross. Optimized Spatial Hashing for Collision Detection of Deformable Objects. 2003
// John McDonald, Mark Kilgard. Crack-Free Point-Normal Triangles using Adjacent Edge Normals. 2010
// John Hable. Variable Rate Shading with Visibility Buffer Rendering. 2024
namespace meshopt
{
static unsigned int hashUpdate(unsigned int h, const unsigned char* key, size_t len)
{
// MurmurHash2
const unsigned int m = 0x5bd1e995;
const int r = 24;
while (len >= 4)
{
unsigned int k;
memcpy(&k, key, sizeof(k));
k *= m;
k ^= k >> r;
k *= m;
h *= m;
h ^= k;
key += 4;
len -= 4;
}
if (len > 0)
{
h ^= key[0] | (key[len / 2] << 8) | (key[len - 1] << 16);
h *= m;
h ^= h >> r;
}
return h;
}
struct VertexHasher
{
const unsigned char* vertices;
size_t vertex_size;
size_t vertex_stride;
size_t hash(unsigned int index) const
{
return hashUpdate(0, vertices + index * vertex_stride, vertex_size);
}
bool equal(unsigned int lhs, unsigned int rhs) const
{
return memcmp(vertices + lhs * vertex_stride, vertices + rhs * vertex_stride, vertex_size) == 0;
}
};
struct VertexStreamHasher
{
const meshopt_Stream* streams;
size_t stream_count;
size_t hash(unsigned int index) const
{
unsigned int h = 0;
for (size_t i = 0; i < stream_count; ++i)
{
const meshopt_Stream& s = streams[i];
const unsigned char* data = static_cast<const unsigned char*>(s.data);
h = hashUpdate(h, data + index * s.stride, s.size);
}
return h;
}
bool equal(unsigned int lhs, unsigned int rhs) const
{
for (size_t i = 0; i < stream_count; ++i)
{
const meshopt_Stream& s = streams[i];
const unsigned char* data = static_cast<const unsigned char*>(s.data);
if (memcmp(data + lhs * s.stride, data + rhs * s.stride, s.size) != 0)
return false;
}
return true;
}
};
struct VertexCustomHasher
{
const float* vertex_positions;
size_t vertex_stride_float;
int (*callback)(void*, unsigned int, unsigned int);
void* context;
size_t hash(unsigned int index) const
{
const unsigned int* key = reinterpret_cast<const unsigned int*>(vertex_positions + index * vertex_stride_float);
unsigned int x = key[0], y = key[1], z = key[2];
// replace negative zero with zero
x = (x == 0x80000000) ? 0 : x;
y = (y == 0x80000000) ? 0 : y;
z = (z == 0x80000000) ? 0 : z;
// scramble bits to make sure that integer coordinates have entropy in lower bits
x ^= x >> 17;
y ^= y >> 17;
z ^= z >> 17;
// Optimized Spatial Hashing for Collision Detection of Deformable Objects
return (x * 73856093) ^ (y * 19349663) ^ (z * 83492791);
}
bool equal(unsigned int lhs, unsigned int rhs) const
{
const float* lp = vertex_positions + lhs * vertex_stride_float;
const float* rp = vertex_positions + rhs * vertex_stride_float;
if (lp[0] != rp[0] || lp[1] != rp[1] || lp[2] != rp[2])
return false;
return callback ? callback(context, lhs, rhs) : true;
}
};
struct TriangleKey
{
unsigned int a, b, c;
bool operator==(const TriangleKey& other) const
{
return a == other.a && b == other.b && c == other.c;
}
};
struct TriangleKeyHasher
{
size_t hash(const TriangleKey& k) const
{
// Optimized Spatial Hashing for Collision Detection of Deformable Objects
return (k.a * 73856093) ^ (k.b * 19349663) ^ (k.c * 83492791);
}
bool equal(const TriangleKey& lhs, const TriangleKey& rhs) const
{
return lhs == rhs;
}
};
struct EdgeHasher
{
const unsigned int* remap;
size_t hash(unsigned long long edge) const
{
unsigned int e0 = unsigned(edge >> 32);
unsigned int e1 = unsigned(edge);
unsigned int h1 = remap[e0];
unsigned int h2 = remap[e1];
const unsigned int m = 0x5bd1e995;
// MurmurHash64B finalizer
h1 ^= h2 >> 18;
h1 *= m;
h2 ^= h1 >> 22;
h2 *= m;
h1 ^= h2 >> 17;
h1 *= m;
h2 ^= h1 >> 19;
h2 *= m;
return h2;
}
bool equal(unsigned long long lhs, unsigned long long rhs) const
{
unsigned int l0 = unsigned(lhs >> 32);
unsigned int l1 = unsigned(lhs);
unsigned int r0 = unsigned(rhs >> 32);
unsigned int r1 = unsigned(rhs);
return remap[l0] == remap[r0] && remap[l1] == remap[r1];
}
};
static size_t hashBuckets(size_t count)
{
size_t buckets = 1;
while (buckets < count + count / 4)
buckets *= 2;
return buckets;
}
template <typename T, typename Hash>
static T* hashLookup(T* table, size_t buckets, const Hash& hash, const T& key, const T& empty)
{
assert(buckets > 0);
assert((buckets & (buckets - 1)) == 0);
size_t hashmod = buckets - 1;
size_t bucket = hash.hash(key) & hashmod;
for (size_t probe = 0; probe <= hashmod; ++probe)
{
T& item = table[bucket];
if (item == empty)
return &item;
if (hash.equal(item, key))
return &item;
// hash collision, quadratic probing
bucket = (bucket + probe + 1) & hashmod;
}
assert(false && "Hash table is full"); // unreachable
return NULL;
}
static void buildPositionRemap(unsigned int* remap, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, meshopt_Allocator& allocator)
{
VertexHasher vertex_hasher = {reinterpret_cast<const unsigned char*>(vertex_positions), 3 * sizeof(float), vertex_positions_stride};
size_t vertex_table_size = hashBuckets(vertex_count);
unsigned int* vertex_table = allocator.allocate<unsigned int>(vertex_table_size);
memset(vertex_table, -1, vertex_table_size * sizeof(unsigned int));
for (size_t i = 0; i < vertex_count; ++i)
{
unsigned int index = unsigned(i);
unsigned int* entry = hashLookup(vertex_table, vertex_table_size, vertex_hasher, index, ~0u);
if (*entry == ~0u)
*entry = index;
remap[index] = *entry;
}
allocator.deallocate(vertex_table);
}
template <typename Hash>
static size_t generateVertexRemap(unsigned int* remap, const unsigned int* indices, size_t index_count, size_t vertex_count, const Hash& hash, meshopt_Allocator& allocator)
{
memset(remap, -1, vertex_count * sizeof(unsigned int));
size_t table_size = hashBuckets(vertex_count);
unsigned int* table = allocator.allocate<unsigned int>(table_size);
memset(table, -1, table_size * sizeof(unsigned int));
unsigned int next_vertex = 0;
for (size_t i = 0; i < index_count; ++i)
{
unsigned int index = indices ? indices[i] : unsigned(i);
assert(index < vertex_count);
if (remap[index] != ~0u)
continue;
unsigned int* entry = hashLookup(table, table_size, hash, index, ~0u);
if (*entry == ~0u)
{
*entry = index;
remap[index] = next_vertex++;
}
else
{
assert(remap[*entry] != ~0u);
remap[index] = remap[*entry];
}
}
assert(next_vertex <= vertex_count);
return next_vertex;
}
template <size_t BlockSize>
static void remapVertices(void* destination, const void* vertices, size_t vertex_count, size_t vertex_size, const unsigned int* remap)
{
size_t block_size = BlockSize == 0 ? vertex_size : BlockSize;
assert(block_size == vertex_size);
for (size_t i = 0; i < vertex_count; ++i)
if (remap[i] != ~0u)
{
assert(remap[i] < vertex_count);
memcpy(static_cast<unsigned char*>(destination) + remap[i] * block_size, static_cast<const unsigned char*>(vertices) + i * block_size, block_size);
}
}
template <typename Hash>
static void generateShadowBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const Hash& hash, meshopt_Allocator& allocator)
{
unsigned int* remap = allocator.allocate<unsigned int>(vertex_count);
memset(remap, -1, vertex_count * sizeof(unsigned int));
size_t table_size = hashBuckets(vertex_count);
unsigned int* table = allocator.allocate<unsigned int>(table_size);
memset(table, -1, table_size * sizeof(unsigned int));
for (size_t i = 0; i < index_count; ++i)
{
unsigned int index = indices[i];
assert(index < vertex_count);
if (remap[index] == ~0u)
{
unsigned int* entry = hashLookup(table, table_size, hash, index, ~0u);
if (*entry == ~0u)
*entry = index;
remap[index] = *entry;
}
destination[i] = remap[index];
}
}
static size_t filterIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const unsigned int* remap, size_t vertex_count, meshopt_Allocator& allocator)
{
(void)vertex_count;
TriangleKeyHasher hasher = {};
TriangleKey empty = {~0u, ~0u, ~0u};
size_t face_count = index_count / 3;
size_t table_size = hashBuckets(face_count);
TriangleKey* table = allocator.allocate<TriangleKey>(table_size);
memset(table, -1, table_size * sizeof(TriangleKey));
size_t write = 0;
for (size_t i = 0; i < face_count; ++i)
{
unsigned int a = indices[i * 3 + 0], b = indices[i * 3 + 1], c = indices[i * 3 + 2];
assert(a < vertex_count && b < vertex_count && c < vertex_count);
unsigned int ra = remap[a], rb = remap[b], rc = remap[c];
// degenerate triangle
if (ra == rb || ra == rc || rb == rc)
continue;
// canonicalize triangle wrt winding preserving rotation
if (rb < ra && rb < rc)
{
// abc -> bca
unsigned int t = ra;
ra = rb, rb = rc, rc = t;
}
else if (rc < ra && rc < rb)
{
// abc -> cab
unsigned int t = rc;
rc = rb, rb = ra, ra = t;
}
TriangleKey key = {ra, rb, rc};
TriangleKey* entry = hashLookup(table, table_size, hasher, key, empty);
// duplicate triangle
if (entry->a != ~0u)
continue;
*entry = key;
destination[write + 0] = a;
destination[write + 1] = b;
destination[write + 2] = c;
write += 3;
}
return write;
}
} // namespace meshopt
size_t meshopt_generateVertexRemap(unsigned int* destination, const unsigned int* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size)
{
using namespace meshopt;
assert(indices || index_count == vertex_count);
assert(vertex_size > 0 && vertex_size <= 256);
meshopt_Allocator allocator;
VertexHasher hasher = {static_cast<const unsigned char*>(vertices), vertex_size, vertex_size};
return generateVertexRemap(destination, indices, index_count, vertex_count, hasher, allocator);
}
size_t meshopt_generateVertexRemapMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count)
{
using namespace meshopt;
assert(indices || index_count == vertex_count);
assert(stream_count > 0 && stream_count <= 16);
for (size_t i = 0; i < stream_count; ++i)
{
assert(streams[i].size > 0 && streams[i].size <= 256);
assert(streams[i].size <= streams[i].stride);
}
meshopt_Allocator allocator;
VertexStreamHasher hasher = {streams, stream_count};
return generateVertexRemap(destination, indices, index_count, vertex_count, hasher, allocator);
}
size_t meshopt_generateVertexRemapCustom(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, int (*callback)(void*, unsigned int, unsigned int), void* context)
{
using namespace meshopt;
assert(indices || index_count == vertex_count);
assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
assert(vertex_positions_stride % sizeof(float) == 0);
meshopt_Allocator allocator;
VertexCustomHasher hasher = {vertex_positions, vertex_positions_stride / sizeof(float), callback, context};
return generateVertexRemap(destination, indices, index_count, vertex_count, hasher, allocator);
}
void meshopt_remapVertexBuffer(void* destination, const void* vertices, size_t vertex_count, size_t vertex_size, const unsigned int* remap)
{
using namespace meshopt;
assert(vertex_size > 0 && vertex_size <= 256);
meshopt_Allocator allocator;
// support in-place remap
if (destination == vertices)
{
unsigned char* vertices_copy = allocator.allocate<unsigned char>(vertex_count * vertex_size);
memcpy(vertices_copy, vertices, vertex_count * vertex_size);
vertices = vertices_copy;
}
// specialize the loop for common vertex sizes to ensure memcpy is compiled as an inlined intrinsic
switch (vertex_size)
{
case 4:
return remapVertices<4>(destination, vertices, vertex_count, vertex_size, remap);
case 8:
return remapVertices<8>(destination, vertices, vertex_count, vertex_size, remap);
case 12:
return remapVertices<12>(destination, vertices, vertex_count, vertex_size, remap);
case 16:
return remapVertices<16>(destination, vertices, vertex_count, vertex_size, remap);
default:
return remapVertices<0>(destination, vertices, vertex_count, vertex_size, remap);
}
}
void meshopt_remapIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const unsigned int* remap)
{
for (size_t i = 0; i < index_count; ++i)
{
unsigned int index = indices ? indices[i] : unsigned(i);
assert(remap[index] != ~0u);
destination[i] = remap[index];
}
}
size_t meshopt_filterIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride)
{
using namespace meshopt;
assert(index_count % 3 == 0);
assert(vertex_size > 0 && vertex_size <= 256);
assert(vertex_size <= vertex_stride);
meshopt_Allocator allocator;
VertexHasher hasher = {static_cast<const unsigned char*>(vertices), vertex_size, vertex_stride};
unsigned int* remap = allocator.allocate<unsigned int>(vertex_count);
generateVertexRemap(remap, indices, index_count, vertex_count, hasher, allocator);
return filterIndexBuffer(destination, indices, index_count, remap, vertex_count, allocator);
}
size_t meshopt_filterIndexBufferMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count)
{
using namespace meshopt;
assert(index_count % 3 == 0);
assert(stream_count > 0 && stream_count <= 16);
for (size_t i = 0; i < stream_count; ++i)
{
assert(streams[i].size > 0 && streams[i].size <= 256);
assert(streams[i].size <= streams[i].stride);
}
meshopt_Allocator allocator;
VertexStreamHasher hasher = {streams, stream_count};
unsigned int* remap = allocator.allocate<unsigned int>(vertex_count);
generateVertexRemap(remap, indices, index_count, vertex_count, hasher, allocator);
return filterIndexBuffer(destination, indices, index_count, remap, vertex_count, allocator);
}
void meshopt_generateShadowIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride)
{
using namespace meshopt;
assert(index_count % 3 == 0);
assert(vertex_size > 0 && vertex_size <= 256);
assert(vertex_size <= vertex_stride);
meshopt_Allocator allocator;
VertexHasher hasher = {static_cast<const unsigned char*>(vertices), vertex_size, vertex_stride};
generateShadowBuffer(destination, indices, index_count, vertex_count, hasher, allocator);
}
void meshopt_generateShadowIndexBufferMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count)
{
using namespace meshopt;
assert(index_count % 3 == 0);
assert(stream_count > 0 && stream_count <= 16);
for (size_t i = 0; i < stream_count; ++i)
{
assert(streams[i].size > 0 && streams[i].size <= 256);
assert(streams[i].size <= streams[i].stride);
}
meshopt_Allocator allocator;
VertexStreamHasher hasher = {streams, stream_count};
generateShadowBuffer(destination, indices, index_count, vertex_count, hasher, allocator);
}
void meshopt_generatePositionRemap(unsigned int* destination, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride)
{
using namespace meshopt;
assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
assert(vertex_positions_stride % sizeof(float) == 0);
meshopt_Allocator allocator;
VertexCustomHasher hasher = {vertex_positions, vertex_positions_stride / sizeof(float), NULL, NULL};
size_t table_size = hashBuckets(vertex_count);
unsigned int* table = allocator.allocate<unsigned int>(table_size);
memset(table, -1, table_size * sizeof(unsigned int));
for (size_t i = 0; i < vertex_count; ++i)
{
unsigned int* entry = hashLookup(table, table_size, hasher, unsigned(i), ~0u);
if (*entry == ~0u)
*entry = unsigned(i);
destination[i] = *entry;
}
}
void meshopt_generateAdjacencyIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride)
{
using namespace meshopt;
assert(index_count % 3 == 0);
assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
assert(vertex_positions_stride % sizeof(float) == 0);
meshopt_Allocator allocator;
static const int next[4] = {1, 2, 0, 1};
// build position remap: for each vertex, which other (canonical) vertex does it map to?
unsigned int* remap = allocator.allocate<unsigned int>(vertex_count);
buildPositionRemap(remap, vertex_positions, vertex_count, vertex_positions_stride, allocator);
// build edge set; this stores all triangle edges but we can look these up by any other wedge
EdgeHasher edge_hasher = {remap};
size_t edge_table_size = hashBuckets(index_count);
unsigned long long* edge_table = allocator.allocate<unsigned long long>(edge_table_size);
unsigned int* edge_vertex_table = allocator.allocate<unsigned int>(edge_table_size);
memset(edge_table, -1, edge_table_size * sizeof(unsigned long long));
memset(edge_vertex_table, -1, edge_table_size * sizeof(unsigned int));
for (size_t i = 0; i < index_count; i += 3)
{
for (int e = 0; e < 3; ++e)
{
unsigned int i0 = indices[i + e];
unsigned int i1 = indices[i + next[e]];
unsigned int i2 = indices[i + next[e + 1]];
assert(i0 < vertex_count && i1 < vertex_count && i2 < vertex_count);
unsigned long long edge = ((unsigned long long)i0 << 32) | i1;
unsigned long long* entry = hashLookup(edge_table, edge_table_size, edge_hasher, edge, ~0ull);
if (*entry == ~0ull)
{
*entry = edge;
// store vertex opposite to the edge
edge_vertex_table[entry - edge_table] = i2;
}
}
}
// build resulting index buffer: 6 indices for each input triangle
for (size_t i = 0; i < index_count; i += 3)
{
unsigned int patch[6];
for (int e = 0; e < 3; ++e)
{
unsigned int i0 = indices[i + e];
unsigned int i1 = indices[i + next[e]];
assert(i0 < vertex_count && i1 < vertex_count);
// note: this refers to the opposite edge!
unsigned long long edge = ((unsigned long long)i1 << 32) | i0;
unsigned long long* oppe = hashLookup(edge_table, edge_table_size, edge_hasher, edge, ~0ull);
patch[e * 2 + 0] = i0;
patch[e * 2 + 1] = (*oppe == ~0ull) ? i0 : edge_vertex_table[oppe - edge_table];
}
memcpy(destination + i * 2, patch, sizeof(patch));
}
}
void meshopt_generateTessellationIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride)
{
using namespace meshopt;
assert(index_count % 3 == 0);
assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
assert(vertex_positions_stride % sizeof(float) == 0);
meshopt_Allocator allocator;
static const int next[3] = {1, 2, 0};
// build position remap: for each vertex, which other (canonical) vertex does it map to?
unsigned int* remap = allocator.allocate<unsigned int>(vertex_count);
buildPositionRemap(remap, vertex_positions, vertex_count, vertex_positions_stride, allocator);
// build edge set; this stores all triangle edges but we can look these up by any other wedge
EdgeHasher edge_hasher = {remap};
size_t edge_table_size = hashBuckets(index_count);
unsigned long long* edge_table = allocator.allocate<unsigned long long>(edge_table_size);
memset(edge_table, -1, edge_table_size * sizeof(unsigned long long));
for (size_t i = 0; i < index_count; i += 3)
{
for (int e = 0; e < 3; ++e)
{
unsigned int i0 = indices[i + e];
unsigned int i1 = indices[i + next[e]];
assert(i0 < vertex_count && i1 < vertex_count);
unsigned long long edge = ((unsigned long long)i0 << 32) | i1;
unsigned long long* entry = hashLookup(edge_table, edge_table_size, edge_hasher, edge, ~0ull);
if (*entry == ~0ull)
*entry = edge;
}
}
// build resulting index buffer: 12 indices for each input triangle
for (size_t i = 0; i < index_count; i += 3)
{
unsigned int patch[12];
for (int e = 0; e < 3; ++e)
{
unsigned int i0 = indices[i + e];
unsigned int i1 = indices[i + next[e]];
assert(i0 < vertex_count && i1 < vertex_count);
// note: this refers to the opposite edge!
unsigned long long edge = ((unsigned long long)i1 << 32) | i0;
unsigned long long oppe = *hashLookup(edge_table, edge_table_size, edge_hasher, edge, ~0ull);
// use the same edge if opposite edge doesn't exist (border)
oppe = (oppe == ~0ull) ? edge : oppe;
// triangle index (0, 1, 2)
patch[e] = i0;
// opposite edge (3, 4; 5, 6; 7, 8)
patch[3 + e * 2 + 0] = unsigned(oppe);
patch[3 + e * 2 + 1] = unsigned(oppe >> 32);
// dominant vertex (9, 10, 11)
patch[9 + e] = remap[i0];
}
memcpy(destination + i * 4, patch, sizeof(patch));
}
}
size_t meshopt_generateProvokingIndexBuffer(unsigned int* destination, unsigned int* reorder, const unsigned int* indices, size_t index_count, size_t vertex_count)
{
assert(index_count % 3 == 0);
meshopt_Allocator allocator;
unsigned int* remap = allocator.allocate<unsigned int>(vertex_count);
memset(remap, -1, vertex_count * sizeof(unsigned int));
// compute vertex valence; this is used to prioritize least used corner
// note: we use 8-bit counters for performance; for outlier vertices the valence is incorrect but that just affects the heuristic
unsigned char* valence = allocator.allocate<unsigned char>(vertex_count);
memset(valence, 0, vertex_count);
for (size_t i = 0; i < index_count; ++i)
{
unsigned int index = indices[i];
assert(index < vertex_count);
valence[index]++;
}
unsigned int reorder_offset = 0;
// assign provoking vertices; leave the rest for the next pass
for (size_t i = 0; i < index_count; i += 3)
{
unsigned int a = indices[i + 0], b = indices[i + 1], c = indices[i + 2];
assert(a < vertex_count && b < vertex_count && c < vertex_count);
// try to rotate triangle such that provoking vertex hasn't been seen before
// if multiple vertices are new, prioritize the one with least valence
// this reduces the risk that a future triangle will have all three vertices seen
unsigned int va = remap[a] == ~0u ? valence[a] : ~0u;
unsigned int vb = remap[b] == ~0u ? valence[b] : ~0u;
unsigned int vc = remap[c] == ~0u ? valence[c] : ~0u;
if (vb != ~0u && vb <= va && vb <= vc)
{
// abc -> bca
unsigned int t = a;
a = b, b = c, c = t;
}
else if (vc != ~0u && vc <= va && vc <= vb)
{
// abc -> cab
unsigned int t = c;
c = b, b = a, a = t;
}
unsigned int newidx = reorder_offset;
// now remap[a] = ~0u or all three vertices are old
// recording remap[a] makes it possible to remap future references to the same index, conserving space
if (remap[a] == ~0u)
remap[a] = newidx;
// we need to clone the provoking vertex to get a unique index
// if all three are used the choice is arbitrary since no future triangle will be able to reuse any of these
reorder[reorder_offset++] = a;
// note: first vertex is final, the other two will be fixed up in next pass
destination[i + 0] = newidx;
destination[i + 1] = b;
destination[i + 2] = c;
// update vertex valences for corner heuristic
valence[a]--;
valence[b]--;
valence[c]--;
}
// remap or clone non-provoking vertices (iterating to skip provoking vertices)
int step = 1;
for (size_t i = 1; i < index_count; i += step, step ^= 3)
{
unsigned int index = destination[i];
if (remap[index] == ~0u)
{
// we haven't seen the vertex before as a provoking vertex
// to maintain the reference to the original vertex we need to clone it
unsigned int newidx = reorder_offset;
remap[index] = newidx;
reorder[reorder_offset++] = index;
}
destination[i] = remap[index];
}
assert(reorder_offset <= vertex_count + index_count / 3);
return reorder_offset;
}
File diff suppressed because it is too large Load Diff
+619
View File
@@ -0,0 +1,619 @@
// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details
#include "meshoptimizer.h"
#include <assert.h>
#include <float.h>
#include <math.h>
#include <string.h>
// This work is based on:
// Matthaeus Chajdas. GeometryFX 1.2 - Cluster Culling. 2016
// Jack Ritter. An Efficient Bounding Sphere. 1990
// Thomas Larsson. Fast and Tight Fitting Bounding Spheres. 2008
namespace meshopt
{
// This must be <= 256 since meshlet indices are stored as bytes
const size_t kMeshletMaxVertices = 256;
// A reasonable limit is around 2*max_vertices or less
const size_t kMeshletMaxTriangles = 512;
static void computeBoundingSphere(float result[4], const float* points, size_t count, size_t points_stride, const float* radii, size_t radii_stride, size_t axis_count, const unsigned int* indices = NULL)
{
static const float axes[7][3] = {
// X, Y, Z
{1, 0, 0},
{0, 1, 0},
{0, 0, 1},
// XYZ, -XYZ, X-YZ, XY-Z; normalized to unit length
{0.57735026f, 0.57735026f, 0.57735026f},
{-0.57735026f, 0.57735026f, 0.57735026f},
{0.57735026f, -0.57735026f, 0.57735026f},
{0.57735026f, 0.57735026f, -0.57735026f},
};
assert(count > 0);
assert(axis_count <= sizeof(axes) / sizeof(axes[0]));
size_t points_stride_float = points_stride / sizeof(float);
size_t radii_stride_float = radii_stride / sizeof(float);
// find extremum points along all axes; for each axis we get a pair of points with min/max coordinates
unsigned int pmin[7], pmax[7];
float tmin[7], tmax[7];
for (size_t axis = 0; axis < axis_count; ++axis)
{
pmin[axis] = pmax[axis] = 0;
tmin[axis] = FLT_MAX;
tmax[axis] = -FLT_MAX;
}
for (size_t i = 0; i < count; ++i)
{
unsigned int v = indices ? indices[i] : unsigned(i);
const float* p = points + v * points_stride_float;
float r = radii[v * radii_stride_float];
for (size_t axis = 0; axis < axis_count; ++axis)
{
const float* ax = axes[axis];
float tp = ax[0] * p[0] + ax[1] * p[1] + ax[2] * p[2];
float tpmin = tp - r, tpmax = tp + r;
pmin[axis] = (tpmin < tmin[axis]) ? v : pmin[axis];
pmax[axis] = (tpmax > tmax[axis]) ? v : pmax[axis];
tmin[axis] = (tpmin < tmin[axis]) ? tpmin : tmin[axis];
tmax[axis] = (tpmax > tmax[axis]) ? tpmax : tmax[axis];
}
}
// find the pair of points with largest distance
size_t paxis = 0;
float paxisdr = 0;
for (size_t axis = 0; axis < axis_count; ++axis)
{
const float* p1 = points + pmin[axis] * points_stride_float;
const float* p2 = points + pmax[axis] * points_stride_float;
float r1 = radii[pmin[axis] * radii_stride_float];
float r2 = radii[pmax[axis] * radii_stride_float];
float d2 = (p2[0] - p1[0]) * (p2[0] - p1[0]) + (p2[1] - p1[1]) * (p2[1] - p1[1]) + (p2[2] - p1[2]) * (p2[2] - p1[2]);
float dr = sqrtf(d2) + r1 + r2;
if (dr > paxisdr)
{
paxisdr = dr;
paxis = axis;
}
}
// use the longest segment as the initial sphere diameter
const float* p1 = points + pmin[paxis] * points_stride_float;
const float* p2 = points + pmax[paxis] * points_stride_float;
float r1 = radii[pmin[paxis] * radii_stride_float];
float r2 = radii[pmax[paxis] * radii_stride_float];
float paxisd = sqrtf((p2[0] - p1[0]) * (p2[0] - p1[0]) + (p2[1] - p1[1]) * (p2[1] - p1[1]) + (p2[2] - p1[2]) * (p2[2] - p1[2]));
float paxisk = paxisd > 0 ? (paxisd + r2 - r1) / (2 * paxisd) : 0.f;
float center[3] = {p1[0] + (p2[0] - p1[0]) * paxisk, p1[1] + (p2[1] - p1[1]) * paxisk, p1[2] + (p2[2] - p1[2]) * paxisk};
float radius = paxisdr / 2;
// iteratively adjust the sphere up until all points fit
for (size_t i = 0; i < count; ++i)
{
unsigned int v = indices ? indices[i] : unsigned(i);
const float* p = points + v * points_stride_float;
float r = radii[v * radii_stride_float];
float d2 = (p[0] - center[0]) * (p[0] - center[0]) + (p[1] - center[1]) * (p[1] - center[1]) + (p[2] - center[2]) * (p[2] - center[2]);
float d = sqrtf(d2);
if (d + r > radius)
{
float k = d > 0 ? (d + r - radius) / (2 * d) : 0.f;
center[0] += k * (p[0] - center[0]);
center[1] += k * (p[1] - center[1]);
center[2] += k * (p[2] - center[2]);
radius = (radius + d + r) / 2;
}
}
result[0] = center[0];
result[1] = center[1];
result[2] = center[2];
result[3] = radius;
}
static meshopt_Bounds computeClusterBounds(const unsigned int* indices, size_t index_count, const unsigned int* corners, size_t corner_count, const float* vertex_positions, size_t vertex_positions_stride)
{
size_t vertex_stride_float = vertex_positions_stride / sizeof(float);
// compute triangle normals (.w completes plane equation)
float normals[kMeshletMaxTriangles][4];
size_t triangles = 0;
for (size_t i = 0; i < index_count; i += 3)
{
unsigned int a = indices[i + 0], b = indices[i + 1], c = indices[i + 2];
const float* p0 = vertex_positions + vertex_stride_float * a;
const float* p1 = vertex_positions + vertex_stride_float * b;
const float* p2 = vertex_positions + vertex_stride_float * c;
float p10[3] = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]};
float p20[3] = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]};
float normalx = p10[1] * p20[2] - p10[2] * p20[1];
float normaly = p10[2] * p20[0] - p10[0] * p20[2];
float normalz = p10[0] * p20[1] - p10[1] * p20[0];
float area = sqrtf(normalx * normalx + normaly * normaly + normalz * normalz);
// no need to include degenerate triangles - they will be invisible anyway
if (area == 0.f)
continue;
normalx /= area;
normaly /= area;
normalz /= area;
// record triangle normals; normal and corner 0 define a plane equation
normals[triangles][0] = normalx;
normals[triangles][1] = normaly;
normals[triangles][2] = normalz;
normals[triangles][3] = -(normalx * p0[0] + normaly * p0[1] + normalz * p0[2]);
triangles++;
}
meshopt_Bounds bounds = {};
// degenerate cluster, no valid triangles => trivial reject (cone data is 0)
if (triangles == 0)
return bounds;
const float rzero = 0.f;
// compute cluster bounding sphere; we'll use the center to determine normal cone apex as well
float psphere[4] = {};
computeBoundingSphere(psphere, vertex_positions, corner_count, vertex_positions_stride, &rzero, 0, 7, corners);
float center[3] = {psphere[0], psphere[1], psphere[2]};
// treating triangle normals as points, find the bounding sphere - the sphere center determines the optimal cone axis
float nsphere[4] = {};
computeBoundingSphere(nsphere, normals[0], triangles, sizeof(float) * 4, &rzero, 0, 3);
float axis[3] = {nsphere[0], nsphere[1], nsphere[2]};
float axislength = sqrtf(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]);
float invaxislength = axislength == 0.f ? 0.f : 1.f / axislength;
axis[0] *= invaxislength;
axis[1] *= invaxislength;
axis[2] *= invaxislength;
// compute a tight cone around all normals, mindp = cos(angle/2)
float mindp = 1.f;
for (size_t i = 0; i < triangles; ++i)
{
float dp = normals[i][0] * axis[0] + normals[i][1] * axis[1] + normals[i][2] * axis[2];
mindp = (dp < mindp) ? dp : mindp;
}
// fill bounding sphere info; note that below we can return bounds without cone information for degenerate cones
bounds.center[0] = center[0];
bounds.center[1] = center[1];
bounds.center[2] = center[2];
bounds.radius = psphere[3];
// degenerate cluster, normal cone is larger than a hemisphere => trivial accept
// note that if mindp is positive but close to 0, the triangle intersection code below gets less stable
// we arbitrarily decide that if a normal cone is ~168 degrees wide or more, the cone isn't useful
if (mindp <= 0.1f)
{
bounds.cone_cutoff = 1;
bounds.cone_cutoff_s8 = 127;
return bounds;
}
float maxt = 0;
// we need to find the point on center-t*axis ray that lies in negative half-space of all triangles
for (size_t i = 0; i < triangles; ++i)
{
// dot(center-t*axis-corner, trinormal) = 0
// dot(center-corner, trinormal) - t * dot(axis, trinormal) = 0
float dc = center[0] * normals[i][0] + center[1] * normals[i][1] + center[2] * normals[i][2] + normals[i][3];
float dn = axis[0] * normals[i][0] + axis[1] * normals[i][1] + axis[2] * normals[i][2];
// dn should be larger than mindp cutoff above
assert(dn > 0.f);
float t = dc / dn;
maxt = (t > maxt) ? t : maxt;
}
// cone apex should be in the negative half-space of all cluster triangles by construction
bounds.cone_apex[0] = center[0] - axis[0] * maxt;
bounds.cone_apex[1] = center[1] - axis[1] * maxt;
bounds.cone_apex[2] = center[2] - axis[2] * maxt;
// note: this axis is the axis of the normal cone, but our test for perspective camera effectively negates the axis
bounds.cone_axis[0] = axis[0];
bounds.cone_axis[1] = axis[1];
bounds.cone_axis[2] = axis[2];
// cos(a) for normal cone is mindp; we need to add 90 degrees on both sides and invert the cone
// which gives us -cos(a+90) = -(-sin(a)) = sin(a) = sqrt(1 - cos^2(a))
bounds.cone_cutoff = sqrtf(1 - mindp * mindp);
// quantize axis & cutoff to 8-bit SNORM format
bounds.cone_axis_s8[0] = (signed char)(meshopt_quantizeSnorm(bounds.cone_axis[0], 8));
bounds.cone_axis_s8[1] = (signed char)(meshopt_quantizeSnorm(bounds.cone_axis[1], 8));
bounds.cone_axis_s8[2] = (signed char)(meshopt_quantizeSnorm(bounds.cone_axis[2], 8));
// for the 8-bit test to be conservative, we need to adjust the cutoff by measuring the max. error
float cone_axis_s8_e0 = fabsf(bounds.cone_axis_s8[0] / 127.f - bounds.cone_axis[0]);
float cone_axis_s8_e1 = fabsf(bounds.cone_axis_s8[1] / 127.f - bounds.cone_axis[1]);
float cone_axis_s8_e2 = fabsf(bounds.cone_axis_s8[2] / 127.f - bounds.cone_axis[2]);
// note that we need to round this up instead of rounding to nearest, hence +1
int cone_cutoff_s8 = int(127 * (bounds.cone_cutoff + cone_axis_s8_e0 + cone_axis_s8_e1 + cone_axis_s8_e2) + 1);
bounds.cone_cutoff_s8 = (cone_cutoff_s8 > 127) ? 127 : (signed char)(cone_cutoff_s8);
return bounds;
}
} // namespace meshopt
meshopt_Bounds meshopt_computeClusterBounds(const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride)
{
using namespace meshopt;
assert(index_count % 3 == 0);
assert(index_count / 3 <= kMeshletMaxTriangles);
assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
assert(vertex_positions_stride % sizeof(float) == 0);
(void)vertex_count;
unsigned int cache[512];
memset(cache, -1, sizeof(cache));
unsigned int corners[kMeshletMaxTriangles * 3 + 1]; // +1 for branchless slot
size_t corner_count = 0;
for (size_t i = 0; i < index_count; ++i)
{
unsigned int v = indices[i];
assert(v < vertex_count);
unsigned int& c = cache[v & (sizeof(cache) / sizeof(cache[0]) - 1)];
// branchless append if vertex isn't in cache
corners[corner_count] = v;
corner_count += (c != v);
c = v;
}
return computeClusterBounds(indices, index_count, corners, corner_count, vertex_positions, vertex_positions_stride);
}
meshopt_Bounds meshopt_computeMeshletBounds(const unsigned int* meshlet_vertices, const unsigned char* meshlet_triangles, size_t triangle_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride)
{
using namespace meshopt;
assert(triangle_count <= kMeshletMaxTriangles);
assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256);
assert(vertex_positions_stride % sizeof(float) == 0);
(void)vertex_count;
unsigned int indices[kMeshletMaxTriangles * 3];
size_t corner_count = 0;
for (size_t i = 0; i < triangle_count * 3; ++i)
{
unsigned char t = meshlet_triangles[i];
unsigned int index = meshlet_vertices[t];
assert(index < vertex_count);
indices[i] = index;
// meshlet_vertices[] slice should only contain vertices used by triangle indices, which is the case for any well formed meshlet
corner_count = t >= corner_count ? t + 1 : corner_count;
}
return computeClusterBounds(indices, triangle_count * 3, meshlet_vertices, corner_count, vertex_positions, vertex_positions_stride);
}
meshopt_Bounds meshopt_computeSphereBounds(const float* positions, size_t count, size_t positions_stride, const float* radii, size_t radii_stride)
{
using namespace meshopt;
assert(positions_stride >= 12 && positions_stride <= 256);
assert(positions_stride % sizeof(float) == 0);
assert((radii_stride >= 4 && radii_stride <= 256) || radii == NULL);
assert(radii_stride % sizeof(float) == 0);
meshopt_Bounds bounds = {};
if (count == 0)
return bounds;
const float rzero = 0.f;
float psphere[4] = {};
computeBoundingSphere(psphere, positions, count, positions_stride, radii ? radii : &rzero, radii ? radii_stride : 0, 7);
bounds.center[0] = psphere[0];
bounds.center[1] = psphere[1];
bounds.center[2] = psphere[2];
bounds.radius = psphere[3];
return bounds;
}
void meshopt_optimizeMeshletLevel(unsigned int* meshlet_vertices, size_t vertex_count, unsigned char* meshlet_triangles, size_t triangle_count, int level)
{
using namespace meshopt;
assert(triangle_count <= kMeshletMaxTriangles);
assert(vertex_count <= kMeshletMaxVertices);
assert(level >= 0 && level <= 9);
unsigned char* indices = meshlet_triangles;
unsigned int* vertices = meshlet_vertices;
// cache tracks vertex timestamps (corresponding to triangle index! all 3 vertices are added at the same time and never removed)
unsigned char cache[kMeshletMaxVertices];
memset(cache, 0, vertex_count);
// note that we start from a value that means all vertices aren't in cache
unsigned char cache_last = 128;
const unsigned char cache_cutoff = 3; // 3 triangles = ~5..9 vertices depending on reuse
// vertex valence is used to prioritize triangles for level>0
// note: we use 8-bit counters for performance; for outlier vertices the valence is incorrect but that just affects the heuristic
unsigned char valence[kMeshletMaxVertices];
memset(valence, 0, vertex_count);
for (size_t i = 0; i < triangle_count; ++i)
{
unsigned char a = indices[i * 3 + 0], b = indices[i * 3 + 1], c = indices[i * 3 + 2];
assert(a < vertex_count && b < vertex_count && c < vertex_count);
valence[a]++;
valence[b]++;
valence[c]++;
}
for (size_t i = 0; i < triangle_count; ++i)
{
int next = -1;
int next_score = -1;
int edges = 0;
for (size_t j = i; j < triangle_count; ++j)
{
unsigned char a = indices[j * 3 + 0], b = indices[j * 3 + 1], c = indices[j * 3 + 2];
assert(a < vertex_count && b < vertex_count && c < vertex_count);
// compute cache distance using unsigned 8-bit subtraction, so cache timestamp overflow is handled gracefully
unsigned char ad = (unsigned char)(cache_last - cache[a]);
unsigned char bd = (unsigned char)(cache_last - cache[b]);
unsigned char cd = (unsigned char)(cache_last - cache[c]);
int match = (ad < cache_cutoff) + (bd < cache_cutoff) + (cd < cache_cutoff);
if (level)
{
// prefer low minimum valence
int vmin = valence[a] < valence[b] ? valence[a] : valence[b];
vmin = valence[c] < vmin ? valence[c] : vmin;
// prefer vertices with smaller cache distance and valence to improve traversal locality
int score = match * 1024 + (1023 - ad - bd - cd);
score = score * 256 + (255 - vmin);
next = (score > next_score) ? int(j) : next;
next_score = (score > next_score) ? score : next_score;
// terminate after finding enough edge matches
if (match >= 2 && ++edges >= level)
break;
}
else
{
int score = match;
next = (score > next_score) ? int(j) : next;
next_score = (score > next_score) ? score : next_score;
// settle for a first edge match, which makes the function ~linear in practice
if (match >= 2)
break;
}
}
assert(next >= 0);
unsigned char a = indices[next * 3 + 0], b = indices[next * 3 + 1], c = indices[next * 3 + 2];
// shift triangles before the next one forward so that we always keep an ordered partition
// note: this could have swapped triangles [i] and [next] but that distorts the order and may skew the output sequence
memmove(indices + (i + 1) * 3, indices + i * 3, (next - i) * 3 * sizeof(unsigned char));
indices[i * 3 + 0] = a;
indices[i * 3 + 1] = b;
indices[i * 3 + 2] = c;
// cache timestamp is the same between all vertices of each triangle to reduce overflow
cache_last++;
cache[a] = cache_last;
cache[b] = cache_last;
cache[c] = cache_last;
// update vertex valences for scoring heuristic
valence[a]--;
valence[b]--;
valence[c]--;
}
// rotate triangles to maximize compressibility; only done at level >= 1 for compatibility
if (level >= 1)
{
memset(cache, 0, vertex_count);
for (size_t i = 0; i < triangle_count; ++i)
{
unsigned char a = indices[i * 3 + 0], b = indices[i * 3 + 1], c = indices[i * 3 + 2];
// if only the middle vertex has been used, rotate triangle to ensure new vertices are always sequential
if (!cache[a] && cache[b] && !cache[c])
{
// abc -> bca
unsigned char t = a;
a = b, b = c, c = t;
}
else if (!cache[a] && !cache[b] && !cache[c])
{
// out of three edges, the edge ab can not be reused by subsequent triangles in some encodings
// if subsequent triangles don't share edges ca or bc, we can rotate the triangle to fix this
bool needab = false, needbc = false, needca = false;
for (size_t j = i + 1; j < triangle_count && j <= i + cache_cutoff; ++j)
{
unsigned char oa = indices[j * 3 + 0], ob = indices[j * 3 + 1], oc = indices[j * 3 + 2];
// note: edge comparisons are reversed as reused edges are flipped
needab |= (oa == b && ob == a) || (ob == b && oc == a) || (oc == b && oa == a);
needbc |= (oa == c && ob == b) || (ob == c && oc == b) || (oc == c && oa == b);
needca |= (oa == a && ob == c) || (ob == a && oc == c) || (oc == a && oa == c);
}
if (needab && !needbc)
{
// abc -> bca
unsigned char t = a;
a = b, b = c, c = t;
}
else if (needab && !needca)
{
// abc -> cab
unsigned char t = c;
c = b, b = a, a = t;
}
else if (!needab && needbc && !needca)
{
// abc -> cab: put bc at most recent FIFO position
unsigned char t = c;
c = b, b = a, a = t;
}
}
indices[i * 3 + 0] = a, indices[i * 3 + 1] = b, indices[i * 3 + 2] = c;
cache[a] = cache[b] = cache[c] = 1;
}
}
// reorder meshlet vertices for access locality assuming index buffer is scanned sequentially
unsigned int order[kMeshletMaxVertices];
short remap[kMeshletMaxVertices];
memset(remap, -1, vertex_count * sizeof(short));
size_t vertex_offset = 0;
for (size_t i = 0; i < triangle_count * 3; ++i)
{
short& r = remap[indices[i]];
if (r < 0)
{
r = short(vertex_offset);
order[vertex_offset] = vertices[indices[i]];
vertex_offset++;
}
indices[i] = (unsigned char)r;
}
assert(vertex_offset <= vertex_count);
memcpy(vertices, order, vertex_offset * sizeof(unsigned int));
}
void meshopt_optimizeMeshlet(unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, size_t triangle_count, size_t vertex_count)
{
meshopt_optimizeMeshletLevel(meshlet_vertices, vertex_count, meshlet_triangles, triangle_count, 0);
}
size_t meshopt_extractMeshletIndices(unsigned int* vertices, unsigned char* triangles, const unsigned int* indices, size_t index_count)
{
using namespace meshopt;
assert(index_count % 3 == 0);
assert(index_count / 3 <= kMeshletMaxTriangles);
size_t unique = 0;
// direct mapped cache for fast lookups based on low index bits; inspired by vk_lod_clusters from NVIDIA
short cache[1024];
memset(cache, -1, sizeof(cache));
for (size_t i = 0; i < index_count; ++i)
{
unsigned int v = indices[i];
unsigned int key = v & (sizeof(cache) / sizeof(cache[0]) - 1);
short c = cache[key];
// fast path: vertex has been seen before
if (c >= 0 && vertices[c] == v)
{
triangles[i] = (unsigned char)c;
continue;
}
// fast path: vertex has never been seen before
if (c < 0)
{
assert(unique < kMeshletMaxVertices);
cache[key] = short(unique);
triangles[i] = (unsigned char)unique;
vertices[unique++] = v;
continue;
}
// slow path: collision with a different vertex, so we need to look through all vertices
int pos = -1;
for (size_t j = 0; j < unique; ++j)
if (vertices[j] == v)
{
pos = int(j);
break;
}
if (pos < 0)
{
assert(unique < kMeshletMaxVertices);
pos = int(unique);
vertices[unique++] = v;
}
cache[key] = short(pos);
triangles[i] = (unsigned char)pos;
}
assert(unique <= kMeshletMaxVertices);
return unique;
}
File diff suppressed because it is too large Load Diff
+557
View File
@@ -0,0 +1,557 @@
// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details
#include "meshoptimizer.h"
#include <assert.h>
#include <math.h>
#include <string.h>
namespace meshopt
{
// opacity micromaps use a "bird" traversal order which recursively subdivides the triangles:
// https://docs.vulkan.org/spec/latest/_images/micromap-subd.svg
// note that triangles 0 and 2 have the same winding as the source triangle, however triangles 1 (flipped)
// and 3 (upright) have flipped winding; this is obvious from the level 2 subdivision in the diagram above
inline size_t getLevelSize(int level, int states)
{
// 1-bit 2-state or 2-bit 4-state per micro triangle, rounded up to whole bytes
return ((1 << (level * 2)) * (states >> 1) + 7) >> 3;
}
struct Texture
{
const unsigned char* data;
size_t stride, pitch;
unsigned int width, height;
float widthf, heightf; // width * 256.f, height * 256.f
};
static float sampleTexture(const Texture& texture, float u, float v)
{
// wrap texture coordinates; floor is expensive so only call it if we're outside of [0, 1] range (+eps)
u = fabsf(u - 0.5f) > 0.5f ? u - floorf(u) : u;
v = fabsf(v - 0.5f) > 0.5f ? v - floorf(v) : v;
// convert from [0, 1] to 16.8 fixed point coordinates (rounded to nearest subpixel) with texel centers on integer grid
int uf = int(u * texture.widthf - 127.5f);
int vf = int(v * texture.heightf - 127.5f);
// clamp to avoid extrapolation past left/top edge since we don't wrap across the edge
uf = uf < 0 ? 0 : uf;
vf = vf < 0 ? 0 : vf;
// x/y are texel coordinates, rx/ry are subpixel offsets
int x = uf >> 8;
int y = vf >> 8;
int rx = uf & 255;
int ry = vf & 255;
// safeguard: this should not happen but if it ever does, ensure the accesses are inbounds
if (unsigned(x) >= texture.width || unsigned(y) >= texture.height)
return 0.f;
// clamp the offsets instead of wrapping for simplicity and performance
size_t offset = size_t(y) * texture.pitch + x * texture.stride;
size_t offsetx = (x + 1 < int(texture.width)) ? texture.stride : 0;
size_t offsety = (y + 1 < int(texture.height)) ? texture.pitch : 0;
unsigned char a00 = texture.data[offset];
unsigned char a10 = texture.data[offset + offsetx];
unsigned char a01 = texture.data[offset + offsety];
unsigned char a11 = texture.data[offset + offsetx + offsety];
// bilinear interpolation in integer space: result is 8.16 fixed point
int ax0 = a00 * 256 + (a10 - a00) * rx;
int ax1 = a01 * 256 + (a11 - a01) * rx;
int axy = ax0 * 256 + (ax1 - ax0) * ry;
return float(axy) * (1.f / (255.f * 65536.f));
}
static unsigned int hashUpdate4u(unsigned int h, const unsigned char* key, size_t len)
{
// MurmurHash2
const unsigned int m = 0x5bd1e995;
const int r = 24;
while (len >= 4)
{
unsigned int k;
memcpy(&k, key, sizeof(k));
k *= m;
k ^= k >> r;
k *= m;
h *= m;
h ^= k;
key += 4;
len -= 4;
}
return h;
}
struct TriangleOMM
{
int uvs[6];
int level;
};
struct TriangleOMMHasher
{
const TriangleOMM* data;
size_t hash(unsigned int index) const
{
const TriangleOMM& tri = data[index];
return hashUpdate4u(tri.level, reinterpret_cast<const unsigned char*>(tri.uvs), sizeof(tri.uvs));
}
bool equal(unsigned int lhs, unsigned int rhs) const
{
const TriangleOMM& lt = data[lhs];
const TriangleOMM& rt = data[rhs];
return lt.level == rt.level && memcmp(lt.uvs, rt.uvs, sizeof(lt.uvs)) == 0;
}
};
struct OMMHasher
{
const unsigned char* data;
const unsigned int* offsets;
const unsigned char* levels;
int states;
size_t hash(unsigned int index) const
{
const unsigned char* key = data + offsets[index];
size_t size = getLevelSize(levels[index], states);
unsigned int h = levels[index];
// MurmurHash2 for large keys, simple fold for small; note that size is a power of two
if (size < 4)
h ^= key[0] | (key[size - 1] << 8);
else
h = hashUpdate4u(h, key, size);
// MurmurHash2 finalizer
h ^= h >> 13;
h *= 0x5bd1e995;
h ^= h >> 15;
return h;
}
bool equal(unsigned int lhs, unsigned int rhs) const
{
size_t size = getLevelSize(levels[lhs], states);
return levels[lhs] == levels[rhs] && memcmp(data + offsets[lhs], data + offsets[rhs], size) == 0;
}
};
static size_t hashBuckets3(size_t count)
{
size_t buckets = 1;
while (buckets < count + count / 4)
buckets *= 2;
return buckets;
}
template <typename T, typename Hash>
static T* hashLookup3(T* table, size_t buckets, const Hash& hash, const T& key, const T& empty)
{
assert(buckets > 0);
assert((buckets & (buckets - 1)) == 0);
size_t hashmod = buckets - 1;
size_t bucket = hash.hash(key) & hashmod;
for (size_t probe = 0; probe <= hashmod; ++probe)
{
T& item = table[bucket];
if (item == empty)
return &item;
if (hash.equal(item, key))
return &item;
// hash collision, quadratic probing
bucket = (bucket + probe + 1) & hashmod;
}
assert(false && "Hash table is full"); // unreachable
return NULL;
}
inline int quantizeSubpixel(float v, unsigned int size)
{
return int(v * float(int(size) * 4) + (v >= 0 ? 0.5f : -0.5f));
}
static int rasterizeEdge(float u0, float v0, float u1, float v1, int edgeres, const Texture& texture)
{
float edgestep = 1.f / float(edgeres + 1);
float ud = (u1 - u0) * edgestep, vd = (v1 - v0) * edgestep;
float u = u0, v = v0;
int mask = 0;
int count = 0;
for (int i = 0; i < edgeres; ++i)
{
u += ud;
v += vd;
float a = sampleTexture(texture, u, v);
mask |= (a >= 0.5f) << i;
count += a >= 0.5f;
}
return mask | (count << 16);
}
template <int States>
static void rasterizeOpacity0(unsigned char* result, size_t index, float a0, float a1, float a2, float ac, int e0, int e1, int e2, int edgeres)
{
int states = States;
// basic coverage estimator from center and corner values; trained to minimize error
float coverage = (a0 + a1 + a2) * 0.12f + ac * 0.64f;
if (edgeres)
{
float edgescale = 1.f / edgeres;
// if we have edge samples, we can get a better coverage estimate by including them; trained to minimize error
coverage = ac * 0.22f + float((e0 >> 16) + (e1 >> 16) + (e2 >> 16)) * edgescale * 0.23f + (a0 + a1 + a2) * 0.03f;
}
if (states == 2)
{
result[index / 8] |= (coverage >= 0.5f) << (index % 8);
return;
}
int transp = (a0 < 0.5f) & (a1 < 0.5f) & (a2 < 0.5f) & (ac < 0.5f);
int opaque = (a0 > 0.5f) & (a1 > 0.5f) & (a2 > 0.5f) & (ac > 0.5f);
// treat state as known if thresholding of corners & centers against wider bounds is consistent
// for unknown states, we currently use the same formula as the 2-state opacity for better consistency with forced 2-state
int unknown = 2 + (coverage >= 0.5f);
int state = (transp | opaque) ? opaque : unknown;
if (edgeres && (transp | opaque))
{
// if we have edge samples, ensure they are consistent too, falling back to unknown if not
int exp = opaque ? (1 << edgeres) - 1 : 0;
int eok = ((e0 & 0xffff) == exp) & ((e1 & 0xffff) == exp) & ((e2 & 0xffff) == exp);
state = eok ? state : unknown;
}
result[index / 4] |= state << ((index % 4) * 2);
}
template <int States>
static void rasterizeOpacity1(unsigned char* result, size_t index, int edgeres, const float* c0, const float* c1, const float* c2, const Texture& texture)
{
// compute each edge midpoint & sample
float c01[3] = {(c0[0] + c1[0]) / 2, (c0[1] + c1[1]) / 2, 0.f};
float c12[3] = {(c1[0] + c2[0]) / 2, (c1[1] + c2[1]) / 2, 0.f};
float c20[3] = {(c2[0] + c0[0]) / 2, (c2[1] + c0[1]) / 2, 0.f};
c01[2] = sampleTexture(texture, c01[0], c01[1]);
c12[2] = sampleTexture(texture, c12[0], c12[1]);
c20[2] = sampleTexture(texture, c20[0], c20[1]);
// corner tables for each edge, and corner + edge tables for each triangle
// edges are numbered counter clockwise, 6 outer first, 3 inner last; triangle vertex and edge references are in triangle winding order
static const unsigned char edges[9][2] = {{0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 0}, {5, 1}, {1, 3}, {3, 5}};
static const unsigned char triangles[4][6] = {{0, 1, 5, 0, 6, 5}, {5, 3, 1, 8, 7, 6}, {1, 2, 3, 1, 2, 7}, {3, 5, 4, 8, 4, 3}};
const float* points[] = {c0, c01, c1, c12, c2, c20};
int em[9] = {};
// sample additional points on the edges to improve state estimation
if (edgeres > 0)
for (size_t i = 0; i < 9; ++i)
em[i] = rasterizeEdge(points[edges[i][0]][0], points[edges[i][0]][1], points[edges[i][1]][0], points[edges[i][1]][1], edgeres, texture);
for (size_t i = 0; i < 4; ++i)
{
const unsigned char* tri = triangles[i];
const float* p0 = points[tri[0]];
const float* p1 = points[tri[1]];
const float* p2 = points[tri[2]];
// compute triangle center & sample
float uc = (p0[0] + p1[0] + p2[0]) * (1.f / 3.f);
float vc = (p0[1] + p1[1] + p2[1]) * (1.f / 3.f);
float ac = sampleTexture(texture, uc, vc);
// rasterize opacity state based on alpha values in corners and center (and optionally edges)
rasterizeOpacity0<States>(result, index * 4 + i, p0[2], p1[2], p2[2], ac, em[tri[3]], em[tri[4]], em[tri[5]], edgeres);
}
}
template <int States>
static void rasterizeOpacityRec(unsigned char* result, size_t index, int level, int edgeres, const float* c0, const float* c1, const float* c2, const Texture& texture)
{
if (level == 0)
{
// compute triangle center & sample
float uc = (c0[0] + c1[0] + c2[0]) * (1.f / 3.f);
float vc = (c0[1] + c1[1] + c2[1]) * (1.f / 3.f);
float ac = sampleTexture(texture, uc, vc);
int e0 = 0, e1 = 0, e2 = 0;
if (edgeres > 0)
{
// sample additional points on the edges to improve state estimation
e0 = rasterizeEdge(c0[0], c0[1], c1[0], c1[1], edgeres, texture);
e1 = rasterizeEdge(c1[0], c1[1], c2[0], c2[1], edgeres, texture);
e2 = rasterizeEdge(c2[0], c2[1], c0[0], c0[1], edgeres, texture);
}
// rasterize opacity state based on alpha values in corners and center (and optionally edges)
return rasterizeOpacity0<States>(result, index, c0[2], c1[2], c2[2], ac, e0, e1, e2, edgeres);
}
// fast path: equivalent to recursive rasterization, but reuses edge data to reduce sample count
if (level == 1 && edgeres > 0)
return rasterizeOpacity1<States>(result, index, edgeres, c0, c1, c2, texture);
// compute each edge midpoint & sample
float c01[3] = {(c0[0] + c1[0]) / 2, (c0[1] + c1[1]) / 2, 0.f};
float c12[3] = {(c1[0] + c2[0]) / 2, (c1[1] + c2[1]) / 2, 0.f};
float c20[3] = {(c2[0] + c0[0]) / 2, (c2[1] + c0[1]) / 2, 0.f};
c01[2] = sampleTexture(texture, c01[0], c01[1]);
c12[2] = sampleTexture(texture, c12[0], c12[1]);
c20[2] = sampleTexture(texture, c20[0], c20[1]);
// recursively rasterize each triangle
// note: triangles 1 and 3 have flipped winding, and 1 is flipped upside down
rasterizeOpacityRec<States>(result, index * 4 + 0, level - 1, edgeres, c0, c01, c20, texture);
rasterizeOpacityRec<States>(result, index * 4 + 1, level - 1, edgeres, c20, c12, c01, texture);
rasterizeOpacityRec<States>(result, index * 4 + 2, level - 1, edgeres, c01, c1, c12, texture);
rasterizeOpacityRec<States>(result, index * 4 + 3, level - 1, edgeres, c12, c20, c2, texture);
}
static int getSpecialIndex(const unsigned char* data, int level, int states)
{
int first = data[0] & (states == 2 ? 1 : 3);
int special = -(1 + first);
// at level 0, every micromap can be converted to a special index
if (level == 0)
return special;
// at level 1 with 2 states, the byte is partially filled so we need a separate check
if (level == 1 && states == 2)
return (data[0] & 15) == ((-first) & 15) ? special : 0;
// otherwise we need to check that all bytes are consistent with the first value and we can do this byte-wise
int expected = first * (states == 2 ? 0xff : 0x55);
size_t size = getLevelSize(level, states);
for (size_t i = 0; i < size; ++i)
if (data[i] != expected)
return 0;
return special;
}
} // namespace meshopt
size_t meshopt_opacityMapMeasure(unsigned char* levels, unsigned int* sources, int* omm_indices, const unsigned int* indices, size_t index_count, const float* vertex_uvs, size_t vertex_count, size_t vertex_uvs_stride, unsigned int texture_width, unsigned int texture_height, int max_level, float target_edge)
{
using namespace meshopt;
assert(index_count % 3 == 0);
assert(vertex_uvs_stride >= 8 && vertex_uvs_stride <= 256);
assert(vertex_uvs_stride % sizeof(float) == 0);
assert(unsigned(texture_width - 1) < 16384 && unsigned(texture_height - 1) < 16384);
assert(max_level >= 0 && max_level <= 12);
assert(target_edge >= 0);
(void)vertex_count;
meshopt_Allocator allocator;
size_t vertex_stride_float = vertex_uvs_stride / sizeof(float);
float texture_area = float(texture_width) * float(texture_height);
// hash map used to deduplicate triangle rasterization requests based on UV
size_t table_size = hashBuckets3(index_count / 3);
unsigned int* table = allocator.allocate<unsigned int>(table_size);
memset(table, -1, table_size * sizeof(unsigned int));
TriangleOMM* triangles = allocator.allocate<TriangleOMM>(index_count / 3);
TriangleOMMHasher hasher = {triangles};
size_t result = 0;
for (size_t i = 0; i < index_count; i += 3)
{
unsigned int a = indices[i + 0], b = indices[i + 1], c = indices[i + 2];
assert(a < vertex_count && b < vertex_count && c < vertex_count);
float u0 = vertex_uvs[a * vertex_stride_float + 0], v0 = vertex_uvs[a * vertex_stride_float + 1];
float u1 = vertex_uvs[b * vertex_stride_float + 0], v1 = vertex_uvs[b * vertex_stride_float + 1];
float u2 = vertex_uvs[c * vertex_stride_float + 0], v2 = vertex_uvs[c * vertex_stride_float + 1];
int level = max_level;
if (target_edge > 0)
{
// compute ratio of edge length (in texels) to target and determine subdivision level
float uvarea = fabsf((u1 - u0) * (v2 - v0) - (u2 - u0) * (v1 - v0)) * 0.5f * texture_area;
float ratio = sqrtf(uvarea) / target_edge;
float levelf = log2f(ratio > 1 ? ratio : 1);
// round to nearest and clamp
level = int(levelf + 0.5f);
level = level < 0 ? 0 : level;
level = level < max_level ? level : max_level;
}
// deduplicate rasterization requests based on UV
int su0 = quantizeSubpixel(u0, texture_width), sv0 = quantizeSubpixel(v0, texture_height);
int su1 = quantizeSubpixel(u1, texture_width), sv1 = quantizeSubpixel(v1, texture_height);
int su2 = quantizeSubpixel(u2, texture_width), sv2 = quantizeSubpixel(v2, texture_height);
TriangleOMM tri = {{su0, sv0, su1, sv1, su2, sv2}, level};
triangles[result] = tri; // speculatively write triangle data to give hasher a way to compare it
unsigned int* entry = hashLookup3(table, table_size, hasher, unsigned(result), ~0u);
if (*entry == ~0u)
{
*entry = unsigned(result);
levels[result] = (unsigned char)level;
sources[result] = unsigned(i / 3);
result++;
}
omm_indices[i / 3] = int(*entry);
}
return result;
}
size_t meshopt_opacityMapEntrySize(int level, int states)
{
assert(level >= 0 && level <= 12);
assert(states == 2 || states == 4);
return meshopt::getLevelSize(level, states);
}
void meshopt_opacityMapRasterize(unsigned char* result, int level, int states, const float* uv0, const float* uv1, const float* uv2, const unsigned char* texture_data, size_t texture_stride, size_t texture_pitch, unsigned int texture_width, unsigned int texture_height)
{
using namespace meshopt;
assert(level >= 0 && level <= 12);
assert(states == 2 || states == 4);
assert(unsigned(texture_width - 1) < 16384 && unsigned(texture_height - 1) < 16384);
assert(texture_stride >= 1 && texture_stride <= 4);
assert(texture_pitch >= texture_stride * texture_width);
memset(result, 0, getLevelSize(level, states));
Texture texture = {texture_data, texture_stride, texture_pitch, texture_width, texture_height, float(int(texture_width)) * 256.f, float(int(texture_height)) * 256.f};
// determine number of edge samples for conservative state estimation
float texture_area = float(int(texture_width)) * float(int(texture_height));
float uvarea = fabsf((uv1[0] - uv0[0]) * (uv2[1] - uv0[1]) - (uv2[0] - uv0[0]) * (uv1[1] - uv0[1])) * 0.5f * texture_area;
float uvedge = sqrtf(uvarea) / float(1 << level);
// target ~2px distance between edge samples (assuming equilateral microtriangles)
int edgeres = int(uvedge * 0.75f);
edgeres = edgeres < 0 ? 0 : edgeres;
edgeres = edgeres > 7 ? 7 : edgeres;
// rasterize all micro triangles recursively, passing corner data down to reduce redundant sampling
float c0[3] = {uv0[0], uv0[1], sampleTexture(texture, uv0[0], uv0[1])};
float c1[3] = {uv1[0], uv1[1], sampleTexture(texture, uv1[0], uv1[1])};
float c2[3] = {uv2[0], uv2[1], sampleTexture(texture, uv2[0], uv2[1])};
(states == 2 ? rasterizeOpacityRec<2> : rasterizeOpacityRec<4>)(result, 0, level, edgeres, c0, c1, c2, texture);
}
size_t meshopt_opacityMapCompact(unsigned char* data, size_t data_size, unsigned char* levels, unsigned int* offsets, size_t omm_count, int* omm_indices, size_t triangle_count, int states)
{
using namespace meshopt;
assert(states == 2 || states == 4);
meshopt_Allocator allocator;
unsigned char* data_old = allocator.allocate<unsigned char>(data_size);
memcpy(data_old, data, data_size);
size_t table_size = hashBuckets3(omm_count);
unsigned int* table = allocator.allocate<unsigned int>(table_size);
memset(table, -1, table_size * sizeof(unsigned int));
OMMHasher hasher = {data, offsets, levels, states};
int* remap = allocator.allocate<int>(omm_count);
size_t next = 0;
size_t offset = 0;
for (size_t i = 0; i < omm_count; ++i)
{
int level = levels[i];
assert(level >= 0 && level <= 12);
const unsigned char* old = data_old + offsets[i];
size_t size = getLevelSize(level, states);
assert(offsets[i] + size <= data_size);
// try to convert to a special index if all micro-triangle states are the same
int special = getSpecialIndex(old, level, states);
if (special < 0)
{
remap[i] = special;
continue;
}
// speculatively write data to give hasher a way to compare it
memcpy(data + offset, old, size);
offsets[next] = unsigned(offset);
levels[next] = (unsigned char)level;
unsigned int* entry = hashLookup3(table, table_size, hasher, unsigned(next), ~0u);
if (*entry == ~0u)
{
*entry = unsigned(next);
next++;
offset += size;
}
remap[i] = int(*entry);
}
// remap triangle indices to new indices or special indices
for (size_t i = 0; i < triangle_count; ++i)
{
assert(omm_indices[i] < 0 || unsigned(omm_indices[i]) < omm_count);
omm_indices[i] = omm_indices[i] < 0 ? omm_indices[i] : remap[omm_indices[i]];
}
return next;
}

Some files were not shown because too many files have changed in this diff Show More