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)')
|
print(f'assembled body + {len(ARGS) - 2} garment file(s)')
|
||||||
export(ARGS[1])
|
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:
|
else:
|
||||||
raise SystemExit(f'unknown op {OP}')
|
raise SystemExit(f'unknown op {OP}')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user