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 抓的)。
1979 lines
75 KiB
C++
1979 lines
75 KiB
C++
#include "../src/meshoptimizer.h"
|
||
|
||
#include <assert.h>
|
||
#include <float.h>
|
||
#include <math.h>
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
#include <time.h>
|
||
|
||
#include <vector>
|
||
|
||
#include "../extern/fast_obj.h"
|
||
|
||
#define SDEFL_IMPLEMENTATION
|
||
#include "../extern/sdefl.h"
|
||
|
||
// This file uses assert() to verify algorithm correctness
|
||
#undef NDEBUG
|
||
#include <assert.h>
|
||
|
||
#if defined(__linux__)
|
||
double timestamp()
|
||
{
|
||
timespec ts;
|
||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||
return double(ts.tv_sec) + 1e-9 * double(ts.tv_nsec);
|
||
}
|
||
#elif defined(_WIN32)
|
||
struct LARGE_INTEGER
|
||
{
|
||
__int64 QuadPart;
|
||
};
|
||
extern "C" __declspec(dllimport) int __stdcall QueryPerformanceCounter(LARGE_INTEGER* lpPerformanceCount);
|
||
extern "C" __declspec(dllimport) int __stdcall QueryPerformanceFrequency(LARGE_INTEGER* lpFrequency);
|
||
|
||
double timestamp()
|
||
{
|
||
LARGE_INTEGER freq, counter;
|
||
QueryPerformanceFrequency(&freq);
|
||
QueryPerformanceCounter(&counter);
|
||
return double(counter.QuadPart) / double(freq.QuadPart);
|
||
}
|
||
#else
|
||
double timestamp()
|
||
{
|
||
return double(clock()) / double(CLOCKS_PER_SEC);
|
||
}
|
||
#endif
|
||
|
||
struct Vertex
|
||
{
|
||
float px, py, pz;
|
||
float nx, ny, nz;
|
||
float tx, ty;
|
||
};
|
||
|
||
struct Mesh
|
||
{
|
||
std::vector<Vertex> vertices;
|
||
std::vector<unsigned int> indices;
|
||
};
|
||
|
||
Mesh parseObj(const char* path, double& reindex)
|
||
{
|
||
fastObjMesh* obj = fast_obj_read(path);
|
||
if (!obj)
|
||
{
|
||
printf("Error loading %s: file not found\n", path);
|
||
return Mesh();
|
||
}
|
||
|
||
size_t total_indices = 0;
|
||
|
||
for (unsigned int i = 0; i < obj->face_count; ++i)
|
||
if (obj->face_vertices[i] > 2)
|
||
total_indices += 3 * (obj->face_vertices[i] - 2);
|
||
|
||
std::vector<Vertex> vertices(total_indices);
|
||
|
||
size_t vertex_offset = 0;
|
||
size_t index_offset = 0;
|
||
|
||
for (unsigned int i = 0; i < obj->face_count; ++i)
|
||
{
|
||
if (obj->face_vertices[i] <= 2)
|
||
continue;
|
||
|
||
for (unsigned int j = 0; j < obj->face_vertices[i]; ++j)
|
||
{
|
||
fastObjIndex gi = obj->indices[index_offset + j];
|
||
|
||
Vertex v =
|
||
{
|
||
obj->positions[gi.p * 3 + 0],
|
||
obj->positions[gi.p * 3 + 1],
|
||
obj->positions[gi.p * 3 + 2],
|
||
obj->normals[gi.n * 3 + 0],
|
||
obj->normals[gi.n * 3 + 1],
|
||
obj->normals[gi.n * 3 + 2],
|
||
obj->texcoords[gi.t * 2 + 0],
|
||
obj->texcoords[gi.t * 2 + 1],
|
||
};
|
||
|
||
// triangulate polygon on the fly; offset-3 is always the first polygon vertex
|
||
if (j >= 3)
|
||
{
|
||
vertices[vertex_offset + 0] = vertices[vertex_offset - 3];
|
||
vertices[vertex_offset + 1] = vertices[vertex_offset - 1];
|
||
vertex_offset += 2;
|
||
}
|
||
|
||
vertices[vertex_offset] = v;
|
||
vertex_offset++;
|
||
}
|
||
|
||
index_offset += obj->face_vertices[i];
|
||
}
|
||
|
||
fast_obj_destroy(obj);
|
||
|
||
reindex = timestamp();
|
||
|
||
Mesh result;
|
||
|
||
// empty mesh
|
||
if (total_indices == 0)
|
||
return result;
|
||
|
||
std::vector<unsigned int> remap(total_indices);
|
||
|
||
size_t total_vertices = meshopt_generateVertexRemap(&remap[0], NULL, total_indices, &vertices[0], total_indices, sizeof(Vertex));
|
||
|
||
result.indices.resize(total_indices);
|
||
meshopt_remapIndexBuffer(&result.indices[0], NULL, total_indices, &remap[0]);
|
||
|
||
result.vertices.resize(total_vertices);
|
||
meshopt_remapVertexBuffer(&result.vertices[0], &vertices[0], total_indices, sizeof(Vertex), &remap[0]);
|
||
|
||
return result;
|
||
}
|
||
|
||
void dumpObj(const std::vector<Vertex>& vertices, const std::vector<unsigned int>& indices, bool recomputeNormals = false)
|
||
{
|
||
std::vector<float> normals;
|
||
|
||
if (recomputeNormals)
|
||
{
|
||
normals.resize(vertices.size() * 3);
|
||
|
||
for (size_t i = 0; i < indices.size(); i += 3)
|
||
{
|
||
unsigned int a = indices[i], b = indices[i + 1], c = indices[i + 2];
|
||
|
||
const Vertex& va = vertices[a];
|
||
const Vertex& vb = vertices[b];
|
||
const Vertex& vc = vertices[c];
|
||
|
||
float nx = (vb.py - va.py) * (vc.pz - va.pz) - (vb.pz - va.pz) * (vc.py - va.py);
|
||
float ny = (vb.pz - va.pz) * (vc.px - va.px) - (vb.px - va.px) * (vc.pz - va.pz);
|
||
float nz = (vb.px - va.px) * (vc.py - va.py) - (vb.py - va.py) * (vc.px - va.px);
|
||
|
||
for (int k = 0; k < 3; ++k)
|
||
{
|
||
unsigned int index = indices[i + k];
|
||
|
||
normals[index * 3 + 0] += nx;
|
||
normals[index * 3 + 1] += ny;
|
||
normals[index * 3 + 2] += nz;
|
||
}
|
||
}
|
||
}
|
||
|
||
for (size_t i = 0; i < vertices.size(); ++i)
|
||
{
|
||
const Vertex& v = vertices[i];
|
||
|
||
float nx = v.nx, ny = v.ny, nz = v.nz;
|
||
|
||
if (recomputeNormals)
|
||
{
|
||
nx = normals[i * 3 + 0];
|
||
ny = normals[i * 3 + 1];
|
||
nz = normals[i * 3 + 2];
|
||
|
||
float l = sqrtf(nx * nx + ny * ny + nz * nz);
|
||
float s = l == 0.f ? 0.f : 1.f / l;
|
||
|
||
nx *= s;
|
||
ny *= s;
|
||
nz *= s;
|
||
}
|
||
|
||
fprintf(stderr, "v %f %f %f\n", v.px, v.py, v.pz);
|
||
fprintf(stderr, "vn %f %f %f\n", nx, ny, nz);
|
||
}
|
||
|
||
for (size_t i = 0; i < indices.size(); i += 3)
|
||
{
|
||
unsigned int a = indices[i], b = indices[i + 1], c = indices[i + 2];
|
||
|
||
fprintf(stderr, "f %d//%d %d//%d %d//%d\n", a + 1, a + 1, b + 1, b + 1, c + 1, c + 1);
|
||
}
|
||
}
|
||
|
||
void dumpObj(const char* section, const std::vector<unsigned int>& indices)
|
||
{
|
||
fprintf(stderr, "o %s\n", section);
|
||
|
||
for (size_t j = 0; j < indices.size(); j += 3)
|
||
{
|
||
unsigned int a = indices[j], b = indices[j + 1], c = indices[j + 2];
|
||
|
||
fprintf(stderr, "f %d//%d %d//%d %d//%d\n", a + 1, a + 1, b + 1, b + 1, c + 1, c + 1);
|
||
}
|
||
}
|
||
|
||
struct PackedVertex
|
||
{
|
||
unsigned short px, py, pz;
|
||
unsigned short pw; // padding to 4b boundary
|
||
signed char nx, ny, nz, nw;
|
||
unsigned short tx, ty;
|
||
};
|
||
|
||
void packMesh(std::vector<PackedVertex>& pv, const std::vector<Vertex>& vertices)
|
||
{
|
||
for (size_t i = 0; i < vertices.size(); ++i)
|
||
{
|
||
const Vertex& vi = vertices[i];
|
||
PackedVertex& pvi = pv[i];
|
||
|
||
pvi.px = meshopt_quantizeHalf(vi.px);
|
||
pvi.py = meshopt_quantizeHalf(vi.py);
|
||
pvi.pz = meshopt_quantizeHalf(vi.pz);
|
||
pvi.pw = 0;
|
||
|
||
pvi.nx = char(meshopt_quantizeSnorm(vi.nx, 8));
|
||
pvi.ny = char(meshopt_quantizeSnorm(vi.ny, 8));
|
||
pvi.nz = char(meshopt_quantizeSnorm(vi.nz, 8));
|
||
pvi.nw = 0;
|
||
|
||
pvi.tx = meshopt_quantizeHalf(vi.tx);
|
||
pvi.ty = meshopt_quantizeHalf(vi.ty);
|
||
}
|
||
}
|
||
|
||
struct PackedVertexOct
|
||
{
|
||
unsigned short px, py, pz;
|
||
signed char nu, nv; // octahedron encoded normal, aliases .pw
|
||
unsigned short tx, ty;
|
||
};
|
||
|
||
void packMesh(std::vector<PackedVertexOct>& pv, const std::vector<Vertex>& vertices)
|
||
{
|
||
for (size_t i = 0; i < vertices.size(); ++i)
|
||
{
|
||
const Vertex& vi = vertices[i];
|
||
PackedVertexOct& pvi = pv[i];
|
||
|
||
pvi.px = meshopt_quantizeHalf(vi.px);
|
||
pvi.py = meshopt_quantizeHalf(vi.py);
|
||
pvi.pz = meshopt_quantizeHalf(vi.pz);
|
||
|
||
float nsum = fabsf(vi.nx) + fabsf(vi.ny) + fabsf(vi.nz);
|
||
float nx = vi.nx / nsum;
|
||
float ny = vi.ny / nsum;
|
||
float nz = vi.nz;
|
||
|
||
float nu = nz >= 0 ? nx : (1 - fabsf(ny)) * (nx >= 0 ? 1 : -1);
|
||
float nv = nz >= 0 ? ny : (1 - fabsf(nx)) * (ny >= 0 ? 1 : -1);
|
||
|
||
pvi.nu = char(meshopt_quantizeSnorm(nu, 8));
|
||
pvi.nv = char(meshopt_quantizeSnorm(nv, 8));
|
||
|
||
pvi.tx = meshopt_quantizeHalf(vi.tx);
|
||
pvi.ty = meshopt_quantizeHalf(vi.ty);
|
||
}
|
||
}
|
||
|
||
void simplify(const Mesh& mesh, float threshold = 0.2f, unsigned int options = 0)
|
||
{
|
||
Mesh lod;
|
||
|
||
double start = timestamp();
|
||
|
||
size_t target_index_count = size_t(mesh.indices.size() * threshold);
|
||
float target_error = 1e-2f;
|
||
float result_error = 0;
|
||
|
||
lod.indices.resize(mesh.indices.size()); // note: simplify needs space for index_count elements in the destination array, not target_index_count
|
||
lod.indices.resize(meshopt_simplify(&lod.indices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), target_index_count, target_error, options, &result_error));
|
||
|
||
lod.vertices.resize(lod.indices.size() < mesh.vertices.size() ? lod.indices.size() : mesh.vertices.size()); // note: this is just to reduce the cost of resize()
|
||
lod.vertices.resize(meshopt_optimizeVertexFetch(&lod.vertices[0], &lod.indices[0], lod.indices.size(), &mesh.vertices[0], mesh.vertices.size(), sizeof(Vertex)));
|
||
|
||
double end = timestamp();
|
||
|
||
printf("%-9s: %d triangles => %d triangles (%.2f%% deviation) in %.2f msec\n",
|
||
"Simplify",
|
||
int(mesh.indices.size() / 3), int(lod.indices.size() / 3),
|
||
result_error * 100,
|
||
(end - start) * 1000);
|
||
}
|
||
|
||
void simplifyAttr(const Mesh& mesh, float threshold = 0.2f, unsigned int options = 0)
|
||
{
|
||
Mesh lod;
|
||
|
||
double start = timestamp();
|
||
|
||
size_t target_index_count = size_t(mesh.indices.size() * threshold);
|
||
float target_error = 1e-2f;
|
||
float result_error = 0;
|
||
|
||
const float nrm_weight = 0.5f;
|
||
const float attr_weights[3] = {nrm_weight, nrm_weight, nrm_weight};
|
||
|
||
lod.indices.resize(mesh.indices.size()); // note: simplify needs space for index_count elements in the destination array, not target_index_count
|
||
lod.indices.resize(meshopt_simplifyWithAttributes(&lod.indices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), &mesh.vertices[0].nx, sizeof(Vertex), attr_weights, 3, NULL, target_index_count, target_error, options, &result_error));
|
||
|
||
lod.vertices.resize(lod.indices.size() < mesh.vertices.size() ? lod.indices.size() : mesh.vertices.size()); // note: this is just to reduce the cost of resize()
|
||
lod.vertices.resize(meshopt_optimizeVertexFetch(&lod.vertices[0], &lod.indices[0], lod.indices.size(), &mesh.vertices[0], mesh.vertices.size(), sizeof(Vertex)));
|
||
|
||
double end = timestamp();
|
||
|
||
printf("%-9s: %d triangles => %d triangles (%.2f%% deviation) in %.2f msec\n",
|
||
"SimplifyAttr",
|
||
int(mesh.indices.size() / 3), int(lod.indices.size() / 3),
|
||
result_error * 100,
|
||
(end - start) * 1000);
|
||
}
|
||
|
||
void simplifyUpdate(const Mesh& mesh, float threshold = 0.2f, unsigned int options = 0)
|
||
{
|
||
Mesh lod;
|
||
|
||
double start = timestamp();
|
||
|
||
size_t target_index_count = size_t(mesh.indices.size() * threshold);
|
||
float target_error = 1e-2f;
|
||
float result_error = 0;
|
||
|
||
const float nrm_weight = 0.5f;
|
||
const float attr_weights[3] = {nrm_weight, nrm_weight, nrm_weight};
|
||
|
||
lod = mesh; // start from the original mesh
|
||
lod.indices.resize(meshopt_simplifyWithUpdate(&lod.indices[0], mesh.indices.size(), &lod.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), &lod.vertices[0].nx, sizeof(Vertex), attr_weights, 3, NULL, target_index_count, target_error, options, &result_error));
|
||
|
||
lod.vertices.resize(meshopt_optimizeVertexFetch(&lod.vertices[0], &lod.indices[0], lod.indices.size(), &lod.vertices[0], lod.vertices.size(), sizeof(Vertex)));
|
||
|
||
for (size_t i = 0; i < lod.vertices.size(); ++i)
|
||
{
|
||
// update normals
|
||
Vertex& v = lod.vertices[i];
|
||
float nl = sqrtf(v.nx * v.nx + v.ny * v.ny + v.nz * v.nz);
|
||
if (nl > 0)
|
||
{
|
||
v.nx /= nl;
|
||
v.ny /= nl;
|
||
v.nz /= nl;
|
||
}
|
||
}
|
||
|
||
double end = timestamp();
|
||
|
||
printf("%-9s: %d triangles => %d triangles (%.2f%% deviation) in %.2f msec\n",
|
||
"SimplifyUpdt",
|
||
int(mesh.indices.size() / 3), int(lod.indices.size() / 3),
|
||
result_error * 100,
|
||
(end - start) * 1000);
|
||
}
|
||
|
||
void simplifySloppy(const Mesh& mesh, float threshold = 0.2f)
|
||
{
|
||
Mesh lod;
|
||
|
||
double start = timestamp();
|
||
|
||
size_t target_index_count = size_t(mesh.indices.size() * threshold);
|
||
float target_error = 1e-1f;
|
||
float result_error = 0;
|
||
|
||
lod.indices.resize(mesh.indices.size()); // note: simplify needs space for index_count elements in the destination array, not target_index_count
|
||
lod.indices.resize(meshopt_simplifySloppy(&lod.indices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), target_index_count, target_error, &result_error));
|
||
|
||
lod.vertices.resize(lod.indices.size() < mesh.vertices.size() ? lod.indices.size() : mesh.vertices.size()); // note: this is just to reduce the cost of resize()
|
||
lod.vertices.resize(meshopt_optimizeVertexFetch(&lod.vertices[0], &lod.indices[0], lod.indices.size(), &mesh.vertices[0], mesh.vertices.size(), sizeof(Vertex)));
|
||
|
||
double end = timestamp();
|
||
|
||
printf("%-9s: %d triangles => %d triangles (%.2f%% deviation) in %.2f msec\n",
|
||
"SimplifyS",
|
||
int(mesh.indices.size() / 3), int(lod.indices.size() / 3),
|
||
result_error * 100,
|
||
(end - start) * 1000);
|
||
}
|
||
|
||
void simplifyPoints(const Mesh& mesh, float threshold = 0.2f)
|
||
{
|
||
double start = timestamp();
|
||
|
||
size_t target_vertex_count = size_t(mesh.vertices.size() * threshold);
|
||
if (target_vertex_count == 0)
|
||
return;
|
||
|
||
std::vector<unsigned int> indices(target_vertex_count);
|
||
indices.resize(meshopt_simplifyPoints(&indices[0], &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), NULL, 0, 0, target_vertex_count));
|
||
|
||
double end = timestamp();
|
||
|
||
printf("%-9s: %d points => %d points in %.2f msec\n",
|
||
"SimplifyP",
|
||
int(mesh.vertices.size()), int(indices.size()), (end - start) * 1000);
|
||
}
|
||
|
||
void simplifyComplete(const Mesh& mesh)
|
||
{
|
||
static const size_t lod_count = 5;
|
||
|
||
double start = timestamp();
|
||
|
||
// generate 4 LOD levels (1-4), with each subsequent LOD using 70% triangles
|
||
// note that each LOD uses the same (shared) vertex buffer
|
||
std::vector<unsigned int> lods[lod_count];
|
||
|
||
lods[0] = mesh.indices;
|
||
|
||
for (size_t i = 1; i < lod_count; ++i)
|
||
{
|
||
std::vector<unsigned int>& lod = lods[i];
|
||
|
||
float threshold = powf(0.7f, float(i));
|
||
size_t target_index_count = size_t(mesh.indices.size() * threshold) / 3 * 3;
|
||
float target_error = 1e-2f;
|
||
|
||
// we can simplify all the way from base level or from the last result
|
||
// simplifying from the base level sometimes produces better results, but simplifying from last level is faster
|
||
const std::vector<unsigned int>& source = lods[i - 1];
|
||
|
||
if (source.size() < target_index_count)
|
||
target_index_count = source.size();
|
||
|
||
lod.resize(source.size());
|
||
lod.resize(meshopt_simplify(&lod[0], &source[0], source.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), target_index_count, target_error));
|
||
}
|
||
|
||
double middle = timestamp();
|
||
|
||
// optimize each individual LOD for vertex cache & overdraw
|
||
for (size_t i = 0; i < lod_count; ++i)
|
||
{
|
||
std::vector<unsigned int>& lod = lods[i];
|
||
|
||
meshopt_optimizeVertexCache(&lod[0], &lod[0], lod.size(), mesh.vertices.size());
|
||
meshopt_optimizeOverdraw(&lod[0], &lod[0], lod.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), 1.0f);
|
||
}
|
||
|
||
// concatenate all LODs into one IB
|
||
// note: the order of concatenation is important - since we optimize the entire IB for vertex fetch,
|
||
// putting coarse LODs first makes sure that the vertex range referenced by them is as small as possible
|
||
// some GPUs process the entire range referenced by the index buffer region so doing this optimizes the vertex transform
|
||
// cost for coarse LODs
|
||
// this order also produces much better vertex fetch cache coherency for coarse LODs (since they're essentially optimized first)
|
||
// somewhat surprisingly, the vertex fetch cache coherency for fine LODs doesn't seem to suffer that much.
|
||
size_t lod_index_offsets[lod_count] = {};
|
||
size_t lod_index_counts[lod_count] = {};
|
||
size_t total_index_count = 0;
|
||
|
||
for (int i = lod_count - 1; i >= 0; --i)
|
||
{
|
||
lod_index_offsets[i] = total_index_count;
|
||
lod_index_counts[i] = lods[i].size();
|
||
|
||
total_index_count += lods[i].size();
|
||
}
|
||
|
||
std::vector<unsigned int> indices(total_index_count);
|
||
|
||
for (size_t i = 0; i < lod_count; ++i)
|
||
{
|
||
memcpy(&indices[lod_index_offsets[i]], &lods[i][0], lods[i].size() * sizeof(lods[i][0]));
|
||
}
|
||
|
||
std::vector<Vertex> vertices = mesh.vertices;
|
||
|
||
// vertex fetch optimization should go last as it depends on the final index order
|
||
// note that the order of LODs above affects vertex fetch results
|
||
meshopt_optimizeVertexFetch(&vertices[0], &indices[0], indices.size(), &vertices[0], vertices.size(), sizeof(Vertex));
|
||
|
||
double end = timestamp();
|
||
|
||
printf("%-9s: %d triangles => %d LOD levels down to %d triangles in %.2f msec, optimized in %.2f msec\n",
|
||
"SimplifyC",
|
||
int(lod_index_counts[0]) / 3, int(lod_count), int(lod_index_counts[lod_count - 1]) / 3,
|
||
(middle - start) * 1000, (end - middle) * 1000);
|
||
|
||
// for using LOD data at runtime, in addition to vertices and indices you have to save lod_index_offsets/lod_index_counts.
|
||
|
||
{
|
||
meshopt_VertexCacheStatistics vcs0 = meshopt_analyzeVertexCache(&indices[lod_index_offsets[0]], lod_index_counts[0], vertices.size(), 16, 0, 0);
|
||
meshopt_VertexFetchStatistics vfs0 = meshopt_analyzeVertexFetch(&indices[lod_index_offsets[0]], lod_index_counts[0], vertices.size(), sizeof(Vertex));
|
||
meshopt_VertexCacheStatistics vcsN = meshopt_analyzeVertexCache(&indices[lod_index_offsets[lod_count - 1]], lod_index_counts[lod_count - 1], vertices.size(), 16, 0, 0);
|
||
meshopt_VertexFetchStatistics vfsN = meshopt_analyzeVertexFetch(&indices[lod_index_offsets[lod_count - 1]], lod_index_counts[lod_count - 1], vertices.size(), sizeof(Vertex));
|
||
|
||
typedef PackedVertexOct PV;
|
||
|
||
std::vector<PV> pv(vertices.size());
|
||
packMesh(pv, vertices);
|
||
|
||
std::vector<unsigned char> vbuf(meshopt_encodeVertexBufferBound(vertices.size(), sizeof(PV)));
|
||
vbuf.resize(meshopt_encodeVertexBuffer(&vbuf[0], vbuf.size(), &pv[0], vertices.size(), sizeof(PV)));
|
||
|
||
std::vector<unsigned char> ibuf(meshopt_encodeIndexBufferBound(indices.size(), vertices.size()));
|
||
ibuf.resize(meshopt_encodeIndexBuffer(&ibuf[0], ibuf.size(), &indices[0], indices.size()));
|
||
|
||
printf("%-9s ACMR %f...%f Overfetch %f..%f Codec VB %.1f bits/vertex IB %.1f bits/triangle\n",
|
||
"",
|
||
vcs0.acmr, vcsN.acmr, vfs0.overfetch, vfsN.overfetch,
|
||
double(vbuf.size()) / double(vertices.size()) * 8,
|
||
double(ibuf.size()) / double(indices.size() / 3) * 8);
|
||
}
|
||
}
|
||
|
||
void simplifyClusters(const Mesh& mesh, float threshold = 0.2f)
|
||
{
|
||
const size_t max_vertices = 64;
|
||
const size_t max_triangles = 64;
|
||
const size_t target_group_size = 8;
|
||
|
||
double start = timestamp();
|
||
|
||
// build clusters (meshlets) out of the mesh
|
||
size_t max_meshlets = meshopt_buildMeshletsBound(mesh.indices.size(), max_vertices, max_triangles);
|
||
std::vector<meshopt_Meshlet> meshlets(max_meshlets);
|
||
std::vector<unsigned int> meshlet_vertices(mesh.indices.size());
|
||
std::vector<unsigned char> meshlet_triangles(mesh.indices.size());
|
||
|
||
meshlets.resize(meshopt_buildMeshlets(&meshlets[0], &meshlet_vertices[0], &meshlet_triangles[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), max_vertices, max_triangles, 0.f));
|
||
|
||
double middle = timestamp();
|
||
|
||
// generate position remap; we'll use that to partition clusters using position-only adjacency
|
||
std::vector<unsigned int> remap(mesh.vertices.size());
|
||
meshopt_generatePositionRemap(&remap[0], &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex));
|
||
|
||
// partition clusters in groups; each group will be simplified separately and the boundaries between groups will be preserved
|
||
std::vector<unsigned int> cluster_indices;
|
||
cluster_indices.reserve(mesh.indices.size());
|
||
std::vector<unsigned int> cluster_sizes(meshlets.size());
|
||
|
||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||
{
|
||
const meshopt_Meshlet& m = meshlets[i];
|
||
|
||
for (size_t j = 0; j < m.triangle_count * 3; ++j)
|
||
{
|
||
unsigned int v = meshlet_vertices[m.vertex_offset + meshlet_triangles[m.triangle_offset + j]];
|
||
|
||
// use the first vertex with equivalent position so that cluster adjacency ignores attribute seams
|
||
cluster_indices.push_back(remap[v]);
|
||
}
|
||
|
||
cluster_sizes[i] = m.triangle_count * 3;
|
||
}
|
||
|
||
std::vector<unsigned int> partition(meshlets.size());
|
||
size_t partition_count = meshopt_partitionClusters(&partition[0], &cluster_indices[0], cluster_indices.size(), &cluster_sizes[0], cluster_sizes.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), target_group_size);
|
||
|
||
// convert partitions to linked lists to make it easier to iterate over (vectors of vectors would work too)
|
||
std::vector<int> partnext(meshlets.size(), -1);
|
||
std::vector<int> partlast(partition_count, -1);
|
||
|
||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||
{
|
||
unsigned int part = partition[i];
|
||
|
||
if (partlast[part] >= 0)
|
||
partnext[partlast[part]] = int(i);
|
||
|
||
partlast[part] = int(i);
|
||
partnext[i] = -1;
|
||
}
|
||
|
||
double parttime = timestamp();
|
||
|
||
float scale = meshopt_simplifyScale(&mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex));
|
||
|
||
std::vector<unsigned int> lod;
|
||
lod.reserve(mesh.indices.size());
|
||
|
||
float error = 0.f;
|
||
|
||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||
{
|
||
if (partlast[partition[i]] < 0)
|
||
continue; // part of a group that was already processed
|
||
|
||
// mark group as processed
|
||
partlast[partition[i]] = -1;
|
||
|
||
size_t group_offset = lod.size();
|
||
|
||
for (int j = int(i); j >= 0; j = partnext[j])
|
||
{
|
||
const meshopt_Meshlet& m = meshlets[j];
|
||
|
||
for (size_t k = 0; k < m.triangle_count * 3; ++k)
|
||
lod.push_back(meshlet_vertices[m.vertex_offset + meshlet_triangles[m.triangle_offset + k]]);
|
||
}
|
||
|
||
size_t group_triangles = (lod.size() - group_offset) / 3;
|
||
|
||
// simplify the group, preserving the border vertices
|
||
// note: this technically also locks the exterior border; a full mesh analysis (see clusterlod.h / lockBoundary) would work better for some meshes
|
||
unsigned int options = meshopt_SimplifyLockBorder | meshopt_SimplifySparse | meshopt_SimplifyErrorAbsolute;
|
||
|
||
float group_target_error = 1e-2f * scale;
|
||
size_t group_target = size_t(float(group_triangles) * threshold) * 3;
|
||
float group_error = 0.f;
|
||
size_t group_size = meshopt_simplify(&lod[group_offset], &lod[group_offset], group_triangles * 3, &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), group_target, group_target_error, options, &group_error);
|
||
|
||
error = group_error > error ? group_error : error;
|
||
|
||
// simplified group is available in lod[group_offset..group_offset + group_size]
|
||
lod.resize(group_offset + group_size);
|
||
}
|
||
|
||
double end = timestamp();
|
||
|
||
printf("%-9s: %d triangles => %d triangles (%.2f%% deviation) in %.2f msec, clusterized in %.2f msec, partitioned in %.2f msec (%d clusters in %d groups, %.1f avg)\n",
|
||
"SimplifyG",
|
||
int(mesh.indices.size() / 3), int(lod.size() / 3),
|
||
error / scale * 100,
|
||
(end - parttime) * 1000, (middle - start) * 1000, (parttime - middle) * 1000,
|
||
int(meshlets.size()), int(partition_count), double(meshlets.size()) / double(partition_count));
|
||
}
|
||
|
||
void optimize(const Mesh& mesh, bool fifo = false)
|
||
{
|
||
Mesh copy = mesh;
|
||
// note: we assume that the mesh is already optimally indexed (via parseObj); if that is not the case, you'd need to reindex first
|
||
|
||
double start = timestamp();
|
||
|
||
// vertex cache optimization should go first as it provides starting order for overdraw
|
||
// note: fifo optimization is not recommended as a default, since it produces worse results, but it's faster to run so it can be useful for procedural meshes
|
||
if (fifo)
|
||
meshopt_optimizeVertexCacheFifo(©.indices[0], ©.indices[0], copy.indices.size(), copy.vertices.size(), /* cache_size= */ 16);
|
||
else
|
||
meshopt_optimizeVertexCache(©.indices[0], ©.indices[0], copy.indices.size(), copy.vertices.size());
|
||
|
||
// reorder indices for overdraw, balancing overdraw and vertex cache efficiency
|
||
const float kThreshold = 1.01f; // allow up to 1% worse ACMR to get more reordering opportunities for overdraw
|
||
meshopt_optimizeOverdraw(©.indices[0], ©.indices[0], copy.indices.size(), ©.vertices[0].px, copy.vertices.size(), sizeof(Vertex), kThreshold);
|
||
|
||
// vertex fetch optimization should go last as it depends on the final index order
|
||
meshopt_optimizeVertexFetch(©.vertices[0], ©.indices[0], copy.indices.size(), ©.vertices[0], copy.vertices.size(), sizeof(Vertex));
|
||
|
||
double end = timestamp();
|
||
|
||
meshopt_VertexCacheStatistics vcs = meshopt_analyzeVertexCache(©.indices[0], copy.indices.size(), copy.vertices.size(), 16, 0, 0);
|
||
meshopt_VertexFetchStatistics vfs = meshopt_analyzeVertexFetch(©.indices[0], copy.indices.size(), copy.vertices.size(), sizeof(Vertex));
|
||
meshopt_OverdrawStatistics os = meshopt_analyzeOverdraw(©.indices[0], copy.indices.size(), ©.vertices[0].px, copy.vertices.size(), sizeof(Vertex));
|
||
|
||
meshopt_VertexCacheStatistics vcs_nv = meshopt_analyzeVertexCache(©.indices[0], copy.indices.size(), copy.vertices.size(), 32, 32, 32);
|
||
meshopt_VertexCacheStatistics vcs_amd = meshopt_analyzeVertexCache(©.indices[0], copy.indices.size(), copy.vertices.size(), 14, 64, 128);
|
||
meshopt_VertexCacheStatistics vcs_intel = meshopt_analyzeVertexCache(©.indices[0], copy.indices.size(), copy.vertices.size(), 128, 0, 0);
|
||
|
||
printf("Optimize%s: ACMR %f ATVR %f (NV %f AMD %f Intel %f) overfetch %f overdraw %f in %.2f msec\n",
|
||
fifo ? "F" : " ",
|
||
vcs.acmr, vcs.atvr, vcs_nv.atvr, vcs_amd.atvr, vcs_intel.atvr, vfs.overfetch, os.overdraw, (end - start) * 1000);
|
||
}
|
||
|
||
template <typename T>
|
||
size_t compress(const std::vector<T>& data, int level = SDEFL_LVL_DEF)
|
||
{
|
||
std::vector<unsigned char> cbuf(sdefl_bound(int(data.size() * sizeof(T))));
|
||
sdefl s = {};
|
||
return sdeflate(&s, &cbuf[0], reinterpret_cast<const unsigned char*>(&data[0]), int(data.size() * sizeof(T)), level);
|
||
}
|
||
|
||
void encodeIndex(const std::vector<unsigned int>& indices, size_t vertex_count, char desc)
|
||
{
|
||
// allocate result outside of the timing loop to exclude memset() from decode timing
|
||
std::vector<unsigned int> result(indices.size());
|
||
|
||
double start = timestamp();
|
||
|
||
std::vector<unsigned char> buffer(meshopt_encodeIndexBufferBound(indices.size(), vertex_count));
|
||
buffer.resize(meshopt_encodeIndexBuffer(&buffer[0], buffer.size(), &indices[0], indices.size()));
|
||
|
||
double middle = timestamp();
|
||
|
||
int res = meshopt_decodeIndexBuffer(&result[0], indices.size(), &buffer[0], buffer.size());
|
||
assert(res == 0);
|
||
(void)res;
|
||
|
||
double end = timestamp();
|
||
|
||
size_t csize = compress(buffer);
|
||
|
||
for (size_t i = 0; i < indices.size(); i += 3)
|
||
{
|
||
assert(
|
||
(result[i + 0] == indices[i + 0] && result[i + 1] == indices[i + 1] && result[i + 2] == indices[i + 2]) ||
|
||
(result[i + 1] == indices[i + 0] && result[i + 2] == indices[i + 1] && result[i + 0] == indices[i + 2]) ||
|
||
(result[i + 2] == indices[i + 0] && result[i + 0] == indices[i + 1] && result[i + 1] == indices[i + 2]));
|
||
}
|
||
|
||
printf("IdxCodec%c: %.1f bits/triangle (post-deflate %.1f bits/triangle); encode %.2f msec (%.3f GB/s), decode %.2f msec (%.2f GB/s)\n",
|
||
desc,
|
||
double(buffer.size() * 8) / double(indices.size() / 3),
|
||
double(csize * 8) / double(indices.size() / 3),
|
||
(middle - start) * 1000,
|
||
(double(result.size() * 4) / 1e9) / (middle - start),
|
||
(end - middle) * 1000,
|
||
(double(result.size() * 4) / 1e9) / (end - middle));
|
||
}
|
||
|
||
void encodeIndex(const Mesh& mesh, char desc)
|
||
{
|
||
encodeIndex(mesh.indices, mesh.vertices.size(), desc);
|
||
}
|
||
|
||
void encodeIndexSequence(const std::vector<unsigned int>& data, size_t vertex_count, char desc)
|
||
{
|
||
// allocate result outside of the timing loop to exclude memset() from decode timing
|
||
std::vector<unsigned int> result(data.size());
|
||
|
||
double start = timestamp();
|
||
|
||
std::vector<unsigned char> buffer(meshopt_encodeIndexSequenceBound(data.size(), vertex_count));
|
||
buffer.resize(meshopt_encodeIndexSequence(&buffer[0], buffer.size(), &data[0], data.size()));
|
||
|
||
double middle = timestamp();
|
||
|
||
int res = meshopt_decodeIndexSequence(&result[0], data.size(), &buffer[0], buffer.size());
|
||
assert(res == 0);
|
||
(void)res;
|
||
|
||
double end = timestamp();
|
||
|
||
size_t csize = compress(buffer);
|
||
|
||
assert(memcmp(&data[0], &result[0], data.size() * sizeof(unsigned int)) == 0);
|
||
|
||
printf("IdxCodec%c: %.1f bits/index (post-deflate %.1f bits/index); encode %.2f msec (%.3f GB/s), decode %.2f msec (%.2f GB/s)\n",
|
||
desc,
|
||
double(buffer.size() * 8) / double(data.size()),
|
||
double(csize * 8) / double(data.size()),
|
||
(middle - start) * 1000,
|
||
(double(result.size() * 4) / 1e9) / (middle - start),
|
||
(end - middle) * 1000,
|
||
(double(result.size() * 4) / 1e9) / (end - middle));
|
||
}
|
||
|
||
template <typename V, typename T>
|
||
static void validateDecodeMeshlet(const unsigned char* data, size_t size, const unsigned int* vertices, size_t vertex_count, const unsigned char* triangles, size_t triangle_count)
|
||
{
|
||
V rv[256];
|
||
T rt[sizeof(T) == 1 ? 256 * 3 : 256];
|
||
|
||
int rc = meshopt_decodeMeshlet(rv, vertex_count, rt, triangle_count, data, size);
|
||
assert(rc == 0);
|
||
|
||
for (size_t j = 0; j < vertex_count; ++j)
|
||
assert(rv[j] == V(vertices[j]));
|
||
|
||
for (size_t j = 0; j < triangle_count; ++j)
|
||
{
|
||
unsigned int a = triangles[j * 3 + 0];
|
||
unsigned int b = triangles[j * 3 + 1];
|
||
unsigned int c = triangles[j * 3 + 2];
|
||
|
||
unsigned int tri = sizeof(T) == 1 ? rt[j * 3] | (rt[j * 3 + 1] << 8) | (rt[j * 3 + 2] << 16) : rt[j];
|
||
|
||
unsigned int abc = (a << 0) | (b << 8) | (c << 16);
|
||
unsigned int bca = (b << 0) | (c << 8) | (a << 16);
|
||
unsigned int cba = (c << 0) | (a << 8) | (b << 16);
|
||
|
||
assert(tri == abc || tri == bca || tri == cba);
|
||
}
|
||
}
|
||
|
||
void encodeMeshlets(const Mesh& mesh, size_t max_vertices, size_t max_triangles, bool reorder = true, int level = 3)
|
||
{
|
||
size_t max_meshlets = meshopt_buildMeshletsBound(mesh.indices.size(), max_vertices, max_triangles);
|
||
std::vector<meshopt_Meshlet> meshlets(max_meshlets);
|
||
std::vector<unsigned int> meshlet_vertices(mesh.indices.size());
|
||
std::vector<unsigned char> meshlet_triangles(mesh.indices.size());
|
||
|
||
meshlets.resize(meshopt_buildMeshlets(&meshlets[0], &meshlet_vertices[0], &meshlet_triangles[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), max_vertices, max_triangles, 0.f));
|
||
|
||
if (meshlets.size())
|
||
{
|
||
const meshopt_Meshlet& last = meshlets.back();
|
||
|
||
// this is an example of how to trim the vertex/triangle arrays when copying data out to GPU storage
|
||
meshlet_vertices.resize(last.vertex_offset + last.vertex_count);
|
||
meshlet_triangles.resize(last.triangle_offset + last.triangle_count * 3);
|
||
}
|
||
|
||
std::vector<unsigned char> cbuf(meshopt_encodeMeshletBound(max_vertices, max_triangles));
|
||
|
||
// optimize each meshlet for locality; this is important for performance, and critical for good compression
|
||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||
meshopt_optimizeMeshletLevel(&meshlet_vertices[meshlets[i].vertex_offset], meshlets[i].vertex_count, &meshlet_triangles[meshlets[i].triangle_offset], meshlets[i].triangle_count, level);
|
||
|
||
// optimize the order of vertex references within each meshlet and globally; this is valuable for access locality and critical for compression of vertex references
|
||
// note that this reorders the vertex buffer too, so if a traditional index buffer is required it would need to be reconstructed from the meshlet data for optimal locality
|
||
std::vector<Vertex> vertices = mesh.vertices;
|
||
if (reorder)
|
||
meshopt_optimizeVertexFetch(&vertices[0], &meshlet_vertices[0], meshlet_vertices.size(), &mesh.vertices[0], mesh.vertices.size(), sizeof(Vertex));
|
||
|
||
size_t mbst = 0;
|
||
|
||
std::vector<unsigned char> packed;
|
||
|
||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||
{
|
||
const meshopt_Meshlet& meshlet = meshlets[i];
|
||
|
||
size_t mbs = meshopt_encodeMeshlet(&cbuf[0], cbuf.size(), &meshlet_vertices[meshlet.vertex_offset], meshlet.vertex_count, &meshlet_triangles[meshlet.triangle_offset], meshlet.triangle_count);
|
||
assert(mbs > 0);
|
||
|
||
// 24-bit header: 7 bit (vertex_count-1), 7 bit (triangle_count-1), 10 bit size
|
||
// fits up to 128v/128t meshlet with 1024 bytes of encoded data; meshopt_encodeMeshletBound(128,128) < 1000
|
||
assert(size_t(meshlet.vertex_count - 1) < 128 && size_t(meshlet.triangle_count - 1) < 128 && mbs < 1024);
|
||
unsigned int header = ((meshlet.vertex_count - 1) & 0x7f) | (((meshlet.triangle_count - 1) & 0x7f) << 7) | ((unsigned(mbs) & 0x3ff) << 14);
|
||
packed.push_back((unsigned char)(header & 0xff));
|
||
packed.push_back((unsigned char)((header >> 8) & 0xff));
|
||
packed.push_back((unsigned char)((header >> 16) & 0xff));
|
||
packed.insert(packed.end(), &cbuf[0], &cbuf[mbs]);
|
||
|
||
validateDecodeMeshlet<unsigned int, unsigned int>(&cbuf[0], mbs, &meshlet_vertices[meshlet.vertex_offset], meshlet.vertex_count, &meshlet_triangles[meshlet.triangle_offset], meshlet.triangle_count);
|
||
validateDecodeMeshlet<unsigned int, unsigned char>(&cbuf[0], mbs, &meshlet_vertices[meshlet.vertex_offset], meshlet.vertex_count, &meshlet_triangles[meshlet.triangle_offset], meshlet.triangle_count);
|
||
validateDecodeMeshlet<unsigned short, unsigned int>(&cbuf[0], mbs, &meshlet_vertices[meshlet.vertex_offset], meshlet.vertex_count, &meshlet_triangles[meshlet.triangle_offset], meshlet.triangle_count);
|
||
validateDecodeMeshlet<unsigned short, unsigned char>(&cbuf[0], mbs, &meshlet_vertices[meshlet.vertex_offset], meshlet.vertex_count, &meshlet_triangles[meshlet.triangle_offset], meshlet.triangle_count);
|
||
|
||
mbst += mbs;
|
||
}
|
||
|
||
size_t mbc = compress(packed);
|
||
|
||
printf("MeshletCodec (%d/%d): %d meshlets, %d bytes/meshlet; %d bytes, %.1f bits/triangle\n",
|
||
int(max_vertices), int(max_triangles),
|
||
int(meshlets.size()),
|
||
int(mbst / meshlets.size()),
|
||
int(mbst), double(mbst * 8) / double(mesh.indices.size() / 3));
|
||
printf("MeshletCodec (%d/%d, packed): %d bytes/meshlet, %.1f bits/triangle; post-deflate: %d bytes/meshlet, %.1f bits/triangle)\n",
|
||
int(max_vertices), int(max_triangles),
|
||
int(packed.size() / meshlets.size()), double(packed.size() * 8) / double(mesh.indices.size() / 3),
|
||
int(mbc / meshlets.size()), double(mbc * 8) / double(mesh.indices.size() / 3));
|
||
|
||
#if !TRACE
|
||
double mbtime = 0;
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
{
|
||
unsigned int rv[256];
|
||
unsigned int rt[256];
|
||
double t0 = timestamp();
|
||
unsigned char* p = &packed[0];
|
||
for (size_t j = 0; j < meshlets.size(); ++j)
|
||
{
|
||
unsigned int header = p[0] | (p[1] << 8) | (p[2] << 16);
|
||
size_t vertex_count = (header & 0x7f) + 1;
|
||
size_t triangle_count = ((header >> 7) & 0x7f) + 1;
|
||
size_t size = (header >> 14) & 0x3ff;
|
||
meshopt_decodeMeshletRaw(rv, vertex_count, rt, triangle_count, p + 3, size);
|
||
p += 3 + size;
|
||
}
|
||
double t1 = timestamp();
|
||
|
||
mbtime = (mbtime == 0 || t1 - t0 < mbtime) ? (t1 - t0) : mbtime;
|
||
}
|
||
|
||
printf("MeshletCodec (%d/%d, packed): decode time %.3f msec, %.3fB tri/sec, %.1f ns/meshlet\n",
|
||
int(max_vertices), int(max_triangles),
|
||
mbtime * 1000, double(mesh.indices.size() / 3) / 1e9 / mbtime, mbtime * 1e9 / double(meshlets.size()));
|
||
#endif
|
||
}
|
||
|
||
static void writeBits(std::vector<unsigned char>& data, unsigned int& acc, int& acc_bits, unsigned int value, int bits)
|
||
{
|
||
assert(value < (1u << bits));
|
||
assert(acc_bits + bits <= 32);
|
||
|
||
acc |= value << acc_bits;
|
||
acc_bits += bits;
|
||
|
||
while (acc_bits >= 8)
|
||
{
|
||
data.push_back((unsigned char)(acc & 0xff));
|
||
acc >>= 8;
|
||
acc_bits -= 8;
|
||
}
|
||
}
|
||
|
||
void encodeMeshletsDXR(const Mesh& mesh, size_t max_triangles, int min_exp, int level = 3)
|
||
{
|
||
// split mesh into clusters, optimized for RT
|
||
size_t min_triangles = max_triangles / 4;
|
||
size_t max_vertices = 256;
|
||
float fill_weight = 0.25f;
|
||
size_t max_meshlets = meshopt_buildMeshletsBound(mesh.indices.size(), max_vertices, min_triangles);
|
||
|
||
std::vector<unsigned int> shadow_indices(mesh.indices.size());
|
||
meshopt_generateShadowIndexBuffer(&shadow_indices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(float) * 3, sizeof(Vertex));
|
||
|
||
std::vector<meshopt_Meshlet> meshlets(max_meshlets);
|
||
std::vector<unsigned int> meshlet_vertices(mesh.indices.size());
|
||
std::vector<unsigned char> meshlet_triangles(mesh.indices.size());
|
||
meshlets.resize(meshopt_buildMeshletsSpatial(&meshlets[0], &meshlet_vertices[0], &meshlet_triangles[0], &shadow_indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), max_vertices, min_triangles, max_triangles, fill_weight));
|
||
|
||
if (meshlets.size())
|
||
{
|
||
const meshopt_Meshlet& last = meshlets.back();
|
||
|
||
// this is an example of how to trim the vertex/triangle arrays when copying data out to GPU storage
|
||
meshlet_vertices.resize(last.vertex_offset + last.vertex_count);
|
||
meshlet_triangles.resize(last.triangle_offset + last.triangle_count * 3);
|
||
}
|
||
|
||
// optimize each meshlet for locality; this is critical for good compression
|
||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||
meshopt_optimizeMeshletLevel(&meshlet_vertices[meshlets[i].vertex_offset], meshlets[i].vertex_count, &meshlet_triangles[meshlets[i].triangle_offset], meshlets[i].triangle_count, level);
|
||
|
||
// compute shared exponent per mesh:
|
||
// - must be at most min_exp which should be set to maximum required precision (e.g. -10 for 1mm accuracy given metric units)
|
||
// - must accommodate each cluster to fit into Compressed1 DXR encoding (16-bit offsets from 24-bit anchors)
|
||
// note, for DGF parity min_exp should be set based on mesh AABB; we do not do this as we do not consider this rigorous.
|
||
int exponent = min_exp;
|
||
|
||
for (const meshopt_Meshlet& meshlet : meshlets)
|
||
{
|
||
float minv[3] = {FLT_MAX, FLT_MAX, FLT_MAX}, maxv[3] = {-FLT_MAX, -FLT_MAX, -FLT_MAX};
|
||
|
||
for (size_t j = 0; j < meshlet.vertex_count; ++j)
|
||
{
|
||
unsigned int v = meshlet_vertices[meshlet.vertex_offset + j];
|
||
const float* p = &mesh.vertices[v].px;
|
||
|
||
for (int k = 0; k < 3; ++k)
|
||
{
|
||
minv[k] = fminf(minv[k], p[k]);
|
||
maxv[k] = fmaxf(maxv[k], p[k]);
|
||
}
|
||
}
|
||
|
||
int cexp = meshopt_computePositionExponent(minv, maxv, min_exp, /* max_bits= */ 16);
|
||
exponent = (exponent > cexp) ? exponent : cexp;
|
||
}
|
||
|
||
std::vector<unsigned char> packed;
|
||
size_t stats_headers = 0, stats_positions = 0, stats_topology = 0;
|
||
|
||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||
{
|
||
const meshopt_Meshlet& meshlet = meshlets[i];
|
||
|
||
// for each cluster, use meshopt codec for meshlet topology; will need to decode using meshopt_decodeMeshlet at runtime
|
||
unsigned char topology[1024];
|
||
assert(meshopt_encodeMeshletBound(0, max_triangles) <= sizeof(topology));
|
||
|
||
size_t tops = meshopt_encodeMeshlet(topology, sizeof(topology), NULL, 0, &meshlet_triangles[meshlet.triangle_offset], meshlet.triangle_count);
|
||
assert(tops > 0);
|
||
|
||
// for each cluster, positions are encoded using 24-bit anchor and up-to-16-bit offset per axis
|
||
float scale = ldexpf(1.f, exponent);
|
||
|
||
int positions[256][3];
|
||
int anchor[3] = {INT_MAX, INT_MAX, INT_MAX};
|
||
|
||
for (size_t j = 0; j < meshlet.vertex_count; ++j)
|
||
{
|
||
unsigned int v = meshlet_vertices[meshlet.vertex_offset + j];
|
||
const float* p = &mesh.vertices[v].px;
|
||
|
||
for (int k = 0; k < 3; ++k)
|
||
{
|
||
positions[j][k] = int(roundf(p[k] / scale));
|
||
anchor[k] = anchor[k] < positions[j][k] ? anchor[k] : positions[j][k];
|
||
}
|
||
}
|
||
|
||
// compute optimal per-cluster per-axis bit count
|
||
int bits[3] = {1, 1, 1};
|
||
|
||
for (size_t j = 0; j < meshlet.vertex_count; ++j)
|
||
for (int k = 0; k < 3; ++k)
|
||
{
|
||
int v = positions[j][k] - anchor[k];
|
||
assert(v >= 0 && v < (1 << 16));
|
||
|
||
while (v >= (1 << bits[k]))
|
||
bits[k]++;
|
||
}
|
||
|
||
// prepare cluster header: 1 word with cluster sizes, and 3 words for DXR-compatible cluster position header
|
||
// see https://microsoft.github.io/DirectX-Specs/d3d/Raytracing2.html#compressed1-position-encoding
|
||
unsigned int header[4] = {
|
||
(meshlet.vertex_count - 1) | ((meshlet.triangle_count - 1) << 8) | (unsigned(tops) << 16),
|
||
(unsigned(anchor[0]) << 8) | (exponent + 127),
|
||
(unsigned(anchor[1]) << 8) | (unsigned(bits[1] - 1) << 4) | unsigned(bits[0] - 1),
|
||
(unsigned(anchor[2]) << 8) | unsigned(bits[2] - 1),
|
||
};
|
||
|
||
// write cluster header, cluster positions, and cluster topology
|
||
// note: we write the data in a way that is easiest to consume for the decoding; however, for best post-deflate compression consider reordering it
|
||
// (by grouping headers, positions, and topology into three separate streams)
|
||
// maximum post-deflate compression can be achieved by interleaving the cluster position bits per axis (all X => all Y => all Z), but that requires extra decoding work
|
||
// this layout can be decoded using `meshopt_decodeMeshlet` + `memcpy` when the consumer is DXR2-compatible.
|
||
for (unsigned int word : header)
|
||
for (int k = 0; k < 4; ++k)
|
||
packed.push_back((unsigned char)((word >> (k * 8)) & 0xff));
|
||
|
||
unsigned int acc = 0;
|
||
int acc_bits = 0;
|
||
|
||
for (size_t j = 0; j < meshlet.vertex_count; ++j)
|
||
for (int k = 0; k < 3; ++k)
|
||
writeBits(packed, acc, acc_bits, positions[j][k] - anchor[k], bits[k]);
|
||
|
||
if (acc_bits)
|
||
packed.push_back((unsigned char)(acc & 0xff));
|
||
|
||
packed.insert(packed.end(), topology, topology + tops);
|
||
|
||
stats_headers += 4 * 4;
|
||
stats_positions += (meshlet.vertex_count * (bits[0] + bits[1] + bits[2]) + 7) / 8;
|
||
stats_topology += tops;
|
||
}
|
||
|
||
size_t mbc = compress(packed);
|
||
size_t tris = mesh.indices.size() / 3;
|
||
|
||
printf("MeshletCodecDXR (%d@%d): %d meshlets, %d bytes; %.1f bytes/triangle (%.1f headers, %.1f positions, %.1f topology); post-deflate: %.1f bytes/triangle\n",
|
||
int(max_triangles), exponent,
|
||
int(meshlets.size()), int(packed.size()),
|
||
double(packed.size()) / double(tris), double(stats_headers) / double(tris), double(stats_positions) / double(tris), double(stats_topology) / double(tris),
|
||
double(mbc) / double(tris));
|
||
|
||
#if !TRACE
|
||
double mbtime = 0;
|
||
size_t mbsize = 0;
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
{
|
||
unsigned char clp[12 + 256 * 6]; // up to 48 bits per vertex
|
||
unsigned char clt[256 * 3 + 4]; // +4 bytes for padding
|
||
|
||
double t0 = timestamp();
|
||
unsigned char* p = &packed[0];
|
||
for (size_t j = 0; j < meshlets.size(); ++j)
|
||
{
|
||
// decode cluster header
|
||
size_t vertex_count = p[0] + 1;
|
||
size_t triangle_count = p[1] + 1;
|
||
size_t topology_size = p[2] | (p[3] << 8);
|
||
|
||
// decode DXR header to establish position size
|
||
size_t position_bitsx = (p[8] & 15) + 1;
|
||
size_t position_bitsy = ((p[8] >> 4) & 15) + 1;
|
||
size_t position_bitsz = (p[12] & 15) + 1;
|
||
size_t position_size = (vertex_count * (position_bitsx + position_bitsy + position_bitsz) + 7) / 8;
|
||
|
||
// memcpy DXR Compressed1 portion as is
|
||
void* clpopt = topology_size ? clp : clt; // note: hack to ensure memcpy is not elided for benchmarking purposes; topology_size is always >0 so this always copies into clp
|
||
memcpy(clpopt, p + 4, 12 + position_size);
|
||
|
||
// decode topology as 3 bytes per triangle
|
||
int rc = meshopt_decodeMeshlet(static_cast<unsigned int*>(NULL), 0, clt, triangle_count, p + 16 + position_size, topology_size);
|
||
assert(rc == 0);
|
||
(void)rc;
|
||
|
||
p += 16 + position_size + topology_size;
|
||
if (i == 0) // stats for decode throughput
|
||
mbsize += position_size + triangle_count * 3;
|
||
}
|
||
double t1 = timestamp();
|
||
|
||
mbtime = (mbtime == 0 || t1 - t0 < mbtime) ? (t1 - t0) : mbtime;
|
||
}
|
||
|
||
printf("MeshletCodecDXR (%d): decode time %.3f msec, %.3fB tri/sec, %.3f GB/sec\n",
|
||
int(max_triangles),
|
||
mbtime * 1000, double(tris) / 1e9 / mbtime, double(mbsize) / mbtime * 1e-9);
|
||
#endif
|
||
}
|
||
|
||
template <typename PV>
|
||
void packVertex(const Mesh& mesh, const char* pvn)
|
||
{
|
||
std::vector<PV> pv(mesh.vertices.size());
|
||
packMesh(pv, mesh.vertices);
|
||
|
||
size_t csize = compress(pv);
|
||
|
||
printf("VtxPack%s : %.1f bits/vertex (post-deflate %.1f bits/vertex)\n", pvn,
|
||
double(pv.size() * sizeof(PV) * 8) / double(mesh.vertices.size()),
|
||
double(csize * 8) / double(mesh.vertices.size()));
|
||
}
|
||
|
||
template <typename PV>
|
||
void encodeVertex(const Mesh& mesh, const char* pvn, int level = 2)
|
||
{
|
||
std::vector<PV> pv(mesh.vertices.size());
|
||
packMesh(pv, mesh.vertices);
|
||
|
||
// allocate result outside of the timing loop to exclude memset() from decode timing
|
||
std::vector<PV> result(mesh.vertices.size());
|
||
|
||
double start = timestamp();
|
||
|
||
std::vector<unsigned char> vbuf(meshopt_encodeVertexBufferBound(mesh.vertices.size(), sizeof(PV)));
|
||
vbuf.resize(meshopt_encodeVertexBufferLevel(&vbuf[0], vbuf.size(), &pv[0], mesh.vertices.size(), sizeof(PV), level, -1));
|
||
|
||
double middle = timestamp();
|
||
|
||
int res = meshopt_decodeVertexBuffer(&result[0], mesh.vertices.size(), sizeof(PV), &vbuf[0], vbuf.size());
|
||
assert(res == 0);
|
||
(void)res;
|
||
|
||
double end = timestamp();
|
||
|
||
assert(memcmp(&pv[0], &result[0], pv.size() * sizeof(PV)) == 0);
|
||
|
||
size_t csize = compress(vbuf);
|
||
|
||
printf("VtxCodec%1s: %.1f bits/vertex (post-deflate %.1f bits/vertex); encode %.2f msec (%.3f GB/s), decode %.2f msec (%.2f GB/s)\n", pvn,
|
||
double(vbuf.size() * 8) / double(mesh.vertices.size()),
|
||
double(csize * 8) / double(mesh.vertices.size()),
|
||
(middle - start) * 1000,
|
||
(double(result.size() * sizeof(PV)) / 1e9) / (middle - start),
|
||
(end - middle) * 1000,
|
||
(double(result.size() * sizeof(PV)) / 1e9) / (end - middle));
|
||
}
|
||
|
||
void stripify(const Mesh& mesh, bool use_restart, char desc)
|
||
{
|
||
unsigned int restart_index = use_restart ? ~0u : 0;
|
||
|
||
// note: input mesh is assumed to be optimized for vertex cache and vertex fetch
|
||
double start = timestamp();
|
||
std::vector<unsigned int> strip(meshopt_stripifyBound(mesh.indices.size()));
|
||
strip.resize(meshopt_stripify(&strip[0], &mesh.indices[0], mesh.indices.size(), mesh.vertices.size(), restart_index));
|
||
double end = timestamp();
|
||
|
||
size_t restarts = 0;
|
||
for (size_t i = 0; i < strip.size(); ++i)
|
||
restarts += use_restart && strip[i] == restart_index;
|
||
|
||
Mesh copy = mesh;
|
||
copy.indices.resize(meshopt_unstripify(©.indices[0], &strip[0], strip.size(), restart_index));
|
||
assert(copy.indices.size() <= meshopt_unstripifyBound(strip.size()));
|
||
|
||
meshopt_VertexCacheStatistics vcs = meshopt_analyzeVertexCache(©.indices[0], mesh.indices.size(), mesh.vertices.size(), 16, 0, 0);
|
||
meshopt_VertexCacheStatistics vcs_nv = meshopt_analyzeVertexCache(©.indices[0], mesh.indices.size(), mesh.vertices.size(), 32, 32, 32);
|
||
meshopt_VertexCacheStatistics vcs_amd = meshopt_analyzeVertexCache(©.indices[0], mesh.indices.size(), mesh.vertices.size(), 14, 64, 128);
|
||
meshopt_VertexCacheStatistics vcs_intel = meshopt_analyzeVertexCache(©.indices[0], mesh.indices.size(), mesh.vertices.size(), 128, 0, 0);
|
||
|
||
printf("Stripify%c: ACMR %f ATVR %f (NV %f AMD %f Intel %f); %.1f run avg, %d strip indices (%.1f%%) in %.2f msec\n",
|
||
desc,
|
||
vcs.acmr, vcs.atvr, vcs_nv.atvr, vcs_amd.atvr, vcs_intel.atvr,
|
||
use_restart ? double(strip.size() - restarts) / double(restarts + 1) : 0,
|
||
int(strip.size()), double(strip.size()) / double(mesh.indices.size()) * 100,
|
||
(end - start) * 1000);
|
||
}
|
||
|
||
void shadow(const Mesh& mesh)
|
||
{
|
||
// note: input mesh is assumed to be optimized for vertex cache and vertex fetch
|
||
|
||
double start = timestamp();
|
||
// this index buffer can be used for position-only rendering using the same vertex data that the original index buffer uses
|
||
std::vector<unsigned int> shadow_indices(mesh.indices.size());
|
||
meshopt_generateShadowIndexBuffer(&shadow_indices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0], mesh.vertices.size(), sizeof(float) * 3, sizeof(Vertex));
|
||
double end = timestamp();
|
||
|
||
// while you can't optimize the vertex data after shadow IB was constructed, you can and should optimize the shadow IB for vertex cache
|
||
// this is valuable even if the original indices array was optimized for vertex cache!
|
||
meshopt_optimizeVertexCache(&shadow_indices[0], &shadow_indices[0], shadow_indices.size(), mesh.vertices.size());
|
||
|
||
meshopt_VertexCacheStatistics vcs = meshopt_analyzeVertexCache(&mesh.indices[0], mesh.indices.size(), mesh.vertices.size(), 16, 0, 0);
|
||
meshopt_VertexCacheStatistics vcss = meshopt_analyzeVertexCache(&shadow_indices[0], shadow_indices.size(), mesh.vertices.size(), 16, 0, 0);
|
||
|
||
std::vector<char> shadow_flags(mesh.vertices.size());
|
||
size_t shadow_vertices = 0;
|
||
|
||
for (size_t i = 0; i < shadow_indices.size(); ++i)
|
||
{
|
||
unsigned int index = shadow_indices[i];
|
||
shadow_vertices += 1 - shadow_flags[index];
|
||
shadow_flags[index] = 1;
|
||
}
|
||
|
||
printf("ShadowIB : ACMR %f (%.2fx improvement); %d shadow vertices (%.2fx improvement) in %.2f msec\n",
|
||
vcss.acmr, double(vcs.vertices_transformed) / double(vcss.vertices_transformed),
|
||
int(shadow_vertices), double(mesh.vertices.size()) / double(shadow_vertices),
|
||
(end - start) * 1000);
|
||
}
|
||
|
||
static int follow(int* parents, int index)
|
||
{
|
||
while (index != parents[index])
|
||
{
|
||
int parent = parents[index];
|
||
parents[index] = parents[parent];
|
||
index = parent;
|
||
}
|
||
|
||
return index;
|
||
}
|
||
|
||
void meshlets(const Mesh& mesh, bool scan = false, bool uniform = false, bool flex = false, bool spatial = false, bool dump = false)
|
||
{
|
||
// NVidia recommends 64/126; we also test uniform configuration with 64/64 which is better for earlier AMD GPUs
|
||
const size_t max_vertices = 64;
|
||
const size_t max_triangles = uniform ? 64 : 126;
|
||
const size_t min_triangles = spatial ? 16 : (uniform ? 24 : 32); // only used in flex/spatial modes
|
||
|
||
// note: should be set to 0 unless cone culling is used at runtime!
|
||
const float cone_weight = 0.25f;
|
||
const float split_factor = flex ? 2.0f : 0.0f;
|
||
|
||
// note: input mesh is assumed to be optimized for vertex cache and vertex fetch
|
||
double start = timestamp();
|
||
size_t max_meshlets = meshopt_buildMeshletsBound(mesh.indices.size(), max_vertices, min_triangles);
|
||
std::vector<meshopt_Meshlet> meshlets(max_meshlets);
|
||
std::vector<unsigned int> meshlet_vertices(mesh.indices.size());
|
||
std::vector<unsigned char> meshlet_triangles(mesh.indices.size());
|
||
|
||
if (scan)
|
||
meshlets.resize(meshopt_buildMeshletsScan(&meshlets[0], &meshlet_vertices[0], &meshlet_triangles[0], &mesh.indices[0], mesh.indices.size(), mesh.vertices.size(), max_vertices, max_triangles));
|
||
else if (flex)
|
||
meshlets.resize(meshopt_buildMeshletsFlex(&meshlets[0], &meshlet_vertices[0], &meshlet_triangles[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), max_vertices, min_triangles, max_triangles, cone_weight, split_factor));
|
||
else if (spatial)
|
||
meshlets.resize(meshopt_buildMeshletsSpatial(&meshlets[0], &meshlet_vertices[0], &meshlet_triangles[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), max_vertices, min_triangles, max_triangles, 0.f));
|
||
else // note: equivalent to the call of buildMeshletsFlex() with split_factor = 0 and min_triangles = max_triangles
|
||
meshlets.resize(meshopt_buildMeshlets(&meshlets[0], &meshlet_vertices[0], &meshlet_triangles[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), max_vertices, max_triangles, cone_weight));
|
||
|
||
if (!dump)
|
||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||
meshopt_optimizeMeshlet(&meshlet_vertices[meshlets[i].vertex_offset], &meshlet_triangles[meshlets[i].triangle_offset], meshlets[i].triangle_count, meshlets[i].vertex_count);
|
||
|
||
if (meshlets.size())
|
||
{
|
||
const meshopt_Meshlet& last = meshlets.back();
|
||
|
||
// this is an example of how to trim the vertex/triangle arrays when copying data out to GPU storage
|
||
meshlet_vertices.resize(last.vertex_offset + last.vertex_count);
|
||
meshlet_triangles.resize(last.triangle_offset + last.triangle_count * 3);
|
||
}
|
||
|
||
double end = timestamp();
|
||
|
||
if (dump)
|
||
dumpObj(mesh.vertices, std::vector<unsigned>());
|
||
|
||
double avg_vertices = 0;
|
||
double avg_triangles = 0;
|
||
double avg_boundary = 0;
|
||
double avg_connected = 0;
|
||
size_t not_full = 0;
|
||
|
||
std::vector<int> boundary(mesh.vertices.size());
|
||
|
||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||
{
|
||
const meshopt_Meshlet& m = meshlets[i];
|
||
|
||
for (unsigned int j = 0; j < m.vertex_count; ++j)
|
||
boundary[meshlet_vertices[m.vertex_offset + j]]++;
|
||
}
|
||
|
||
std::vector<unsigned int> cluster;
|
||
|
||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||
{
|
||
const meshopt_Meshlet& m = meshlets[i];
|
||
|
||
if (dump)
|
||
{
|
||
cluster.clear();
|
||
for (unsigned int j = 0; j < m.triangle_count * 3; ++j)
|
||
cluster.push_back(meshlet_vertices[m.vertex_offset + meshlet_triangles[m.triangle_offset + j]]);
|
||
|
||
char cname[32];
|
||
snprintf(cname, sizeof(cname), "ml_%d", int(i));
|
||
dumpObj(cname, cluster);
|
||
}
|
||
|
||
avg_vertices += m.vertex_count;
|
||
avg_triangles += m.triangle_count;
|
||
not_full += uniform ? m.triangle_count < max_triangles : m.vertex_count < max_vertices;
|
||
|
||
for (unsigned int j = 0; j < m.vertex_count; ++j)
|
||
if (boundary[meshlet_vertices[m.vertex_offset + j]] > 1)
|
||
avg_boundary += 1;
|
||
|
||
// union-find vertices to check if the meshlet is connected
|
||
int parents[256];
|
||
for (unsigned int j = 0; j < m.vertex_count; ++j)
|
||
parents[j] = int(j);
|
||
|
||
for (unsigned int j = 0; j < m.triangle_count * 3; ++j)
|
||
{
|
||
int v0 = meshlet_triangles[m.triangle_offset + j];
|
||
int v1 = meshlet_triangles[m.triangle_offset + j + (j % 3 == 2 ? -2 : 1)];
|
||
|
||
v0 = follow(parents, v0);
|
||
v1 = follow(parents, v1);
|
||
|
||
parents[v0] = v1;
|
||
}
|
||
|
||
int roots = 0;
|
||
for (unsigned int j = 0; j < m.vertex_count; ++j)
|
||
roots += follow(parents, j) == int(j);
|
||
|
||
assert(roots != 0);
|
||
avg_connected += roots;
|
||
}
|
||
|
||
avg_vertices /= double(meshlets.size());
|
||
avg_triangles /= double(meshlets.size());
|
||
avg_boundary /= double(meshlets.size());
|
||
avg_connected /= double(meshlets.size());
|
||
|
||
printf("Meshlets%c: %d meshlets (avg vertices %.1f, avg triangles %.1f, avg boundary %.1f, avg connected %.2f, not full %d) in %.2f msec\n",
|
||
scan ? 'S' : (flex ? 'F' : (spatial ? 'X' : (uniform ? 'U' : ' '))),
|
||
int(meshlets.size()), avg_vertices, avg_triangles, avg_boundary, avg_connected, int(not_full), (end - start) * 1000);
|
||
|
||
float camera[3] = {100, 100, 100};
|
||
|
||
size_t rejected = 0;
|
||
size_t accepted = 0;
|
||
double radius_mean = 0;
|
||
double cone_mean = 0;
|
||
|
||
std::vector<float> radii(meshlets.size());
|
||
std::vector<float> cones(meshlets.size());
|
||
|
||
double startc = timestamp();
|
||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||
{
|
||
const meshopt_Meshlet& m = meshlets[i];
|
||
|
||
meshopt_Bounds bounds = meshopt_computeMeshletBounds(&meshlet_vertices[m.vertex_offset], &meshlet_triangles[m.triangle_offset], m.triangle_count, &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex));
|
||
|
||
radii[i] = bounds.radius;
|
||
cones[i] = 90.f - acosf(bounds.cone_cutoff) * (180.f / 3.1415926f);
|
||
|
||
radius_mean += radii[i];
|
||
cone_mean += cones[i];
|
||
|
||
// trivial accept: we can't ever backface cull this meshlet
|
||
accepted += (bounds.cone_cutoff >= 1);
|
||
|
||
// perspective projection: dot(normalize(cone_apex - camera_position), cone_axis) > cone_cutoff
|
||
// alternative formulation for perspective projection that doesn't use apex (and uses cluster bounding sphere instead):
|
||
// dot(normalize(center - camera_position), cone_axis) > cone_cutoff + radius / length(center - camera_position)
|
||
float cview[3] = {bounds.center[0] - camera[0], bounds.center[1] - camera[1], bounds.center[2] - camera[2]};
|
||
float cviewlength = sqrtf(cview[0] * cview[0] + cview[1] * cview[1] + cview[2] * cview[2]);
|
||
|
||
rejected += cview[0] * bounds.cone_axis[0] + cview[1] * bounds.cone_axis[1] + cview[2] * bounds.cone_axis[2] >= bounds.cone_cutoff * cviewlength + bounds.radius;
|
||
}
|
||
double endc = timestamp();
|
||
|
||
radius_mean /= double(meshlets.size());
|
||
cone_mean /= double(meshlets.size());
|
||
|
||
double radius_variance = 0;
|
||
|
||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||
radius_variance += (radii[i] - radius_mean) * (radii[i] - radius_mean);
|
||
|
||
radius_variance /= double(meshlets.size() - 1);
|
||
|
||
double radius_stddev = sqrt(radius_variance);
|
||
|
||
size_t meshlets_std = 0;
|
||
|
||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||
meshlets_std += radii[i] < radius_mean + radius_stddev;
|
||
|
||
printf("Bounds : radius mean %f stddev %f; %.1f%% meshlets under 1σ; cone angle %.1f°; cone reject %.1f%% trivial accept %.1f%% in %.2f msec\n",
|
||
radius_mean, radius_stddev,
|
||
double(meshlets_std) / double(meshlets.size()) * 100,
|
||
cone_mean, double(rejected) / double(meshlets.size()) * 100, double(accepted) / double(meshlets.size()) * 100,
|
||
(endc - startc) * 1000);
|
||
}
|
||
|
||
void spatialSort(const Mesh& mesh)
|
||
{
|
||
typedef PackedVertexOct PV;
|
||
|
||
std::vector<PV> pv(mesh.vertices.size());
|
||
packMesh(pv, mesh.vertices);
|
||
|
||
double start = timestamp();
|
||
|
||
std::vector<unsigned int> remap(mesh.vertices.size());
|
||
meshopt_spatialSortRemap(&remap[0], &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex));
|
||
|
||
double end = timestamp();
|
||
|
||
meshopt_remapVertexBuffer(&pv[0], &pv[0], mesh.vertices.size(), sizeof(PV), &remap[0]);
|
||
|
||
std::vector<unsigned char> vbuf(meshopt_encodeVertexBufferBound(mesh.vertices.size(), sizeof(PV)));
|
||
vbuf.resize(meshopt_encodeVertexBuffer(&vbuf[0], vbuf.size(), &pv[0], mesh.vertices.size(), sizeof(PV)));
|
||
|
||
size_t csize = compress(vbuf);
|
||
|
||
printf("Spatial : %.1f bits/vertex (post-deflate %.1f bits/vertex); sort %.2f msec\n",
|
||
double(vbuf.size() * 8) / double(mesh.vertices.size()),
|
||
double(csize * 8) / double(mesh.vertices.size()),
|
||
(end - start) * 1000);
|
||
}
|
||
|
||
void spatialSortTriangles(const Mesh& mesh)
|
||
{
|
||
typedef PackedVertexOct PV;
|
||
|
||
Mesh copy = mesh;
|
||
|
||
double start = timestamp();
|
||
|
||
meshopt_spatialSortTriangles(©.indices[0], ©.indices[0], mesh.indices.size(), ©.vertices[0].px, copy.vertices.size(), sizeof(Vertex));
|
||
|
||
double end = timestamp();
|
||
|
||
meshopt_optimizeVertexCache(©.indices[0], ©.indices[0], copy.indices.size(), copy.vertices.size());
|
||
meshopt_optimizeVertexFetch(©.vertices[0], ©.indices[0], copy.indices.size(), ©.vertices[0], copy.vertices.size(), sizeof(Vertex));
|
||
|
||
std::vector<PV> pv(mesh.vertices.size());
|
||
packMesh(pv, copy.vertices);
|
||
|
||
std::vector<unsigned char> vbuf(meshopt_encodeVertexBufferBound(mesh.vertices.size(), sizeof(PV)));
|
||
vbuf.resize(meshopt_encodeVertexBuffer(&vbuf[0], vbuf.size(), &pv[0], mesh.vertices.size(), sizeof(PV)));
|
||
|
||
std::vector<unsigned char> ibuf(meshopt_encodeIndexBufferBound(mesh.indices.size(), mesh.vertices.size()));
|
||
ibuf.resize(meshopt_encodeIndexBuffer(&ibuf[0], ibuf.size(), ©.indices[0], mesh.indices.size()));
|
||
|
||
size_t csizev = compress(vbuf);
|
||
size_t csizei = compress(ibuf);
|
||
|
||
printf("SpatialT : %.1f bits/vertex (post-deflate %.1f bits/vertex); %.1f bits/triangle (post-deflate %.1f bits/triangle); sort %.2f msec\n",
|
||
double(vbuf.size() * 8) / double(mesh.vertices.size()),
|
||
double(csizev * 8) / double(mesh.vertices.size()),
|
||
double(ibuf.size() * 8) / double(mesh.indices.size() / 3),
|
||
double(csizei * 8) / double(mesh.indices.size() / 3),
|
||
(end - start) * 1000);
|
||
}
|
||
|
||
void spatialClusterPoints(const Mesh& mesh, size_t cluster_size)
|
||
{
|
||
typedef PackedVertexOct PV;
|
||
|
||
double start = timestamp();
|
||
|
||
std::vector<unsigned int> index(mesh.vertices.size());
|
||
meshopt_spatialClusterPoints(&index[0], &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), cluster_size);
|
||
|
||
double end = timestamp();
|
||
|
||
std::vector<PV> pv(mesh.vertices.size());
|
||
packMesh(pv, mesh.vertices);
|
||
|
||
std::vector<PV> pvo(mesh.vertices.size());
|
||
for (size_t i = 0; i < index.size(); ++i)
|
||
pvo[i] = pv[index[i]];
|
||
|
||
std::vector<unsigned char> vbuf(meshopt_encodeVertexBufferBound(mesh.vertices.size(), sizeof(PV)));
|
||
vbuf.resize(meshopt_encodeVertexBuffer(&vbuf[0], vbuf.size(), &pvo[0], mesh.vertices.size(), sizeof(PV)));
|
||
|
||
size_t csize = compress(vbuf);
|
||
|
||
printf("SpatialCP: %.1f bits/vertex (post-deflate %.1f bits/vertex); sort %.2f msec\n",
|
||
double(vbuf.size() * 8) / double(mesh.vertices.size()),
|
||
double(csize * 8) / double(mesh.vertices.size()),
|
||
(end - start) * 1000);
|
||
}
|
||
|
||
void filterTriangles(const Mesh& mesh)
|
||
{
|
||
double start = timestamp();
|
||
|
||
std::vector<unsigned int> newindices(mesh.indices.size());
|
||
newindices.resize(meshopt_filterIndexBuffer(&newindices[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0], mesh.vertices.size(), sizeof(float) * 3, sizeof(Vertex)));
|
||
|
||
double end = timestamp();
|
||
|
||
printf("FilterIB : %d triangles -> %d (%.2f%% redundant) in %.2f msec\n",
|
||
int(mesh.indices.size() / 3), int(newindices.size() / 3),
|
||
double((mesh.indices.size() - newindices.size()) / 3) / double(mesh.indices.size() / 3) * 100.0,
|
||
(end - start) * 1000);
|
||
}
|
||
|
||
void tessellationAdjacency(const Mesh& mesh)
|
||
{
|
||
double start = timestamp();
|
||
|
||
// 12 indices per input triangle
|
||
std::vector<unsigned int> tessib(mesh.indices.size() * 4);
|
||
meshopt_generateTessellationIndexBuffer(&tessib[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex));
|
||
|
||
double middle = timestamp();
|
||
|
||
// 6 indices per input triangle
|
||
std::vector<unsigned int> adjib(mesh.indices.size() * 2);
|
||
meshopt_generateAdjacencyIndexBuffer(&adjib[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex));
|
||
|
||
double end = timestamp();
|
||
|
||
printf("Tesselltn: %d patches in %.2f msec\n", int(mesh.indices.size() / 3), (middle - start) * 1000);
|
||
printf("Adjacency: %d patches in %.2f msec\n", int(mesh.indices.size() / 3), (end - middle) * 1000);
|
||
}
|
||
|
||
void provoking(const Mesh& mesh)
|
||
{
|
||
double start = timestamp();
|
||
|
||
// worst case number of vertices: vertex count + triangle count
|
||
std::vector<unsigned int> pib(mesh.indices.size());
|
||
std::vector<unsigned int> reorder(mesh.vertices.size() + mesh.indices.size() / 3);
|
||
|
||
size_t pcount = meshopt_generateProvokingIndexBuffer(&pib[0], &reorder[0], &mesh.indices[0], mesh.indices.size(), mesh.vertices.size());
|
||
reorder.resize(pcount);
|
||
|
||
double end = timestamp();
|
||
|
||
// validate invariant: pib[i] == i/3 for provoking vertices
|
||
for (size_t i = 0; i < mesh.indices.size(); i += 3)
|
||
assert(pib[i] == i / 3);
|
||
|
||
// validate invariant: reorder[pib[x]] == ib[x] modulo triangle rotation
|
||
// note: this is technically not promised by the interface (it may reorder triangles!), it just happens to hold right now
|
||
for (size_t i = 0; i < mesh.indices.size(); i += 3)
|
||
{
|
||
unsigned int a = mesh.indices[i + 0], b = mesh.indices[i + 1], c = mesh.indices[i + 2];
|
||
unsigned int ra = reorder[pib[i + 0]], rb = reorder[pib[i + 1]], rc = reorder[pib[i + 2]];
|
||
|
||
assert((a == ra && b == rb && c == rc) || (a == rb && b == rc && c == ra) || (a == rc && b == ra && c == rb));
|
||
}
|
||
|
||
// best case number of vertices: max(vertex count, triangle count), assuming non-redundant indexing (all vertices are used)
|
||
// note: this is a lower bound, and it's not theoretically possible on some meshes;
|
||
// for example, a union of a flat shaded cube (12t 24v) and a smooth shaded icosahedron (20t 12v) will have 36 vertices and 32 triangles
|
||
// however, the best case for that union is 44 vertices (24 cube vertices + 20 icosahedron vertices due to provoking invariant)
|
||
size_t bestv = mesh.vertices.size() > mesh.indices.size() / 3 ? mesh.vertices.size() : mesh.indices.size() / 3;
|
||
|
||
printf("Provoking: %d triangles / %d vertices (+%.1f%% extra) in %.2f msec\n",
|
||
int(mesh.indices.size() / 3), int(pcount), double(pcount) / double(bestv) * 100.0 - 100.0, (end - start) * 1000);
|
||
}
|
||
|
||
static int reindexCompare(void* context, unsigned int lhs, unsigned int rhs)
|
||
{
|
||
const Vertex* vertices = static_cast<Vertex*>(context);
|
||
const Vertex& lv = vertices[lhs];
|
||
const Vertex& rv = vertices[rhs];
|
||
|
||
float ln = lv.nx * lv.nx + lv.ny * lv.ny + lv.nz * lv.nz;
|
||
float rn = rv.nx * rv.nx + rv.ny * rv.ny + rv.nz * rv.nz;
|
||
|
||
// 1/1024px UV tolerance, 3 degree normal tolerance
|
||
return fabsf(lv.tx - rv.tx) < 1e-3f &&
|
||
fabsf(lv.ty - rv.ty) < 1e-3f &&
|
||
(lv.nx * rv.nx + lv.ny * rv.ny + lv.nz * rv.nz >= 0.9986f * sqrtf(ln * rn));
|
||
}
|
||
|
||
void reindexFuzzy(const Mesh& mesh)
|
||
{
|
||
std::vector<PackedVertex> pv(mesh.vertices.size());
|
||
packMesh(pv, mesh.vertices);
|
||
|
||
std::vector<unsigned int> remap(mesh.vertices.size());
|
||
|
||
double start = timestamp();
|
||
|
||
size_t up = meshopt_generateVertexRemap(&remap[0], &mesh.indices[0], mesh.indices.size(), &pv[0], mesh.vertices.size(), sizeof(PackedVertex));
|
||
|
||
double middle = timestamp();
|
||
|
||
size_t uf = meshopt_generateVertexRemapCustom(&remap[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), reindexCompare, const_cast<Vertex*>(&mesh.vertices[0]));
|
||
|
||
double end = timestamp();
|
||
|
||
printf("ReindexQ : %d vertices => %d unique vertices in %.2f msec\n",
|
||
int(mesh.vertices.size()), int(up), (middle - start) * 1000);
|
||
printf("ReindexF : %d vertices => %d unique vertices in %.2f msec\n",
|
||
int(mesh.vertices.size()), int(uf), (end - middle) * 1000);
|
||
}
|
||
|
||
void coverage(const Mesh& mesh)
|
||
{
|
||
double start = timestamp();
|
||
|
||
meshopt_CoverageStatistics cs = meshopt_analyzeCoverage(&mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex));
|
||
|
||
double end = timestamp();
|
||
|
||
printf("Coverage : X %.1f%% Y %.1f%% Z %.1f%% in %.2f msec\n",
|
||
cs.coverage[0] * 100, cs.coverage[1] * 100, cs.coverage[2] * 100, (end - start) * 1000);
|
||
}
|
||
|
||
struct VertexTangent
|
||
{
|
||
float px, py, pz;
|
||
float nx, ny, nz;
|
||
float tu, tv;
|
||
float tx, ty, tz, tw;
|
||
};
|
||
|
||
void tangents(const Mesh& mesh)
|
||
{
|
||
double start = timestamp();
|
||
|
||
// note: tangent generation produces a tangent vector and orientation (+-1) per *corner* (3 per triangle), even for indexed inputs
|
||
// you *could* then copy the tangents to the existing vertices, but that will not work correctly in case the input has UV mirroring
|
||
// a simple way to handle this is to work on unindexed data (indices==NULL), generate tangents, and re-index
|
||
// we show a more involved way that splits vertices with divergent tangents on the fly.
|
||
std::vector<float> tangents(mesh.indices.size() * 4);
|
||
|
||
meshopt_generateTangents(&tangents[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), &mesh.vertices[0].nx, sizeof(Vertex), &mesh.vertices[0].tx, sizeof(Vertex));
|
||
|
||
std::vector<VertexTangent> newvertices(mesh.vertices.size());
|
||
std::vector<unsigned int> newindices = mesh.indices;
|
||
|
||
// copy positions/normals/UVs (Vertex has no tangent field, so we use VertexTangent)
|
||
for (size_t i = 0; i < mesh.vertices.size(); ++i)
|
||
{
|
||
VertexTangent& vt = newvertices[i];
|
||
const Vertex& v = mesh.vertices[i];
|
||
|
||
vt.px = v.px, vt.py = v.py, vt.pz = v.pz;
|
||
vt.nx = v.nx, vt.ny = v.ny, vt.nz = v.nz;
|
||
vt.tu = v.tx, vt.tv = v.ty;
|
||
}
|
||
|
||
// seed each vertex with one of its corner tangents; the loop below fixes any mismatches
|
||
for (size_t i = 0; i < mesh.indices.size(); ++i)
|
||
{
|
||
VertexTangent& vt = newvertices[mesh.indices[i]];
|
||
const float* t = &tangents[i * 4];
|
||
vt.tx = t[0], vt.ty = t[1], vt.tz = t[2], vt.tw = t[3];
|
||
}
|
||
|
||
std::vector<unsigned int> splits(mesh.vertices.size(), ~0u);
|
||
|
||
for (size_t i = 0; i < mesh.indices.size(); ++i)
|
||
{
|
||
const float* t = &tangents[i * 4];
|
||
unsigned int v = mesh.indices[i];
|
||
|
||
// walk the chain of split copies looking for a vertex whose tangent matches
|
||
while (v != ~0u && (newvertices[v].tx != t[0] || newvertices[v].ty != t[1] || newvertices[v].tz != t[2] || newvertices[v].tw != t[3]))
|
||
v = splits[v];
|
||
|
||
// no match in chain: append a new split copy with the target tangent and chain it
|
||
if (v == ~0u)
|
||
{
|
||
v = unsigned(newvertices.size());
|
||
newvertices.push_back(newvertices[mesh.indices[i]]);
|
||
newvertices[v].tx = t[0], newvertices[v].ty = t[1], newvertices[v].tz = t[2], newvertices[v].tw = t[3];
|
||
splits.push_back(splits[mesh.indices[i]]);
|
||
splits[mesh.indices[i]] = v;
|
||
}
|
||
|
||
newindices[i] = v;
|
||
}
|
||
|
||
double end = timestamp();
|
||
|
||
printf("Tangents : %d corners, %d splits in %.2f msec\n", int(mesh.indices.size()), int(newvertices.size() - mesh.vertices.size()), (end - start) * 1000);
|
||
}
|
||
|
||
void normals(const Mesh& mesh, float smoothing)
|
||
{
|
||
// note: normal generation produces a normal vector per *corner* (3 per triangle), even for indexed inputs
|
||
// you could split the existing vertices on the fly (see tangents()), here we demonstrate an alternative approach with full reindexing
|
||
std::vector<float> normals(mesh.indices.size() * 3);
|
||
|
||
double start = timestamp();
|
||
|
||
const float crease = 1.f; // 1 rad ~= 60 deg
|
||
meshopt_generateNormals(&normals[0], &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), crease, smoothing);
|
||
|
||
double middle = timestamp();
|
||
|
||
std::vector<Vertex> unindexed(mesh.indices.size());
|
||
|
||
for (size_t i = 0; i < mesh.indices.size(); ++i)
|
||
{
|
||
unindexed[i] = mesh.vertices[mesh.indices[i]];
|
||
unindexed[i].nx = normals[i * 3 + 0];
|
||
unindexed[i].ny = normals[i * 3 + 1];
|
||
unindexed[i].nz = normals[i * 3 + 2];
|
||
}
|
||
|
||
std::vector<unsigned int> remap(mesh.indices.size());
|
||
size_t vertex_count = meshopt_generateVertexRemap(&remap[0], NULL, mesh.indices.size(), &unindexed[0], mesh.indices.size(), sizeof(Vertex));
|
||
|
||
std::vector<unsigned int> newindices(mesh.indices.size());
|
||
meshopt_remapIndexBuffer(&newindices[0], NULL, mesh.indices.size(), &remap[0]);
|
||
|
||
std::vector<Vertex> newvertices(vertex_count);
|
||
meshopt_remapVertexBuffer(&newvertices[0], &unindexed[0], mesh.indices.size(), sizeof(Vertex), &remap[0]);
|
||
|
||
double end = timestamp();
|
||
|
||
printf("Normals %d: %d corners, %d vertices in %.2f msec; reindexed in %.2f msec\n",
|
||
int(ceilf(smoothing)), int(mesh.indices.size()), int(vertex_count), (middle - start) * 1000, (end - middle) * 1000);
|
||
}
|
||
|
||
void remesh(const Mesh& mesh, int resolution, unsigned int options = 0)
|
||
{
|
||
double start = timestamp();
|
||
size_t capacity = meshopt_remesh(NULL, 0, &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), resolution, options);
|
||
double middle = timestamp();
|
||
std::vector<float> triangles(capacity * 3 * 3);
|
||
size_t count = meshopt_remesh(&triangles[0], capacity, &mesh.indices[0], mesh.indices.size(), &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), resolution, options);
|
||
double end = timestamp();
|
||
|
||
assert(count <= capacity);
|
||
|
||
printf("Remesh%3d: %d triangles => %d triangles in %.2f msec (count %.2f msec, compute %.2f msec)\n", resolution,
|
||
int(mesh.indices.size() / 3), int(count), (end - start) * 1000, (middle - start) * 1000, (end - middle) * 1000);
|
||
}
|
||
|
||
void nanite(const std::vector<Vertex>& vertices, const std::vector<unsigned int>& indices); // nanite.cpp
|
||
|
||
bool loadMesh(Mesh& mesh, const char* path)
|
||
{
|
||
double start = timestamp();
|
||
double middle;
|
||
mesh = parseObj(path, middle);
|
||
double end = timestamp();
|
||
|
||
if (mesh.vertices.empty())
|
||
{
|
||
printf("Mesh %s is empty, skipping\n", path);
|
||
return false;
|
||
}
|
||
|
||
printf("# %s: %d vertices, %d triangles; read in %.2f msec; indexed in %.2f msec\n", path, int(mesh.vertices.size()), int(mesh.indices.size() / 3), (middle - start) * 1000, (end - middle) * 1000);
|
||
return true;
|
||
}
|
||
|
||
void processDeinterleaved(const char* path)
|
||
{
|
||
// Most algorithms in the library work out of the box with deinterleaved geometry, but some require slightly special treatment;
|
||
// this code runs a simplified version of complete opt. pipeline using deinterleaved geo. There's no compression performed but you
|
||
// can trivially run it by quantizing all elements and running meshopt_encodeVertexBuffer once for each vertex stream.
|
||
fastObjMesh* obj = fast_obj_read(path);
|
||
if (!obj)
|
||
{
|
||
printf("Error loading %s: file not found\n", path);
|
||
return;
|
||
}
|
||
|
||
size_t total_indices = 0;
|
||
|
||
for (unsigned int i = 0; i < obj->face_count; ++i)
|
||
total_indices += 3 * (obj->face_vertices[i] - 2);
|
||
|
||
std::vector<float> unindexed_pos(total_indices * 3);
|
||
std::vector<float> unindexed_nrm(total_indices * 3);
|
||
std::vector<float> unindexed_uv(total_indices * 2);
|
||
|
||
size_t vertex_offset = 0;
|
||
size_t index_offset = 0;
|
||
|
||
for (unsigned int i = 0; i < obj->face_count; ++i)
|
||
{
|
||
if (obj->face_vertices[i] <= 2)
|
||
continue;
|
||
|
||
for (unsigned int j = 0; j < obj->face_vertices[i]; ++j)
|
||
{
|
||
fastObjIndex gi = obj->indices[index_offset + j];
|
||
|
||
// triangulate polygon on the fly; offset-3 is always the first polygon vertex
|
||
if (j >= 3)
|
||
{
|
||
memcpy(&unindexed_pos[(vertex_offset + 0) * 3], &unindexed_pos[(vertex_offset - 3) * 3], 3 * sizeof(float));
|
||
memcpy(&unindexed_nrm[(vertex_offset + 0) * 3], &unindexed_nrm[(vertex_offset - 3) * 3], 3 * sizeof(float));
|
||
memcpy(&unindexed_uv[(vertex_offset + 0) * 2], &unindexed_uv[(vertex_offset - 3) * 2], 2 * sizeof(float));
|
||
memcpy(&unindexed_pos[(vertex_offset + 1) * 3], &unindexed_pos[(vertex_offset - 1) * 3], 3 * sizeof(float));
|
||
memcpy(&unindexed_nrm[(vertex_offset + 1) * 3], &unindexed_nrm[(vertex_offset - 1) * 3], 3 * sizeof(float));
|
||
memcpy(&unindexed_uv[(vertex_offset + 1) * 2], &unindexed_uv[(vertex_offset - 1) * 2], 2 * sizeof(float));
|
||
vertex_offset += 2;
|
||
}
|
||
|
||
memcpy(&unindexed_pos[vertex_offset * 3], &obj->positions[gi.p * 3], 3 * sizeof(float));
|
||
memcpy(&unindexed_nrm[vertex_offset * 3], &obj->normals[gi.n * 3], 3 * sizeof(float));
|
||
memcpy(&unindexed_uv[vertex_offset * 2], &obj->texcoords[gi.t * 2], 2 * sizeof(float));
|
||
vertex_offset++;
|
||
}
|
||
|
||
index_offset += obj->face_vertices[i];
|
||
}
|
||
|
||
fast_obj_destroy(obj);
|
||
|
||
double start = timestamp();
|
||
|
||
meshopt_Stream streams[] = {
|
||
{&unindexed_pos[0], sizeof(float) * 3, sizeof(float) * 3},
|
||
{&unindexed_nrm[0], sizeof(float) * 3, sizeof(float) * 3},
|
||
{&unindexed_uv[0], sizeof(float) * 2, sizeof(float) * 2},
|
||
};
|
||
|
||
std::vector<unsigned int> remap(total_indices);
|
||
|
||
size_t total_vertices = meshopt_generateVertexRemapMulti(&remap[0], NULL, total_indices, total_indices, streams, sizeof(streams) / sizeof(streams[0]));
|
||
|
||
std::vector<unsigned int> indices(total_indices);
|
||
meshopt_remapIndexBuffer(&indices[0], NULL, total_indices, &remap[0]);
|
||
|
||
std::vector<float> pos(total_vertices * 3);
|
||
meshopt_remapVertexBuffer(&pos[0], &unindexed_pos[0], total_indices, sizeof(float) * 3, &remap[0]);
|
||
|
||
std::vector<float> nrm(total_vertices * 3);
|
||
meshopt_remapVertexBuffer(&nrm[0], &unindexed_nrm[0], total_indices, sizeof(float) * 3, &remap[0]);
|
||
|
||
std::vector<float> uv(total_vertices * 2);
|
||
meshopt_remapVertexBuffer(&uv[0], &unindexed_uv[0], total_indices, sizeof(float) * 2, &remap[0]);
|
||
|
||
double reindex = timestamp();
|
||
|
||
meshopt_optimizeVertexCache(&indices[0], &indices[0], total_indices, total_vertices);
|
||
|
||
meshopt_optimizeVertexFetchRemap(&remap[0], &indices[0], total_indices, total_vertices);
|
||
meshopt_remapVertexBuffer(&pos[0], &pos[0], total_vertices, sizeof(float) * 3, &remap[0]);
|
||
meshopt_remapVertexBuffer(&nrm[0], &nrm[0], total_vertices, sizeof(float) * 3, &remap[0]);
|
||
meshopt_remapVertexBuffer(&uv[0], &uv[0], total_vertices, sizeof(float) * 2, &remap[0]);
|
||
|
||
double optimize = timestamp();
|
||
|
||
// note: since shadow index buffer is computed based on regular vertex/index buffer, the stream points at the indexed data - not unindexed_pos
|
||
meshopt_Stream shadow_stream = {&pos[0], sizeof(float) * 3, sizeof(float) * 3};
|
||
|
||
std::vector<unsigned int> shadow_indices(total_indices);
|
||
meshopt_generateShadowIndexBufferMulti(&shadow_indices[0], &indices[0], total_indices, total_vertices, &shadow_stream, 1);
|
||
|
||
meshopt_optimizeVertexCache(&shadow_indices[0], &shadow_indices[0], total_indices, total_vertices);
|
||
|
||
double shadow = timestamp();
|
||
|
||
printf("Deintrlvd: %d vertices, reindexed in %.2f msec, optimized in %.2f msec, generated & optimized shadow indices in %.2f msec\n",
|
||
int(total_vertices), (reindex - start) * 1000, (optimize - reindex) * 1000, (shadow - optimize) * 1000);
|
||
}
|
||
|
||
void process(const char* path)
|
||
{
|
||
Mesh mesh;
|
||
if (!loadMesh(mesh, path))
|
||
return;
|
||
|
||
optimize(mesh);
|
||
optimize(mesh, /* fifo= */ true);
|
||
|
||
Mesh copy = mesh;
|
||
meshopt_optimizeVertexCache(©.indices[0], ©.indices[0], copy.indices.size(), copy.vertices.size());
|
||
meshopt_optimizeVertexFetch(©.vertices[0], ©.indices[0], copy.indices.size(), ©.vertices[0], copy.vertices.size(), sizeof(Vertex));
|
||
|
||
Mesh copystrip = mesh;
|
||
meshopt_optimizeVertexCacheStrip(©strip.indices[0], ©strip.indices[0], copystrip.indices.size(), copystrip.vertices.size());
|
||
meshopt_optimizeVertexFetch(©strip.vertices[0], ©strip.indices[0], copystrip.indices.size(), ©strip.vertices[0], copystrip.vertices.size(), sizeof(Vertex));
|
||
|
||
stripify(copy, false, ' ');
|
||
stripify(copy, /* use_restart= */ true, 'R');
|
||
stripify(copystrip, /* use_restart= */ true, 'S');
|
||
|
||
meshlets(copy, /* scan= */ true);
|
||
meshlets(copy, /* scan= */ false);
|
||
meshlets(copy, /* scan= */ false, /* uniform= */ true);
|
||
meshlets(copy, /* scan= */ false, /* uniform= */ false, /* flex= */ true);
|
||
meshlets(copy, /* scan= */ false, /* uniform= */ true, /* flex= */ false, /* spatial= */ true);
|
||
|
||
shadow(copy);
|
||
filterTriangles(copy);
|
||
tessellationAdjacency(copy);
|
||
provoking(copy);
|
||
|
||
encodeIndex(copy, ' ');
|
||
encodeIndex(copystrip, 'S');
|
||
|
||
std::vector<unsigned int> strip(meshopt_stripifyBound(copystrip.indices.size()));
|
||
strip.resize(meshopt_stripify(&strip[0], ©strip.indices[0], copystrip.indices.size(), copystrip.vertices.size(), 0));
|
||
|
||
encodeIndexSequence(strip, copystrip.vertices.size(), 'D');
|
||
|
||
packVertex<PackedVertex>(copy, "");
|
||
encodeVertex<PackedVertex>(copy, "");
|
||
encodeVertex<PackedVertexOct>(copy, "O");
|
||
|
||
encodeMeshlets(mesh, 64, 96);
|
||
encodeMeshletsDXR(mesh, 128, /* min_exp= */ -14);
|
||
|
||
simplify(mesh);
|
||
simplify(mesh, 0.1f, meshopt_SimplifyPrune);
|
||
simplifyAttr(mesh);
|
||
simplifyAttr(mesh, 0.1f, meshopt_SimplifyPermissive);
|
||
simplifyUpdate(mesh);
|
||
simplifyUpdate(mesh, 0.1f, meshopt_SimplifyPermissive);
|
||
simplifySloppy(mesh);
|
||
simplifyComplete(mesh);
|
||
simplifyPoints(mesh);
|
||
simplifyClusters(mesh);
|
||
|
||
spatialSort(mesh);
|
||
spatialSortTriangles(mesh);
|
||
spatialClusterPoints(mesh, 64);
|
||
|
||
reindexFuzzy(mesh);
|
||
coverage(mesh);
|
||
|
||
tangents(mesh);
|
||
normals(mesh, 1.5f);
|
||
|
||
remesh(mesh, 128);
|
||
remesh(mesh, 128, meshopt_RemeshSolve);
|
||
|
||
if (path)
|
||
processDeinterleaved(path);
|
||
}
|
||
|
||
void processDev(const char* path)
|
||
{
|
||
Mesh mesh;
|
||
if (!loadMesh(mesh, path))
|
||
return;
|
||
|
||
remesh(mesh, 160, meshopt_RemeshSolve);
|
||
}
|
||
|
||
void processNanite(const char* path)
|
||
{
|
||
Mesh mesh;
|
||
if (!loadMesh(mesh, path))
|
||
return;
|
||
|
||
nanite(mesh.vertices, mesh.indices);
|
||
}
|
||
|
||
int main(int argc, char** argv)
|
||
{
|
||
void runTests();
|
||
|
||
if (argc == 1)
|
||
{
|
||
runTests();
|
||
}
|
||
else
|
||
{
|
||
if (strcmp(argv[1], "-d") == 0)
|
||
{
|
||
for (int i = 2; i < argc; ++i)
|
||
processDev(argv[i]);
|
||
}
|
||
else if (strcmp(argv[1], "-n") == 0)
|
||
{
|
||
for (int i = 2; i < argc; ++i)
|
||
processNanite(argv[i]);
|
||
}
|
||
else
|
||
{
|
||
for (int i = 1; i < argc; ++i)
|
||
process(argv[i]);
|
||
|
||
runTests();
|
||
}
|
||
}
|
||
}
|