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
+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