champions-of-luneraria/Assets/Luneraria/Editor/Art/LuneModelPostprocessor.cs
Martin Stickelbroeck 49cb4af407 U2A Blender/FBX Pipeline
Reproduzierbarer Export aus den Blender-Quellen der Godot-Referenz nach FBX,
geprüft auf beiden Seiten; sechs repräsentative Assets vollständig in Unity
verifiziert (normale Einheit g01, Einheit mit Skelett Funkenlanze, Turm
Grenzwacht, Ancient, Schlundkrone, Vegetation/Kartenrequisiten), dazu Bastion,
Kernwacht und Bossgrotte für die Szene.

- tools/blender: Manifest mit Exportregeln je Asset, Exportskript mit Prüfungen
  (Einheiten, Maßstab, Achsen, Vorderseite, Mesh-Namen, Dreiecke, Materialslots,
  Normalen, Tangenten, Vertexfarben, Maße, Root Bone, Bone-Namen, Clips, Blend
  Shapes) gegen die Manifeste der Godot-GLBs, FBX-Rundreise, byteidentische
  Ausgabe (feste Uhrzeit, PYTHONHASHSEED).
- Regeln wie beim GLB-Export: HIGH-RES-Modifikatoren weglassen, logische Namen,
  Skin-Teile zusammenfügen; 180°-Drehung, damit die Vorderseite nach Unitys
  Achsen-Bake auf +Z liegt (Unity = (-x, y, z) der Godot-Koordinaten).
- Unity: Importregeln (Postprocessor), Verifier gegen das Blender-Protokoll,
  Vorschaubilder; URP-Asset, Linear-Farbraum, URP-Settings nach
  Assets/Luneraria/Settings/Rendering.
- Ergebnis: 176/176 Blender-Prüfungen, 128/128 Unity-Prüfungen, Dreiecke gleich
  den Godot-GLBs, Maße auf 0,05 mm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:41:24 +02:00

78 lines
3.8 KiB
C#

using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace Luneraria.EditorTools
{
/// <summary>
/// Import rules for every FBX of the Blender pipeline under Assets/Luneraria/Art (U2A).
/// Metres stay metres, Blender's Z-up axes are converted by baking (no root rotation),
/// normals and tangents come from the file, rigs are generic with the Blender bone names,
/// and clips get their Blender action names, loop flags and contact events.
/// </summary>
public sealed class LuneModelPostprocessor : AssetPostprocessor
{
public const string ArtRoot = "Assets/Luneraria/Art/";
/// <summary>Raised on the figure at the hit frame of attack and ability clips.</summary>
public const string ContactEvent = "OnLuneContact";
public override uint GetVersion() { return 3; }
void OnPreprocessModel()
{
if (!assetPath.StartsWith(ArtRoot) || !assetPath.EndsWith(".fbx"))
return;
ExportReport report = ExportReport.ForModel(assetPath);
var importer = (ModelImporter)assetImporter;
importer.globalScale = 1f;
importer.useFileScale = true;
importer.bakeAxisConversion = true;
importer.importCameras = false;
importer.importLights = false;
importer.importVisibility = false;
importer.importBlendShapes = false;
importer.importNormals = ModelImporterNormals.Import;
importer.importTangents = ModelImporterTangents.Import;
importer.meshCompression = ModelImporterMeshCompression.Off;
importer.isReadable = true;
importer.materialImportMode = ModelImporterMaterialImportMode.ImportViaMaterialDescription;
importer.materialLocation = ModelImporterMaterialLocation.InPrefab;
bool skinned = report != null && report.Skinned;
importer.animationType = skinned ? ModelImporterAnimationType.Generic : ModelImporterAnimationType.None;
importer.avatarSetup = skinned ? ModelImporterAvatarSetup.CreateFromThisModel : ModelImporterAvatarSetup.NoAvatar;
importer.importAnimation = skinned;
importer.optimizeGameObjects = false;
importer.animationCompression = ModelImporterAnimationCompression.Off;
importer.resampleCurves = false;
}
void OnPreprocessAnimation()
{
if (!assetPath.StartsWith(ArtRoot) || !assetPath.EndsWith(".fbx"))
return;
ExportReport report = ExportReport.ForModel(assetPath);
if (report == null || !report.Skinned)
return;
var importer = (ModelImporter)assetImporter;
var blender = report.Clips.ToDictionary(c => (string)c["name"], c => c);
var clips = new List<ModelImporterClipAnimation>();
foreach (ModelImporterClipAnimation clip in importer.defaultClipAnimations)
{
// Blender names takes "<armature>|<action>"; Unity keeps the action name.
string name = clip.takeName.Contains("|") ? clip.takeName.Substring(clip.takeName.LastIndexOf('|') + 1) : clip.takeName;
clip.name = name;
if (blender.TryGetValue(name, out var facts))
{
clip.loopTime = (bool)facts["loop"];
double contact = (double)facts["contact_seconds"];
double seconds = (double)facts["seconds"];
if (contact > 0.0 && seconds > 0.0)
clip.events = new[] { new AnimationEvent { functionName = ContactEvent, time = (float)(contact / seconds) } };
}
clips.Add(clip);
}
importer.clipAnimations = clips.ToArray();
}
}
}