U2 Exporter: pfadneutraler FBX-Kopf (Version 11)

Blenders FBX-Schreiber trug den absoluten Pfad der geöffneten .blend-Datei
in den Dateikopf ein (ApplicationNativeFile, z. B. /Users/martin/Documents/…).
Die Bytes hingen damit vom Ort des Checkouts ab; nach dem Umzug nach
~/Developer galt die byteidentische Reproduzierbarkeit für die eingecheckten
Dateien nicht mehr.

Ab Version 11 steht dort der relative Quellpfad aus dem Manifest (Generatoren:
leer). Geprüft an der Grenzwacht: zwei Läufe byteidentisch, kein absoluter Pfad,
Prüfprotokoll gleich bis auf Version und Größe. Die eingecheckten FBX-Dateien
bleiben Version 10 bis zum nächsten Gesamtexport (spätestens U5); Inhalt
unverändert. Dokumentiert in U2-WORLD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Martin Stickelbroeck 2026-10-01 23:57:13 +02:00
parent 255bf2939f
commit 4744a47114
2 changed files with 38 additions and 3 deletions

View file

@ -213,6 +213,14 @@ Behandlung:
- Nach den Abstürzen fragte macOS beim nächsten Start, ob die Fenster wiederhergestellt werden sollen, und wartete auf einen Klick. Der Benchmark startet deshalb mit `-ApplePersistenceIgnoreState YES`. Das gilt nur für diesen Start und ändert keine Einstellung. - Nach den Abstürzen fragte macOS beim nächsten Start, ob die Fenster wiederhergestellt werden sollen, und wartete auf einen Klick. Der Benchmark startet deshalb mit `-ApplePersistenceIgnoreState YES`. Das gilt nur für diesen Start und ändert keine Einstellung.
- Dauerhaft hilft nur ein Projektort außerhalb von iCloud Drive. Deshalb zieht der Arbeitsordner nach `~/Developer/TowerDefense` (Entscheidung vom 1. Oktober 2026, [PORT-STATUS.md](PORT-STATUS.md)). - Dauerhaft hilft nur ein Projektort außerhalb von iCloud Drive. Deshalb zieht der Arbeitsordner nach `~/Developer/TowerDefense` (Entscheidung vom 1. Oktober 2026, [PORT-STATUS.md](PORT-STATUS.md)).
#### Befund nach dem Umzug: absoluter Pfad im FBX-Kopf
Blenders FBX-Schreiber trägt den absoluten Pfad der geöffneten `.blend`-Datei in den Dateikopf ein (`ApplicationNativeFile`). Deshalb enthalten 83 der 86 FBX-Dateien aus Exporter-Version 10 `/Users/martin/Documents/…`, und ihre Bytes hängen vom Ort des Checkouts ab. Die byteidentische Reproduzierbarkeit galt damit nur am selben Pfad; nach dem Umzug nach `~/Developer` gilt sie für die eingecheckten Dateien nicht mehr.
- **Korrektur:** Ab Exporter-Version 11 steht dort der relative Quellpfad aus dem Manifest, bei Generatoren nichts.
- **Geprüft an der Grenzwacht:** Zwei Läufe sind byteidentisch, es steht kein absoluter Pfad mehr in der Datei, und das Prüfprotokoll ist bis auf Version und Dateigröße gleich (64 Byte kürzer, genau der weggefallene Pfadanfang).
- **Eingecheckte Dateien bleiben Version 10** bis zum nächsten Gesamtexport, spätestens in U5, das alle Modelle neu erzeugt. Ein Neuexport aller Dateien nur für diesen Kopfeintrag hätte das Repository um rund 140 MB vergrößert. Geometrie, Skelette, Clips und Materialien sind nicht betroffen.
#### Leistung mit allen Modellen #### Leistung mit allen Modellen
Messung wie in U2D (macOS-Player, Apple M5, 1920 × 1080 im Fenster, VSync aus, Seed 9 ab Runde 21), drei Läufe: Messung wie in U2D (macOS-Player, Apple M5, 1920 × 1080 im Fenster, VSync aus, Seed 9 ab Runde 21), drei Läufe:

