champions-of-luneraria/Assets/Luneraria/Editor/Art/LuneModelPostprocessor.cs
Martin Stickelbroeck f0d1e9388c U5.5 Lane-Fassung: LOD für den Meshy-Kohlenbeißer
Lane-Kopie mit 4.998 statt 20.714 Dreiecken aus dem Generator (Nähte
verschweißt, Collapse, Skin-Gewichte bleiben), LODGroup aus den Namen,
Wechsel unter rund 10 % Bildhöhe; Porträts erzwingen die volle Fassung.
Stresstest 35 Kohlenbeißer: Mid 109,8 -> 120,2 fps, Karte 93,9 -> 103,8
(starr 133,3 / 106,4). Tests grün, Fingerabdrücke unverändert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:17:28 +02:00

101 lines
4.9 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 come from the file, tangents are computed with MikkTSpace (as in Blender), 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 5; }
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.CalculateMikk;
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;
}
/// <summary>
/// U5.5: a figure with a lane copy (meshes &lt;body&gt;_LOD0/_LOD1, Unity builds the LODGroup
/// from the names) switches to it below the screen height the generator names; the lane
/// copy is never culled, so a figure never vanishes at map zoom.
/// </summary>
void OnPostprocessModel(GameObject root)
{
if (!assetPath.StartsWith(ArtRoot) || !assetPath.EndsWith(".fbx"))
return;
LODGroup group = root.GetComponentInChildren<LODGroup>();
ExportReport report = ExportReport.ForModel(assetPath);
var lod = report?.Data["generator"]?["lod"];
if (group == null || lod == null)
return;
LOD[] lods = group.GetLODs();
if (lods.Length != 2)
return;
lods[0].screenRelativeTransitionHeight = (float)lod["lod0_screen"];
lods[1].screenRelativeTransitionHeight = 0f;
group.SetLODs(lods);
}
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();
}
}
}