generate_asset: meshgod-grade metal PBR lane — RAM-sized face cap, forced-opaque alpha, 4096 textures

- SAFETY_FACE_TARGET: flat 200k laptop mtlbvh guard -> RAM-sized (>=96GB
  Studios get 500k ~ fal-parity detail; TRELLIS2_MAX_BAKE_FACES override,
  0 = uncapped)
- _force_opaque: rewrite GLB materials to alphaMode OPAQUE after metal/
  kdtree bakes — o_voxel's noisy baked alpha + min<250 BLEND auto-detect
  rendered solid hair as a transparent speckle veil
  (TRELLIS2_ALPHA_MODE=auto keeps baked alpha for glassy subjects)
- --texture-size: allow 4096

m3ultra verified (seed 0, 1024_cascade, metal@500k): 156s total, 26.2GB
peak, candidate_pbr 391,782 tris + 2048 PBR textures, raw master 2.19M
tris — vs 94,535-face BLEND output before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-22 19:36:04 +10:00
parent 0db816b55d
commit ba4e2ad42b

View File

@ -37,7 +37,23 @@ from trellis2.model_revisions import ( # noqa: E402
TRELLIS_REVISION,
)
SAFETY_FACE_TARGET = 200_000
def _default_safety_faces() -> int:
"""RAM-sized pre-bake face cap (MODELBEAST BENCHMARKS 2026-07-22): the
flat 200k laptop mtlbvh guard crushed 2.2M-tri decodes to ~95k-face GLBs
(fal ships ~495k from the same weights). 500k is verified crash-free on
the M3 Ultra Studio; small-memory boxes keep the safe cap. Env override:
TRELLIS2_MAX_BAKE_FACES (0 = uncapped)."""
env = os.environ.get("TRELLIS2_MAX_BAKE_FACES")
if env is not None:
return int(env) or 10_000_000
try:
mem_gb = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") / 1e9
except (ValueError, OSError):
mem_gb = 0
return 500_000 if mem_gb >= 96 else 200_000
SAFETY_FACE_TARGET = _default_safety_faces()
WATCHDOG_SIGNATURES = (
"non-zero size",
"BVH needs at least 8 triangles",
@ -165,6 +181,43 @@ def _export_raw(mesh, path: Path) -> dict[str, Any]:
}
def _force_opaque(glb_path: Path) -> None:
"""Rewrite every material's alphaMode to OPAQUE in a GLB, in place.
The o_voxel Metal baker samples the decoder's alpha attr noisily (same
family as the dark-patch base-color bug): solid hair bakes with scattered
near-zero alpha texels, to_glb's min<250 auto-detect flips the material
to BLEND, and the model renders as a transparent speckle veil.
TRELLIS2_ALPHA_MODE=auto keeps the baked behaviour for genuinely
transparent subjects (glass etc.); the default assumes solid objects.
"""
import struct
data = glb_path.read_bytes()
magic, version, _length = struct.unpack_from("<III", data, 0)
if magic != 0x46546C67: # b'glTF'
return
json_len, json_type = struct.unpack_from("<II", data, 12)
if json_type != 0x4E4F534A: # b'JSON'
return
gltf = json.loads(data[20:20 + json_len])
changed = False
for material in gltf.get("materials") or []:
if material.get("alphaMode", "OPAQUE") != "OPAQUE":
material["alphaMode"] = "OPAQUE"
material.pop("alphaCutoff", None)
changed = True
if not changed:
return
payload = json.dumps(gltf, separators=(",", ":")).encode()
payload += b" " * (-len(payload) % 4)
rest = data[20 + json_len:]
out = struct.pack("<III", magic, version, 20 + len(payload) + len(rest))
out += struct.pack("<II", len(payload), json_type) + payload + rest
glb_path.write_bytes(out)
print("Forced alphaMode=OPAQUE (TRELLIS2_ALPHA_MODE=auto keeps baked alpha)")
def _metal_baker_available() -> tuple[bool, str]:
try:
from trellis2.backends import probe_metal_backends
@ -273,6 +326,8 @@ def _export_pbr(mesh, path: Path, *, baker: str, target: Optional[int], texture_
verbose=True,
)
result.export(path)
if os.environ.get("TRELLIS2_ALPHA_MODE", "opaque") != "auto":
_force_opaque(Path(path))
return result, pre_simplified
@ -437,7 +492,7 @@ def _parse_args():
parser.add_argument("--pipeline-type", choices=("512", "1024", "1024_cascade"), default="512")
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--steps", type=int)
parser.add_argument("--texture-size", type=int, choices=(512, 1024, 2048), default=1024)
parser.add_argument("--texture-size", type=int, choices=(512, 1024, 2048, 4096), default=1024)
parser.add_argument("--background", choices=("auto", "keep"), default="auto")
parser.add_argument("--pbr-decimation-target", type=_parse_target, default=None, metavar="none|N")
parser.add_argument("--cache-dir", type=Path)