Files
AIGame/Tools/model_tools/bl_keep_main_island.py
udxandClaude Opus 5.5 89e0fa246f Add formal enemies, remaining formal partners and synthesized UI SFX
- UI SFX v3: procedurally synthesized one-shots (Tools/synth_ui_sfx.py) wired to
  buttons, rewards, errors and red dots via UiSfx, gated by the SFX setting.
- Five formal enemies (Hornlet, Dashlet, Shellhorn, HornCaptain, GreatHorn):
  Hunyuan 50k -> 2000-tri XPbr, DGXA TokenRig, Idle/Walk/Attack, nested per type
  in PrototypeArena; PrototypeBattle.Enemy gains presentation-only Moving/Strikes.
- Formal partners for all ten pets in the arena and portraits, including new
  VineSprite, ThunderLeopard and TideWhale.
- Pipeline fixes: reject local TokenRig fallback, drop loose mesh islands,
  configurable Unity run_script timeout, companion static stage.
- Also includes concurrent work from another tool in this working tree
  (FlameLion, enemy combat FX, challenge mode, home explorer, server updates).
- Resources registered in Doc/ResDesc_*.md; verified on isolated :18081.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:23:55 +08:00

39 lines
2.5 KiB
Python

"""Drop loose mesh islands that TokenRig discards (thin floating tail tips, specks) so the baked game mesh and the rig agree.
blender -b --factory-startup --python Tools/model_tools/bl_keep_main_island.py -- in.glb out.glb [--min-fraction 0.05]
Islands are found after welding UV-seam duplicates (1e-4); only the welding used for the test, the export keeps the original split vertices and UVs.
"""
import argparse, json, sys
from pathlib import Path
import bmesh, bpy
p = argparse.ArgumentParser(); p.add_argument('source', type=Path); p.add_argument('output', type=Path)
p.add_argument('--min-fraction', type=float, default=.05, help='keep islands with at least this fraction of the largest island vertices')
a = p.parse_args(sys.argv[sys.argv.index('--') + 1:])
bpy.ops.wm.read_factory_settings(use_empty=True); bpy.ops.import_scene.gltf(filepath=str(a.source.resolve()))
report = []
for o in [o for o in bpy.data.objects if o.type == 'MESH']:
bm = bmesh.new(); bm.from_mesh(o.data); bm.verts.ensure_lookup_table()
# Union-find over original vertices, joining edges and co-located (seam-split) vertices.
parent = list(range(len(bm.verts)))
def find(i):
while parent[i] != i: parent[i] = parent[parent[i]]; i = parent[i]
return i
def union(i, j): parent[find(i)] = find(j)
for e in bm.edges: union(e.verts[0].index, e.verts[1].index)
cells = {}
for v in bm.verts:
key = tuple(round(c / 1e-4) for c in v.co)
if key in cells: union(v.index, cells[key])
else: cells[key] = v.index
sizes = {}
for v in bm.verts: r = find(v.index); sizes[r] = sizes.get(r, 0) + 1
largest = max(sizes.values()); keep = {r for r, n in sizes.items() if n >= largest * a.min_fraction}
drop = [v for v in bm.verts if find(v.index) not in keep]
report.append(dict(object=o.name, islands=len(sizes), kept=len(keep), droppedVertices=len(drop), droppedIslandSizes=sorted((n for r, n in sizes.items() if r not in keep), reverse=True)[:20]))
bmesh.ops.delete(bm, geom=drop, context='VERTS'); bm.to_mesh(o.data); bm.free(); o.data.update()
a.output.parent.mkdir(parents=True, exist_ok=True)
bpy.ops.export_scene.gltf(filepath=str(a.output.resolve()), export_format='GLB')
rec = dict(source=str(a.source), output=str(a.output), minFraction=a.min_fraction, objects=report, note='Only loose islands removed; remaining geometry, UVs and textures unchanged.')
a.output.with_suffix('.islands.json').write_text(json.dumps(rec, indent=2), encoding='utf-8'); print(json.dumps(rec))