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>
95 lines
6.3 KiB
Python
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()
|