-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This is a (vastly simplified) replacement for TU's shader replacement functionality. A new top-level **`SHABBY_MATERIAL_DEF`** node is introduced. It specifies a replacement shader and any number of shader properties to be modified and contains the following items: - **`name`** (required): a unique identifier for this material. - `displayName` (optional): a human-facing name. Not used in Shabby but may be accessed by other mods depending on this mod. Defaults to `name` if not specified. - `updateExisting` (optional): whether to apply changes to the existing material, or to create a new material from scratch. Defaults to `true`. - **`shader`** (optional in update-existing mode, otherwise required): name of the shader to apply. May be a stock shader or one loaded by Shabby. - `preserveRenderQueue` (optional): whether the existing render queue of the material should be preserved when its shader is replaced. Defaults to `false`, which will reset the render queue to the shader's default. - One or none of each the following nodes, to specify the corresponding type of shader property to be applied. They contain any number of keys of the format `_Property = value`: - `KEYWORD {}`: the value is a boolean (`true` or `false`). - `FLOAT {}` - `COLOR {}`: the value is either a float color (`r, g, b` or `r, g, b, a` normalized to `[0, 1]`), an HTML hex color (`#rgb`, `#rrggbb`, `#rgba`, or `#rrggbbaa`), or a named Unity color. - `VECTOR {}`: the value is a Vector4. All four components must be specified. - `TEXTURE {}`: the value is a GameData-relative path to a texture file, sans extension. There is also a new configuration under the existing `SHABBY` top-level node, **`ICON_SHADER`**, with the string-valued keys `shader` and `iconShader`. It specifies that the shader with the given name is to be substituted when used in an icon prefab. **Without this configuration, all custom shaders will be replaced by stock code with `KSP/ScreenSpaceMask` in icon prefabs and thus editor part icons.** Material replacements are applied in `PART`s using the **`SHABBY_MATERIAL_REPLACE`** node. It contains the following items: - **`materialDef`** (required): the unique identifier of the material definition to apply. - Optionally, exactly one of the following. If neither are specified, the replacement is applied to the entire part. 1. At least one `targetMaterial` key with a string value, specifying that the existing material with that name is to be replaced. This is the recommended workflow. 2. At least one `targetTransform` key with a string value, specifying that all meshes under that transform (recursively) are to have their materials replaced. - Optionally, any number of `ignoreMesh` keys with string values, specifying that the mesh with the given name is to be ignored even if it matches one of the target conditions. This applies to the mesh itself only, not any of its children. Multiple material replacement nodes may be defined in the part, but they should apply to distinct meshes. The behavior in case of overlap is unspecified. All of the above configurations may be modified by ModuleManager. The material replacement is performed once per part, during prefab compilation. The result is indistinguishable from the model `mu` natively containing the replaced material. In particular, this is compatible with existing texture switching mods such as B9PartSwitch. An example configuration: ```text SHABBY_MATERIAL_DEF { name = ExampleMaterial shader = TU/Metallic // This is just an example; to use a TU shader one must load it as a `.shab` bundle first. Texture { _MainTex = MyMod/Assets/PBRdiff _MetallicGlossMap = MyMod/Assets/PBRmet _AOMap = MyMod/Assets/PBRmet } Float { _Metal = 0.5 _Smoothness = 0.96 } } @part[MyPart]:FOR[MyMod] { SHABBY_MATERIAL_REPLACE { materialDef = ExampleMaterial targetTransform = transformA targetTransform = transformB excludeMesh = Flag } } ``` Fixes #7. --------- Co-authored-by: JonnyOThan <[email protected]>
- Loading branch information
1 parent
e61ec50
commit 1aaa293
Showing
6 changed files
with
452 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/* | ||
This file is part of Shabby. | ||
Shabby is free software: you can redistribute it and/or | ||
modify it under the terms of the GNU General Public License as published by | ||
the Free Software Foundation, either version 3 of the License, or | ||
(at your option) any later version. | ||
Shabby is distributed in the hope that it will be useful, | ||
but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
GNU General Public License for more details. | ||
You should have received a copy of the GNU General Public License | ||
along with Shabby. If not, see | ||
<http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Reflection; | ||
using System.Reflection.Emit; | ||
using HarmonyLib; | ||
using UnityEngine; | ||
|
||
namespace Shabby | ||
{ | ||
|
||
[HarmonyPatch(typeof(PartLoader), "SetPartIconMaterials")] | ||
class SetPartIconMaterialsPatch | ||
{ | ||
static MethodInfo mInfo_ShaderFind = AccessTools.Method(typeof(Shader), nameof(Shader.Find)); | ||
static MethodInfo mInfo_FindOverrideIconShader = AccessTools.Method(typeof(SetPartIconMaterialsPatch), nameof(FindOverrideIconShader)); | ||
|
||
static Shader FindOverrideIconShader(Material material) | ||
{ | ||
if (Shabby.iconShaders.TryGetValue(material.shader.name, out var shader)) { | ||
Debug.Log($"[Shabby] custom icon shader {material.shader.name} -> {shader.name}"); | ||
return shader; | ||
} | ||
return Shabby.FindShader("KSP/ScreenSpaceMask"); | ||
} | ||
|
||
/// <summary> | ||
/// The stock method iterates through every material in the icon prefab and replaces some | ||
/// stock shaders with 'ScreenSpaceMask'-prefixed ones. All shaders not explicitly checked, | ||
/// including custom shaders, are replaced with 'KSP/ScreenSpaceMask'. | ||
/// This transpiler inserts logic to check for additional replacements. | ||
/// </summary> | ||
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) | ||
{ | ||
var code = instructions.ToList(); | ||
object locMaterial = null; | ||
|
||
for (var i = 0; i < code.Count; ++i) { | ||
// Material material = sharedMaterials[j]; | ||
// IL_002C ldloc.3 | ||
// IL_002D ldloc.s 4 | ||
// IL_002F ldelem.ref | ||
// IL_0030 stloc.s 6 | ||
if (locMaterial == null | ||
&& code[i].opcode == OpCodes.Ldloc_3 | ||
&& code[i+1].opcode == OpCodes.Ldloc_S | ||
&& code[i+2].opcode == OpCodes.Ldelem_Ref | ||
&& code[i+3].opcode == OpCodes.Stloc_S) { | ||
// Extract the stack index of the material local. | ||
locMaterial = code[i+3].operand; | ||
} | ||
|
||
// material2 = new Material(Shader.Find("KSP/ScreenSpaceMask")); | ||
// IL_0191 ldstr "KSP/ScreenSpaceMask" | ||
// IL_0196 call class UnityEngine.Shader UnityEngine.Shader::Find(string) | ||
// IL_019D newobj instance void UnityEngine.Material::.ctor(class UnityEngine.Shader) | ||
// IL_01A2 stloc.s 7 | ||
if (code[i].Is(OpCodes.Ldstr, "KSP/ScreenSpaceMask") && code[i+1].Calls(mInfo_ShaderFind)) { | ||
// Replace the call to Shader.Find with FindOverrideIconShader(material). | ||
if (locMaterial == null) break; | ||
code[i].opcode = OpCodes.Ldloc_S; | ||
code[i].operand = locMaterial; | ||
code[i+1].operand = mInfo_FindOverrideIconShader; | ||
Debug.Log("[Shabby] patched part icon shader replacement"); | ||
return code; | ||
} | ||
} | ||
|
||
Debug.LogError("[Shabby] failed to patch part icon shader replacement"); | ||
return code; | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
/* | ||
This file is part of Shabby. | ||
Shabby is free software: you can redistribute it and/or | ||
modify it under the terms of the GNU General Public License as published by | ||
the Free Software Foundation, either version 3 of the License, or | ||
(at your option) any later version. | ||
Shabby is distributed in the hope that it will be useful, | ||
but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
GNU General Public License for more details. | ||
You should have received a copy of the GNU General Public License | ||
along with Shabby. If not, see | ||
<http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using HarmonyLib; | ||
using UnityEngine; | ||
|
||
namespace Shabby | ||
{ | ||
|
||
public static class MaterialDefLibrary | ||
{ | ||
public static readonly Dictionary<string, MaterialDef> items = new Dictionary<string, MaterialDef>(); | ||
|
||
public static void Load() | ||
{ | ||
foreach (var node in GameDatabase.Instance.GetConfigNodes("SHABBY_MATERIAL_DEF")) { | ||
var def = new MaterialDef(node); | ||
if (string.IsNullOrEmpty(def.name) || !def.isValid) { | ||
Debug.LogError($"[Shabby][MaterialDef {def.name}] removing invalid definition"); | ||
} else { | ||
items[def.name] = def; | ||
} | ||
} | ||
} | ||
} | ||
|
||
public class MaterialDef | ||
{ | ||
[Persistent] public string name; | ||
[Persistent(name = nameof(displayName))] private string _displayName = null; | ||
public string displayName => _displayName ?? name; | ||
|
||
[Persistent] public bool updateExisting = true; | ||
|
||
[Persistent(name = "shader")] public string shaderName = null; | ||
public readonly Shader shader = null; | ||
|
||
[Persistent] public bool preserveRenderQueue = false; | ||
|
||
public readonly Dictionary<string, bool> keywords; | ||
public readonly Dictionary<string, float> floats; | ||
public readonly Dictionary<string, Color> colors; | ||
public readonly Dictionary<string, Vector4> vectors; | ||
public readonly Dictionary<string, Texture> textures; | ||
|
||
public readonly bool isValid = true; | ||
|
||
public MaterialDef(ConfigNode node) | ||
{ | ||
ConfigNode.LoadObjectFromConfig(this, node); | ||
|
||
if (shaderName != null) { | ||
shader = Shabby.FindShader(shaderName); | ||
if (shader == null) { | ||
Debug.LogError($"[Shabby][MaterialDef {name}] failed to find shader {shaderName}"); | ||
isValid = false; | ||
} | ||
} | ||
|
||
if (!updateExisting && shader == null) { | ||
Debug.LogError($"[Shabby][MaterialDef {name}] from-scratch material must define a valid shader"); | ||
isValid = false; | ||
} | ||
|
||
keywords = LoadDictionary<bool>(node, "KEYWORD"); | ||
floats = LoadDictionary<float>(node, "FLOAT"); | ||
colors = LoadDictionary<Color>( | ||
node, "COLOR", | ||
value => ParseColor(value, out var color) ? (object)color : null); | ||
vectors = LoadDictionary<Vector4>(node, "VECTOR"); | ||
textures = LoadDictionary<Texture>( | ||
node, "TEXTURE", | ||
value => GameDatabase.Instance.GetTexture(value, asNormalMap: false)); | ||
} | ||
|
||
static readonly Func<Type, string, object> ReadValue = | ||
AccessTools.MethodDelegate<Func<Type, string, object>>( | ||
AccessTools.DeclaredMethod(typeof(ConfigNode), "ReadValue")); | ||
|
||
Dictionary<string, T> LoadDictionary<T>(ConfigNode defNode, string propKind, Func<string, object> parser = null) | ||
{ | ||
var items = new Dictionary<string, T>(); | ||
|
||
var propNode = defNode.GetNode(propKind); | ||
if (propNode == null) return items; | ||
|
||
foreach (ConfigNode.Value item in propNode.values) { | ||
object value = parser != null ? parser(item.value) : ReadValue(typeof(T), item.value); | ||
if (value is T parsed) { | ||
items[item.name] = parsed; | ||
} else { | ||
Debug.LogError($"[Shabby][MaterialDef {name}] failed to load {propKind} property {item.name} = {item.value}"); | ||
} | ||
} | ||
|
||
Debug.Log($"[Shabby][MaterialDef {name}] loaded {items.Count} {propKind} properties"); | ||
return items; | ||
} | ||
|
||
public static bool ParseColor(string value, out Color color) | ||
{ | ||
if (ColorUtility.TryParseHtmlString(value, out color)) return true; | ||
if (ParseExtensions.TryParseColor(value, out color)) return true; | ||
return false; | ||
} | ||
|
||
static bool CheckProperty(Material mat, string propName) | ||
{ | ||
var exists = mat.HasProperty(propName); | ||
if (!exists) Debug.LogWarning($"[Shabby] shader {mat.shader.name} does not have property {propName}"); | ||
return exists; | ||
} | ||
|
||
/// <summary> | ||
/// Create a new material based on this definition. The material name is copied from the | ||
/// passed reference material. In update-existing mode, all properties are also copied from | ||
/// the reference material. | ||
/// </summary> | ||
public Material Instantiate(Material referenceMaterial) | ||
{ | ||
if (!isValid) return new Material(referenceMaterial); | ||
|
||
Material material; | ||
if (updateExisting) { | ||
material = new Material(referenceMaterial); | ||
if (shader != null) material.shader = shader; | ||
} else { | ||
material = new Material(shader) { name = referenceMaterial.name }; | ||
} | ||
|
||
// Replacing the shader resets the render queue to the shader's default. | ||
if (preserveRenderQueue) material.renderQueue = referenceMaterial.renderQueue; | ||
|
||
foreach (var kvp in keywords) { | ||
if (!CheckProperty(material, kvp.Key)) continue; | ||
if (kvp.Value) material.EnableKeyword(kvp.Key); | ||
else material.DisableKeyword(kvp.Key); | ||
} | ||
|
||
foreach (var kvp in floats) { | ||
if (CheckProperty(material, kvp.Key)) material.SetFloat(kvp.Key, kvp.Value); | ||
} | ||
|
||
foreach (var kvp in colors) { | ||
if (CheckProperty(material, kvp.Key)) material.SetColor(kvp.Key, kvp.Value); | ||
} | ||
|
||
foreach (var kvp in vectors) { | ||
if (CheckProperty(material, kvp.Key)) material.SetVector(kvp.Key, kvp.Value); | ||
} | ||
|
||
foreach (var kvp in textures) { | ||
if (CheckProperty(material, kvp.Key)) material.SetTexture(kvp.Key, kvp.Value); | ||
} | ||
|
||
return material; | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
/* | ||
This file is part of Shabby. | ||
Shabby is free software: you can redistribute it and/or | ||
modify it under the terms of the GNU General Public License as published by | ||
the Free Software Foundation, either version 3 of the License, or | ||
(at your option) any later version. | ||
Shabby is distributed in the hope that it will be useful, | ||
but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
GNU General Public License for more details. | ||
You should have received a copy of the GNU General Public License | ||
along with Shabby. If not, see | ||
<http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
using System.Collections.Generic; | ||
using System.Linq; | ||
using HarmonyLib; | ||
using UnityEngine; | ||
|
||
namespace Shabby | ||
{ | ||
|
||
public class MaterialReplacement : ModelFilter | ||
{ | ||
public readonly MaterialDef materialDef = null; | ||
readonly Dictionary<Material, Material> replacedMaterials = new Dictionary<Material, Material>(); | ||
|
||
public MaterialReplacement(ConfigNode node) : base(node) | ||
{ | ||
var defName = node.GetValue("materialDef"); | ||
if (string.IsNullOrEmpty(defName)) { | ||
Debug.LogError("[Shabby] material replacement must reference a material definition"); | ||
return; | ||
} | ||
if (!MaterialDefLibrary.items.TryGetValue(defName, out materialDef)) { | ||
Debug.LogError($"[Shabby] failed to find valid material definition {defName}"); | ||
} | ||
} | ||
|
||
public void ApplyToSharedMaterialIfNotIgnored(Renderer renderer) | ||
{ | ||
if (MatchIgnored(renderer)) return; | ||
var sharedMat = renderer.sharedMaterial; | ||
if (!replacedMaterials.TryGetValue(sharedMat, out var replacementMat)) { | ||
replacementMat = materialDef.Instantiate(sharedMat); | ||
replacedMaterials[sharedMat] = replacementMat; | ||
} | ||
renderer.sharedMaterial = replacementMat; | ||
} | ||
} | ||
|
||
[HarmonyPatch(typeof(PartLoader), "CompileModel")] | ||
class MaterialReplacementPatch | ||
{ | ||
static void Postfix(ref GameObject __result, ConfigNode partCfg) | ||
{ | ||
const string replacementNodeName = "SHABBY_MATERIAL_REPLACE"; | ||
if (!partCfg.HasNode(replacementNodeName)) return; | ||
|
||
var replacements = new List<MaterialReplacement>(); | ||
foreach (ConfigNode node in partCfg.nodes) { | ||
if (node.name != replacementNodeName) continue; | ||
var replacement = new MaterialReplacement(node); | ||
if (replacement.materialDef != null) replacements.Add(replacement); | ||
} | ||
|
||
// Apply blanket replacements or material name replacements. | ||
foreach (var renderer in __result.GetComponentsInChildren<Renderer>()) { | ||
foreach (var replacement in replacements) { | ||
if (!replacement.blanketApply && !replacement.MatchMaterial(renderer)) continue; | ||
replacement.ApplyToSharedMaterialIfNotIgnored(renderer); | ||
break; | ||
} | ||
} | ||
|
||
// Apply transform replacements. | ||
if (replacements.Any(rep => rep.targetTransforms.Count > 0)) { | ||
foreach (var transform in __result.GetComponentsInChildren<Transform>()) { | ||
foreach (var replacement in replacements) { | ||
if (!replacement.MatchTransform(transform)) continue; | ||
foreach (var renderer in transform.GetComponentsInChildren<Renderer>()) { | ||
replacement.ApplyToSharedMaterialIfNotIgnored(renderer); | ||
} | ||
break; | ||
} | ||
} | ||
} | ||
|
||
var replacementNames = string.Join(", ", replacements.Select(rep => rep.materialDef.name)); | ||
Debug.Log($"[Shabby] applied material replacements {replacementNames}"); | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.