diff --git a/include/trellis_args.h b/include/trellis_args.h index ffbc9c6..863fc19 100644 --- a/include/trellis_args.h +++ b/include/trellis_args.h @@ -58,6 +58,12 @@ struct TrellisParams { bool voxply = false; // dump out/myvox.ply (debug) bool dump_slat = false; // dump /tmp/hr_slat.bin (debug) bool dump_bg = false; // also write the bg-removal cutout as _cutout.png + std::string dump_post; // write the raw decoded mesh + sparse PBR volume to this + // path and exit (skips remesh/decimate/UV/bake/GLB) -- + // machine-readable output for external post-processing + // pipelines (binary: i32 V,F,Mv,res; f32 verts[V*3]; + // i32 faces[F*3]; i32 coords[Mv*3]; f32 pbr6[Mv*6]; + // same layout as the TRELLIS_DUMP_POST debug env) bool bg_only = false; // background removal only: write the cutout and skip the rest bool help = false; // --help requested diff --git a/src/trellis_args.cpp b/src/trellis_args.cpp index 60fa075..73da7b7 100644 --- a/src/trellis_args.cpp +++ b/src/trellis_args.cpp @@ -47,6 +47,9 @@ void print_usage(const char* argv0, bool server) { " --webp on|off encode GLB textures as WebP (default: on when built with\n" " WebP support; off = PNG)\n" " --dump-bg also write the background-removal cutout as _cutout.png\n" + " --dump-post PATH write the raw decoded mesh + sparse PBR volume to PATH and\n" + " exit (skips remesh/decimate/UV/bake/GLB) -- for external\n" + " post-processing pipelines\n" " --bg-only background removal only: write the cutout and skip the rest\n" " --f32 f32 sparse-conv compute\n" " --no-fa disable FlashAttention\n" @@ -87,6 +90,7 @@ bool parse_args(int argc, char** argv, TrellisParams& p) { else if (a == "--no-texture") { p.texture = false; } else if (a == "--xatlas") { p.xatlas = true; } else if (a == "--box-uv") { p.xatlas = false; } + else if (a == "--dump-post") { const char* v = need(a.c_str()); if (!v) return false; p.dump_post = v; } else if (a == "--band") { const char* v = need(a.c_str()); if (!v) return false; p.band = atoi(v); } else if (a == "--decim") { const char* v = need(a.c_str()); if (!v) return false; p.decim = atoi(v); } else if (a == "--atlas" || a == "--tex"){ const char* v = need(a.c_str()); if (!v) return false; p.tex = atoi(v); } diff --git a/src/trellis_cli.cpp b/src/trellis_cli.cpp index 0dd031d..6bf3d8f 100644 --- a/src/trellis_cli.cpp +++ b/src/trellis_cli.cpp @@ -348,6 +348,57 @@ int trellis_run(const trellis::TrellisParams& cfg) { } } + if (!cfg.dump_post.empty()) { + // Machine-readable output for external post-processing pipelines + // (simplification budgets / UV / baking done by the caller, e.g. + // QtMeshEditor): the CLEANED geometry mesh + the sparse PBR volume it + // would be baked from, then exit before decimate/UV/bake/GLB. + // + // The reference cleanup chain runs first (same as the GLB path): + // weld -> fill_small_holes -> narrow-band DC remesh -> clean -> + // drop floaters. The raw dual-grid decode is non-manifold at + // sub-voxel scale with inconsistently-wound patches — downstream + // consumers hit unwrap/simplify/culling pathologies on it — while + // the remeshed shell is watertight with consistent winding. Falls + // back to the raw decoded mesh if the remesh produces nothing. + // + // Binary layout matches the TRELLIS_DUMP_POST debug env: + // i32 V,F,Mv,res; f32 verts[V*3]; i32 faces[F*3]; i32 coords[Mv*3]; + // f32 pbr6[Mv*6] (base_color.rgb, metallic, roughness, alpha in [0,1]). + trellis::weld_vertices(mesh.verts, mesh.faces, nullptr, + 1.0f / ((float)so.res * 8.0f)); + trellis::fill_small_holes(mesh.faces); + trellis::TriBvh dbvh = trellis::TriBvh::build(mesh.verts.data(), mesh.V(), + mesh.faces.data(), mesh.F()); + const int dband = cfg.band > 0 ? cfg.band : std::max(1, so.res / 512); + trellis::Mesh rm = trellis::remesh_narrow_band_dc(mesh.verts.data(), mesh.V(), + mesh.faces.data(), mesh.F(), + dbvh, so.res, dband); + if (rm.F() > 0) { + trellis::clean_mesh(rm.V(), rm.faces); + trellis::drop_small_components(rm.verts, rm.faces, 0.02f); + } + const std::vector& dverts = rm.F() > 0 ? rm.verts : mesh.verts; + const std::vector& dfaces = rm.F() > 0 ? rm.faces : mesh.faces; + FILE* dfp = fopen(cfg.dump_post.c_str(), "wb"); + if (!dfp) { fprintf(stderr, "cannot write %s\n", cfg.dump_post.c_str()); return 1; } + const int dV = (int)dverts.size() / 3, dFc = (int)dfaces.size() / 3; + const int Mv = pbr6.empty() ? 0 : (int)pbr_coords->size(), res = pbr_res; + fwrite(&dV,4,1,dfp); fwrite(&dFc,4,1,dfp); fwrite(&Mv,4,1,dfp); fwrite(&res,4,1,dfp); + fwrite(dverts.data(),4,(size_t)dV*3,dfp); + fwrite(dfaces.data(),4,(size_t)dFc*3,dfp); + if (Mv) { + for (auto& c : *pbr_coords) { int xyz[3] = {c[0],c[1],c[2]}; fwrite(xyz,4,3,dfp); } + fwrite(pbr6.data(),4,(size_t)Mv*6,dfp); + } + fclose(dfp); + printf("[7/7] cleaned dump -> %s (V=%d F=%d%s, PBR=%d @res%d)\n", + cfg.dump_post.c_str(), dV, dFc, + rm.F() > 0 ? " remeshed" : " raw (remesh empty)", Mv, res); + printf("done in %.1fs -> %s\n", now() - t0, cfg.dump_post.c_str()); + return 0; + } + printf("[7/7] write %s\n", outglb.c_str()); bool textured = false; if (!pbr6.empty()) { // UV-baked textured GLB (PBR material)