Die Mid-Lane steht auf Zielqualität (stilisierter Realismus). Die übrige Karte bleibt auf dem U2-Stand, bis über den Weg entschieden ist. - U5A: tools/blender/bake_pbr.py backt sechs nahtlose PBR-Sätze (limestone, soil, bark, leaf, rock, water) mit Albedo, Normal aus dem Höhenfeld (Sobel) und ORM (R AO, G Rauigkeit, B Metall), je 2k. Torus-Mapping für Kantenfluss wie phase10/bake_bark.py. - U5B: Luneraria/Surface trägt _AlbedoMap, _NormalMap, _OrmMap triplanar über den Godot-Weltfarben; ORM steuert Rauigkeit, Metall und AO. LuneMaterialFactory bindet je Oberfläche den passenden Satz, LN_Rock den Felsensatz. - U5C: Mid-Lane-Licht — Nebel (ExponentialSquared, Dichte 0,0085), scharfe Schatten (Bias 0,02), Weißabgleich und Vignette, ACES und Bloom leicht angehoben. - U5D: Hit- und Angriffs-Flash über MaterialPropertyBlock (warm beim Angriff, weißglühend beim Treffer), Turmblitze als dreipunktiger Funke mit Breitenkurve und Farbverlauf. CaptureMidLane schreibt fünf Nachweisbilder nach docs/port/screenshots/u5/. - Leistung (macOS-Player, 116 Einheiten): 132 fps Übersicht, 182 fps Mid nah — 20–25 % unter U3 durch PBR, Normals und Nebel. Mid-Lane bleibt über 120 fps. - Tests: EditMode 29/29, PlayMode 13/13, Kerntests 457/457. Keine Regeländerung; Golden 51.221 Werte unverändert. - Grenze eingehalten: keine prozeduralen Gesichter, Haare oder Anatomie. Sculpts bleiben vorbehalten (Freigabe und Lizenzprüfung). - Dokumentation: U5-VISUAL (Umsetzung, Nachweis, Entscheidungsvorlage für die ganze Karte), PORT-QA (U5), PORT-STATUS, KNOWN-BEHAVIOR- DIFFERENCES. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
5.7 KiB
C#
126 lines
5.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Luneraria.Core;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
|
|
namespace Luneraria.View
|
|
{
|
|
/// <summary>
|
|
/// What a unit looks like (port of AssetLibrary.instantiate_fighter): the model id is the
|
|
/// unit definition's id (troops, monsters, heroes and champions carry their model there), the
|
|
/// boss is the Schlundkrone. Skinned figures (Ringborke for w04, Basaltbastion for g04, ...)
|
|
/// replace the rigid Phase-4 model of the same id, as in Godot. Rigid models keep their named
|
|
/// parts in the FBX; at first use they are combined into one mesh per material and cached
|
|
/// (Godot's rig groups did the same at load time).
|
|
/// </summary>
|
|
public sealed class LuneUnitLibrary : ScriptableObject
|
|
{
|
|
[Serializable]
|
|
public sealed class UnitModel
|
|
{
|
|
public string Id;
|
|
public GameObject Prefab;
|
|
/// <summary>Set for skinned figures; rigid models have none.</summary>
|
|
public RuntimeAnimatorController Controller;
|
|
}
|
|
|
|
public List<UnitModel> Models = new List<UnitModel>();
|
|
public string BossModel = "schlundkrone";
|
|
public string FallbackModel = "g01";
|
|
public LuneMaterialSet Materials;
|
|
public LuneGround Ground;
|
|
public Mesh RingRound;
|
|
public Mesh RingDiamond;
|
|
public Mesh RingHexagon;
|
|
public Mesh Quad;
|
|
public Material[] TeamRing = new Material[3];
|
|
public Material HeroRing;
|
|
public Material BarBack;
|
|
public Material[] BarFill = new Material[3];
|
|
public Material[] Bolt = new Material[3];
|
|
/// <summary>U5 spark colour of a tower shot (team light colour of lune_materials.gd).</summary>
|
|
public Color[] BoltLight = new Color[3];
|
|
/// <summary>Wide owner ring of a deployed boss (lune_unit_view.gd, G3): round for Blau, diamond for Rot.</summary>
|
|
public Mesh OwnerRingRound;
|
|
public Mesh OwnerRingDiamond;
|
|
public Material[] OwnerRing = new Material[2];
|
|
/// <summary>Fill of the Ancients' health bars (team light colour, lune_ancient_view.gd).</summary>
|
|
public Material[] AncientBarFill = new Material[2];
|
|
|
|
Dictionary<string, UnitModel> _index;
|
|
readonly Dictionary<GameObject, GameObject> _combined = new Dictionary<GameObject, GameObject>();
|
|
|
|
public UnitModel Find(string id)
|
|
{
|
|
if (_index == null)
|
|
{
|
|
_index = new Dictionary<string, UnitModel>();
|
|
foreach (UnitModel model in Models)
|
|
_index[model.Id] = model;
|
|
}
|
|
_index.TryGetValue(id, out UnitModel found);
|
|
return found;
|
|
}
|
|
|
|
/// <summary>Model id of a unit: the boss model for bosses, otherwise the definition id.</summary>
|
|
public string ModelId(LuneUnit unit)
|
|
{
|
|
return unit.Kind == "boss" ? BossModel : unit.Def.Id;
|
|
}
|
|
|
|
public GameObject PrefabFor(LuneUnit unit, out RuntimeAnimatorController controller)
|
|
{
|
|
UnitModel model = Find(ModelId(unit)) ?? Find(FallbackModel);
|
|
controller = model.Controller;
|
|
return controller != null ? model.Prefab : Combined(model.Prefab);
|
|
}
|
|
|
|
/// <summary>One-mesh template of a rigid figure (parts merged per material), built once.</summary>
|
|
GameObject Combined(GameObject source)
|
|
{
|
|
if (_combined.TryGetValue(source, out GameObject template) && template != null)
|
|
return template;
|
|
var groups = new Dictionary<Material, List<CombineInstance>>();
|
|
var order = new List<Material>();
|
|
foreach (MeshFilter filter in source.GetComponentsInChildren<MeshFilter>(true))
|
|
{
|
|
Material[] slots = filter.GetComponent<MeshRenderer>().sharedMaterials;
|
|
Matrix4x4 toRoot = source.transform.worldToLocalMatrix * filter.transform.localToWorldMatrix;
|
|
for (int sub = 0; sub < filter.sharedMesh.subMeshCount; sub++)
|
|
{
|
|
Material material = slots[Mathf.Min(sub, slots.Length - 1)];
|
|
if (!groups.TryGetValue(material, out List<CombineInstance> list))
|
|
{
|
|
groups[material] = list = new List<CombineInstance>();
|
|
order.Add(material);
|
|
}
|
|
list.Add(new CombineInstance { mesh = filter.sharedMesh, subMeshIndex = sub, transform = toRoot });
|
|
}
|
|
}
|
|
var parts = new CombineInstance[order.Count];
|
|
for (int i = 0; i < order.Count; i++)
|
|
{
|
|
var merged = new Mesh { indexFormat = IndexFormat.UInt32 };
|
|
merged.CombineMeshes(groups[order[i]].ToArray(), true, true);
|
|
parts[i] = new CombineInstance { mesh = merged, transform = Matrix4x4.identity };
|
|
}
|
|
var mesh = new Mesh { name = source.name + "_combined", indexFormat = IndexFormat.UInt32 };
|
|
mesh.CombineMeshes(parts, false, false);
|
|
mesh.RecalculateBounds();
|
|
foreach (CombineInstance part in parts)
|
|
{
|
|
if (Application.isPlaying)
|
|
Destroy(part.mesh);
|
|
else
|
|
DestroyImmediate(part.mesh);
|
|
}
|
|
template = new GameObject(source.name + "_combined") { hideFlags = HideFlags.HideAndDontSave };
|
|
template.SetActive(false);
|
|
template.AddComponent<MeshFilter>().sharedMesh = mesh;
|
|
template.AddComponent<MeshRenderer>().sharedMaterials = order.ToArray();
|
|
_combined[source] = template;
|
|
return template;
|
|
}
|
|
}
|
|
}
|