// flexible_dual_grid.h — single-header CPU port of TRELLIS.2's flexible dual // grid mesh extraction (o-voxel/o_voxel/convert/flexible_dual_grid.py, eval // path). Turns the shape decoder's per-voxel 7-channel output into a triangle // mesh, replacing the CUDA hashmap kernel with an std::unordered_map. // // Per active voxel v at integer coord c (in [0, grid_size)): // dual vertex V_v = (c + offset_v) * voxel_size + aabb0 (unit cube here) // offset_v = (1 + 2*margin) * sigmoid(feat[0:3]) - margin // intersected feat[3:6] > 0, one flag per axis (x, y, z) // split_weight softplus(feat[6]) // For each voxel with an intersected axis, the 4 voxels around that edge // (offsets below) contribute their dual vertices as a quad; if all 4 exist the // quad is split into 2 triangles along the diagonal chosen by the decoder's // learned split_weight = softplus(feat[6]) (reference eval-path tie-break). #pragma once #include #include #include #include #include #include namespace fdg { struct Mesh { std::vector verts; // 3 per vertex std::vector tris; // 3 indices per triangle size_t n_verts() const { return verts.size() / 3; } size_t n_tris() const { return tris.size() / 3; } }; namespace detail { inline uint64_t key(int32_t x, int32_t y, int32_t z) { return ((uint64_t) (uint32_t) x << 40) | ((uint64_t) (uint32_t) y << 20) | (uint64_t) (uint32_t) z; } // The 4 neighbor-voxel offsets around an edge, per axis (matches // edge_neighbor_voxel_offset in the reference). static const int EDGE_OFF[3][4][3] = { {{0,0,0},{0,0,1},{0,1,1},{0,1,0}}, // x-axis edge {{0,0,0},{1,0,0},{1,0,1},{0,0,1}}, // y-axis edge {{0,0,0},{0,1,0},{1,1,0},{1,0,0}}, // z-axis edge }; inline void cross(const float * a, const float * b, float * o) { o[0] = a[1]*b[2] - a[2]*b[1]; o[1] = a[2]*b[0] - a[0]*b[2]; o[2] = a[0]*b[1] - a[1]*b[0]; } } // namespace detail // feats: [n_voxels * 7], voxel-major (dec output). coords: [n_voxels * 3]. // grid_size = input_res * decoder upscale (e.g. 32 * 16 = 512). margin 0.5. inline Mesh extract(const float * feats, const int32_t * coords, int n, int grid_size, float margin = 0.5f) { using namespace detail; Mesh m; if (n <= 0) return m; const float vs = 1.0f / (float) grid_size; // voxel size (aabb span 1) const float aabb0 = -0.5f; // dual vertices + hashmap std::vector V((size_t) n * 3); std::unordered_map idx; idx.reserve((size_t) n * 2); for (int v = 0; v < n; ++v) { const float * f = feats + (size_t) v * 7; for (int a = 0; a < 3; ++a) { const float s = 1.0f / (1.0f + std::exp(-f[a])); // sigmoid const float off = (1.0f + 2.0f * margin) * s - margin; V[(size_t) v * 3 + a] = ((float) coords[(size_t) v * 3 + a] + off) * vs + aabb0; } idx[key(coords[(size_t) v * 3], coords[(size_t) v * 3 + 1], coords[(size_t) v * 3 + 2])] = v; } m.verts = V; // quads from intersected edges for (int v = 0; v < n; ++v) { const float * f = feats + (size_t) v * 7; const int32_t cx = coords[(size_t) v * 3]; const int32_t cy = coords[(size_t) v * 3 + 1]; const int32_t cz = coords[(size_t) v * 3 + 2]; for (int axis = 0; axis < 3; ++axis) { if (f[3 + axis] <= 0.0f) continue; // not intersected on this axis int q[4]; bool ok = true; for (int i = 0; i < 4; ++i) { const int32_t nx = cx + EDGE_OFF[axis][i][0]; const int32_t ny = cy + EDGE_OFF[axis][i][1]; const int32_t nz = cz + EDGE_OFF[axis][i][2]; auto it = idx.find(key(nx, ny, nz)); if (it == idx.end()) { ok = false; break; } q[i] = it->second; } if (!ok) continue; // Choose the quad diagonal by the decoder's learned split_weight // (softplus of feat[6]), exactly as the reference eval path does // (FlexiDualGridVaeDecoder -> flexible_dual_grid_to_mesh, train=False): // split 1: (0,1,2)+(0,2,3) when sw0*sw2 > sw1*sw3 // split 2: (0,1,3)+(3,1,2) otherwise // (A geometric best-aligned-normals heuristic is the reference's // split_weight=None fallback; the shipped decoder always emits // feat[6], so we follow the learned choice.) auto sw = [&](int i) { const float x = feats[(size_t) q[i] * 7 + 6]; return x > 20.0f ? x : std::log1p(std::exp(x)); // softplus }; if (sw(0) * sw(2) > sw(1) * sw(3)) { m.tris.push_back(q[0]); m.tris.push_back(q[1]); m.tris.push_back(q[2]); m.tris.push_back(q[0]); m.tris.push_back(q[2]); m.tris.push_back(q[3]); } else { m.tris.push_back(q[0]); m.tris.push_back(q[1]); m.tris.push_back(q[3]); m.tris.push_back(q[3]); m.tris.push_back(q[1]); m.tris.push_back(q[2]); } } } return m; } // Per-vertex shading normals for the dual grid's *unoriented*, heavily // non-manifold mesh. // // The reference mesher emits every quad with a fixed vertex order regardless of // which way the surface crosses the edge, so a large fraction of faces are wound // opposite to their neighbours. Two failure modes follow: (a) a naive // area-weighted normal cancels at those seams, and (b) any *sign* fix that leaves // stray flips is not harmless — the normal is interpolated across the triangle // *before* the fragment shader's abs(dot), so two adjacent vertices with opposite // normals make the interpolated normal cross zero mid-face → normalize() explodes // → speckled/blocky specular. The mesh is too non-manifold to 2-colour the // winding cleanly (edges shared by >2 faces frustrate it), so we don't try to. // // 1. Recover a smooth, winding-INDEPENDENT normal *direction* per vertex as the // dominant eigenvector of the area-weighted structure tensor Σ area·n̂n̂ᵀ // (immune to winding sign since n̂n̂ᵀ == (−n̂)(−n̂)ᵀ). // 2. Resolve the arbitrary per-vertex *sign* consistently with a parity // union-find over mesh edges, so edge-adjacent vertices share a hemisphere // and the interpolated normal stays clear of zero. Only genuinely frustrated // (odd-cycle / non-manifold) edges are left flipped. // Final shading is orientation-independent (viewer uses abs(dot)), so only local // smoothness matters, not a globally correct outward sign. (Winding unification // and sign diffusion were both tried and are worse on this mesh: 2-colouring the // >2-face non-manifold edges frustrates more, and Jacobi diffusion checkerboards.) inline std::vector vertex_normals(const Mesh & m) { const size_t nv = m.n_verts(); std::vector A((size_t) nv * 6, 0.0); // sym structure tensor per vertex std::vector seed((size_t) nv * 3, 0.0f); // signed area sum: a sign hint for (size_t t = 0; t < m.tris.size(); t += 3) { const int i0 = m.tris[t], i1 = m.tris[t + 1], i2 = m.tris[t + 2]; const float * a = &m.verts[(size_t) i0 * 3]; const float * b = &m.verts[(size_t) i1 * 3]; const float * c = &m.verts[(size_t) i2 * 3]; float e1[3], e2[3], fn[3]; for (int k = 0; k < 3; ++k) { e1[k] = b[k]-a[k]; e2[k] = c[k]-a[k]; } detail::cross(e1, e2, fn); const double area = std::sqrt((double) fn[0]*fn[0] + (double) fn[1]*fn[1] + (double) fn[2]*fn[2]); if (area <= 1e-20) continue; const double inv = 1.0 / area; const double xx = fn[0]*fn[0]*inv, yy = fn[1]*fn[1]*inv, zz = fn[2]*fn[2]*inv; const double xy = fn[0]*fn[1]*inv, xz = fn[0]*fn[2]*inv, yz = fn[1]*fn[2]*inv; for (int i : {i0, i1, i2}) { double * Av = &A[(size_t) i * 6]; Av[0]+=xx; Av[1]+=yy; Av[2]+=zz; Av[3]+=xy; Av[4]+=xz; Av[5]+=yz; float * sv = &seed[(size_t) i * 3]; sv[0]+=fn[0]; sv[1]+=fn[1]; sv[2]+=fn[2]; } } // (1) smooth, sign-ambiguous direction per vertex std::vector dir((size_t) nv * 3, 0.0f); for (size_t v = 0; v < nv; ++v) { const double * Av = &A[v * 6]; double x = seed[v*3], y = seed[v*3+1], z = seed[v*3+2]; double l = std::sqrt(x*x + y*y + z*z); if (l < 1e-20) { x = Av[0]; y = Av[3]; z = Av[4]; l = std::sqrt(x*x+y*y+z*z); } if (l < 1e-20) { dir[v*3+2] = 1.0f; continue; } x/=l; y/=l; z/=l; for (int it = 0; it < 8; ++it) { const double nx = Av[0]*x + Av[3]*y + Av[4]*z; const double ny = Av[3]*x + Av[1]*y + Av[5]*z; const double nz = Av[4]*x + Av[5]*y + Av[2]*z; const double nl = std::sqrt(nx*nx + ny*ny + nz*nz); if (nl < 1e-20) break; x = nx/nl; y = ny/nl; z = nz/nl; } dir[v*3] = (float) x; dir[v*3+1] = (float) y; dir[v*3+2] = (float) z; } // (2) Resolve the arbitrary per-vertex sign *consistently* via a parity // union-find over mesh edges: two edge-adjacent vertices whose directions // are anti-aligned must end up with opposite signs (and vice versa), so // within each connected component neighbours share a hemisphere. Only // genuinely frustrated (odd-cycle / non-manifold) edges are left flipped. std::vector ufp(nv), ufr(nv, 0); std::vector ufb(nv, 0); // parity of a vertex relative to its parent for (size_t i = 0; i < nv; ++i) ufp[i] = (int) i; auto find = [&](int v, int & parity) { int p = 0; while (ufp[v] != v) { p ^= ufb[v]; v = ufp[v]; } parity = p; return v; }; auto join = [&](int a, int b) { const float * na = &dir[(size_t) a * 3]; const float * nb = &dir[(size_t) b * 3]; const int rel = (na[0]*nb[0] + na[1]*nb[1] + na[2]*nb[2]) < 0.0f ? 1 : 0; int pa, pb, ra = find(a, pa), rb = find(b, pb); if (ra == rb) return; if (ufr[ra] < ufr[rb]) { std::swap(ra, rb); std::swap(pa, pb); } ufp[rb] = ra; ufb[rb] = (uint8_t) (pa ^ pb ^ rel); if (ufr[ra] == ufr[rb]) ufr[ra]++; }; for (size_t t = 0; t < m.tris.size(); t += 3) { const int a = m.tris[t], b = m.tris[t+1], c = m.tris[t+2]; join(a, b); join(b, c); join(c, a); } // (3) pick each component's global sign toward the signed-sum seed (so the // result is deterministic and roughly outward), then emit oriented normals std::unordered_map comp_sign; for (size_t v = 0; v < nv; ++v) { int p, r = find((int) v, p); const double s = p ? -1.0 : 1.0; const float * sv = &seed[v*3]; comp_sign[r] += s * (dir[v*3]*sv[0] + dir[v*3+1]*sv[1] + dir[v*3+2]*sv[2]); } std::vector nrm((size_t) nv * 3, 0.0f); for (size_t v = 0; v < nv; ++v) { int p, r = find((int) v, p); double s = p ? -1.0 : 1.0; if (comp_sign[r] < 0.0) s = -s; nrm[v*3] = (float) (s * dir[v*3]); nrm[v*3+1] = (float) (s * dir[v*3+1]); nrm[v*3+2] = (float) (s * dir[v*3+2]); } return nrm; } // Remove triangles that belong to tiny disconnected islands (the floating // specks that read as "blemishes"), keeping the vertex array and its indexing // intact so a parallel per-vertex attribute array (e.g. baked PBR) stays // aligned. Components are face groups connected through shared vertices; a // component is dropped when its face count is below min_frac of the total. inline void drop_small_components(Mesh & m, float min_frac = 0.0005f) { const size_t nt = m.n_tris(); if (nt == 0) return; const size_t nv = m.n_verts(); std::vector p(nv); for (size_t i = 0; i < nv; ++i) p[i] = (int) i; auto find = [&](int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; }; auto uni = [&](int a, int b) { a = find(a); b = find(b); if (a != b) p[a] = b; }; for (size_t t = 0; t < m.tris.size(); t += 3) { uni(m.tris[t], m.tris[t+1]); uni(m.tris[t+1], m.tris[t+2]); } std::unordered_map faces; for (size_t t = 0; t < m.tris.size(); t += 3) faces[find(m.tris[t])]++; const int min_faces = std::max(1, (int) (min_frac * (double) nt)); std::vector keep; keep.reserve(m.tris.size()); for (size_t t = 0; t < m.tris.size(); t += 3) { if (faces[find(m.tris[t])] >= min_faces) { keep.push_back(m.tris[t]); keep.push_back(m.tris[t+1]); keep.push_back(m.tris[t+2]); } } m.tris.swap(keep); } // Fill the small holes extract() leaves where a dual-grid quad was skipped (a // neighbour voxel was missing). A boundary edge is one used by exactly one // triangle; boundary edges chain into loops around each hole. Each loop up to // `max_loop` vertices is fan-triangulated from its first vertex. Only triangles // are added — no new vertices — so a parallel per-vertex attribute array (baked // PBR) stays aligned. Large loops (genuine openings) are left unfilled. inline void fill_holes(Mesh & m, int max_loop = 64, int max_passes = 4) { if (m.n_tris() == 0) return; auto key = [](int a, int b) { const uint32_t lo = a < b ? a : b, hi = a < b ? b : a; return ((uint64_t) lo << 32) | hi; }; // Iterate: the greedy loop walk misses some loops at non-manifold junctions, // and each fill can expose newly closeable loops, so repeat until a pass // adds nothing (or the pass cap is hit). for (int pass = 0; pass < max_passes; ++pass) { const size_t before = m.tris.size(); // run-length count of undirected edges via sort (lighter than a hashmap) std::vector ek; ek.reserve(m.tris.size()); for (size_t t = 0; t < m.tris.size(); t += 3) { ek.push_back(key(m.tris[t], m.tris[t+1])); ek.push_back(key(m.tris[t+1], m.tris[t+2])); ek.push_back(key(m.tris[t+2], m.tris[t])); } std::sort(ek.begin(), ek.end()); std::unordered_map> adj; // boundary-vertex adjacency for (size_t i = 0; i < ek.size(); ) { size_t j = i + 1; while (j < ek.size() && ek[j] == ek[i]) ++j; if (j - i == 1) { // used by exactly one triangle -> boundary edge const int a = (int) (ek[i] >> 32), b = (int) (ek[i] & 0xffffffffu); adj[a].push_back(b); adj[b].push_back(a); } i = j; } if (adj.empty()) break; std::unordered_set used; // consumed boundary edges for (const auto & kv : adj) { const int start = kv.first; for (const int nb0 : kv.second) { if (used.count(key(start, nb0))) continue; std::vector loop{start}; int prev = start, cur = nb0; used.insert(key(prev, cur)); bool closed = false; while ((int) loop.size() <= max_loop) { loop.push_back(cur); if (cur == start) { closed = true; break; } int next = -1; auto it = adj.find(cur); if (it != adj.end()) for (const int c : it->second) if (c != prev && !used.count(key(cur, c))) { next = c; break; } if (next < 0) break; // open chain / dead end used.insert(key(cur, next)); prev = cur; cur = next; } if (!closed) continue; loop.pop_back(); // drop the repeated start vertex const int k = (int) loop.size(); if (k < 3 || k > max_loop) continue; for (int t = 1; t < k - 1; ++t) { // fan-triangulate from loop[0] m.tris.push_back(loop[0]); m.tris.push_back(loop[t]); m.tris.push_back(loop[t + 1]); } } } if (m.tris.size() == before) break; // converged } } } // namespace fdg