View file

@ -27,7 +27,7 @@ from pathlib import Path
from mathutils import Matrix, Vector from mathutils import Matrix, Vector
UNITY_ROOT = Path(__file__).resolve().parents[2] UNITY_ROOT = Path(__file__).resolve().parents[2]
EXPORTER_VERSION = 10 EXPORTER_VERSION = 11
# Tangents are not written: Unity computes them with MikkTSpace like Blender (import rule # Tangents are not written: Unity computes them with MikkTSpace like Blender (import rule
# CalculateMikk); per-corner tangents and binormals in double precision doubled the file size. # CalculateMikk); per-corner tangents and binormals in double precision doubled the file size.
@ -385,10 +385,18 @@ def run_checks(spec, report, before, clips, expect):
report.check('forward_marker_unity_plus_z', marker is not None and marker[2] > 0.1 and abs(marker[0]) < 1e-3, marker) report.check('forward_marker_unity_plus_z', marker is not None and marker[2] > 0.1 and abs(marker[0]) < 1e-3, marker)
# Source file named in the FBX header (ApplicationNativeFile): the manifest path relative to the
# reference repository, set per asset; generators have no saved file.
NATIVE_FILE = {'path': ''}
def reproducible_fbx(): def reproducible_fbx():
"""Byte-identical FBX for identical sources: Blender's FBX writer stamps the current time into """Byte-identical FBX for identical sources: Blender's FBX writer stamps the current time into
the header and derives object ids from Python's string hash. The clock is fixed here; the hash the header, writes the absolute path of the open .blend file (ApplicationNativeFile) and derives
seed comes from PYTHONHASHSEED=0 with --python-use-system-env (tools/blender/export_fbx.sh).""" object ids from Python's string hash. The clock is fixed and the path is the manifest's relative
source path here (since exporter version 11; version 10 wrote the absolute path, so its files
depend on the checkout location); the hash seed comes from PYTHONHASHSEED=0 with
--python-use-system-env (tools/blender/export_fbx.sh)."""
import datetime import datetime
import io_scene_fbx.export_fbx_bin as writer import io_scene_fbx.export_fbx_bin as writer
@ -402,6 +410,23 @@ def reproducible_fbx():
writer.datetime = Clock writer.datetime = Clock
class Data:
"""bpy.data with a neutral filepath; everything else is the real data of the open file."""
@property
def filepath(self):
return NATIVE_FILE['path']
def __getattr__(self, name):
return getattr(bpy.data, name)
class Bpy:
data = Data()
def __getattr__(self, name):
return getattr(bpy, name)
writer.bpy = Bpy()
def export_fbx(path, objects): def export_fbx(path, objects):
bpy.ops.object.select_all(action='DESELECT') bpy.ops.object.select_all(action='DESELECT')
@ -474,8 +499,10 @@ def export_asset(spec, godot_root):
source = godot_root / spec['source'] source = godot_root / spec['source']
if spec.get('generator'): if spec.get('generator'):
run_generator(spec, godot_root, source) run_generator(spec, godot_root, source)
NATIVE_FILE['path'] = ''
else: else:
bpy.ops.wm.open_mainfile(filepath=str(source)) bpy.ops.wm.open_mainfile(filepath=str(source))
NATIVE_FILE['path'] = spec['source']
report.data['source_sha256'] = hashlib.sha256(source.read_bytes()).hexdigest() report.data['source_sha256'] = hashlib.sha256(source.read_bytes()).hexdigest()
copy_textures(spec, godot_root, report) copy_textures(spec, godot_root, report)
expect = godot_expectations(spec, godot_root) expect = godot_expectations(spec, godot_root)