Entscheidung des Projektinhabers vom 2. Oktober 2026: Weg 2 (Stufen) statt Gleichziehen oder Drosseln. Der Lankorridor trägt die vollen PBR-Sätze; Berge, Kluft und ferner Hintergrund behalten Farbe und Steinschicht. - LuneZones: Korridor von 22 m um eine Lane-Mittellinie, ein Gebäude oder die Grotte. NearLane entscheidet die Stufe eines Weltknotens. - Surface-Shader _Quality: 1 bindet Albedo, Normal und ORM triplanar; 0 lässt die neun Map-Samples weg und behält Farbe, Vertex-Tönung, Steinschicht und Emission — das U2-Aussehen der Berge. - LuneMaterialFactory: Spec.Quality, TerrainEdge.mat für die umgebenden Berge; Randmaterialien binden keine PBR-Sätze. - LuneWorldBuilder.ApplyTier: jeder Weltknoten jenseits des Korridors bekommt _Quality 0 über einen MaterialPropertyBlock. Gebäude, Einheiten, Wege, Wasser und der innere Boden bleiben Lane-Stufe. - Nachweis: screenshots/u5/edge-mountains.png (Randstufe) und lane-and-edge.png (Übergang Lane → Rand in einem Bild). - Leistung (macOS-Player, 116 Einheiten): 133 fps Übersicht, 180 fps Mid nah — auf dem Niveau des Slices, weil die Messfenster den Korridor sehen. Die Ersparnis liegt in Weitwinkeln und am Rand. - Tests: EditMode 29/29, PlayMode 13/13, Kerntests 457/457. Keine Regeländerung. - Dokumentation: U5-VISUAL (Entscheidung und Umsetzung), PORT-QA, PORT-STATUS, KNOWN-BEHAVIOR-DIFFERENCES. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
371 lines
19 KiB
C#
371 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Luneraria.View;
|
|
using Newtonsoft.Json.Linq;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace Luneraria.EditorTools
|
|
{
|
|
/// <summary>
|
|
/// Creates the shared materials (U2B) from the Godot material library
|
|
/// (scripts/luneraria/view/lune_materials.gd), the baked world materials and the prop
|
|
/// materials of map_props.fbx, with team variants for team surfaces. Every material uses
|
|
/// Luneraria/Surface (modes see the shader) or Luneraria/Pad. Idempotent: existing assets are
|
|
/// updated in place, so scene references stay valid.
|
|
/// </summary>
|
|
public static class LuneMaterialFactory
|
|
{
|
|
public const string Folder = "Assets/Luneraria/Materials";
|
|
public const string SetPath = Folder + "/LuneMaterials.asset";
|
|
const string Textures = "Assets/Luneraria/Art/Textures";
|
|
const string StoneMapPath = Textures + "/limestone-albedo.png";
|
|
|
|
// Surface modes of Luneraria/Surface.
|
|
public const int Plain = 0, Terrain = 1, Stone = 2, Paving = 3, Soil = 4, Bark = 5, Foliage = 6, Water = 7, Vein = 8, Crystal = 9;
|
|
|
|
/// <summary>Team style (Blau, Rot, neutral grotto) of lune_materials.gd TEAM_STYLE.</summary>
|
|
public static readonly Color[] TeamCrystal = { Hex("78c6ff"), Hex("ff7d45"), Hex("b784ff") };
|
|
public static readonly Color[] TeamRune = { Hex("86d2ff"), Hex("ffae5a"), Hex("c29cff") };
|
|
public static readonly Color[] TeamMetal = { new Color(.80f, .85f, .92f), new Color(.92f, .64f, .34f), new Color(.70f, .62f, .50f) };
|
|
public static readonly Color[] TeamCloth = { Hex("2a5aa8"), Hex("9a2c20"), Hex("45305f") };
|
|
public static readonly Color[] TeamLight = { Hex("8cc8ff"), Hex("ffae72"), Hex("b37cff") };
|
|
public static readonly Color[] TeamColor = { Hex("5aa8ff"), Hex("ff6a55"), Hex("b37cff") };
|
|
static readonly string[] TeamNames = { "Blau", "Rot", "Neutral" };
|
|
|
|
/// <summary>U5 PBR sets of tools/blender/bake_pbr.py: albedo, normal, ORM.</summary>
|
|
static readonly string[] PbrSets = { "limestone", "soil", "bark", "leaf", "rock", "water" };
|
|
|
|
sealed class Spec
|
|
{
|
|
public int Mode;
|
|
public Color Base = Color.white;
|
|
public float Roughness = .88f;
|
|
public float Metallic;
|
|
public Color Emission = Color.black;
|
|
public float Energy;
|
|
public bool VertexColor;
|
|
public bool DoubleSided;
|
|
public float StoneScale = .23f;
|
|
public float Relief = .016f;
|
|
/// <summary>U5 PBR set name (limestone, soil, bark, leaf, rock, water) or "" for none.</summary>
|
|
public string Pbr = "";
|
|
public float NormalStrength = 1f;
|
|
/// <summary>Weg 2: 1 = lane tier (full PBR), 0 = edge tier (colour + stone relief).</summary>
|
|
public float Quality = 1f;
|
|
}
|
|
|
|
static Shader _surface;
|
|
static Texture2D _stone;
|
|
static readonly Dictionary<string, Texture2D> _albedo = new Dictionary<string, Texture2D>();
|
|
static readonly Dictionary<string, Texture2D> _normal = new Dictionary<string, Texture2D>();
|
|
static readonly Dictionary<string, Texture2D> _orm = new Dictionary<string, Texture2D>();
|
|
|
|
public static Color Hex(string hex)
|
|
{
|
|
ColorUtility.TryParseHtmlString("#" + hex, out Color color);
|
|
return color;
|
|
}
|
|
|
|
/// <summary>
|
|
/// An HDR emission value for an [HDR] material colour: Godot's emission (sRGB colour, linear
|
|
/// energy multiplier) as the linear value Unity hands to the shader unchanged.
|
|
/// </summary>
|
|
public static Color Hdr(Color srgb, float energy)
|
|
{
|
|
Color linear = srgb.linear * energy;
|
|
return new Color(linear.r, linear.g, linear.b, 1f);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A plain (non-HDR) colour that Unity converts from gamma to linear, carrying a linear
|
|
/// value above one (RenderSettings.ambientLight = colour * energy).
|
|
/// </summary>
|
|
public static Color GammaEncoded(Color srgb, float energy)
|
|
{
|
|
Color linear = srgb.linear * energy;
|
|
return new Color(Mathf.LinearToGammaSpace(linear.r), Mathf.LinearToGammaSpace(linear.g), Mathf.LinearToGammaSpace(linear.b), 1f);
|
|
}
|
|
|
|
public static LuneMaterialSet Build(LuneWorldBake bake, out Dictionary<int, Material> worldMaterials)
|
|
{
|
|
Directory.CreateDirectory(Folder + "/Buildings");
|
|
Directory.CreateDirectory(Folder + "/World");
|
|
Directory.CreateDirectory(Folder + "/Props");
|
|
AssetDatabase.Refresh();
|
|
_surface = Shader.Find("Luneraria/Surface");
|
|
if (_surface == null)
|
|
throw new InvalidOperationException("shader Luneraria/Surface missing");
|
|
_stone = AssetDatabase.LoadAssetAtPath<Texture2D>(StoneMapPath);
|
|
_albedo.Clear();
|
|
_normal.Clear();
|
|
_orm.Clear();
|
|
foreach (string set in PbrSets)
|
|
{
|
|
_albedo[set] = AssetDatabase.LoadAssetAtPath<Texture2D>(Textures + "/" + set + "-albedo-2k.png");
|
|
_normal[set] = AssetDatabase.LoadAssetAtPath<Texture2D>(Textures + "/" + set + "-normal-2k.png");
|
|
_orm[set] = AssetDatabase.LoadAssetAtPath<Texture2D>(Textures + "/" + set + "-orm-2k.png");
|
|
}
|
|
var setAsset = AssetDatabase.LoadAssetAtPath<LuneMaterialSet>(SetPath);
|
|
if (setAsset == null)
|
|
{
|
|
setAsset = ScriptableObject.CreateInstance<LuneMaterialSet>();
|
|
AssetDatabase.CreateAsset(setAsset, SetPath);
|
|
}
|
|
setAsset.Entries.Clear();
|
|
BuildingMaterials(setAsset);
|
|
PropMaterials(setAsset);
|
|
worldMaterials = WorldMaterials(bake);
|
|
EditorUtility.SetDirty(setAsset);
|
|
AssetDatabase.SaveAssets();
|
|
return setAsset;
|
|
}
|
|
|
|
// ------------------------------------------------------------ G2 buildings and boss (LN_*)
|
|
static void BuildingMaterials(LuneMaterialSet set)
|
|
{
|
|
Fixed(set, "LN_Stone_Pale", new Spec { Mode = Stone, Base = new Color(.72f, .68f, .61f), VertexColor = true, Pbr = "limestone" });
|
|
Fixed(set, "LN_Stone_Dark", new Spec { Mode = Stone, Base = new Color(.30f, .30f, .33f), Roughness = .9f, VertexColor = true, Pbr = "limestone" });
|
|
Fixed(set, "LN_Stone_Grotto", new Spec { Mode = Stone, Base = new Color(.43f, .43f, .46f), VertexColor = true, Pbr = "limestone" });
|
|
Fixed(set, "LN_Rock", new Spec { Mode = Stone, Base = new Color(.38f, .38f, .40f), Roughness = .93f, VertexColor = true, StoneScale = .13f, Pbr = "rock" });
|
|
Teams(set, "LN_Metal_Team", team => new Spec { Mode = Plain, Base = TeamMetal[team] * .92f, Metallic = .9f, Roughness = .32f, VertexColor = true });
|
|
Fixed(set, "LN_Metal_Iron", new Spec { Mode = Plain, Base = new Color(.24f, .24f, .26f), Metallic = .8f, Roughness = .52f, VertexColor = true });
|
|
Fixed(set, "LN_Metal_Bronze", new Spec { Mode = Plain, Base = new Color(.66f, .47f, .24f), Metallic = .9f, Roughness = .42f, VertexColor = true });
|
|
Teams(set, "LN_Crystal_Team", team => new Spec { Mode = Crystal, Base = TeamCrystal[team], Emission = TeamCrystal[team], Energy = 3.2f });
|
|
Fixed(set, "LN_Crystal_Grotto", new Spec { Mode = Crystal, Base = TeamCrystal[2], Emission = TeamCrystal[2], Energy = 3.2f });
|
|
// Godot lights only the glyph strokes of a rune band; a dim band stands in for them.
|
|
Teams(set, "LN_Rune_Team", team => new Spec { Mode = Plain, Base = new Color(.16f, .16f, .18f), Emission = TeamRune[team], Energy = .45f, Roughness = .5f, VertexColor = true });
|
|
Fixed(set, "LN_Rune_Grotto", new Spec { Mode = Plain, Base = new Color(.16f, .16f, .18f), Emission = TeamRune[2], Energy = .45f, Roughness = .5f, VertexColor = true });
|
|
Teams(set, "LN_Cloth_Team", team => new Spec { Mode = Plain, Base = TeamCloth[team], Roughness = .85f, DoubleSided = true, VertexColor = true });
|
|
Fixed(set, "LN_Water_Glow", new Spec { Mode = Plain, Base = new Color(.45f, .35f, .80f) * .3f, Emission = new Color(.45f, .35f, .80f), Energy = 1.2f, Roughness = .1f });
|
|
Fixed(set, "LN_Boss_Hide", new Spec { Mode = Plain, Base = new Color(.24f, .25f, .29f), Roughness = .84f, VertexColor = true });
|
|
Fixed(set, "LN_Boss_Bone", new Spec { Mode = Plain, Base = new Color(.74f, .71f, .64f), Roughness = .62f, VertexColor = true });
|
|
Fixed(set, "LN_Boss_Bronze", new Spec { Mode = Plain, Base = new Color(.42f, .31f, .18f), Metallic = .85f, Roughness = .42f, VertexColor = true });
|
|
// The grotto boss glows in the neutral colour; a deployed boss in its team's.
|
|
Teams(set, "LN_Crystal_Boss", team => new Spec { Mode = Crystal, Base = TeamCrystal[team], Emission = TeamCrystal[team], Energy = 2.4f }, neutral: true);
|
|
Fixed(set, "LN_Wood", new Spec { Mode = Plain, Base = new Color(.33f, .23f, .15f), Roughness = .82f, VertexColor = true });
|
|
Fixed(set, "LN_Cave", new Spec { Mode = Plain, Base = new Color(.02f, .015f, .03f), Roughness = 1f });
|
|
}
|
|
|
|
// ------------------------------------------------------------ props of map_props.fbx (ENV_*)
|
|
static void PropMaterials(LuneMaterialSet set)
|
|
{
|
|
const string props = "Assets/Luneraria/Art/Environment/map_props.fbx";
|
|
foreach (Material source in AssetDatabase.LoadAllAssetsAtPath(props).OfType<Material>())
|
|
{
|
|
string key = source.name;
|
|
Color color = source.HasProperty("_BaseColor") ? source.GetColor("_BaseColor") : source.color;
|
|
Spec spec;
|
|
if (key.Contains("brass"))
|
|
// Godot keeps the glTF material for brass (no Kluft surface).
|
|
spec = new Spec { Mode = Plain, Base = color, Metallic = source.HasProperty("_Metallic") ? source.GetFloat("_Metallic") : .8f, Roughness = .4f };
|
|
else if (key.StartsWith("ENV_"))
|
|
spec = new Spec { Mode = SurfaceMode(key), Base = color };
|
|
else
|
|
spec = new Spec { Mode = Plain, Base = color, Roughness = .8f, DoubleSided = true };
|
|
Add(set, key, Create(Folder + "/Props/" + key + ".mat", spec), null, null);
|
|
}
|
|
}
|
|
|
|
public static int SurfaceMode(string key)
|
|
{
|
|
if (key.Contains("leaf"))
|
|
return Foliage;
|
|
if (key.Contains("bark"))
|
|
return Bark;
|
|
if (key.Contains("soil"))
|
|
return Soil;
|
|
if (key.Contains("paving"))
|
|
return Paving;
|
|
return Stone;
|
|
}
|
|
|
|
// ------------------------------------------------------------ baked world materials
|
|
static Dictionary<int, Material> WorldMaterials(LuneWorldBake bake)
|
|
{
|
|
var result = new Dictionary<int, Material>();
|
|
var materials = (JArray)bake.Data["materials"];
|
|
var veins = 0;
|
|
for (int index = 0; index < materials.Count; index++)
|
|
{
|
|
var entry = (JObject)materials[index];
|
|
string type = (string)entry["type"];
|
|
string shader = (string)entry["shader"] ?? "";
|
|
string key = (string)entry["surface_key"];
|
|
JObject parameters = (JObject)entry["params"] ?? new JObject();
|
|
string name;
|
|
Spec spec;
|
|
if (key != null)
|
|
{
|
|
name = key;
|
|
spec = new Spec { Mode = SurfaceMode(key), Base = ColorOf(parameters["base_color"]) };
|
|
}
|
|
else if (shader.EndsWith("lune_terrain.gdshader"))
|
|
{
|
|
name = "Terrain";
|
|
spec = new Spec { Mode = Terrain, StoneScale = .21f };
|
|
result[index] = Create(Folder + "/World/" + name + ".mat", spec);
|
|
// Weg 2 edge tier: the surrounding mountains keep colour and stone relief only.
|
|
Create(Folder + "/World/TerrainEdge.mat", new Spec { Mode = Terrain, StoneScale = .21f, Quality = 0f });
|
|
continue;
|
|
}
|
|
else if (shader.EndsWith("river.gdshader"))
|
|
{
|
|
name = "River";
|
|
spec = new Spec { Mode = Water };
|
|
}
|
|
else if (shader.EndsWith("lune_vein.gdshader"))
|
|
{
|
|
// Vein tint and seed are per-instance parameters in Godot; the builder assigns
|
|
// one material per vein (see VeinMaterial).
|
|
continue;
|
|
}
|
|
else if (shader.EndsWith("lune_pad.gdshader"))
|
|
{
|
|
Color tint = ColorOf(parameters["tint"]);
|
|
int team = tint.r > tint.b ? 1 : 0;
|
|
name = "Pad_" + TeamNames[team];
|
|
result[index] = PadMaterial(Folder + "/World/" + name + ".mat", tint, (float)parameters["pad_length"], (float)parameters["row_start"]);
|
|
continue;
|
|
}
|
|
else if (type == "standard")
|
|
{
|
|
Color albedo = ColorOf(entry["albedo"]);
|
|
bool glow = entry["emission"] != null;
|
|
name = "Glow_" + ColorUtility.ToHtmlStringRGB(albedo) + (glow ? "_" + ((float)entry["emission_energy"]).ToString("0.0#", System.Globalization.CultureInfo.InvariantCulture) : "");
|
|
spec = new Spec { Mode = Plain, Base = albedo, Roughness = (float)entry["roughness"], Metallic = (float)entry["metallic"] };
|
|
if (glow)
|
|
{
|
|
spec.Emission = ColorOf(entry["emission"]);
|
|
spec.Energy = (float)entry["emission_energy"];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidOperationException("unknown world material " + entry);
|
|
}
|
|
result[index] = Create(Folder + "/World/" + name + ".mat", spec);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>One material per energy vein (tint from its team, seed from the bake).</summary>
|
|
public static Material VeinMaterial(string name, Color tint, float seed)
|
|
{
|
|
Material material = Create(Folder + "/World/" + name + ".mat", new Spec { Mode = Vein, Base = tint });
|
|
material.SetFloat("_Seed", seed);
|
|
material.SetFloat("_Energy", 1f);
|
|
EditorUtility.SetDirty(material);
|
|
return material;
|
|
}
|
|
|
|
static Material PadMaterial(string path, Color tint, float length, float rowStart)
|
|
{
|
|
Shader shader = Shader.Find("Luneraria/Pad");
|
|
var material = AssetDatabase.LoadAssetAtPath<Material>(path);
|
|
if (material == null)
|
|
{
|
|
material = new Material(shader);
|
|
AssetDatabase.CreateAsset(material, path);
|
|
}
|
|
material.shader = shader;
|
|
material.SetColor("_Tint", tint);
|
|
material.SetFloat("_PadLength", length);
|
|
material.SetFloat("_RowStart", rowStart);
|
|
EditorUtility.SetDirty(material);
|
|
return material;
|
|
}
|
|
|
|
public static Color ColorOf(JToken values)
|
|
{
|
|
return new Color((float)values[0], (float)values[1], (float)values[2], values.Count() > 3 ? (float)values[3] : 1f);
|
|
}
|
|
|
|
static void Fixed(LuneMaterialSet set, string key, Spec spec)
|
|
{
|
|
Add(set, key, Create(Folder + "/Buildings/" + key + ".mat", spec), null, null);
|
|
}
|
|
|
|
static void Teams(LuneMaterialSet set, string key, Func<int, Spec> spec, bool neutral = true)
|
|
{
|
|
Material blau = Create(Folder + "/Buildings/" + key + "_Blau.mat", spec(0));
|
|
Material rot = Create(Folder + "/Buildings/" + key + "_Rot.mat", spec(1));
|
|
Material none = neutral ? Create(Folder + "/Buildings/" + key + "_Neutral.mat", spec(2)) : blau;
|
|
Add(set, key, none, blau, rot);
|
|
}
|
|
|
|
static void Add(LuneMaterialSet set, string key, Material neutral, Material blau, Material rot)
|
|
{
|
|
set.Entries.Add(new LuneMaterialSet.Entry { Key = key, Neutral = neutral, Blau = blau, Rot = rot });
|
|
}
|
|
|
|
static Material Create(string path, Spec spec)
|
|
{
|
|
var material = AssetDatabase.LoadAssetAtPath<Material>(path);
|
|
if (material == null)
|
|
{
|
|
material = new Material(_surface);
|
|
AssetDatabase.CreateAsset(material, path);
|
|
}
|
|
material.shader = _surface;
|
|
material.enableInstancing = true;
|
|
material.SetColor("_BaseColor", spec.Base);
|
|
material.SetFloat("_Mode", spec.Mode);
|
|
material.SetFloat("_VertexColor", spec.VertexColor ? 1f : 0f);
|
|
material.SetFloat("_Metallic", spec.Metallic);
|
|
material.SetFloat("_Roughness", spec.Roughness);
|
|
material.SetColor("_EmissionColor", spec.Energy > 0f ? Hdr(spec.Emission, spec.Energy) : Color.black);
|
|
material.SetFloat("_StoneScale", spec.StoneScale);
|
|
material.SetFloat("_Relief", spec.Relief);
|
|
material.SetFloat("_NormalStrength", spec.NormalStrength);
|
|
material.SetFloat("_Quality", spec.Quality);
|
|
material.SetFloat("_Cull", spec.DoubleSided ? 0f : 2f);
|
|
material.SetTexture("_StoneMap", _stone);
|
|
ApplyPbr(material, spec);
|
|
material.globalIlluminationFlags = MaterialGlobalIlluminationFlags.None;
|
|
EditorUtility.SetDirty(material);
|
|
return material;
|
|
}
|
|
|
|
/// <summary>U5: bind the baked albedo/normal/ORM set of the surface family (lane tier only).</summary>
|
|
static void ApplyPbr(Material material, Spec spec)
|
|
{
|
|
string key = spec.Pbr;
|
|
if (key.Length == 0)
|
|
key = PbrForMode(spec.Mode);
|
|
Texture2D albedo;
|
|
if (spec.Quality < .5f || key.Length == 0 || !_albedo.TryGetValue(key, out albedo) || albedo == null)
|
|
{
|
|
material.SetTexture("_AlbedoMap", Texture2D.whiteTexture);
|
|
material.SetTexture("_NormalMap", Texture2D.normalTexture);
|
|
material.SetTexture("_OrmMap", Texture2D.whiteTexture);
|
|
return;
|
|
}
|
|
material.SetTexture("_AlbedoMap", albedo);
|
|
material.SetTexture("_NormalMap", _normal[key]);
|
|
material.SetTexture("_OrmMap", _orm[key]);
|
|
}
|
|
|
|
public static string PbrForMode(int mode)
|
|
{
|
|
switch (mode)
|
|
{
|
|
case Stone:
|
|
case Paving:
|
|
return "limestone";
|
|
case Soil:
|
|
return "soil";
|
|
case Bark:
|
|
return "bark";
|
|
case Foliage:
|
|
return "leaf";
|
|
case Water:
|
|
return "water";
|
|
default:
|
|
return mode == Terrain ? "limestone" : "";
|
|
}
|
|
}
|
|
}
|
|
}
|