champions-of-luneraria/Assets/Luneraria/Editor/World/LuneMaterialFactory.cs
Martin Stickelbroeck 4883d95a2f U5.4 Figuren: Surface-Materialien, Randlicht, SSAO
Die Figuren (Einheiten, Monster, Völker, Helden) bekommen den Materialweg
der Welt; dazu Umgebungsverdeckung für die ganze Szene.

- Surface-Materialien für alle 146 Figurenmaterialien (142 neu unter
  Materials/Units/, die LN_Boss_* gehörten schon zu den Gebäuden). Farbe,
  Rauigkeit und Emission bleiben die Blender-Werte; Metalle werden
  metallisch (vorher war keines der 414 Figurenmaterialien metallisch),
  ein Randlicht hebt die Silhouetten im Draufblick vom Boden ab.
- Oberflächenstruktur je Materialfamilie im eigenen Raum der Figur, nur
  in den HUD-Porträts. Im Feld (30-60 px hohe Figuren) war sie nicht zu
  sehen und kostete rund 10 % Bildrate.
- Ringborkes 4k-Satz liegt am Schnittholz (RB_Cutwood), wohin er gehört;
  U5.3 hatte ihn an die Rinde gebunden. Verwaistes RB_Bark.mat entfernt.
- SSAO über LuneProjectSetup: nach den opaken Objekten, halbe Auflösung,
  Radius 0,35 m, Ausblendung ab 260 m. Standardmäßig an (Entscheidung des
  Projektinhabers), in den Optionen abschaltbar, -luneSsao 0 für eine
  Sitzung. Die Variante mit Tiefen-Normalen-Vorpass kostete 19-28 %.
- Shader: Steinreliefkarte nur noch für Gelände, Stein, Pflaster, Erde.
- Werkzeug: Übersichtsbilder mit LUNE_DRESS=1 eingekleidet wie im Spiel;
  das Materialset wird nach der leeren Vorschauszene geladen (sie entlädt
  unreferenzierte Assets).
- Tests: EditMode 29/29, PlayMode 14/14, Verify 718/0, Player-Selbsttest
  30/30 (4 neue zur SSAO-Option), Fingerabdrücke unverändert
  (DCE95C96556F3A1A, 7B8E43AEFC0D60C5).
- Leistung (A/B mit U5.3 im selben Lauf): Mid nah 177,7 -> 148,5 fps
  (-16 %), Karte 127,8 -> 111,5 fps (-13 %); ohne SSAO -3,5 / -4,5 %.
- Doku: U5-VISUAL (U5.4), PORT-STATUS, PORT-QA; Bilder screenshots/u5.4/.

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

