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:
parent
5b63aee88d
commit
d4cd8e7ad7
@ -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}')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user