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>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
"""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))
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Rigidly orient a generated character before baking/rigging, preserving UVs."""
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import bpy
|
||||
from mathutils import Matrix,Vector
|
||||
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('source',type=Path);p.add_argument('output',type=Path)
|
||||
p.add_argument('--blender-yaw',type=float,required=True)
|
||||
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()))
|
||||
if any(o.type=='ARMATURE' for o in bpy.data.objects):raise ValueError('Orient the static high source before rigging')
|
||||
rotation=Matrix.Rotation(math.radians(a.blender_yaw),4,'Z')
|
||||
roots=[o for o in bpy.context.scene.objects if o.parent is None]
|
||||
for o in roots:o.matrix_world=rotation@o.matrix_world
|
||||
bpy.context.view_layer.update()
|
||||
points=[o.matrix_world@v.co for o in bpy.context.scene.objects if o.type=='MESH' for v in o.data.vertices]
|
||||
mn=Vector([min(p[c] for p in points) for c in range(3)]);mx=Vector([max(p[c] for p in points) for c in range(3)])
|
||||
offset=Matrix.Translation(-Vector(((mn.x+mx.x)/2,(mn.y+mx.y)/2,mn.z)))
|
||||
for o in roots:o.matrix_world=offset@o.matrix_world
|
||||
a.output.parent.mkdir(parents=True,exist_ok=True)
|
||||
bpy.ops.export_scene.gltf(filepath=str(a.output.resolve()),export_format='GLB')
|
||||
record=dict(source=str(a.source),output=str(a.output),blender_yaw_degrees=a.blender_yaw,orientation_note='Verify final forward in Unity views: FBX handedness conversion must be included.',height=mx.z-mn.z,mesh_unchanged_except_rigid_transform=True)
|
||||
a.output.with_suffix('.orientation.json').write_text(json.dumps(record,indent=2),encoding='utf-8')
|
||||
print(json.dumps(record))
|
||||
@@ -157,6 +157,33 @@ def main():
|
||||
# recipe-defined geodesic masks can bind that appendage to an existing
|
||||
# stable bone without changing geometry, UVs or the animation skeleton.
|
||||
override_reports=[]
|
||||
# Explicit source-space regions can bind a rigid carapace without altering
|
||||
# geometry/UVs. The recipe must be fitted to this character's inspected mesh.
|
||||
if args.weight_overrides:
|
||||
rules=json.loads(args.weight_overrides.read_text(encoding='utf-8-sig')).get('skinRegions',[])
|
||||
extent=max(p.y for p in original_points)-min(p.y for p in original_points)
|
||||
points=[p/extent for p in original_points]
|
||||
for rule in rules:
|
||||
if rule['bone'] not in arm.data.bones:raise ValueError('Region bone missing')
|
||||
lo,hi=rule['min'],rule['max'];feather=rule.get('feather',0)
|
||||
if feather<0 or any(lo[c]>=hi[c] for c in range(3)):raise ValueError('Invalid rigid region')
|
||||
target=low.vertex_groups[rule['bone']];affected=[];full=[]
|
||||
for i,p in enumerate(points):
|
||||
distance=max(max(lo[c]-p[c],0,p[c]-hi[c]) for c in range(3))
|
||||
if distance>feather:continue
|
||||
weight=1 if distance==0 else 1-distance/feather
|
||||
weight=weight*weight*(3-2*weight)
|
||||
previous={g.group:g.weight for g in low.data.vertices[i].groups}
|
||||
for group,value in previous.items():low.vertex_groups[group].add([i],value*(1-weight),'REPLACE')
|
||||
target.add([i],previous.get(target.index,0)*(1-weight)+weight,'REPLACE')
|
||||
affected.append(i)
|
||||
if distance==0:full.append(i)
|
||||
if not full:raise ValueError('Rigid region selected no vertices')
|
||||
override_reports.append(dict(kind='source_space_region',bone=rule['bone'],affected=len(affected),full_weight_vertices=len(full),
|
||||
normalized_min=[min(points[i][c] for i in affected) for c in range(3)],
|
||||
normalized_max=[max(points[i][c] for i in affected) for c in range(3)]))
|
||||
|
||||
# More precise geodesic appendage overrides run after broad spatial regions.
|
||||
if args.weight_overrides:
|
||||
overrides=json.loads(args.weight_overrides.read_text(encoding='utf-8-sig')).get('skinOverrides',[])
|
||||
extent=max(p.y for p in original_points)-min(p.y for p in original_points)
|
||||
|
||||
Reference in New Issue
Block a user