Initial release

This commit is contained in:
civ
2026-08-16 18:33:03 +07:00
commit 7ade4e1152
1966 changed files with 412966 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
# Example: inspect a .dinodata DINOv3 conditioning file.
add_executable(dino_info dino_info.cpp)
target_link_libraries(dino_info PRIVATE trellis2)
target_compile_features(dino_info PRIVATE cxx_std_14)
# Example: inspect a converted SS-flow DiT GGUF (hparams + tensor inventory).
add_executable(ss_flow_info ss_flow_info.cpp)
target_link_libraries(ss_flow_info PRIVATE trellis2)
target_compile_features(ss_flow_info PRIVATE cxx_std_14)
# Example: stage-1 sampling, .dinodata cond -> sparse-structure latent z_s.
add_executable(ss_sample ss_sample.cpp)
target_link_libraries(ss_sample PRIVATE trellis2)
target_compile_features(ss_sample PRIVATE cxx_std_14)
# Example: stage-1 decoding, z_s latent -> occupancy logit grid (64^3).
add_executable(ss_decode ss_decode.cpp)
target_link_libraries(ss_decode PRIVATE trellis2)
target_compile_features(ss_decode PRIVATE cxx_std_14)
# Example: decode + extract the occupancy isosurface as an OBJ (marching cubes).
add_executable(ss_mesh ss_mesh.cpp)
target_link_libraries(ss_mesh PRIVATE trellis2)
target_compile_features(ss_mesh PRIVATE cxx_std_14)
# Example: image -> preprocessed 512x512 -> DINOv3 conditioning (.dinodata).
add_executable(dino_encode dino_encode.cpp)
target_link_libraries(dino_encode PRIVATE trellis2)
target_compile_features(dino_encode PRIVATE cxx_std_14)
# Example: export a demo mesh (T2MESH01/02/03) into a portable GLB (no GPU).
add_executable(mesh2glb mesh2glb.cpp)
target_link_libraries(mesh2glb PRIVATE trellis2)
target_compile_features(mesh2glb PRIVATE cxx_std_14)
+97
View File
@@ -0,0 +1,97 @@
// Encode an image into the TRELLIS.2 DINOv3 conditioning tensor (.dinodata),
// replacing the external dump_dinodata.py:
//
// image (PNG/JPG; solid black/white backgrounds are removed automatically)
// -> background cleanup -> alpha bbox crop, premultiply, LANCZOS 512
// -> DINOv3 ViT-L/16 -> [1, 1029, 1024] cond -> .dinodata
//
// usage: dino_encode <dino.gguf> <image> [out.dinodata] [--size N] [--pre out.png]
//
#include "trellis2.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
int main(int argc, char ** argv) {
if (argc < 3) {
std::fprintf(stderr,
"usage: %s <dino.gguf> <image> [out.dinodata] [--size N] [--pre out.png]\n",
argv[0]);
return 2;
}
const std::string gguf_path = argv[1];
const std::string img_path = argv[2];
std::string out_path = "cond.dinodata";
std::string pre_path;
int size = 512;
for (int i = 3; i < argc; ++i) {
if (std::strcmp(argv[i], "--size") == 0 && i + 1 < argc) {
size = std::atoi(argv[++i]);
} else if (std::strcmp(argv[i], "--pre") == 0 && i + 1 < argc) {
pre_path = argv[++i];
} else {
out_path = argv[i];
}
}
int w = 0, h = 0, comp = 0;
unsigned char * pixels = stbi_load(img_path.c_str(), &w, &h, &comp, 4);
if (!pixels) {
std::fprintf(stderr, "failed to decode image %s: %s\n",
img_path.c_str(), stbi_failure_reason());
return 1;
}
const int removed = trellis2_remove_solid_background_rgba(
pixels, w, h, TRELLIS2_BACKGROUND_AUTO);
std::printf("image : %s %dx%d (%d channels, background pixels changed: %d)\n",
img_path.c_str(), w, h, comp, removed);
std::string err;
std::vector<uint8_t> rgb;
if (!trellis2_preprocess_rgba(pixels, w, h, size, rgb, &err)) {
std::fprintf(stderr, "preprocess failed: %s\n", err.c_str());
stbi_image_free(pixels);
return 1;
}
stbi_image_free(pixels);
if (!pre_path.empty()) {
stbi_write_png(pre_path.c_str(), size, size, 3, rgb.data(), size * 3);
std::printf("wrote : %s (preprocessed %dx%d RGB)\n", pre_path.c_str(), size, size);
}
trellis2_dino_model * model = trellis2_dino_load(gguf_path, true, &err);
if (!model) {
std::fprintf(stderr, "model load failed: %s\n", err.c_str());
return 1;
}
std::printf("model : %s (backend %s)\n", gguf_path.c_str(),
trellis2_dino_backend_name(model));
trellis2_dino_cond cond;
if (!trellis2_dino_encode_rgb(model, rgb.data(), size, cond, &err)) {
std::fprintf(stderr, "encode failed: %s\n", err.c_str());
trellis2_dino_free(model);
return 1;
}
trellis2_dino_free(model);
const trellis2_dino_fingerprint fp = trellis2_dino_fingerprints(cond);
std::printf("cond : [1, %lld, %lld] min=%.4f max=%.4f mean=%.6f l2=%.4f\n",
(long long) cond.tokens(), (long long) cond.channels(),
fp.vmin, fp.vmax, fp.mean, fp.l2);
if (!trellis2_save_dinodata(out_path, cond, &err)) {
std::fprintf(stderr, "save failed: %s\n", err.c_str());
return 1;
}
std::printf("wrote : %s (%zu floats)\n", out_path.c_str(), cond.count());
return 0;
}
+54
View File
@@ -0,0 +1,54 @@
// dino_info — load a .dinodata DINOv3 conditioning file and print its shape,
// token breakdown, and fingerprints. The fingerprints should match the values
// in the matching `<stem>.dino.txt` JSON sidecar bit-for-bit.
//
// usage: dino_info <path-to.dinodata>
#include "trellis2.h"
#include <cstdio>
#include <string>
int main(int argc, char ** argv) {
if (argc < 2) {
std::fprintf(stderr, "usage: %s <path-to.dinodata>\n", argv[0]);
return 2;
}
std::printf("trellis2.cpp %s\n", trellis2_version());
const std::string path = argv[1];
trellis2_dino_cond cond;
std::string err;
if (!trellis2_load_dinodata(path, cond, &err)) {
std::fprintf(stderr, "error: %s\n", err.c_str());
return 1;
}
std::printf("file : %s\n", path.c_str());
std::printf("format version : %u\n", cond.format_version);
std::printf("shape : [");
for (size_t i = 0; i < cond.shape.size(); ++i) {
std::printf("%lld%s", (long long) cond.shape[i],
i + 1 < cond.shape.size() ? ", " : "");
}
std::printf("]\n");
const long long tok = (long long) cond.tokens();
std::printf("tokens : %lld (cls:1 + register:4 + patch:%lld)\n",
tok, tok >= 5 ? tok - 5 : 0);
std::printf("channels : %lld\n", (long long) cond.channels());
std::printf("count : %zu floats\n", cond.count());
const trellis2_dino_fingerprint fp = trellis2_dino_fingerprints(cond);
std::printf("fingerprints:\n");
std::printf(" min : %.6f\n", fp.vmin);
std::printf(" max : %.6f\n", fp.vmax);
std::printf(" mean : %.9f\n", fp.mean);
std::printf(" sum : %.6f\n", fp.sum);
std::printf(" l2 : %.6f\n", fp.l2);
std::printf(" count : %zu\n", fp.count);
return 0;
}
+345
View File
@@ -0,0 +1,345 @@
// 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 <algorithm>
#include <cmath>
#include <cstdint>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace fdg {
struct Mesh {
std::vector<float> verts; // 3 per vertex
std::vector<int> 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<float> V((size_t) n * 3);
std::unordered_map<uint64_t, int> 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<float> vertex_normals(const Mesh & m) {
const size_t nv = m.n_verts();
std::vector<double> A((size_t) nv * 6, 0.0); // sym structure tensor per vertex
std::vector<float> 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<float> 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<int> ufp(nv), ufr(nv, 0);
std::vector<uint8_t> 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<int, double> 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<float> 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<int> 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<int, int> 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<int> 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<uint64_t> 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<int, std::vector<int>> 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<uint64_t> 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<int> 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
+243
View File
@@ -0,0 +1,243 @@
// marching_cubes.h — single-file, self-contained isosurface extraction.
//
// Extracts a triangle mesh for the level set {f = iso} of a scalar field
// sampled on a regular grid, and writes it as a Wavefront OBJ.
//
// It uses the TETRAHEDRAL variant of marching cubes (a.k.a. marching
// tetrahedra on the Freudenthal/Kuhn subdivision): each grid cube is split
// into 6 tetrahedra that all share the (0,0,0)-(1,1,1) main diagonal, and each
// tetrahedron is contoured with a tiny 16-case table. Compared to classic
// marching cubes this avoids the error-prone 256-row triangle table entirely,
// and — because every cube uses the same diagonal — the subdivision tiles space
// consistently, so the output is a watertight 2-manifold (each interior edge is
// shared by exactly two triangles). Vertices are de-duplicated by the global
// grid edge they sit on, so the mesh is properly indexed.
//
// Header-only and dependency-free (just <vector>/<cmath>/<unordered_map>).
//
// mc::Mesh m = mc::extract(field, nx, ny, nz, iso);
// mc::write_obj(m, "out.obj");
//
// `field` is indexed x + y*nx + z*nx*ny (x fastest). Vertex positions are in
// grid-index units (a voxel is one unit). The field is treated as `pad` (a
// large negative value, i.e. "outside") beyond the grid, so surfaces that reach
// the boundary are closed off.
#pragma once
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <unordered_map>
#include <vector>
namespace mc {
struct Mesh {
std::vector<float> verts; // 3 floats per vertex (x,y,z)
std::vector<float> normals; // 3 floats per vertex (unit, oriented outward)
std::vector<int> tris; // 3 vertex indices per triangle
size_t n_verts() const { return verts.size() / 3; }
size_t n_tris() const { return tris.size() / 3; }
};
namespace detail {
// Cube corner offsets, Bourke/Lorensen order.
static const int CORNER[8][3] = {
{0,0,0},{1,0,0},{1,1,0},{0,1,0},{0,0,1},{1,0,1},{1,1,1},{0,1,1}
};
// The 6 tetrahedra of the Freudenthal subdivision, all sharing the 0-6 diagonal.
static const int TETRA[6][4] = {
{0,1,2,6},{0,2,3,6},{0,3,7,6},{0,7,4,6},{0,4,5,6},{0,5,1,6}
};
// A tetrahedron's 6 edges, as (local-corner, local-corner) pairs.
static const int TET_EDGE[6][2] = {
{0,1},{1,2},{2,0},{0,3},{1,3},{2,3}
};
// Per-case triangle table for one tetrahedron. Index = bitmask of which of the
// 4 local corners are "inside" (value > iso); values are TET_EDGE indices,
// terminated by -1. Derived by hand (see marching_cubes.h history) and checked
// at runtime by extract()'s self-consistency asserts on small fields.
static const int TET_TRI[16][7] = {
{-1,-1,-1,-1,-1,-1,-1}, // 0000
{ 0, 2, 3,-1,-1,-1,-1}, // 0001 {0}
{ 0, 1, 4,-1,-1,-1,-1}, // 0010 {1}
{ 2, 1, 4, 2, 4, 3,-1}, // 0011 {0,1}
{ 2, 1, 5,-1,-1,-1,-1}, // 0100 {2}
{ 0, 1, 5, 0, 5, 3,-1}, // 0101 {0,2}
{ 0, 2, 5, 0, 5, 4,-1}, // 0110 {1,2}
{ 3, 4, 5,-1,-1,-1,-1}, // 0111 {0,1,2} (outside 3)
{ 3, 4, 5,-1,-1,-1,-1}, // 1000 {3}
{ 0, 4, 5, 0, 5, 2,-1}, // 1001 {0,3}
{ 0, 3, 5, 0, 5, 1,-1}, // 1010 {1,3}
{ 2, 1, 5,-1,-1,-1,-1}, // 1011 {0,1,3} (outside 2)
{ 2, 3, 4, 2, 4, 1,-1}, // 1100 {2,3}
{ 0, 1, 4,-1,-1,-1,-1}, // 1101 {0,2,3} (outside 1)
{ 0, 2, 3,-1,-1,-1,-1}, // 1110 {1,2,3} (outside 0)
{-1,-1,-1,-1,-1,-1,-1}, // 1111
};
} // namespace detail
inline Mesh extract(const float * field, int nx, int ny, int nz, float iso,
float pad = -1e30f) {
using namespace detail;
Mesh mesh;
// Tie-breaking jitter (Simulation-of-Simplicity style): a corner whose value
// is *exactly* iso makes the surface pass through that single point, which is
// shared by every incident tetrahedron — a pinch that no winding can make
// coherent. A tiny deterministic per-corner perturbation (consistent across
// cubes, so shared corners still agree -> mesh stays watertight) pushes every
// corner decisively on/off the surface. Scaled to the field so vertex motion
// is sub-micro-voxel; continuous fields (e.g. decoder logits) are unaffected.
double fmax = 0.0;
for (size_t i = 0; i < (size_t) nx * ny * nz; ++i) {
double a = std::fabs((double) field[i]); if (a > fmax) fmax = a;
}
const float eps = (float) (1e-6 * fmax) + 1e-20f;
auto jitter = [](int x, int y, int z) -> float {
uint32_t h = (uint32_t)(x * 73856093) ^ (uint32_t)(y * 19349663) ^ (uint32_t)(z * 83492791);
h ^= h >> 13; h *= 0x5bd1e995u; h ^= h >> 15;
return (float)(h & 0xffff) / 65535.0f - 0.5f; // [-0.5, 0.5)
};
auto at = [&](int x, int y, int z) -> float {
if (x < 0 || y < 0 || z < 0 || x >= nx || y >= ny || z >= nz) return pad;
return field[(size_t) x + (size_t) y * nx + (size_t) z * nx * ny] + eps * jitter(x, y, z);
};
// Central-difference gradient (for outward-oriented vertex normals).
auto grad = [&](int x, int y, int z, float g[3]) {
g[0] = at(x + 1, y, z) - at(x - 1, y, z);
g[1] = at(x, y + 1, z) - at(x, y - 1, z);
g[2] = at(x, y, z + 1) - at(x, y, z - 1);
};
std::unordered_map<int64_t, int> vmap; // quantized-position key -> vertex index
// Interpolate (and cache) the vertex on the grid edge cA->cB. Vertices are
// de-duplicated by QUANTIZED POSITION, not by edge: against the large pad,
// boundary-cap vertices interpolate to t~0 and collapse exactly onto grid
// corners, so several distinct edges land on the same point. Position keying
// welds those into one vertex (killing the degenerate zero-area cap triangles
// and keeping the boundary watertight), while shared edges across cubes —
// which already yield identical positions — still merge as before.
const double QS = 1024.0; // sub-milli-voxel quantization
auto edge_vertex = [&](const int ca[3], const int cb[3], float va, float vb) -> int {
float t = (vb != va) ? (iso - va) / (vb - va) : 0.5f;
if (t < 0.0f) t = 0.0f; else if (t > 1.0f) t = 1.0f;
float p[3] = { ca[0] + t * (cb[0] - ca[0]),
ca[1] + t * (cb[1] - ca[1]),
ca[2] + t * (cb[2] - ca[2]) };
int64_t qx = (int64_t) std::llround(p[0] * QS) + 0x200000;
int64_t qy = (int64_t) std::llround(p[1] * QS) + 0x200000;
int64_t qz = (int64_t) std::llround(p[2] * QS) + 0x200000;
int64_t key = (qx * 0x400000LL + qy) * 0x400000LL + qz;
auto it = vmap.find(key);
if (it != vmap.end()) return it->second;
float ga[3], gb[3];
grad(ca[0], ca[1], ca[2], ga);
grad(cb[0], cb[1], cb[2], gb);
float n[3];
for (int d = 0; d < 3; ++d) n[d] = -(ga[d] + t * (gb[d] - ga[d])); // outward = -grad
// Normalize in double: against the large negative pad the gradient can
// reach ~1e30, and 1e30^2 overflows float32 to inf (-> bogus normal).
double len = std::sqrt((double)n[0]*n[0] + (double)n[1]*n[1] + (double)n[2]*n[2]);
if (len > 1e-12) { n[0]=(float)(n[0]/len); n[1]=(float)(n[1]/len); n[2]=(float)(n[2]/len); }
else { n[0]=0; n[1]=0; n[2]=1; }
int idx = (int) mesh.n_verts();
mesh.verts.insert(mesh.verts.end(), { p[0], p[1], p[2] });
mesh.normals.insert(mesh.normals.end(), { n[0], n[1], n[2] });
vmap.emplace(key, idx);
return idx;
};
// Iterate cube origins over [-1 .. n-1] so boundary cubes (against the pad)
// close the surface.
for (int z = -1; z < nz; ++z)
for (int y = -1; y < ny; ++y)
for (int x = -1; x < nx; ++x) {
float cval[8];
int ccoord[8][3];
for (int c = 0; c < 8; ++c) {
ccoord[c][0] = x + CORNER[c][0];
ccoord[c][1] = y + CORNER[c][1];
ccoord[c][2] = z + CORNER[c][2];
cval[c] = at(ccoord[c][0], ccoord[c][1], ccoord[c][2]);
}
for (int t = 0; t < 6; ++t) {
const int * tc = TETRA[t];
int code = 0;
for (int i = 0; i < 4; ++i) if (cval[tc[i]] > iso) code |= (1 << i);
const int * tri = TET_TRI[code];
if (tri[0] < 0) continue;
// Gather this tetra's triangles (1 or 2), then decide their winding
// ONCE, together: flip so the summed geometric normal (snorm) agrees
// with the summed outward vertex normal (gsum = sum of -grad over the
// triangle vertices). gsum is PARALLEL to the surface normal, so the
// dot is never near zero — unlike a centroid-difference proxy, which
// can be in-plane at boundary/crease tetra and pick a random sign.
// The surface inside a tetra is planar, so a quad's two triangles are
// coplanar and flip as a unit (a per-triangle test would mis-flip a
// sliver triangle, whose own geometric normal is ~0).
int tribuf[2][3]; int ntri = 0;
double snorm[3] = {0, 0, 0}, gsum[3] = {0, 0, 0};
for (int e = 0; tri[e] >= 0; e += 3) {
int vi[3];
for (int k = 0; k < 3; ++k) {
const int * ed = TET_EDGE[tri[e + k]];
int la = tc[ed[0]], lb = tc[ed[1]];
vi[k] = edge_vertex(ccoord[la], ccoord[lb], cval[la], cval[lb]);
}
if (vi[0] == vi[1] || vi[1] == vi[2] || vi[0] == vi[2]) continue; // degenerate
const float * P0 = &mesh.verts[3*vi[0]];
const float * P1 = &mesh.verts[3*vi[1]];
const float * P2 = &mesh.verts[3*vi[2]];
float u[3] = { P1[0]-P0[0], P1[1]-P0[1], P1[2]-P0[2] };
float v[3] = { P2[0]-P0[0], P2[1]-P0[1], P2[2]-P0[2] };
snorm[0] += u[1]*v[2]-u[2]*v[1];
snorm[1] += u[2]*v[0]-u[0]*v[2];
snorm[2] += u[0]*v[1]-u[1]*v[0];
for (int k = 0; k < 3; ++k) {
gsum[0] += mesh.normals[3*vi[k]];
gsum[1] += mesh.normals[3*vi[k]+1];
gsum[2] += mesh.normals[3*vi[k]+2];
}
tribuf[ntri][0] = vi[0]; tribuf[ntri][1] = vi[1]; tribuf[ntri][2] = vi[2];
++ntri;
}
bool flip = (snorm[0]*gsum[0] + snorm[1]*gsum[1] + snorm[2]*gsum[2]) < 0.0;
for (int a = 0; a < ntri; ++a) {
int v0 = tribuf[a][0], v1 = tribuf[a][1], v2 = tribuf[a][2];
if (flip) { int tmp = v1; v1 = v2; v2 = tmp; }
mesh.tris.insert(mesh.tris.end(), { v0, v1, v2 });
}
}
}
return mesh;
}
inline bool write_obj(const Mesh & m, const char * path) {
FILE * f = std::fopen(path, "wb");
if (!f) return false;
std::fprintf(f, "# trellis2.cpp isosurface (%zu verts, %zu tris)\n",
m.n_verts(), m.n_tris());
for (size_t i = 0; i < m.n_verts(); ++i)
std::fprintf(f, "v %.6f %.6f %.6f\n", m.verts[3*i], m.verts[3*i+1], m.verts[3*i+2]);
for (size_t i = 0; i < m.n_verts(); ++i)
std::fprintf(f, "vn %.6f %.6f %.6f\n", m.normals[3*i], m.normals[3*i+1], m.normals[3*i+2]);
for (size_t i = 0; i < m.n_tris(); ++i) {
int a = m.tris[3*i] + 1, b = m.tris[3*i+1] + 1, c = m.tris[3*i+2] + 1;
std::fprintf(f, "f %d//%d %d//%d %d//%d\n", a, a, b, b, c, c);
}
std::fclose(f);
return true;
}
} // namespace mc
+130
View File
@@ -0,0 +1,130 @@
// mesh2glb — export a demo mesh (T2MESH01/T2MESH02/T2MESH03 wire format) to a
// portable vertex-coloured GLB. Exercises the CUDA-free component cleanup and
// glTF path offline, with no models or GPU. T2GLB_XATLAS opts into image baking.
//
// mesh2glb in.bin out.glb [texture_size] [--print [alpha_pct offset_pct]]
//
// The wire format is what the demo server emits at /api/mesh/{id}:
// magic[8] u32 nv u32 nt f32[3nv] verts f32[3nv] normals
// [T2MESH02: f32[5nv] legacy pbr]
// [T2MESH03: f32[6nv] pbr incl. alpha] i32[3nt] tris (little-endian)
#include "mesh_export.h"
#include <cstdint>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <vector>
template <class T>
static bool rd(FILE * f, std::vector<T> & v, size_t n) {
v.resize(n);
return n == 0 || std::fread(v.data(), sizeof(T), n, f) == n;
}
int main(int argc, char ** argv) {
if (argc < 3) {
std::fprintf(stderr,
"usage: %s in.bin out.glb [texture_size] [--print [alpha_pct offset_pct]]\n",
argv[0]);
return 2;
}
t2glb::MeshExportOptions opt;
opt.components = t2glb::ComponentFilter::KeepAll;
bool print_wrap = false;
float alpha_ratio = 0.01f, offset_ratio = 0.01f / 30.0f;
for (int i = 3; i < argc; ++i) {
if (std::strcmp(argv[i], "--print") == 0) {
print_wrap = true;
if (i + 1 < argc && argv[i+1][0] != '-') alpha_ratio = std::atof(argv[++i]) / 100.0f;
if (i + 1 < argc && argv[i+1][0] != '-') offset_ratio = std::atof(argv[++i]) / 100.0f;
} else {
opt.texture_size = std::atoi(argv[i]);
}
}
if (print_wrap && !t2glb::print_remesh_available()) {
std::fprintf(stderr, "print remeshing unavailable: rebuild with CGAL 5.5 or newer\n");
return 1;
}
FILE * f = std::fopen(argv[1], "rb");
if (!f) { std::fprintf(stderr, "open %s failed\n", argv[1]); return 1; }
char magic[9] = {0};
uint32_t nv = 0, nt = 0;
if (std::fread(magic, 1, 8, f) != 8 || std::fread(&nv, 4, 1, f) != 1 || std::fread(&nt, 4, 1, f) != 1) {
std::fprintf(stderr, "bad header\n"); return 1;
}
const bool legacy = std::memcmp(magic, "T2MESH02", 8) == 0;
const bool textured = legacy || std::memcmp(magic, "T2MESH03", 8) == 0;
if (!textured && std::memcmp(magic, "T2MESH01", 8) != 0) {
std::fprintf(stderr, "unknown magic\n"); return 1;
}
std::vector<float> verts, normals, pbr; std::vector<int32_t> tris;
bool ok = rd(f, verts, (size_t) nv * 3) && rd(f, normals, (size_t) nv * 3);
if (ok && textured) {
if (legacy) {
std::vector<float> old;
ok = rd(f, old, (size_t) nv * 5);
if (ok) {
pbr.resize((size_t) nv * 6);
for (uint32_t i = 0; i < nv; ++i) {
std::memcpy(pbr.data() + (size_t) i * 6,
old.data() + (size_t) i * 5, 5 * sizeof(float));
pbr[(size_t) i * 6 + 5] = 1.0f;
}
}
} else {
ok = rd(f, pbr, (size_t) nv * 6);
}
}
ok = ok && rd(f, tris, (size_t) nt * 3);
std::fclose(f);
if (!ok) { std::fprintf(stderr, "truncated mesh\n"); return 1; }
std::fprintf(stderr, "in: %s %u verts %u tris %s\n", magic, nv, nt,
textured ? "textured" : "geometry-only");
std::fprintf(stderr, "export: %s ...\n",
print_wrap ? "constructing watertight CGAL Alpha Wrap" : "preserving input topology");
std::vector<uint8_t> glb; std::string err;
t2glb::PreparedMesh wrapped;
const float * export_verts = verts.data();
const int32_t * export_tris = tris.data();
const float * export_pbr = textured ? pbr.data() : nullptr;
int export_nv = (int) nv, export_nt = (int) nt;
if (print_wrap) {
if (!t2glb::prepare_print_mesh(verts.data(), (int) nv, tris.data(), (int) nt,
export_pbr, opt, alpha_ratio, offset_ratio,
wrapped, err)) {
std::fprintf(stderr, "prepare_print_mesh: %s\n", err.c_str());
return 1;
}
export_verts = wrapped.verts.data(); export_nv = (int) wrapped.verts.size() / 3;
export_tris = wrapped.tris.data(); export_nt = (int) wrapped.tris.size() / 3;
export_pbr = nullptr;
opt.components = t2glb::ComponentFilter::KeepAll;
std::fprintf(stderr, "wrap: %d verts %d tris %s\n", export_nv, export_nt,
textured ? "rebaking source PBR atlas" : "geometry-only");
}
const bool projected = print_wrap && textured;
const bool baked = projected
? t2glb::mesh_to_projected_glb(
export_verts, export_nv, export_tris, export_nt,
verts.data(), (int) nv, tris.data(), (int) nt, pbr.data(),
opt, glb, err)
: t2glb::mesh_to_glb(export_verts, export_nv, export_tris, export_nt,
export_pbr, opt, glb, err);
if (!baked) {
std::fprintf(stderr, "%s: %s\n",
projected ? "mesh_to_projected_glb" : "mesh_to_glb", err.c_str());
return 1;
}
FILE * o = std::fopen(argv[2], "wb");
if (!o) { std::fprintf(stderr, "open %s failed\n", argv[2]); return 1; }
std::fwrite(glb.data(), 1, glb.size(), o);
std::fclose(o);
std::fprintf(stderr, "wrote %s (%.2f MB)\n", argv[2], glb.size() / 1048576.0);
return 0;
}
+75
View File
@@ -0,0 +1,75 @@
// ss_decode — run the stage-1 SS decoder: a sparse-structure latent z_s
// (.latent from ss_sample) + the SS decoder GGUF -> an occupancy logit grid.
// The coarse voxel scaffold is logit > 0.
//
// usage: ss_decode <ss_dec.gguf> <z_s.latent> [out.occ]
//
// Writes the occupancy logit grid (channel-major [out_channels * R_out^3]
// float32) to out.occ if given. Also prints occupancy stats and, if all output
// channels are 1, the count of occupied voxels.
#include "trellis2.h"
#include <cstdio>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
int main(int argc, char ** argv) {
if (argc < 3) {
std::fprintf(stderr, "usage: %s <ss_dec.gguf> <z_s.latent> [out.occ]\n", argv[0]);
return 2;
}
const std::string gguf_path = argv[1];
const std::string lat_path = argv[2];
const std::string out_path = argc > 3 ? argv[3] : "";
std::printf("trellis2.cpp %s\n", trellis2_version());
std::string err;
trellis2_ss_dec_model * m = trellis2_ss_dec_load(gguf_path, true, &err);
if (!m) { std::fprintf(stderr, "model load error: %s\n", err.c_str()); return 1; }
std::printf("backend: %s\n", trellis2_ss_dec_backend_name(m));
const trellis2_ss_dec_hparams hp = trellis2_ss_dec_hparams_of(m); // copy (used after free)
const int Rin = hp.res_in();
const int Rout = hp.res_out();
const size_t n_in = (size_t) hp.latent_channels * Rin * Rin * Rin;
const size_t n_out = (size_t) hp.out_channels * Rout * Rout * Rout;
// load latent
std::ifstream f(lat_path, std::ios::binary);
if (!f) { std::fprintf(stderr, "cannot open latent %s\n", lat_path.c_str()); trellis2_ss_dec_free(m); return 1; }
std::vector<float> latent(n_in);
f.read(reinterpret_cast<char *>(latent.data()), (std::streamsize) (n_in * sizeof(float)));
if ((size_t) f.gcount() != n_in * sizeof(float)) {
std::fprintf(stderr, "latent size mismatch: got %zu floats, want %zu\n",
(size_t) f.gcount() / sizeof(float), n_in);
trellis2_ss_dec_free(m);
return 1;
}
std::printf("latent : [%d,%d,%d,%d]\n", hp.latent_channels, Rin, Rin, Rin);
std::vector<float> occ(n_out, 0.0f);
if (!trellis2_ss_dec_decode(m, latent.data(), occ.data(), &err)) {
std::fprintf(stderr, "decode error: %s\n", err.c_str());
trellis2_ss_dec_free(m);
return 1;
}
trellis2_ss_dec_free(m);
double mn = 1e30, mx = -1e30, sum = 0.0;
size_t occupied = 0;
for (float v : occ) { mn = v < mn ? v : mn; mx = v > mx ? v : mx; sum += v; if (v > 0.0f) ++occupied; }
std::printf("logits : [%d,%d,%d,%d] min=%.4f max=%.4f mean=%.5f occupied(>0)=%zu/%zu (%.2f%%)\n",
hp.out_channels, Rout, Rout, Rout, mn, mx, sum / (double) n_out,
occupied, n_out, 100.0 * (double) occupied / (double) n_out);
if (!out_path.empty()) {
std::ofstream o(out_path, std::ios::binary);
o.write(reinterpret_cast<const char *>(occ.data()), (std::streamsize) (n_out * sizeof(float)));
std::printf("wrote %s (%zu floats)\n", out_path.c_str(), n_out);
}
return 0;
}
+94
View File
@@ -0,0 +1,94 @@
// ss_flow_info — load a converted SS-flow DiT GGUF and print its hyperparameters
// and tensor inventory. Validates that convert_ss_flow_to_gguf.py produced a file
// ggml can read, and that every weight is reachable by name.
//
// usage: ss_flow_info <path-to.gguf> [--load]
//
// By default only metadata is parsed (fast). Pass --load to also read all weight
// data into host memory (~2.6 GB for the f16 checkpoint).
#include "trellis2.h"
#include <cstdio>
#include <cstring>
#include <string>
int main(int argc, char ** argv) {
if (argc < 2) {
std::fprintf(stderr, "usage: %s <path-to.gguf> [--load]\n", argv[0]);
return 2;
}
const std::string path = argv[1];
const bool load_tensors = (argc > 2 && std::strcmp(argv[2], "--load") == 0);
std::printf("trellis2.cpp %s\n", trellis2_version());
std::string err;
trellis2_ss_flow_model * m = trellis2_ss_flow_load(path, load_tensors, &err);
if (!m) {
std::fprintf(stderr, "error: %s\n", err.c_str());
return 1;
}
const trellis2_ss_flow_hparams & hp = trellis2_ss_flow_hparams_of(m);
std::printf("file : %s (%s)\n", path.c_str(),
load_tensors ? "weights loaded" : "metadata only");
if (load_tensors) {
std::printf("backend : %s\n", trellis2_ss_flow_backend_name(m));
}
std::printf("hyperparameters:\n");
std::printf(" resolution : %d (grid %d^3 = %d tokens)\n",
hp.resolution, hp.resolution, hp.resolution * hp.resolution * hp.resolution);
std::printf(" in/out channels : %d / %d\n", hp.in_channels, hp.out_channels);
std::printf(" model_channels : %d\n", hp.model_channels);
std::printf(" cond_channels : %d\n", hp.cond_channels);
std::printf(" num_blocks : %d\n", hp.num_blocks);
std::printf(" num_heads : %d (head_dim %d)\n", hp.num_heads, hp.head_dim());
std::printf(" mlp_ratio : %.4f\n", hp.mlp_ratio);
std::printf(" pe_mode : %s (freq %.1f..%.1f)\n",
hp.pe_mode, hp.rope_freq_min, hp.rope_freq_base);
std::printf(" share_mod : %s\n", hp.share_mod ? "true" : "false");
std::printf(" qk_rms_norm : self=%s cross=%s\n",
hp.qk_rms_norm ? "true" : "false",
hp.qk_rms_norm_cross ? "true" : "false");
std::printf(" file_type : %d (0=f32,1=f16,2=bf16)\n", hp.file_type);
const int n = trellis2_ss_flow_n_tensors(m);
std::printf("tensors : %d\n", n);
// Print the global (non-block) tensors in full, then summarize block 0.
std::printf(" -- global + block 0 (sample) --\n");
size_t total_bytes = 0;
int shown = 0;
for (int i = 0; i < n; ++i) {
trellis2_tensor_info ti;
if (!trellis2_ss_flow_get_tensor_info(m, i, ti)) continue;
total_bytes += ti.n_bytes;
const bool is_global = ti.name.rfind("blocks.", 0) != 0;
const bool is_block0 = ti.name.rfind("blocks.0.", 0) == 0;
if ((is_global || is_block0) && shown < 40) {
std::printf(" %-40s %-4s [", ti.name.c_str(), ti.type_name.c_str());
for (int d = ti.n_dims - 1; d >= 0; --d) {
std::printf("%lld%s", (long long) ti.ne[d], d ? ", " : "");
}
std::printf("]\n");
++shown;
}
}
std::printf("total weight bytes: %.2f MB\n", total_bytes / (1024.0 * 1024.0));
// Spot-check a couple of expected names.
const char * probes[] = {
"input_layer.weight", "adaLN_modulation.1.weight",
"blocks.0.self_attn.to_qkv.weight", "blocks.29.cross_attn.to_kv.weight",
"out_layer.weight",
};
std::printf("name probes:\n");
for (const char * p : probes) {
std::printf(" %-40s %s\n", p,
trellis2_ss_flow_has_tensor(m, p) ? "present" : "MISSING");
}
trellis2_ss_flow_free(m);
return 0;
}
+95
View File
@@ -0,0 +1,95 @@
// ss_mesh — run the stage-1 SS decoder and export the occupancy isosurface as
// a Wavefront OBJ via (tetrahedral) marching cubes.
//
// usage: ss_mesh <ss_dec.gguf> <z_s.latent> [out.obj] [--iso V] [--normalize]
//
// Decodes z_s -> a 64^3 occupancy logit grid, extracts the {logit = iso} surface
// (default iso 0, i.e. the occupancy boundary), and writes it as out.obj
// (default ss_mesh.obj). With --normalize, vertices are mapped from grid-index
// units into the centered unit cube [-0.5, 0.5]^3.
#include "trellis2.h"
#include "marching_cubes.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
int main(int argc, char ** argv) {
if (argc < 3) {
std::fprintf(stderr, "usage: %s <ss_dec.gguf> <z_s.latent> [out.obj] [--iso V] [--normalize]\n", argv[0]);
return 2;
}
const std::string gguf_path = argv[1];
const std::string lat_path = argv[2];
std::string out_path = "ss_mesh.obj";
float iso = 0.0f;
bool normalize = false;
bool out_set = false;
for (int i = 3; i < argc; ++i) {
if (std::strcmp(argv[i], "--iso") == 0 && i + 1 < argc) iso = (float) std::atof(argv[++i]);
else if (std::strcmp(argv[i], "--normalize") == 0) normalize = true;
else if (!out_set) { out_path = argv[i]; out_set = true; }
}
std::printf("trellis2.cpp %s\n", trellis2_version());
std::string err;
trellis2_ss_dec_model * m = trellis2_ss_dec_load(gguf_path, true, &err);
if (!m) { std::fprintf(stderr, "model load error: %s\n", err.c_str()); return 1; }
std::printf("backend: %s\n", trellis2_ss_dec_backend_name(m));
const trellis2_ss_dec_hparams hp = trellis2_ss_dec_hparams_of(m);
const int Rin = hp.res_in();
const int Rout = hp.res_out();
const size_t n_in = (size_t) hp.latent_channels * Rin * Rin * Rin;
const size_t n_out = (size_t) hp.out_channels * Rout * Rout * Rout;
std::ifstream f(lat_path, std::ios::binary);
if (!f) { std::fprintf(stderr, "cannot open latent %s\n", lat_path.c_str()); trellis2_ss_dec_free(m); return 1; }
std::vector<float> latent(n_in);
f.read(reinterpret_cast<char *>(latent.data()), (std::streamsize) (n_in * sizeof(float)));
if ((size_t) f.gcount() != n_in * sizeof(float)) {
std::fprintf(stderr, "latent size mismatch: got %zu floats, want %zu\n",
(size_t) f.gcount() / sizeof(float), n_in);
trellis2_ss_dec_free(m); return 1;
}
std::vector<float> logits(n_out, 0.0f);
if (!trellis2_ss_dec_decode(m, latent.data(), logits.data(), &err)) {
std::fprintf(stderr, "decode error: %s\n", err.c_str());
trellis2_ss_dec_free(m); return 1;
}
trellis2_ss_dec_free(m);
size_t occ = 0;
for (float v : logits) if (v > iso) ++occ;
std::printf("logits : [%d^3] occupied(>%.2f)=%zu/%zu (%.2f%%)\n",
Rout, iso, occ, n_out, 100.0 * (double) occ / (double) n_out);
// Decoder output is channel-major [1, R, R, R] with linear index
// i*R^2 + j*R + k (k fastest) -> matches marching_cubes' x + y*R + z*R^2 with
// (x,y,z) = (k, j, i).
mc::Mesh mesh = mc::extract(logits.data(), Rout, Rout, Rout, iso);
std::printf("mesh : %zu verts, %zu tris\n", mesh.n_verts(), mesh.n_tris());
if (mesh.n_tris() == 0) {
std::fprintf(stderr, "warning: empty surface at iso=%.3f (nothing to write)\n", iso);
return 1;
}
if (normalize) {
for (size_t i = 0; i < mesh.verts.size(); ++i)
mesh.verts[i] = mesh.verts[i] / (float) Rout - 0.5f;
}
if (!mc::write_obj(mesh, out_path.c_str())) {
std::fprintf(stderr, "failed to write %s\n", out_path.c_str());
return 1;
}
std::printf("wrote %s\n", out_path.c_str());
return 0;
}
+76
View File
@@ -0,0 +1,76 @@
// ss_sample — run stage-1 sampling: a DINOv3 .dinodata cond + the SS-flow DiT
// GGUF -> the sparse-structure latent z_s. The occupancy scaffold is z_s > 0.
//
// usage: ss_sample <ss_flow_dit.gguf> <cond.dinodata> [out.latent] [--seed N]
//
// Writes the z_s latent (channel-major [in_channels * R^3] float32) to out.latent
// if given — the input the SS decoder (next stage) will consume.
#include "trellis2.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
int main(int argc, char ** argv) {
if (argc < 3) {
std::fprintf(stderr, "usage: %s <ss_flow_dit.gguf> <cond.dinodata> [out.latent] [--seed N]\n", argv[0]);
return 2;
}
const std::string gguf_path = argv[1];
const std::string dino_path = argv[2];
std::string out_path;
uint64_t seed = 0;
for (int i = 3; i < argc; ++i) {
if (std::strcmp(argv[i], "--seed") == 0 && i + 1 < argc) seed = std::strtoull(argv[++i], nullptr, 10);
else if (out_path.empty()) out_path = argv[i];
}
std::printf("trellis2.cpp %s\n", trellis2_version());
trellis2_dino_cond cond;
std::string err;
if (!trellis2_load_dinodata(dino_path, cond, &err)) {
std::fprintf(stderr, "cond load error: %s\n", err.c_str());
return 1;
}
std::printf("cond : %lld tokens x %lld channels\n",
(long long) cond.tokens(), (long long) cond.channels());
trellis2_ss_flow_model * m = trellis2_ss_flow_load(gguf_path, true, &err);
if (!m) { std::fprintf(stderr, "model load error: %s\n", err.c_str()); return 1; }
std::printf("backend: %s\n", trellis2_ss_flow_backend_name(m));
const trellis2_ss_flow_hparams hp = trellis2_ss_flow_hparams_of(m); // copy (used after free)
const size_t N = (size_t) hp.resolution * hp.resolution * hp.resolution;
const size_t n = (size_t) hp.in_channels * N;
trellis2_ss_sampler_params P; // pipeline defaults (12 steps, gs 7.5, ...)
P.seed = seed;
std::vector<float> latent(n, 0.0f);
if (!trellis2_ss_flow_sample(m, cond.data.data(), (int) cond.tokens(), (int) cond.channels(),
&P, /*noise*/ nullptr, latent.data(), &err)) {
std::fprintf(stderr, "sample error: %s\n", err.c_str());
trellis2_ss_flow_free(m);
return 1;
}
trellis2_ss_flow_free(m);
double mn = 1e30, mx = -1e30, sum = 0.0;
size_t occ = 0;
for (float v : latent) { mn = v < mn ? v : mn; mx = v > mx ? v : mx; sum += v; if (v > 0.0f) ++occ; }
std::printf("z_s : [%d,%d,%d,%d] min=%.4f max=%.4f mean=%.5f occupancy(>0)=%.2f%%\n",
hp.in_channels, hp.resolution, hp.resolution, hp.resolution,
mn, mx, sum / (double) n, 100.0 * (double) occ / (double) n);
if (!out_path.empty()) {
std::ofstream f(out_path, std::ios::binary);
f.write(reinterpret_cast<const char *>(latent.data()), (std::streamsize) (n * sizeof(float)));
std::printf("wrote %s (%zu floats)\n", out_path.c_str(), n);
}
return 0;
}