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>
78 lines
3.8 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|