Pilot für eine generierte Figur: der Kohlenbeißer g01 kommt als Meshy-Ergebnis in den Port und ersetzt dort das Phase-4-Modell. Eine Figur genügt, um Pipeline, Bild und Leistung zu bewerten; Anatomie, Gesichter und Haare bleiben Künstler-Sculpts oder lizenzierte Packs (Vorgabe vom 1. Oktober). - Meshy-Pipeline unter tools/meshy/: API-Client (meshy.py, Ledger, Schlüssel nie geloggt) und Blender-Generator (build_figure.py) aus den unveränderten Downloads in assets-source/meshy/g01/. Export über den Manifest-Eintrag kohlenbeisser mit "replaces": "g01"; LuneUnitLibraryBuilder hängt die Figur zusätzlich unter der Godot-ID ein. - UV-Material AI_Kohlenbeisser (Familie authored): die Karten der Figur selbst, Lava-Glut aus der Albedo-Alpha (Meshy 7.1 ohne Emission-Map) mit _Glow 1,25. - Schatten ohne Normalen-Versatz (LuneShadowBias, _ShadowNormalScale 0): die UV-Naht-Kopien tragen geknickte Normalen; URP ApplyShadowBias zog helle Nähte durch den Schatten. Die Figur wirft nur mit Tiefen-Bias. - GPU-Batched-Skinning (PlayerSettings.meshDeformation, LuneProjectSetup): 35 Meshy-Kohlenbeißer kosteten mit CPU-Skinning rund 1 ms Haupt- und 1,7 ms Render-Thread je Bild. - LuneBenchmark: -luneBenchmarkRound und -luneBenchmarkArmy (Figuren-Stress). LuneAssetPreview: LUNE_ONLY für Vorher/Nachher, Trefferframe aus der Kontaktzeit des Manifests. FigureViewsTests (Explicit) für die vier Spielmomente der Figur. - Tests: EditMode 29/29, PlayMode 14/14 (6 Explicit), Verify 718/0, Player-Selbsttest 30/30, Kerntests 457/457, Fingerabdrücke unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5). - Leistung (A/B im selben Fenster, 35x g01): Meshy -21 % Mid (109,7 zu 138,8) und -13 % Karte (91,7 zu 105,6) gegenüber Phase-4. GPU-Skinning hebt Mid um 7-27 % und entlastet Haupt- und Render-Thread. Standard-Partie ab Runde 21 ohne g01 im Bild: drei Varianten bis 6 %. - Doku: U5-VISUAL (U5.5), PORT-QA, PORT-STATUS, KNOWN-BEHAVIOR-DIFFERENCES; Bilder screenshots/u5.5/ und u5.5-gpu/, Rohdaten logs/u5.5/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
691 lines
36 KiB
C#
691 lines
36 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", "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;
|
||
/// <summary>U5.5: emission of the albedo where its alpha marks glowing parts (UV figures; 0 = none).</summary>
|
||
public float Glow;
|
||
/// <summary>U5.5: share of URP's normal offset in the shadow caster (0 for the Meshy figures' creased seams).</summary>
|
||
public float ShadowNormalScale = 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>();
|
||
/// <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>
|
||
/// U5.5: glow of the lava the Meshy figures mark in their albedo alpha: albedo x 1.25, the
|
||
/// emission the Phase-4 ember material (SW_HD_ember) has over its own colour.
|
||
/// </summary>
|
||
public const float UnitGlow = 1.25f;
|
||
|
||
/// <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 == "authored")
|
||
{
|
||
// U5.5: a Meshy figure (tools/meshy/build_figure.py) brings its own UV maps:
|
||
// AI_Kohlenbeisser -> Textures/ai-kohlenbeisser-{albedo,normal,orm}-2k.png. The
|
||
// maps are the colour, roughness and metalness themselves and glow in the albedo alpha.
|
||
string stem = Textures + "/ai-" + name.Substring(3).ToLowerInvariant();
|
||
Texture2D albedo = Map(stem + "-albedo-2k.png", TextureImporterType.Default, true);
|
||
Texture2D normal = Map(stem + "-normal-2k.png", TextureImporterType.NormalMap, false);
|
||
Texture2D orm = Map(stem + "-orm-2k.png", TextureImporterType.Default, false);
|
||
if (albedo == null || normal == null || orm == null)
|
||
throw new InvalidOperationException("maps missing for " + name + ": " + stem + "-*-2k.png");
|
||
spec.UvMaps = true;
|
||
spec.UvAlbedo = albedo;
|
||
spec.UvNormal = normal;
|
||
spec.UvOrm = orm;
|
||
spec.Base = Color.white;
|
||
spec.Roughness = 1f;
|
||
spec.Metallic = 1f;
|
||
spec.NormalStrength = 1f;
|
||
spec.ObjectSpace = false;
|
||
spec.EmissionRaw = Color.black;
|
||
spec.Glow = UnitGlow;
|
||
spec.ShadowNormalScale = 0f;
|
||
spec.Quality = 1f;
|
||
return spec;
|
||
}
|
||
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.StartsWith("ai_", StringComparison.Ordinal))
|
||
return "authored";
|
||
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("_Glow", spec.Glow);
|
||
material.SetFloat("_ShadowNormalScale", spec.ShadowNormalScale);
|
||
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" : "";
|
||
}
|
||
}
|
||
}
|
||
}
|