652 lines
33 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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", "metal", "cloth", "hide", "bone", "wood" };
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;
/// <summary>U5.3: 1 = sample the authored UVs of a character (Ringborke's cut wood), 0 = triplanar.</summary>
public bool UvMaps;
/// <summary>Authored UV textures of a character; overrides the baked set and carry its colour.</summary>
public Texture2D UvAlbedo;
public Texture2D UvNormal;
public Texture2D UvOrm;
/// <summary>U5.4: triplanar in the mesh's own space (units), not the world.</summary>
public bool ObjectSpace;
/// <summary>U5.4: emission taken over as it is (the imported material of a unit); overrides Emission/Energy.</summary>
public Color? EmissionRaw;
/// <summary>U5.4: rim light strength (units; 0 = none).</summary>
public float Rim;
/// <summary>U5.4: bind the PBR set even when Quality is 0 (units: the portraits switch it on).</summary>
public bool BindMaps;
}
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>();
/// <summary>Mean linear value of a map (albedo colour or ORM channels), measured once per build.</summary>
static readonly Dictionary<Texture2D, Vector4> _means = new Dictionary<Texture2D, Vector4>();
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();
_means.Clear();
foreach (string set in PbrSets)
{
_albedo[set] = Map(Textures + "/" + set + "-albedo-2k.png", TextureImporterType.Default, true);
_normal[set] = Map(Textures + "/" + set + "-normal-2k.png", TextureImporterType.NormalMap, false);
_orm[set] = Map(Textures + "/" + set + "-orm-2k.png", TextureImporterType.Default, false);
}
var setAsset = AssetDatabase.LoadAssetAtPath<LuneMaterialSet>(SetPath);
if (setAsset == null)
{
setAsset = ScriptableObject.CreateInstance<LuneMaterialSet>();
AssetDatabase.CreateAsset(setAsset, SetPath);
}
setAsset.Entries.Clear();
BuildingMaterials(setAsset);
PropMaterials(setAsset);
UnitMaterials(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" });
// U5.3: metal, cloth, hide, bone and wood carry their own baked PBR set (target
// quality of every figure along the lanes; the generators keep their geometry).
Teams(set, "LN_Metal_Team", team => new Spec { Mode = Plain, Base = TeamMetal[team] * .92f, Metallic = .9f, Roughness = .32f, VertexColor = true, Pbr = "metal" });
Fixed(set, "LN_Metal_Iron", new Spec { Mode = Plain, Base = new Color(.24f, .24f, .26f), Metallic = .8f, Roughness = .52f, VertexColor = true, Pbr = "metal" });
Fixed(set, "LN_Metal_Bronze", new Spec { Mode = Plain, Base = new Color(.66f, .47f, .24f), Metallic = .9f, Roughness = .42f, VertexColor = true, Pbr = "metal" });
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, Pbr = "cloth" });
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, Pbr = "hide" });
Fixed(set, "LN_Boss_Bone", new Spec { Mode = Plain, Base = new Color(.74f, .71f, .64f), Roughness = .62f, VertexColor = true, Pbr = "bone" });
Fixed(set, "LN_Boss_Bronze", new Spec { Mode = Plain, Base = new Color(.42f, .31f, .18f), Metallic = .85f, Roughness = .42f, VertexColor = true, Pbr = "metal" });
// 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, Pbr = "wood" });
Fixed(set, "LN_Cave", new Spec { Mode = Plain, Base = new Color(.02f, .015f, .03f), Roughness = 1f });
}
// ------------------------------------------------------------ unit figures (U5.4)
static readonly string[] UnitGroups = { "Units", "Enemies", "Peoples", "Characters" };
const string ArtRoot = "Assets/Luneraria/Art/";
/// <summary>Repeats of a set per metre of a figure: one per metre, so the detail reads at hero zoom and in the portraits.</summary>
public const float UnitDetailScale = 1f;
/// <summary>Rim light of the figures (see LuneSurface.shader).</summary>
public const float UnitRim = .35f;
/// <summary>
/// One shared Surface material per material name of the figures (units, monsters, peoples,
/// heroes; 146 names). The imported colour, roughness and emission stay, metals become
/// metallic and a rim light lifts the silhouettes (U5.4). In the field the figures sample
/// no maps (Quality 0): 30–60 px tall, the detail would not read but cost 10 % frame rate.
/// The family's baked set is bound, and the HUD portraits switch it on in the figure's
/// own space (LunePortraits). Names the building materials already own (the Schlundkrone's
/// LN_Boss_*) stay theirs. Dress swaps them in, in the game, the portraits and the sheets.
/// </summary>
static void UnitMaterials(LuneMaterialSet set)
{
var known = new HashSet<string>(set.Entries.Select(e => e.Key));
var imported = new SortedDictionary<string, Material>(StringComparer.Ordinal);
foreach (string group in UnitGroups)
{
foreach (string path in Directory.GetFiles(ArtRoot + group, "*.fbx").OrderBy(p => p, StringComparer.Ordinal))
{
foreach (Material material in AssetDatabase.LoadAllAssetsAtPath(path).OfType<Material>())
{
if (!known.Contains(material.name) && !imported.ContainsKey(material.name))
imported[material.name] = material;
}
}
}
Directory.CreateDirectory(Folder + "/Units");
foreach (KeyValuePair<string, Material> pair in imported)
Add(set, pair.Key, Create(Folder + "/Units/" + pair.Key + ".mat", UnitSpec(pair.Key, pair.Value)), null, null);
}
/// <summary>Plain Surface material of a figure: imported colour, roughness and emission plus the family's set.</summary>
static Spec UnitSpec(string name, Material imported)
{
Color baseColor = imported.HasProperty("_BaseColor") ? imported.GetColor("_BaseColor") : Color.white;
float smoothness = imported.HasProperty("_Smoothness") ? imported.GetFloat("_Smoothness") : .3f;
Color emission = imported.HasProperty("_EmissionColor") ? imported.GetColor("_EmissionColor") : Color.black;
var spec = new Spec
{
Mode = Plain,
Base = baseColor,
Roughness = Mathf.Clamp(1f - smoothness, .12f, .95f),
ObjectSpace = true,
StoneScale = UnitDetailScale,
NormalStrength = .8f,
EmissionRaw = emission,
Rim = UnitRim,
Quality = 0f,
BindMaps = true,
};
string family = UnitFamily(name);
if (family == "cutwood")
{
// Ringborke's cut log faces carry the authored 4k set (bake_cutwood.py of phase 11).
Texture2D albedo = Map(Textures + "/ringborke-cut-albedo-4k.png", TextureImporterType.Default, true);
Texture2D normal = Map(Textures + "/ringborke-cut-normal-4k.png", TextureImporterType.NormalMap, false);
Texture2D rough = AssetDatabase.LoadAssetAtPath<Texture2D>(Textures + "/ringborke-cut-roughness-4k.png");
if (albedo != null && normal != null && rough != null)
{
spec.UvMaps = true;
spec.UvAlbedo = albedo;
spec.UvNormal = normal;
spec.UvOrm = PackRoughnessOrm(rough);
spec.Base = Color.white;
spec.Roughness = 1f;
spec.NormalStrength = 1f;
// The authored map is the colour itself, so it is sampled in the field too.
spec.Quality = 1f;
}
return spec;
}
switch (family)
{
case "metal":
spec.Pbr = "metal";
spec.Metallic = .7f;
break;
case "paint":
// Painted plates of the Nietenkonvent: the metal set's dents, little metal shine.
spec.Pbr = "metal";
spec.Metallic = .2f;
break;
case "rock":
case "bone":
case "wood":
case "bark":
case "leaf":
case "cloth":
case "hide":
spec.Pbr = family;
break;
}
return spec;
}
/// <summary>
/// Material family of a figure's material name (U5.4): the faction palettes by slot, then
/// keywords. Emissive, glass, skin and dark parts keep a plain surface without a set.
/// </summary>
public static string UnitFamily(string name)
{
string n = name.ToLowerInvariant();
if (n == "rb_cutwood")
return "cutwood";
if (n.Contains("nietenkonvent_"))
return n.Contains("_light_") ? "bone" : "paint";
if (n.Contains("nebelchor_") || n.Contains("neutral_"))
{
if (n.Contains("_light_"))
return "bone";
if (n.Contains("_shadow_") || n.Contains("_rift_"))
return "plain";
return "cloth";
}
if (Any(n, "ember", "_hot", "_rift", "glow", "lamp", "spark", "crystal", "magma", "saplight", "spring", "_w_water"))
return "emissive";
if (Any(n, "metal", "gold", "bronze", "brass", "iron"))
return "metal";
if (Any(n, "glass", "lens"))
return "glass";
if (Any(n, "basalt", "obsidian", "stone", "_edge", "joint"))
return "rock";
if (Any(n, "bone", "ceramic", "clay"))
return "bone";
if (Any(n, "wood", "heart", "endgrain", "groove"))
return "wood";
if (Any(n, "bark", "vein", "crevice"))
return "bark";
if (Any(n, "leaf", "moss", "petal", "vine"))
return "leaf";
if (Any(n, "cloth", "hood", "fur", "hair"))
return "cloth";
if (Any(n, "leather", "boot", "glove"))
return "hide";
return "plain";
}
static bool Any(string text, params string[] words)
{
foreach (string word in words)
{
if (text.Contains(word))
return true;
}
return false;
}
/// <summary>
/// A PBR map with the import its data needs (U5.3 finding): albedo is sRGB colour, normal
/// maps import as normal maps, ORM as linear data. The U5 sets imported all three as sRGB
/// colour, so the GPU gamma-decoded normals (tilted surfaces) and AO/roughness (darker,
/// glossier). Changed settings are written to the .meta and reimported once.
/// </summary>
static Texture2D Map(string path, TextureImporterType type, bool srgb)
{
var importer = AssetImporter.GetAtPath(path) as TextureImporter;
if (importer != null && (importer.textureType != type || importer.sRGBTexture != srgb))
{
importer.textureType = type;
importer.sRGBTexture = srgb;
importer.SaveAndReimport();
}
return AssetDatabase.LoadAssetAtPath<Texture2D>(path);
}
/// <summary>
/// Mean linear value of a map (every 7th pixel of its PNG; sRGB maps decoded to linear).
/// The shader divides by it, so a map adds detail around the Godot colour and roughness of
/// the material instead of multiplying them down (U5 drew the albedo maps over the Godot
/// colours: the terrain lost 80–95 % of its brightness and all of its green).
/// </summary>
static Vector4 Mean(Texture2D map, bool srgb)
{
if (map == null)
return Vector4.one;
if (_means.TryGetValue(map, out Vector4 mean))
return mean;
var probe = new Texture2D(2, 2, TextureFormat.RGBA32, false, true);
probe.LoadImage(File.ReadAllBytes(AssetDatabase.GetAssetPath(map)));
Color32[] pixels = probe.GetPixels32();
UnityEngine.Object.DestroyImmediate(probe);
var decode = new double[256];
for (int i = 0; i < 256; i++)
decode[i] = srgb ? Mathf.GammaToLinearSpace(i / 255f) : i / 255.0;
double r = 0, g = 0, b = 0;
int n = 0;
for (int i = 0; i < pixels.Length; i += 7)
{
r += decode[pixels[i].r];
g += decode[pixels[i].g];
b += decode[pixels[i].b];
n++;
}
mean = n > 0 ? new Vector4((float)(r / n), (float)(g / n), (float)(b / n), 1f) : Vector4.one;
_means[map] = mean;
return mean;
}
/// <summary>ORM from a grayscale roughness map (R ao = 1, G roughness, B metal = 0).</summary>
static Texture2D PackRoughnessOrm(Texture2D roughness)
{
string path = Textures + "/ringborke-cut-orm-4k.png";
var existing = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
if (existing != null)
return existing;
string roughPath = AssetDatabase.GetAssetPath(roughness);
var sourceImporter = AssetImporter.GetAtPath(roughPath) as TextureImporter;
if (sourceImporter != null && !sourceImporter.isReadable)
{
sourceImporter.isReadable = true;
sourceImporter.SaveAndReimport();
}
int width = roughness.width;
int height = roughness.height;
var orm = new Texture2D(width, height, TextureFormat.RGB24, false, true);
Color[] source = roughness.GetPixels();
var packed = new Color[source.Length];
for (int i = 0; i < source.Length; i++)
packed[i] = new Color(1f, source[i].r, 0f);
orm.SetPixels(packed);
orm.Apply(false, false);
File.WriteAllBytes(path, orm.EncodeToPNG());
UnityEngine.Object.DestroyImmediate(orm);
AssetDatabase.ImportAsset(path);
Texture2D imported = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
if (imported != null)
{
var importer = AssetImporter.GetAtPath(path) as TextureImporter;
if (importer != null && importer.sRGBTexture)
{
importer.sRGBTexture = false;
importer.SaveAndReimport();
}
}
return imported != null ? imported : roughness;
}
// ------------------------------------------------------------ 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.EmissionRaw ?? (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("_UvMaps", spec.UvMaps ? 1f : 0f);
material.SetFloat("_ObjectSpace", spec.ObjectSpace ? 1f : 0f);
material.SetFloat("_Rim", spec.Rim);
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 (lane tier) or the authored UVs of a character.</summary>
static void ApplyPbr(Material material, Spec spec)
{
if (spec.UvMaps && spec.UvAlbedo != null)
{
// Authored UV maps (Ringborke's cut wood): the albedo is the colour itself and the
// roughness map the roughness, so neither is divided by its mean (U5.4).
material.SetTexture("_AlbedoMap", spec.UvAlbedo);
material.SetTexture("_NormalMap", spec.UvNormal != null ? spec.UvNormal : Texture2D.normalTexture);
material.SetTexture("_OrmMap", spec.UvOrm != null ? spec.UvOrm : Texture2D.whiteTexture);
material.SetVector("_AlbedoMean", Vector4.one);
material.SetVector("_OrmMean", Vector4.one);
return;
}
string key = spec.Pbr;
if (key.Length == 0)
key = PbrForMode(spec.Mode);
Texture2D albedo;
if ((spec.Quality < .5f && !spec.BindMaps) || 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);
material.SetVector("_AlbedoMean", Vector4.one);
material.SetVector("_OrmMean", Vector4.one);
return;
}
material.SetTexture("_AlbedoMap", albedo);
material.SetTexture("_NormalMap", _normal[key]);
material.SetTexture("_OrmMap", _orm[key]);
material.SetVector("_AlbedoMean", Mean(albedo, true));
material.SetVector("_OrmMean", Mean(_orm[key], false));
}
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" : "";
}
}
}
}