champions-of-luneraria/tools/blender/make_manifest.py
Martin Stickelbroeck 3ca38a8443 U2 Batch Asset Import
Alle 86 Modelle der Godot-Referenz laufen durch die Blender→FBX-Pipeline
(nach bestandenem Vertical Function Test); jede Einheit zeigt ihr eigenes
Modell.

- Exportmanifest aus den Godot-Manifesten (tools/blender/make_manifest.py):
  Units, Enemies, Props, Characters, Peoples, Buildings, Environment; die
  drei Vergleichs-GLBs als Aliase. Neu in der Pipeline: Tangenten berechnet
  Unity mit MikkTSpace, Generatoren laufen in einer temporären Kopie, die
  Grüne Kluft wird wie das GLB zusammengefasst, Texturen kommen mit.
  1.616 Blender- und 1.247 Unity-Prüfungen grün, zwei Exportläufe
  byteidentisch; Kontaktbögen je Gruppe.
- Einheitenbibliothek nach Godots AssetLibrary: 70 Einträge der
  Einheitengruppen, Figuren mit Skelett ersetzen das starre Modell ihrer ID.
  Starre Modelle werden zur Laufzeit je Material zusammengefasst; Controller
  werden an Ort und Stelle aktualisiert.
- Tests mit allen Modellen: EditMode 11/11, PlayMode 2/2. Vertical Function
  Test Seed 2 DCE95C96556F3A1A und Seed 9 ab Runde 21 7B8E43AEFC0D60C5,
  beide gleich dem Kern (Nachweise mit LUNE_EVIDENCE=u2a-batch; die Belege
  von U2C und U2D bleiben unverändert).
- macOS-Player mit allen Modellen: 158-160 fps in der Übersicht
  (116 Einheiten), 205-207 fps auf der Mid-Lane; Haupt- und Render-Thread
  etwa doppelt so lang wie mit dem Platzhalter, Present-Ausreißer kürzer.
- Befund iCloud Drive: Konfliktkopien ("Name 2.ext") im Build-Cache ließen
  den Build die Einheitenbibliothek mit altem Feldlayout schreiben; der
  Player meldete sharedassets0.assets als beschädigt. LunePlayerBuild
  entfernt solche Kopien vor dem Build und bricht ab, wenn während des
  Builds neue entstehen. Bereits committete Kopien: Controller-Duplikate und
  World/Generated 2.meta entfernt, drei U2D-Benchmarkläufe als -run1 bis
  -run3 benannt.
- Dokumentation: U2-WORLD (Batch-Import), PORT-STATUS (Entscheidungen:
  Modelle direkt in Git, Projektort außerhalb von iCloud Drive), PORT-QA.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:10:35 +02:00

