diff --git a/blender_ops.py b/blender_ops.py index 6e76aa0..8b34783 100644 --- a/blender_ops.py +++ b/blender_ops.py @@ -698,5 +698,99 @@ elif OP == 'assemble': print(f'assembled body + {len(ARGS) - 2} garment file(s)') export(ARGS[1]) +elif OP == 'remesh': + # remesh [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}')