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>
101 lines
4.9 KiB
C#
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 <body>_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();
|
|
}
|
|
}
|
|
}
|