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:
@@ -0,0 +1,2 @@
|
||||
/* This file makes sure the library can be used by C89 code */
|
||||
#include "../src/meshoptimizer.h"
|
||||
@@ -0,0 +1,962 @@
|
||||
/**
|
||||
* clusterlod - a small "library"/example built on top of meshoptimizer to generate cluster LOD hierarchies
|
||||
* This is intended to either be used as is, or as a reference for implementing similar functionality in your engine.
|
||||
*
|
||||
* To use this code, you need to have one source file which includes meshoptimizer.h and defines CLUSTERLOD_IMPLEMENTATION
|
||||
* before including this file. Other source files in your project can just include this file and use the provided functions.
|
||||
*
|
||||
* Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
|
||||
* This code is distributed under the MIT License. See notice at the end of this file.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <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.
|
||||
*/
|
||||
@@ -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
@@ -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.
|
||||
*/
|
||||
@@ -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
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user