remesh op: game-LOD TRELLIS heroes — solidify past voxel, voxel remesh, decimate floor 1/3, smart-UV, Cycles bake

Thin-shell physics learned the hard way: fabric thinner than the voxel
perforates in the remesh itself; decimating any thin slab below ~1/3
collapses its surfaces. Verified on the baseball cap: 483k -> 41k tris,
watertight, textured.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-30 21:28:40 +10:00
parent 5b63aee88d
commit d4cd8e7ad7

View File

@ -698,5 +698,99 @@ elif OP == 'assemble':
print(f'assembled body + {len(ARGS) - 2} garment file(s)')
export(ARGS[1])
elif OP == 'remesh':
# remesh <in> <out.glb> <target_tris> [tex_size]
#
# Game-LOD a TRELLIS hero mesh. Plain Decimate moth-eats TRELLIS output at
# ANY ratio (verified 25k and 50k on the baseball cap) because the surface
# is a paper-thin double shell — front and back faces collapse into each
# other. So: voxel-remesh into one solid watertight skin first (kills the
# UVs), decimate THAT, give it fresh smart-project UVs, and Cycles-bake the
# original's textured surface back onto it (selected->active). Rigid props
# only — vertex weights do not survive; use decimate for rigged meshes.
src, out, target = ARGS[0], ARGS[1], int(ARGS[2])
tex_size = int(ARGS[3]) if len(ARGS) > 3 else 2048
load(src)
meshes = [o for o in bpy.data.objects if o.type == 'MESH']
orig = max(meshes, key=lambda o: len(o.data.vertices))
for o in meshes:
if o is not orig:
bpy.data.objects.remove(o, do_unlink=True)
for o in [o for o in bpy.data.objects if o.type in ('EMPTY', 'ARMATURE')]:
bpy.data.objects.remove(o, do_unlink=True)
orig.rotation_mode = 'XYZ'
lod = orig.copy()
lod.data = orig.data.copy()
lod.name = 'lod'
bpy.context.scene.collection.objects.link(lod)
# Voxel-remesh into ONE solid skin at a voxel fine enough to keep thin
# features (brims, straps): 0.8% of bbox — 39mm voxels ate the cap brim
# entirely, 8mm kept it. Then decimate no lower than 1/3: below that the
# thin solid slab's two surfaces collapse into each other (the same
# moth-eating plain Decimate causes). The target is therefore a WISH —
# the shell's physics set the floor, and the op reports what it kept.
bpy.context.view_layer.objects.active = lod
vox = max(orig.dimensions) * 0.008
# SOLIDIFY FIRST: TRELLIS fabric is ~2mm thick — anywhere the shell is
# thinner than the voxel, the remesh itself perforates (the cap crown
# tore at 8mm voxel in every variant while the double-folded brim held).
# Thickening the shell past the voxel size guarantees a watertight solid.
sol = lod.modifiers.new('sol', 'SOLIDIFY')
sol.thickness = vox * 1.6
sol.offset = 0.0
bpy.ops.object.modifier_apply(modifier='sol')
rm = lod.modifiers.new('remesh', 'REMESH')
rm.mode = 'VOXEL'
rm.voxel_size = vox
bpy.ops.object.modifier_apply(modifier='remesh')
tris = sum(len(pg.vertices) - 2 for pg in lod.data.polygons)
if tris > target:
dec = lod.modifiers.new('dec', 'DECIMATE')
dec.ratio = max(target / max(tris, 1), 0.33)
bpy.ops.object.modifier_apply(modifier='dec')
bpy.ops.object.select_all(action='DESELECT')
lod.select_set(True)
bpy.context.view_layer.objects.active = lod
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.uv.smart_project(angle_limit=1.15, island_margin=0.003)
bpy.ops.object.mode_set(mode='OBJECT')
img = bpy.data.images.new('lod_bake', tex_size, tex_size, alpha=False)
mat = bpy.data.materials.new('lod_mat')
mat.use_nodes = True
nt = mat.node_tree
bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED')
tn = nt.nodes.new('ShaderNodeTexImage')
tn.image = img
nt.nodes.active = tn
lod.data.materials.clear()
lod.data.materials.append(mat)
sc = bpy.context.scene
sc.render.engine = 'CYCLES'
sc.cycles.samples = 16 # flat diffuse transfer needs few samples
sc.cycles.device = 'CPU'
sc.render.bake.use_selected_to_active = True
sc.render.bake.cage_extrusion = max(orig.dimensions) * 0.03
sc.render.bake.max_ray_distance = max(orig.dimensions) * 0.08
sc.render.bake.use_pass_direct = False
sc.render.bake.use_pass_indirect = False
bpy.ops.object.select_all(action='DESELECT')
orig.select_set(True)
lod.select_set(True)
bpy.context.view_layer.objects.active = lod
bpy.ops.object.bake(type='DIFFUSE')
nt.links.new(tn.outputs['Color'], bsdf.inputs['Base Color'])
img.pack()
bpy.data.objects.remove(orig, do_unlink=True)
final_tris = sum(len(pg.vertices) - 2 for pg in lod.data.polygons)
print(f'remeshed: voxel {vox * 1000:.1f}mm -> {tris:,} tris -> decimated {final_tris:,} tris, baked {tex_size}px')
export(out)
else:
raise SystemExit(f'unknown op {OP}')