Alle 86 Modelle der Godot-Referenz laufen durch die Blender→FBX-Pipeline
(nach bestandenem Vertical Function Test); jede Einheit zeigt ihr eigenes
Modell.
- Exportmanifest aus den Godot-Manifesten (tools/blender/make_manifest.py):
Units, Enemies, Props, Characters, Peoples, Buildings, Environment; die
drei Vergleichs-GLBs als Aliase. Neu in der Pipeline: Tangenten berechnet
Unity mit MikkTSpace, Generatoren laufen in einer temporären Kopie, die
Grüne Kluft wird wie das GLB zusammengefasst, Texturen kommen mit.
1.616 Blender- und 1.247 Unity-Prüfungen grün, zwei Exportläufe
byteidentisch; Kontaktbögen je Gruppe.
- Einheitenbibliothek nach Godots AssetLibrary: 70 Einträge der
Einheitengruppen, Figuren mit Skelett ersetzen das starre Modell ihrer ID.
Starre Modelle werden zur Laufzeit je Material zusammengefasst; Controller
werden an Ort und Stelle aktualisiert.
- Tests mit allen Modellen: EditMode 11/11, PlayMode 2/2. Vertical Function
Test Seed 2 DCE95C96556F3A1A und Seed 9 ab Runde 21 7B8E43AEFC0D60C5,
beide gleich dem Kern (Nachweise mit LUNE_EVIDENCE=u2a-batch; die Belege
von U2C und U2D bleiben unverändert).
- macOS-Player mit allen Modellen: 158-160 fps in der Übersicht
(116 Einheiten), 205-207 fps auf der Mid-Lane; Haupt- und Render-Thread
etwa doppelt so lang wie mit dem Platzhalter, Present-Ausreißer kürzer.
- Befund iCloud Drive: Konfliktkopien ("Name 2.ext") im Build-Cache ließen
den Build die Einheitenbibliothek mit altem Feldlayout schreiben; der
Player meldete sharedassets0.assets als beschädigt. LunePlayerBuild
entfernt solche Kopien vor dem Build und bricht ab, wenn während des
Builds neue entstehen. Bereits committete Kopien: Controller-Duplikate und
World/Generated 2.meta entfernt, drei U2D-Benchmarkläufe als -run1 bis
-run3 benannt.
- Dokumentation: U2-WORLD (Batch-Import), PORT-STATUS (Entscheidungen:
Modelle direkt in Git, Projektort außerhalb von iCloud Drive), PORT-QA.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
261 lines
13 KiB
C#
261 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
|
|
namespace Luneraria.EditorTools
|
|
{
|
|
/// <summary>
|
|
/// Renders every pipeline asset in a neutral preview scene (URP, one sun, 3/4 front view) to
|
|
/// docs/port/screenshots/u2a/. Buildings show their intact state; the tower also its ruin;
|
|
/// rigged figures their bind pose and the hit frame of attack_a.
|
|
/// Unity -batchmode -projectPath . -executeMethod Luneraria.EditorTools.LuneAssetPreview.RenderAll
|
|
/// </summary>
|
|
public static class LuneAssetPreview
|
|
{
|
|
const int Width = 1600;
|
|
const int Height = 1000;
|
|
|
|
public static void RenderAll()
|
|
{
|
|
int code = 0;
|
|
try
|
|
{
|
|
string folder = Path.Combine(Path.GetDirectoryName(Application.dataPath), "docs/port/screenshots/u2a");
|
|
Directory.CreateDirectory(folder);
|
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|
var sun = new GameObject("Sun").AddComponent<Light>();
|
|
sun.type = LightType.Directional;
|
|
sun.intensity = 1.6f;
|
|
sun.color = new Color(1f, .89f, .74f);
|
|
sun.shadows = LightShadows.Soft;
|
|
sun.transform.rotation = Quaternion.Euler(48f, 150f, 0f);
|
|
RenderSettings.ambientMode = AmbientMode.Trilight;
|
|
RenderSettings.ambientSkyColor = new Color(.55f, .6f, .68f);
|
|
RenderSettings.ambientEquatorColor = new Color(.42f, .42f, .44f);
|
|
RenderSettings.ambientGroundColor = new Color(.2f, .19f, .18f);
|
|
var ground = GameObject.CreatePrimitive(PrimitiveType.Plane);
|
|
ground.transform.localScale = new Vector3(20f, 1f, 20f);
|
|
var camera = new GameObject("PreviewCamera").AddComponent<Camera>();
|
|
camera.clearFlags = CameraClearFlags.SolidColor;
|
|
camera.backgroundColor = new Color(.16f, .18f, .21f);
|
|
camera.fieldOfView = 30f;
|
|
foreach (string model in ExportReport.AllModels("Assets/Luneraria/Art").OrderBy(p => p, StringComparer.Ordinal))
|
|
{
|
|
ExportReport report = ExportReport.ForModel(model);
|
|
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(model);
|
|
foreach (var shot in Shots(report))
|
|
{
|
|
var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
|
|
shot.Value(instance);
|
|
Render(camera, instance, Path.Combine(folder, report.Id + shot.Key + ".png"));
|
|
UnityEngine.Object.DestroyImmediate(instance);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
Debug.LogError("LUNE_PREVIEW failed: " + error);
|
|
code = 1;
|
|
}
|
|
EditorApplication.Exit(code);
|
|
}
|
|
|
|
/// <summary>
|
|
/// One contact sheet per asset group (docs/port/screenshots/u2a-batch/<group>.png), six
|
|
/// columns, assets sorted by id; the order is written to docs/port/logs/u2a/sheets.json.
|
|
/// </summary>
|
|
public static void RenderSheets()
|
|
{
|
|
int code = 0;
|
|
try
|
|
{
|
|
const int tileWidth = 400, tileHeight = 300, columns = 6;
|
|
string folder = Path.Combine(Path.GetDirectoryName(Application.dataPath), "docs/port/screenshots/u2a-batch");
|
|
Directory.CreateDirectory(folder);
|
|
Camera camera = Stage();
|
|
var index = new Newtonsoft.Json.Linq.JObject();
|
|
var reports = ExportReport.AllModels("Assets/Luneraria/Art").Select(ExportReport.ForModel).Where(r => r != null);
|
|
foreach (var group in reports.GroupBy(r => (string)r.Data["group"]).OrderBy(g => g.Key, StringComparer.Ordinal))
|
|
{
|
|
var list = group.OrderBy(r => r.Id, StringComparer.Ordinal).ToList();
|
|
int rows = (list.Count + columns - 1) / columns;
|
|
var sheet = new Texture2D(columns * tileWidth, rows * tileHeight, TextureFormat.RGB24, false);
|
|
var fill = new Color32[sheet.width * sheet.height];
|
|
for (int i = 0; i < fill.Length; i++)
|
|
fill[i] = new Color32(28, 31, 36, 255);
|
|
sheet.SetPixels32(fill);
|
|
for (int i = 0; i < list.Count; i++)
|
|
{
|
|
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(list[i].Path.Replace(".export.json", ".fbx"));
|
|
var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
|
|
Shots(list[i]).First().Value(instance);
|
|
Color[] tile = RenderTile(camera, instance, tileWidth, tileHeight);
|
|
int column = i % columns;
|
|
int row = rows - 1 - i / columns;
|
|
sheet.SetPixels(column * tileWidth, row * tileHeight, tileWidth, tileHeight, tile);
|
|
UnityEngine.Object.DestroyImmediate(instance);
|
|
}
|
|
sheet.Apply();
|
|
File.WriteAllBytes(Path.Combine(folder, group.Key + ".png"), sheet.EncodeToPNG());
|
|
UnityEngine.Object.DestroyImmediate(sheet);
|
|
index[group.Key] = new Newtonsoft.Json.Linq.JArray(list.Select(r => r.Id));
|
|
Debug.Log("LUNE_PREVIEW sheet " + group.Key + ": " + list.Count + " assets");
|
|
}
|
|
string logs = Path.Combine(Path.GetDirectoryName(Application.dataPath), "docs/port/logs/u2a");
|
|
Directory.CreateDirectory(logs);
|
|
File.WriteAllText(Path.Combine(logs, "sheets.json"), index.ToString() + "\n");
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
Debug.LogError("LUNE_PREVIEW failed: " + error);
|
|
code = 1;
|
|
}
|
|
EditorApplication.Exit(code);
|
|
}
|
|
|
|
static Camera Stage()
|
|
{
|
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|
var sun = new GameObject("Sun").AddComponent<Light>();
|
|
sun.type = LightType.Directional;
|
|
sun.intensity = 1.6f;
|
|
sun.color = new Color(1f, .89f, .74f);
|
|
sun.shadows = LightShadows.Soft;
|
|
sun.transform.rotation = Quaternion.Euler(48f, 150f, 0f);
|
|
RenderSettings.ambientMode = AmbientMode.Trilight;
|
|
RenderSettings.ambientSkyColor = new Color(.55f, .6f, .68f);
|
|
RenderSettings.ambientEquatorColor = new Color(.42f, .42f, .44f);
|
|
RenderSettings.ambientGroundColor = new Color(.2f, .19f, .18f);
|
|
var camera = new GameObject("PreviewCamera").AddComponent<Camera>();
|
|
camera.clearFlags = CameraClearFlags.SolidColor;
|
|
camera.backgroundColor = new Color(.16f, .18f, .21f);
|
|
camera.fieldOfView = 30f;
|
|
return camera;
|
|
}
|
|
|
|
static Color[] RenderTile(Camera camera, GameObject root, int width, int height)
|
|
{
|
|
var renderers = root.GetComponentsInChildren<Renderer>().Where(r => r.enabled).ToList();
|
|
Bounds bounds = renderers[0].bounds;
|
|
foreach (Renderer renderer in renderers)
|
|
bounds.Encapsulate(renderer.bounds);
|
|
Vector3 direction = new Vector3(.62f, .42f, 1f).normalized;
|
|
float radius = bounds.extents.magnitude;
|
|
float distance = radius / Mathf.Sin(camera.fieldOfView * .5f * Mathf.Deg2Rad) * .92f;
|
|
camera.aspect = (float)width / height;
|
|
camera.transform.position = bounds.center + direction * distance;
|
|
camera.transform.LookAt(bounds.center);
|
|
camera.nearClipPlane = Mathf.Max(.01f, distance - radius * 2f);
|
|
camera.farClipPlane = distance + radius * 4f;
|
|
var target = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32) { antiAliasing = 8 };
|
|
camera.targetTexture = target;
|
|
camera.Render();
|
|
RenderTexture.active = target;
|
|
var image = new Texture2D(width, height, TextureFormat.RGB24, false);
|
|
image.ReadPixels(new Rect(0, 0, width, height), 0, 0);
|
|
image.Apply();
|
|
Color[] pixels = image.GetPixels();
|
|
RenderTexture.active = null;
|
|
camera.targetTexture = null;
|
|
UnityEngine.Object.DestroyImmediate(target);
|
|
UnityEngine.Object.DestroyImmediate(image);
|
|
return pixels;
|
|
}
|
|
|
|
static IEnumerable<KeyValuePair<string, Action<GameObject>>> Shots(ExportReport report)
|
|
{
|
|
string group = (string)report.Data["group"];
|
|
if (group == "Buildings")
|
|
{
|
|
yield return Shot("", go => Show(go, n => !n.StartsWith("Upper_Damaged") && !n.StartsWith("Debris") && !n.StartsWith("Ruin")));
|
|
if (report.Id == "grenzwacht")
|
|
yield return Shot("-ruin", go => Show(go, n => n.StartsWith("Ruin")));
|
|
}
|
|
else if (group == "Environment")
|
|
{
|
|
yield return Shot("", Row);
|
|
}
|
|
else if (report.Skinned)
|
|
{
|
|
yield return Shot("", go => Sample(go, "idle", 0f));
|
|
yield return Shot("-attack_a", go => Sample(go, "attack_a", .9f));
|
|
}
|
|
else
|
|
{
|
|
yield return Shot("", go => { });
|
|
}
|
|
}
|
|
|
|
static KeyValuePair<string, Action<GameObject>> Shot(string suffix, Action<GameObject> prepare)
|
|
{
|
|
return new KeyValuePair<string, Action<GameObject>>(suffix, prepare);
|
|
}
|
|
|
|
static void Show(GameObject root, Func<string, bool> visible)
|
|
{
|
|
foreach (Renderer renderer in root.GetComponentsInChildren<Renderer>(true))
|
|
renderer.enabled = visible(renderer.gameObject.name);
|
|
}
|
|
|
|
/// <summary>The prop library keeps every prop at the origin; the preview sets them out in a grid.</summary>
|
|
static void Row(GameObject root)
|
|
{
|
|
const int columns = 5;
|
|
const float cell = 7.5f;
|
|
int index = 0;
|
|
foreach (Transform child in root.transform.Cast<Transform>().OrderBy(t => t.name, StringComparer.Ordinal).ToList())
|
|
{
|
|
var renderer = child.GetComponent<Renderer>();
|
|
if (renderer == null)
|
|
continue;
|
|
Vector3 center = renderer.bounds.center;
|
|
var slot = new Vector3((index % columns) * cell, 0f, -(index / columns) * cell);
|
|
child.position += slot - new Vector3(center.x, 0f, center.z);
|
|
index++;
|
|
}
|
|
}
|
|
|
|
static void Sample(GameObject root, string clipName, float seconds)
|
|
{
|
|
string path = AssetDatabase.GetAssetPath(PrefabUtility.GetCorrespondingObjectFromSource(root));
|
|
AnimationClip clip = AssetDatabase.LoadAllAssetsAtPath(path).OfType<AnimationClip>().FirstOrDefault(c => c.name == clipName);
|
|
if (clip != null)
|
|
clip.SampleAnimation(root, seconds);
|
|
}
|
|
|
|
static void Render(Camera camera, GameObject root, string file)
|
|
{
|
|
var renderers = root.GetComponentsInChildren<Renderer>().Where(r => r.enabled).ToList();
|
|
Bounds bounds = renderers[0].bounds;
|
|
foreach (Renderer renderer in renderers)
|
|
bounds.Encapsulate(renderer.bounds);
|
|
// 3/4 front view: the models face +Z, the camera stands in front, to the right, above.
|
|
Vector3 direction = new Vector3(.62f, .42f, 1f).normalized;
|
|
float radius = bounds.extents.magnitude;
|
|
float distance = radius / Mathf.Sin(camera.fieldOfView * .5f * Mathf.Deg2Rad) * .92f;
|
|
camera.aspect = (float)Width / Height;
|
|
camera.transform.position = bounds.center + direction * distance;
|
|
camera.transform.LookAt(bounds.center);
|
|
camera.nearClipPlane = Mathf.Max(.01f, distance - radius * 2f);
|
|
camera.farClipPlane = distance + radius * 4f;
|
|
var target = new RenderTexture(Width, Height, 24, RenderTextureFormat.ARGB32) { antiAliasing = 8 };
|
|
camera.targetTexture = target;
|
|
camera.Render();
|
|
RenderTexture.active = target;
|
|
var image = new Texture2D(Width, Height, TextureFormat.RGB24, false);
|
|
image.ReadPixels(new Rect(0, 0, Width, Height), 0, 0);
|
|
image.Apply();
|
|
File.WriteAllBytes(file, image.EncodeToPNG());
|
|
RenderTexture.active = null;
|
|
camera.targetTexture = null;
|
|
UnityEngine.Object.DestroyImmediate(target);
|
|
UnityEngine.Object.DestroyImmediate(image);
|
|
Debug.Log("LUNE_PREVIEW wrote " + file);
|
|
}
|
|
}
|
|
}
|