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 { /// /// 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. /// 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; /// Team style (Blau, Rot, neutral grotto) of lune_materials.gd TEAM_STYLE. 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" }; /// U5 PBR sets of tools/blender/bake_pbr.py: albedo, normal, ORM. 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; /// U5 PBR set name (limestone, soil, bark, leaf, rock, water) or "" for none. public string Pbr = ""; public float NormalStrength = 1f; /// Weg 2: 1 = lane tier (full PBR), 0 = edge tier (colour + stone relief). public float Quality = 1f; /// U5.3: 1 = sample the authored UVs of a character (Ringborke's cut wood), 0 = triplanar. public bool UvMaps; /// Authored UV textures of a character; overrides the baked set and carry its colour. public Texture2D UvAlbedo; public Texture2D UvNormal; public Texture2D UvOrm; /// U5.4: triplanar in the mesh's own space (units), not the world. public bool ObjectSpace; /// U5.4: emission taken over as it is (the imported material of a unit); overrides Emission/Energy. public Color? EmissionRaw; /// U5.4: rim light strength (units; 0 = none). public float Rim; /// U5.4: bind the PBR set even when Quality is 0 (units: the portraits switch it on). public bool BindMaps; /// U5.5: emission of the albedo where its alpha marks glowing parts (UV figures; 0 = none). public float Glow; /// U5.5: share of URP's normal offset in the shadow caster (0 for the Meshy figures' creased seams). public float ShadowNormalScale = 1f; } static Shader _surface; static Texture2D _stone; static readonly Dictionary _albedo = new Dictionary(); static readonly Dictionary _normal = new Dictionary(); static readonly Dictionary _orm = new Dictionary(); /// Mean linear value of a map (albedo colour or ORM channels), measured once per build. static readonly Dictionary _means = new Dictionary(); public static Color Hex(string hex) { ColorUtility.TryParseHtmlString("#" + hex, out Color color); return color; } /// /// 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. /// public static Color Hdr(Color srgb, float energy) { Color linear = srgb.linear * energy; return new Color(linear.r, linear.g, linear.b, 1f); } /// /// A plain (non-HDR) colour that Unity converts from gamma to linear, carrying a linear /// value above one (RenderSettings.ambientLight = colour * energy). /// 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 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(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(SetPath); if (setAsset == null) { setAsset = ScriptableObject.CreateInstance(); 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/"; /// Repeats of a set per metre of a figure: one per metre, so the detail reads at hero zoom and in the portraits. public const float UnitDetailScale = 1f; /// Rim light of the figures (see LuneSurface.shader). public const float UnitRim = .35f; /// /// 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. /// public const float UnitGlow = 1.25f; /// /// 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. /// static void UnitMaterials(LuneMaterialSet set) { var known = new HashSet(set.Entries.Select(e => e.Key)); var imported = new SortedDictionary(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()) { if (!known.Contains(material.name) && !imported.ContainsKey(material.name)) imported[material.name] = material; } } } Directory.CreateDirectory(Folder + "/Units"); foreach (KeyValuePair pair in imported) Add(set, pair.Key, Create(Folder + "/Units/" + pair.Key + ".mat", UnitSpec(pair.Key, pair.Value)), null, null); SkinMaterials(set, imported); } /// /// H1 skins (variant stage): tools/meshy/skin_variant.py writes a recoloured albedo /// ai-
_-albedo-2k.png beside the maps of a Meshy figure. Each becomes a /// Surface material AI_
_ with the standard normal and ORM maps; /// LuneMaterialSet.Dress takes it for heroes that carry the skin. ///
static void SkinMaterials(LuneMaterialSet set, SortedDictionary imported) { foreach (KeyValuePair pair in imported) { if (UnitFamily(pair.Key) != "authored") continue; string stem = Textures + "/ai-" + pair.Key.Substring(3).ToLowerInvariant(); foreach (string path in Directory.GetFiles(Textures, Path.GetFileName(stem) + "_*-albedo-2k.png").OrderBy(p => p, StringComparer.Ordinal)) { string file = Path.GetFileName(path); string skin = file.Substring(Path.GetFileName(stem).Length + 1, file.Length - Path.GetFileName(stem).Length - 1 - "-albedo-2k.png".Length); if (skin.Length == 0 || skin.Contains("_")) continue; string name = LuneMaterialSet.SkinKey(pair.Key, skin); Spec spec = UnitSpec(pair.Key, pair.Value); spec.UvAlbedo = Map(Textures + "/" + file, TextureImporterType.Default, true); Add(set, name, Create(Folder + "/Units/" + name + ".mat", spec), null, null); } } } /// Plain Surface material of a figure: imported colour, roughness and emission plus the family's set. 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(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; } /// /// 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. /// 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; } /// /// 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. /// 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(path); } /// /// 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). /// 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; } /// ORM from a grayscale roughness map (R ao = 1, G roughness, B metal = 0). static Texture2D PackRoughnessOrm(Texture2D roughness) { string path = Textures + "/ringborke-cut-orm-4k.png"; var existing = AssetDatabase.LoadAssetAtPath(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(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()) { 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 WorldMaterials(LuneWorldBake bake) { var result = new Dictionary(); 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; } /// One material per energy vein (tint from its team, seed from the bake). 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(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 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(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; } /// U5: bind the baked albedo/normal/ORM set (lane tier) or the authored UVs of a character. 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" : ""; } } } }