diff --git a/GameData/ConformalDecals/Plugins/ConformalDecals.dll b/GameData/ConformalDecals/Plugins/ConformalDecals.dll index 71f814c..a28242f 100644 Binary files a/GameData/ConformalDecals/Plugins/ConformalDecals.dll and b/GameData/ConformalDecals/Plugins/ConformalDecals.dll differ diff --git a/Source/ConformalDecals/ModuleConformalDecal.cs b/Source/ConformalDecals/ModuleConformalDecal.cs index d7be8b1..b5a8b78 100644 --- a/Source/ConformalDecals/ModuleConformalDecal.cs +++ b/Source/ConformalDecals/ModuleConformalDecal.cs @@ -220,10 +220,15 @@ namespace ConformalDecals { if (HighLogic.LoadedSceneIsEditor) { UpdateTweakables(); + UpdateTextures(); + UpdateScale(); + UpdateTargets(); } - - if (HighLogic.LoadedSceneIsGame) { - UpdateProjection(); + else if (HighLogic.LoadedSceneIsFlight) { + UpdateTextures(); + UpdateScale(); + UpdateTargets(); + //TODO: Target loading } else { scale = defaultScale; @@ -231,6 +236,9 @@ namespace ConformalDecals { opacity = defaultOpacity; cutoff = defaultCutoff; wear = defaultWear; + + UpdateTextures(); + UpdateScale(); // QUEUE PART FOR ICON FIXING IN VAB DecalIconFixer.QueuePart(part.name); @@ -239,7 +247,8 @@ namespace ConformalDecals { /// public override void OnIconCreate() { - UpdateProjection(); + UpdateTextures(); + UpdateScale(); } /// @@ -306,11 +315,13 @@ namespace ConformalDecals { protected void OnProjectionTweakEvent(BaseField field, object obj) { // scale or depth values have been changed, so update scale // and update projection matrices if attached - UpdateProjection(); + UpdateScale(); + UpdateTargets(); foreach (var counterpart in part.symmetryCounterparts) { var decal = counterpart.GetComponent(); - decal.UpdateProjection(); + decal.UpdateScale(); + decal.UpdateTargets(); } } @@ -333,7 +344,7 @@ namespace ConformalDecals { protected void OnVariantApplied(Part eventPart, PartVariant variant) { if (_isAttached && eventPart == part.parent) { - UpdateProjection(); + UpdateTargets(); } } @@ -348,7 +359,8 @@ namespace ConformalDecals { break; case ConstructionEventType.PartOffsetting: case ConstructionEventType.PartRotating: - UpdateProjection(); + UpdateScale(); + UpdateTargets(); break; } } @@ -379,7 +391,8 @@ namespace ConformalDecals { Camera.onPreCull += Render; UpdateMaterials(); - UpdateProjection(); + UpdateScale(); + UpdateTargets(); } protected virtual void OnDetach() { @@ -395,57 +408,12 @@ namespace ConformalDecals { Camera.onPreCull -= Render; UpdateMaterials(); - UpdateProjection(); + UpdateScale(); } - protected void UpdateProjection() { - // Update projection targets - if (_targets == null) { - _targets = new List(); - } - else { - _targets.Clear(); - } - - if (_isAttached) { - IEnumerable targetParts; - if (projectMultiple) { - if (HighLogic.LoadedSceneIsFlight) { - targetParts = part.vessel.parts; - } - else { - targetParts = EditorLogic.fetch.ship.parts; - } - } - else { - targetParts = new[] {part.parent}; - } - - foreach (var targetPart in targetParts) { - if (targetPart.GetComponent() != null) continue; // skip other decals - - foreach (var targetRenderer in targetPart.FindModelComponents()) { - // skip disabled renderers - if (targetRenderer.gameObject.activeInHierarchy == false) continue; - - // skip blacklisted shaders - if (DecalConfig.IsBlacklisted(targetRenderer.material.shader)) continue; - - var meshFilter = targetRenderer.GetComponent(); - if (meshFilter == null) continue; // object has a meshRenderer with no filter, invalid - var mesh = meshFilter.sharedMesh; - if (mesh == null) continue; // object has a null mesh, invalid - - // create new ProjectionTarget to represent the renderer - var target = new ProjectionTarget(targetPart, targetRenderer, mesh); - - // add the target to the list - _targets.Add(target); - } - } - } + protected void UpdateScale() { - // Update projection matrix + // Update scale and depth scale = Mathf.Max(0.01f, scale); depth = Mathf.Max(0.01f, depth); var aspectRatio = materialProperties.AspectRatio; @@ -479,17 +447,20 @@ namespace ConformalDecals { materialProperties.UpdateScale(size); if (_isAttached) { + // Update projection targets + if (_targets == null) { + _targets = new List(); + } + else { + _targets.Clear(); + } + // update orthogonal matrix _orthoMatrix = Matrix4x4.identity; _orthoMatrix[0, 3] = 0.5f; _orthoMatrix[1, 3] = 0.5f; decalProjectorTransform.localScale = new Vector3(size.x, size.y, depth); - - // update projection - foreach (var target in _targets) { - target.Project(_orthoMatrix, decalProjectorTransform, _boundsRenderer.bounds, useBaseNormal); - } } else { // rescale preview model @@ -502,6 +473,41 @@ namespace ConformalDecals { } } + protected void UpdateTargets() { + if (!_isAttached) return; + + IEnumerable targetParts; + if (projectMultiple) { + targetParts = HighLogic.LoadedSceneIsFlight ? part.vessel.parts : EditorLogic.fetch.ship.parts; + } + else { + targetParts = new[] {part.parent}; + } + + foreach (var targetPart in targetParts) { + if (targetPart.GetComponent() != null) continue; // skip other decals + + foreach (var renderer in targetPart.FindModelComponents()) { + var target = renderer.transform; + var filter = target.GetComponent(); + + // check if the target has any missing data + if (!ProjectionTarget.ValidateTarget(target, renderer, filter)) continue; + + // check bounds for intersection + if (_boundsRenderer.bounds.Intersects(renderer.bounds)) { + // create new ProjectionTarget to represent the renderer + var projectionTarget = new ProjectionTarget(targetPart, target, renderer, filter, _orthoMatrix, decalProjectorTransform, useBaseNormal); + + // add the target to the list + _targets.Add(projectionTarget); + } + } + } + } + + protected virtual void UpdateTextures() { } + protected virtual void UpdateMaterials() { materialProperties.UpdateMaterials(); materialProperties.SetOpacity(opacity); diff --git a/Source/ConformalDecals/ModuleConformalFlag.cs b/Source/ConformalDecals/ModuleConformalFlag.cs index 4142a9a..cc11d07 100644 --- a/Source/ConformalDecals/ModuleConformalFlag.cs +++ b/Source/ConformalDecals/ModuleConformalFlag.cs @@ -95,7 +95,8 @@ namespace ConformalDecals { materialProperties.AddOrGetTextureProperty("_Decal", true).TextureUrl = newFlagUrl; UpdateMaterials(); - UpdateProjection(); + UpdateScale(); + UpdateTargets(); } private void SetFlagSymmetryCounterparts(string newFlagUrl) { diff --git a/Source/ConformalDecals/ModuleConformalText.cs b/Source/ConformalDecals/ModuleConformalText.cs index 7d97e2f..fdd9cb1 100644 --- a/Source/ConformalDecals/ModuleConformalText.cs +++ b/Source/ConformalDecals/ModuleConformalText.cs @@ -98,8 +98,6 @@ namespace ConformalDecals { public override void OnLoad(ConfigNode node) { base.OnLoad(node); OnAfterDeserialize(); - - UpdateTextRecursive(); } public override void OnSave(ConfigNode node) { @@ -107,15 +105,22 @@ namespace ConformalDecals { base.OnSave(node); } - public override void OnStart(StartState state) { - base.OnStart(state); - - UpdateTextRecursive(); - } - public override void OnAwake() { base.OnAwake(); + _font = DecalConfig.GetFont(fontName); + _style = new DecalTextStyle((FontStyles) style, vertical, lineSpacing, charSpacing); + + if (!ParseUtil.TryParseColor32(fillColor, out _fillColor)) { + Logging.LogWarning($"Improperly formatted color value for fill: '{fillColor}'"); + _fillColor = Color.magenta; + } + + if (!ParseUtil.TryParseColor32(outlineColor, out _outlineColor)) { + Logging.LogWarning($"Improperly formatted color value for outline: '{outlineColor}'"); + _outlineColor = Color.magenta; + } + _decalTextureProperty = materialProperties.AddOrGetTextureProperty("_Decal", true); _fillEnabledProperty = materialProperties.AddOrGetProperty("DECAL_FILL"); @@ -130,7 +135,22 @@ namespace ConformalDecals { text = newText; _font = newFont; _style = newStyle; - UpdateTextRecursive(); + UpdateTextures(); + UpdateScale(); + UpdateTargets(); + + foreach (var counterpart in part.symmetryCounterparts) { + var decal = counterpart.GetComponent(); + decal.text = text; + decal._font = _font; + decal._style = _style; + + decal._currentJob = _currentJob; + decal._currentText = _currentText; + decal.UpdateTextures(); + decal.UpdateScale(); + decal.UpdateTargets(); + } } public void OnFillColorUpdate(Color rgb, Util.ColorHSV hsv) { @@ -207,20 +227,7 @@ namespace ConformalDecals { outlineColor = _outlineColor.ToHexString(); } - public void OnAfterDeserialize() { - _font = DecalConfig.GetFont(fontName); - _style = new DecalTextStyle((FontStyles) style, vertical, lineSpacing, charSpacing); - - if (!ParseUtil.TryParseColor32(fillColor, out _fillColor)) { - Logging.LogWarning($"Improperly formatted color value for fill: '{fillColor}'"); - _fillColor = Color.magenta; - } - - if (!ParseUtil.TryParseColor32(outlineColor, out _outlineColor)) { - Logging.LogWarning($"Improperly formatted color value for outline: '{outlineColor}'"); - _outlineColor = Color.magenta; - } - } + public void OnAfterDeserialize() {} public override void OnDestroy() { if (HighLogic.LoadedSceneIsGame && _currentText != null) TextRenderer.UnregisterText(_currentText); @@ -237,41 +244,16 @@ namespace ConformalDecals { base.OnDetach(); } - private void UpdateTextRecursive() { - UpdateText(); - - foreach (var counterpart in part.symmetryCounterparts) { - var decal = counterpart.GetComponent(); - decal.text = text; - decal._font = _font; - decal._style = _style; - - decal._currentJob = _currentJob; - decal._currentText = _currentText; - decal.UpdateText(); - } - } - - private void UpdateText() { + protected override void UpdateTextures() { // Render text var newText = new DecalText(text, _font, _style); var output = TextRenderer.UpdateTextNow(_currentText, newText); _currentText = newText; - UpdateTexture(output); - - // TODO: ASYNC RENDERING - // var newText = new DecalText(text, _font, _style); - // _currentJob = TextRenderer.UpdateText(_currentText, newText, UpdateTexture); - // _currentText = newText; - } - - public void UpdateTexture(TextRenderOutput output) { _decalTextureProperty.Texture = output.Texture; _decalTextureProperty.SetTile(output.Window); UpdateMaterials(); - UpdateProjection(); } protected override void UpdateMaterials() { diff --git a/Source/ConformalDecals/ProjectionTarget.cs b/Source/ConformalDecals/ProjectionTarget.cs index 95ca253..007b50a 100644 --- a/Source/ConformalDecals/ProjectionTarget.cs +++ b/Source/ConformalDecals/ProjectionTarget.cs @@ -1,70 +1,144 @@ +using System; +using System.Text; +using ConformalDecals.Util; +using UniLinq; using UnityEngine; using UnityEngine.Rendering; namespace ConformalDecals { public class ProjectionTarget { // Target object data - public readonly Transform target; - public readonly Part targetPart; - - private readonly Renderer _targetRenderer; - private readonly Mesh _targetMesh; - private bool _projectionEnabled; + private readonly Transform _target; + private readonly Part _targetPart; + private readonly Mesh _targetMesh; + private readonly Matrix4x4 _decalMatrix; + private readonly Vector3 _decalNormal; + private readonly Vector3 _decalTangent; + private readonly bool _useBaseNormal; // property block private readonly MaterialPropertyBlock _decalMPB; - public ProjectionTarget(Part targetPart, MeshRenderer targetRenderer, Mesh targetMesh) { - this.targetPart = targetPart; - this.target = targetRenderer.transform; - _targetRenderer = targetRenderer; - _targetMesh = targetMesh; + public ProjectionTarget(Part targetPart, Transform target, MeshRenderer renderer, MeshFilter filter, + Matrix4x4 orthoMatrix, Transform projector, bool useBaseNormal) { + + _targetPart = targetPart; + _target = target; + _targetMesh = filter.sharedMesh; + _useBaseNormal = useBaseNormal; _decalMPB = new MaterialPropertyBlock(); + + var projectorToTargetMatrix = target.worldToLocalMatrix * projector.localToWorldMatrix; + + _decalMatrix = orthoMatrix * projectorToTargetMatrix.inverse; + _decalNormal = projectorToTargetMatrix.MultiplyVector(Vector3.back).normalized; + _decalTangent = projectorToTargetMatrix.MultiplyVector(Vector3.right).normalized; + + SetupMPB(renderer.sharedMaterial); } - public void Project(Matrix4x4 orthoMatrix, Transform projector, Bounds projectorBounds, bool useBaseNormal) { + public ProjectionTarget(ConfigNode node, Vessel vessel, bool useBaseNormal) { + var flightID = (uint) ParseUtil.ParseInt(node, "part"); + var targetPath = ParseUtil.ParseString(node, "targetPath"); + var targetName = ParseUtil.ParseString(node, "targetName"); + + _decalMatrix = ParseUtil.ParseMatrix4x4(node, "decalMatrix"); + _decalNormal = ParseUtil.ParseVector3(node, "decalNormal"); + _decalTangent = ParseUtil.ParseVector3(node, "decalTangent"); + _useBaseNormal = useBaseNormal; + _decalMPB = new MaterialPropertyBlock(); - if (projectorBounds.Intersects(_targetRenderer.bounds)) { - _projectionEnabled = true; - var targetMaterial = _targetRenderer.sharedMaterial; - var projectorToTargetMatrix = target.worldToLocalMatrix * projector.localToWorldMatrix; + _targetPart = vessel[flightID]; + if (_targetPart == null) throw new IndexOutOfRangeException("Vessel returned null part"); + _target = LoadTransformPath(targetPath, _targetPart.transform); + if (_target.name != targetName) throw new FormatException("Target name does not match"); - var projectionMatrix = orthoMatrix * projectorToTargetMatrix.inverse; - var decalNormal = projectorToTargetMatrix.MultiplyVector(Vector3.back).normalized; - var decalTangent = projectorToTargetMatrix.MultiplyVector(Vector3.right).normalized; + var renderer = _target.GetComponent(); + var filter = _target.GetComponent(); - _decalMPB.SetMatrix(DecalPropertyIDs._ProjectionMatrix, projectionMatrix); - _decalMPB.SetVector(DecalPropertyIDs._DecalNormal, decalNormal); - _decalMPB.SetVector(DecalPropertyIDs._DecalTangent, decalTangent); + if (!ValidateTarget(_target, renderer, filter)) throw new FormatException("Invalid target"); - if (useBaseNormal && targetMaterial.HasProperty(DecalPropertyIDs._BumpMap)) { - _decalMPB.SetTexture(DecalPropertyIDs._BumpMap, targetMaterial.GetTexture(DecalPropertyIDs._BumpMap)); + _targetMesh = filter.sharedMesh; - var normalScale = targetMaterial.GetTextureScale(DecalPropertyIDs._BumpMap); - var normalOffset = targetMaterial.GetTextureOffset(DecalPropertyIDs._BumpMap); + SetupMPB(renderer.sharedMaterial); + } - _decalMPB.SetVector(DecalPropertyIDs._BumpMap_ST, new Vector4(normalScale.x, normalScale.y, normalOffset.x, normalOffset.y)); - } - else { - _decalMPB.SetTexture(DecalPropertyIDs._BumpMap, DecalConfig.BlankNormal); - } + private void SetupMPB(Material targetMaterial) { + _decalMPB.SetMatrix(DecalPropertyIDs._ProjectionMatrix, _decalMatrix); + _decalMPB.SetVector(DecalPropertyIDs._DecalNormal, _decalNormal); + _decalMPB.SetVector(DecalPropertyIDs._DecalTangent, _decalTangent); + + if (_useBaseNormal && targetMaterial.HasProperty(DecalPropertyIDs._BumpMap)) { + _decalMPB.SetTexture(DecalPropertyIDs._BumpMap, targetMaterial.GetTexture(DecalPropertyIDs._BumpMap)); + + var normalScale = targetMaterial.GetTextureScale(DecalPropertyIDs._BumpMap); + var normalOffset = targetMaterial.GetTextureOffset(DecalPropertyIDs._BumpMap); + + _decalMPB.SetVector(DecalPropertyIDs._BumpMap_ST, new Vector4(normalScale.x, normalScale.y, normalOffset.x, normalOffset.y)); } else { - _projectionEnabled = false; + _decalMPB.SetTexture(DecalPropertyIDs._BumpMap, DecalConfig.BlankNormal); } } - public bool Render(Material decalMaterial, MaterialPropertyBlock partMPB, Camera camera) { - if (_projectionEnabled) { - _decalMPB.SetFloat(PropertyIDs._RimFalloff, partMPB.GetFloat(PropertyIDs._RimFalloff)); - _decalMPB.SetColor(PropertyIDs._RimColor, partMPB.GetColor(PropertyIDs._RimColor)); + public void Render(Material decalMaterial, MaterialPropertyBlock partMPB, Camera camera) { + _decalMPB.SetFloat(PropertyIDs._RimFalloff, partMPB.GetFloat(PropertyIDs._RimFalloff)); + _decalMPB.SetColor(PropertyIDs._RimColor, partMPB.GetColor(PropertyIDs._RimColor)); + + Graphics.DrawMesh(_targetMesh, _target.localToWorldMatrix, decalMaterial, 0, camera, 0, _decalMPB, ShadowCastingMode.Off, true); + } + + public ConfigNode Save() { + var node = new ConfigNode("TARGET"); + node.AddValue("part", _targetPart.flightID); + node.AddValue("decalMatrix", _decalMatrix); + node.AddValue("decalNormal", _decalNormal); + node.AddValue("decalTangent", _decalTangent); + node.AddValue("targetPath", SaveTransformPath(_target, _targetPart.transform)); // used to find the target transform + node.AddValue("targetName", _target.name); // used to validate the mesh has not changed since last load + + return node; + } + + + public static bool ValidateTarget(Transform target, MeshRenderer renderer, MeshFilter filter) { + if (renderer == null) return false; + if (filter == null) return false; + if (!target.gameObject.activeInHierarchy) return false; + + var material = renderer.material; + if (material == null) return false; + if (DecalConfig.IsBlacklisted(material.shader)) return false; + + if (filter.sharedMesh == null) return false; + + return true; + } + + private static string SaveTransformPath(Transform leaf, Transform root) { + var builder = new StringBuilder(leaf.name); + var current = leaf.parent; + + while (current != root) { + builder.Insert(0, "/"); + builder.Insert(0, current.GetSiblingIndex()); + current = current.parent; + if (current == null) throw new FormatException("Leaf does not exist as a child of root"); + } + + return builder.ToString(); + } - Graphics.DrawMesh(_targetMesh, target.localToWorldMatrix, decalMaterial, 0, camera, 0, _decalMPB, ShadowCastingMode.Off, true); + private static Transform LoadTransformPath(string path, Transform root) { + var indices = path.Split('/').Select(int.Parse); + var current = root; - return true; + foreach (var index in indices) { + if (index > current.childCount) throw new FormatException("Child index path is invalid"); + current = current.GetChild(index); } - return false; + return current; } } } \ No newline at end of file diff --git a/Source/ConformalDecals/Util/ParseUtil.cs b/Source/ConformalDecals/Util/ParseUtil.cs index 5a712c7..1a883d1 100644 --- a/Source/ConformalDecals/Util/ParseUtil.cs +++ b/Source/ConformalDecals/Util/ParseUtil.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using UniLinq; using UnityEngine; namespace ConformalDecals.Util { @@ -100,6 +101,15 @@ namespace ConformalDecals.Util { return ParseValueIndirect(ref value, node, valueName, ParseExtensions.TryParseVector3); } + public static Matrix4x4 ParseMatrix4x4(ConfigNode node, string valueName, bool isOptional = false, Matrix4x4 defaultValue = default) { + return ParseValue(node, valueName, ParseUtil.TryParseMatrix4x4, isOptional, defaultValue); + } + + public static bool ParseMatrix4x4Indirect(ref Matrix4x4 value, ConfigNode node, string valueName) { + return ParseValueIndirect(ref value, node, valueName, ParseUtil.TryParseMatrix4x4); + + } + public static T ParseValue(ConfigNode node, string valueName, TryParseDelegate tryParse, bool isOptional = false, T defaultValue = default) { string valueString = node.GetValue(valueName); @@ -139,6 +149,26 @@ namespace ConformalDecals.Util { throw new FormatException($"Improperly formatted {typeof(T)} value for {valueName} : '{valueString}"); } + public static bool TryParseMatrix4x4(string valueString, out Matrix4x4 value) { + value = new Matrix4x4(); + + var split = valueString.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < split.Length; i++) { + split[i] = split[i].Trim(); + } + + if (split.Length != 16) return false; + int index = 0; + for (int row = 0; row < 4; row++) { + for (int col = 0; col < 4; col++) { + if (!float.TryParse(split[index], out float component)) return false; + value[row, col] = component; + } + } + + return true; + } + public static bool TryParseHexColor(string valueString, out Color32 value) { value = new Color32(0, 0, 0, byte.MaxValue);