95 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""Writes tools/blender/export_manifest.json for every model of the Godot reference (U2A batch).
The entries come from the Godot manifests (assets/*_manifest.json and assets-source/phase10/
environment-manifest.json): source file, the GLB facts to compare with, and the export rules of
the group. Entries already in the manifest keep their hand-checked rules (the six representative
assets and the scene buildings of U2A). Run from the Unity project root:
python3 tools/blender/make_manifest.py [../saumwacht-godot]
"""
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
GODOT = Path(sys.argv[1] if len(sys.argv) > 1 else ROOT.parent / 'saumwacht-godot').resolve()
MANIFEST = ROOT / 'tools/blender/export_manifest.json'
FIGURE_CLIPS = ['idle', 'walk', 'attack_a', 'attack_b', 'hit', 'ability', 'death']
def entries(relative):
data = json.loads((GODOT / relative).read_text())
assets = data['assets'] if isinstance(data, dict) and 'assets' in data else data
return assets if isinstance(assets, list) else [assets]
def main():
current = json.loads(MANIFEST.read_text())
known = {asset['id'] for asset in current['assets']}
added = []
def add(entry):
if entry['id'] not in known:
known.add(entry['id'])
added.append(entry)
# Phase 4: troops of the four factions, mercenaries, wave enemies, worker, cores, lane segment.
for asset in entries('assets/phase4_manifest.json'):
key = asset['id']
group = 'Enemies' if key.startswith('e') else 'Props' if key in ('worker', 'map_lane_segment', 'core_team0', 'core_team1') else 'Units'
add({'id': key, 'group': group, 'kind': 'rigid', 'source': 'assets-source/phase4/blend/%s.blend' % key,
'godot_manifest': {'file': 'assets/phase4_manifest.json', 'id': key},
'rules': {'skip_modifiers': ['HIGH RES'], 'logical_names': True, 'forward_marker': 'Forward'}})
# Figures with skeleton (the .blend keeps the parts; the GLB joined them into one body).
figures = [('ringborke', 'phase11', 'w04', 'RingborkeRig', ['assets/materials/phase11/ringborke-cut-albedo-4k.png',
'assets/materials/phase11/ringborke-cut-normal-4k.png', 'assets/materials/phase11/ringborke-cut-roughness-4k.png']),
('arbeiter', 'phase12', 'worker', 'ArbeiterRig', []), ('basaltbastion', 'phase15', 'g04', 'BasaltRig', []),
('quellgeflecht', 'phase16', 'w10', 'QuellRig', []), ('glasballiste', 'phase19', 'g06', 'GlassRig', [])]
for key, phase, godot_id, rig, textures in figures:
record = next(a for a in entries('assets/%s_manifest.json' % phase) if a['id'] == godot_id)
# Fighters need the combat clips; the worker has its own set (checked against Godot as a whole).
required = FIGURE_CLIPS if all(c in record['clips'] for c in FIGURE_CLIPS) else ['idle', 'walk']
rules = {'join': {'parent': rig, 'into': record['parts'][0]}, 'rest_action': None, 'root_bone': 'Root', 'required_clips': required}
if textures:
rules['textures'] = textures
add({'id': key, 'group': 'Characters', 'kind': 'skinned', 'source': 'assets-source/%s/%s.blend' % (phase, key),
'godot_manifest': {'file': 'assets/%s_manifest.json' % phase, 'id': godot_id}, 'rules': rules})
# The seven peoples (G1-era representatives).
for record in entries('assets/luneraria/peoples_manifest.json'):
add({'id': record['id'], 'group': 'Peoples', 'kind': 'skinned', 'source': record['source'],
'godot_manifest': {'file': 'assets/luneraria/peoples_manifest.json', 'id': record['id']},
'rules': {'join': {'parent': 'PeopleRig', 'into': record['parts'][0]}, 'rest_action': None, 'root_bone': 'Root', 'required_clips': FIGURE_CLIPS}})
# G1 tower and grotto (replaced in the game by the G2 models, kept for completeness).
add({'id': 'g1_tower', 'group': 'Buildings', 'kind': 'static', 'source': 'assets-source/luneraria/tower.blend',
'godot_manifest': {'file': 'assets/luneraria/tower_manifest.json', 'id': 'lane_tower'}})
add({'id': 'g1_grotto', 'group': 'Buildings', 'kind': 'static', 'source': 'assets-source/luneraria/grotto.blend',
'godot_manifest': {'file': 'assets/luneraria/grotto_manifest.json', 'id': 'boss_grotto'}})
# Environments: the Verdant Rift (merged into runtime chunks like the GLB) and the menu stage.
environment = json.loads((GODOT / 'assets-source/phase10/environment-manifest.json').read_text())
add({'id': 'verdant_rift', 'group': 'Environment', 'kind': 'static', 'source': 'assets-source/phase10/verdant-rift.blend',
'expect': {'triangles': environment['triangles'], 'mesh_count': environment['runtime_chunks']},
# Joining the source objects (as the reference did) leaves zero normals at two corners of
# one face in two chunks; Blender cannot store a normal there (degenerate corner space).
'rules': {'chunk': 16, 'allow_bad_normals': {'Chunk_-2_-1_ENV_cliff': 2, 'Chunk_1_0_ENV_cliff': 2},
'textures': ['assets/materials/phase10/limestone-albedo.png']}})
add({'id': 'menu_stage', 'group': 'Environment', 'kind': 'static', 'source': 'assets-source/phase20/menu-stage.blend',
'godot_manifest': {'file': 'assets/menu_stage_manifest.json', 'id': None}})
# Compare props: generator only (no .blend); the other three compare GLBs are copies of
# ringborke, arbeiter and core_team0 and map to those FBX files.
for key, prefix in [('compare_rock', 'CompareRock'), ('compare_tree', 'CompareTree'), ('compare_arena_tile', 'CompareArenaTile')]:
add({'id': key, 'group': 'Environment', 'kind': 'static', 'source': 'assets-source/phase14/build_compare_props.py',
'generator': {'inputs': ['assets/characters/ringborke.glb', 'assets/characters/arbeiter.glb', 'assets/models/core_team0.glb']},
'godot_manifest': {'file': 'assets/compare/compare-manifest.json', 'id': key}, 'rules': {'objects_prefix': prefix}})
current['assets'].extend(added)
current['aliases'] = {'compare_ringborke': 'ringborke', 'compare_arbeiter': 'arbeiter', 'compare_core': 'core_team0'}
MANIFEST.write_text(json.dumps(current, indent=2, ensure_ascii=False) + '\n')
print('manifest: %d assets (%d added), %d aliases' % (len(current['assets']), len(added), len(current['aliases'])))
main()