using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using ConformalDecals.MaterialProperties; using ConformalDecals.Targets; using ConformalDecals.Util; using UniLinq; using UnityEngine; namespace ConformalDecals { public class ModuleConformalDecal : PartModule { [SuppressMessage("ReSharper", "InconsistentNaming")] public enum DecalScaleMode { HEIGHT, WIDTH, AVERAGE, AREA, MINIMUM, MAXIMUM } // CONFIGURABLE VALUES [KSPField] public string shader = "ConformalDecals/Decal/Standard"; [KSPField] public string decalFront = "Decal-Front"; [KSPField] public string decalBack = "Decal-Back"; [KSPField] public string decalModel = "Decal-Model"; [KSPField] public string decalProjector = "Decal-Projector"; [KSPField] public string decalCollider = "Decal-Collider"; // Parameters [KSPField] public bool scaleAdjustable = true; [KSPField] public float defaultScale = 1; [KSPField] public Vector2 scaleRange = new Vector2(0, 5); [KSPField] public DecalScaleMode scaleMode = DecalScaleMode.HEIGHT; [KSPField] public bool depthAdjustable = true; [KSPField] public float defaultDepth = 0.1f; [KSPField] public Vector2 depthRange = new Vector2(0, 2); [KSPField] public bool opacityAdjustable = true; [KSPField] public float defaultOpacity = 1; [KSPField] public Vector2 opacityRange = new Vector2(0, 1); [KSPField] public bool cutoffAdjustable = true; [KSPField] public float defaultCutoff = 0.5f; [KSPField] public Vector2 cutoffRange = new Vector2(0, 1); [KSPField] public bool useBaseNormal = true; [KSPField] public float defaultWear = 100; [KSPField] public Vector2 wearRange = new Vector2(0, 100); [KSPField] public Rect tileRect = new Rect(-1, -1, 0, 0); [KSPField] public Vector2 tileSize; [KSPField] public int tileIndex = -1; [KSPField] public bool updateBackScale = true; [KSPField] public bool selectableInFlight; // INTERNAL VALUES [KSPField(guiName = "#LOC_ConformalDecals_gui-scale", guiActive = false, guiActiveEditor = true, isPersistant = true, guiFormat = "F2", guiUnits = "m"), UI_FloatRange()] public float scale = 1.0f; [KSPField(guiName = "#LOC_ConformalDecals_gui-depth", guiActive = false, guiActiveEditor = true, isPersistant = true, guiFormat = "F2", guiUnits = "m"), UI_FloatRange()] public float depth = 0.2f; [KSPField(guiName = "#LOC_ConformalDecals_gui-opacity", guiActive = false, guiActiveEditor = true, isPersistant = true, guiFormat = "P0"), UI_FloatRange()] public float opacity = 1.0f; private MaterialFloatProperty _opacityProperty; [KSPField(guiName = "#LOC_ConformalDecals_gui-cutoff", guiActive = false, guiActiveEditor = true, isPersistant = true, guiFormat = "P0"), UI_FloatRange()] public float cutoff = 0.5f; private MaterialFloatProperty _cutoffProperty; [KSPField(guiName = "#LOC_ConformalDecals_gui-wear", guiActive = false, guiActiveEditor = true, isPersistant = true, guiFormat = "F0"), UI_FloatRange()] public float wear = 100; private MaterialFloatProperty _wearProperty; [KSPField(guiName = "#LOC_ConformalDecals_gui-multiproject", guiActive = false, guiActiveEditor = true, isPersistant = true), UI_Toggle()] public bool projectMultiple = true; [KSPField] public MaterialPropertyCollection materialProperties; [KSPField] public Transform decalFrontTransform; [KSPField] public Transform decalBackTransform; [KSPField] public Transform decalModelTransform; [KSPField] public Transform decalProjectorTransform; [KSPField] public Transform decalColliderTransform; [KSPField] public Material backMaterial; [KSPField] public Vector2 backTextureBaseScale; private const int DecalQueueMin = 2100; private const int DecalQueueMax = 2400; private static int _decalQueueCounter = -1; private readonly Dictionary _targets = new Dictionary(); private bool _isAttached; private Matrix4x4 _orthoMatrix; private Material _decalMaterial; private Material _previewMaterial; private MeshRenderer _boundsRenderer; private int DecalQueue { get { _decalQueueCounter++; if (_decalQueueCounter > DecalQueueMax || _decalQueueCounter < DecalQueueMin) { _decalQueueCounter = DecalQueueMin; } return _decalQueueCounter; } } // EVENTS /// public override void OnAwake() { base.OnAwake(); if (materialProperties == null) { materialProperties = ScriptableObject.CreateInstance(); } else { materialProperties = ScriptableObject.Instantiate(materialProperties); } _opacityProperty = materialProperties.AddOrGetProperty("_DecalOpacity"); _cutoffProperty = materialProperties.AddOrGetProperty("_Cutoff"); _wearProperty = materialProperties.AddOrGetProperty("_EdgeWearStrength"); } /// public override void OnLoad(ConfigNode node) { // Load try { LoadDecal(node); } catch (Exception e) { this.LogException("Error loading decal", e); } // Setup try { SetupDecal(); } catch (Exception e) { this.LogException("Error setting up decal", e); } } /// public override void OnIconCreate() { UpdateTextures(); UpdateProjection(); } /// public override void OnStart(StartState state) { materialProperties.RenderQueue = DecalQueue; _boundsRenderer = decalProjectorTransform.GetComponent(); // handle tweakables if (HighLogic.LoadedSceneIsEditor) { GameEvents.onEditorPartEvent.Add(OnEditorEvent); GameEvents.onVariantApplied.Add(OnVariantApplied); UpdateTweakables(); } } /// Called after OnStart is finished for all parts /// This is mostly used to make sure all B9 variants are already in place for the rest of the vessel public override void OnStartFinished(StartState state) { // handle game events UpdateTextures(); if (HighLogic.LoadedSceneIsGame) { // set initial attachment state if (part.parent == null) { OnDetach(); } else { OnAttach(); } } // handle flight events if (HighLogic.LoadedSceneIsFlight) { GameEvents.onPartWillDie.Add(OnPartWillDie); if (part.parent == null) part.explode(); Part.layerMask |= 1 << DecalConfig.DecalLayer; decalColliderTransform.gameObject.layer = DecalConfig.DecalLayer; if (!selectableInFlight || !DecalConfig.SelectableInFlight) { decalColliderTransform.GetComponent().enabled = false; _boundsRenderer.enabled = false; } } } /// Called by Unity when the decal is destroyed public virtual void OnDestroy() { // remove GameEvents if (HighLogic.LoadedSceneIsEditor) { GameEvents.onEditorPartEvent.Remove(OnEditorEvent); GameEvents.onVariantApplied.Remove(OnVariantApplied); } if (HighLogic.LoadedSceneIsFlight) { GameEvents.onPartWillDie.Remove(OnPartWillDie); } // remove from preCull delegate Camera.onPreCull -= Render; // destroy material properties object Destroy(materialProperties); } /// Called when the decal's projection and scale is modified through a tweakable protected void OnProjectionTweakEvent(BaseField field, object obj) { // scale or depth values have been changed, so update scale // and update projection matrices if attached UpdateProjection(); foreach (var counterpart in part.symmetryCounterparts) { var decal = counterpart.GetComponent(); decal.UpdateProjection(); } } /// Called when the decal's material is modified through a tweakable protected void OnMaterialTweakEvent(BaseField field, object obj) { UpdateMaterials(); foreach (var counterpart in part.symmetryCounterparts) { var decal = counterpart.GetComponent(); decal.UpdateMaterials(); } } /// Called by KSP when a new variant is applied in the editor protected void OnVariantApplied(Part eventPart, PartVariant variant) { if (_isAttached && eventPart != null && (projectMultiple || eventPart == part.parent)) { _targets.Remove(eventPart); UpdateProjection(); } } /// Called by KSP when an editor event occurs protected void OnEditorEvent(ConstructionEventType eventType, Part eventPart) { switch (eventType) { case ConstructionEventType.PartAttached: OnPartAttached(eventPart); break; case ConstructionEventType.PartDetached: OnPartDetached(eventPart); break; case ConstructionEventType.PartOffsetting: case ConstructionEventType.PartRotating: OnPartTransformed(eventPart); break; } } /// Called when a part is transformed in the editor protected void OnPartTransformed(Part eventPart, bool firstcall = true) { if (part == eventPart || (firstcall && part.symmetryCounterparts.Contains(eventPart))) { // if this is the top-level call (original event part is a decal) then update symmetry counterparts, otherwise just update this UpdateProjection(); } else if (_isAttached) { UpdatePartTarget(eventPart, _boundsRenderer.bounds); // recursively call for child parts foreach (var child in eventPart.children) { OnPartTransformed(child, false); } } } /// Called when a part is attached in the editor protected void OnPartAttached(Part eventPart, bool firstcall = true) { if (part == eventPart || (firstcall && part.symmetryCounterparts.Contains(eventPart))) { // if this is the top-level call (original event part is a decal) then update symmetry counterparts, otherwise just update this OnAttach(); } else { UpdatePartTarget(eventPart, _boundsRenderer.bounds); // recursively call for child parts foreach (var child in eventPart.children) { OnPartAttached(child, false); } } } /// Called when a part is detached in the editor protected void OnPartDetached(Part eventPart, bool firstcall = true) { if (part == eventPart || (firstcall && part.symmetryCounterparts.Contains(eventPart))) { // if this is the top-level call (original event part is a decal) then update symmetry counterparts, otherwise just update this OnDetach(); } else if (_isAttached) { _targets.Remove(eventPart); // recursively call for child parts foreach (var child in eventPart.children) { OnPartDetached(child, false); } } } /// Called when part `willDie` will be destroyed protected void OnPartWillDie(Part willDie) { if (willDie == part.parent && willDie != null) { this.Log("Parent part about to be destroyed! Killing decal part."); part.Die(); } else if (_isAttached && projectMultiple) { _targets.Remove(willDie); } } /// Called when decal is attached to a new part protected virtual void OnAttach() { if (_isAttached) return; if (part.parent == null) { this.LogError("Attach function called but part has no parent!"); _isAttached = false; return; } _isAttached = true; _targets.Clear(); // hide model decalModelTransform.gameObject.SetActive(false); // unhide projector decalProjectorTransform.gameObject.SetActive(true); // add to preCull delegate Camera.onPreCull += Render; UpdateMaterials(); UpdateProjection(); } /// Called when decal is detached from its parent part protected virtual void OnDetach() { if (!_isAttached) return; _isAttached = false; // unhide model decalModelTransform.gameObject.SetActive(true); // hide projector decalProjectorTransform.gameObject.SetActive(false); // remove from preCull delegate Camera.onPreCull -= Render; UpdateMaterials(); UpdateProjection(); } // FUNCTIONS /// Load any settings from the decal config protected virtual void LoadDecal(ConfigNode node) { // PARSE TRANSFORMS if (!HighLogic.LoadedSceneIsGame) { decalFrontTransform = part.FindModelTransform(decalFront); if (decalFrontTransform == null) throw new FormatException($"Could not find decalFront transform: '{decalFront}'."); decalBackTransform = part.FindModelTransform(decalBack); if (decalBackTransform == null) throw new FormatException($"Could not find decalBack transform: '{decalBack}'."); decalModelTransform = part.FindModelTransform(decalModel); if (decalModelTransform == null) throw new FormatException($"Could not find decalModel transform: '{decalModel}'."); decalProjectorTransform = part.FindModelTransform(decalProjector); if (decalProjectorTransform == null) throw new FormatException($"Could not find decalProjector transform: '{decalProjector}'."); decalColliderTransform = part.FindModelTransform(decalCollider); if (decalColliderTransform == null) throw new FormatException($"Could not find decalCollider transform: '{decalCollider}'."); // SETUP BACK MATERIAL if (updateBackScale) { var backRenderer = decalBackTransform.GetComponent(); if (backRenderer == null) { this.LogError($"Specified decalBack transform {decalBack} has no renderer attached! Setting updateBackScale to false."); updateBackScale = false; } else { backMaterial = backRenderer.material; if (backMaterial == null) { this.LogError($"Specified decalBack transform {decalBack} has a renderer but no material! Setting updateBackScale to false."); updateBackScale = false; } else { if (backTextureBaseScale == default) backTextureBaseScale = backMaterial.GetTextureScale(PropertyIDs._MainTex); } } } } // PARSE MATERIAL PROPERTIES // set shader materialProperties.SetShader(shader); materialProperties.AddOrGetProperty("DECAL_BASE_NORMAL").value = useBaseNormal; materialProperties.Load(node); // handle texture tiling parameters var tileString = node.GetValue("tile"); if (!string.IsNullOrEmpty(tileString)) { var tileValid = ParseExtensions.TryParseRect(tileString, out tileRect); if (!tileValid) throw new FormatException($"Invalid rect value for tile '{tileString}'"); } if (tileRect.x >= 0) { materialProperties.UpdateTile(tileRect); } else if (tileIndex >= 0) { materialProperties.UpdateTile(tileIndex, tileSize); } } /// Setup decal by calling update functions relevent for the current situation protected virtual void SetupDecal() { if (HighLogic.LoadedSceneIsEditor) { // Update tweakables in editor mode UpdateTweakables(); } if (HighLogic.LoadedSceneIsGame) { UpdateAll(); } else { scale = defaultScale; depth = defaultDepth; opacity = defaultOpacity; cutoff = defaultCutoff; wear = defaultWear; UpdateAll(); // QUEUE PART FOR ICON FIXING IN VAB DecalIconFixer.QueuePart(part.name); } } /// Update decal editor tweakables protected virtual void UpdateTweakables() { // setup tweakable fields var scaleField = Fields[nameof(scale)]; var depthField = Fields[nameof(depth)]; var opacityField = Fields[nameof(opacity)]; var cutoffField = Fields[nameof(cutoff)]; var wearField = Fields[nameof(wear)]; var multiprojectField = Fields[nameof(projectMultiple)]; scaleField.guiActiveEditor = scaleAdjustable; depthField.guiActiveEditor = depthAdjustable; opacityField.guiActiveEditor = opacityAdjustable; cutoffField.guiActiveEditor = cutoffAdjustable; wearField.guiActiveEditor = useBaseNormal; var steps = 20; if (scaleAdjustable) { var minValue = Mathf.Max(Mathf.Epsilon, scaleRange.x); var maxValue = Mathf.Max(minValue, scaleRange.y); var scaleEditor = (UI_FloatRange) scaleField.uiControlEditor; scaleEditor.minValue = minValue; scaleEditor.maxValue = maxValue; scaleEditor.stepIncrement = 0.01f; //1cm scaleEditor.onFieldChanged = OnProjectionTweakEvent; } if (depthAdjustable) { var minValue = Mathf.Max(Mathf.Epsilon, depthRange.x); var maxValue = Mathf.Max(minValue, depthRange.y); var depthEditor = (UI_FloatRange) depthField.uiControlEditor; depthEditor.minValue = minValue; depthEditor.maxValue = maxValue; depthEditor.stepIncrement = 0.01f; //1cm depthEditor.onFieldChanged = OnProjectionTweakEvent; } if (opacityAdjustable) { var minValue = Mathf.Max(0, opacityRange.x); var maxValue = Mathf.Max(minValue, opacityRange.y); maxValue = Mathf.Min(1, maxValue); var opacityEditor = (UI_FloatRange) opacityField.uiControlEditor; opacityEditor.minValue = minValue; opacityEditor.maxValue = maxValue; opacityEditor.stepIncrement = (maxValue - minValue) / steps; opacityEditor.onFieldChanged = OnMaterialTweakEvent; } if (cutoffAdjustable) { var minValue = Mathf.Max(0, cutoffRange.x); var maxValue = Mathf.Max(minValue, cutoffRange.y); maxValue = Mathf.Min(1, maxValue); var cutoffEditor = (UI_FloatRange) cutoffField.uiControlEditor; cutoffEditor.minValue = minValue; cutoffEditor.maxValue = maxValue; cutoffEditor.stepIncrement = (maxValue - minValue) / steps; cutoffEditor.onFieldChanged = OnMaterialTweakEvent; } if (useBaseNormal) { var minValue = Mathf.Max(0, wearRange.x); var maxValue = Mathf.Max(minValue, wearRange.y); var wearEditor = (UI_FloatRange) wearField.uiControlEditor; wearEditor.minValue = minValue; wearEditor.maxValue = maxValue; wearEditor.stepIncrement = (maxValue - minValue) / steps; wearEditor.onFieldChanged = OnMaterialTweakEvent; } var multiprojectEditor = (UI_Toggle) multiprojectField.uiControlEditor; multiprojectEditor.onFieldChanged = OnProjectionTweakEvent; } /// Updates textures, materials, scale and targets protected virtual void UpdateAll() { UpdateTextures(); UpdateMaterials(); UpdateProjection(); } /// Update decal textures protected virtual void UpdateTextures() { } /// Update decal materials protected virtual void UpdateMaterials() { _opacityProperty.value = opacity; _cutoffProperty.value = cutoff; _wearProperty.value = wear; materialProperties.UpdateMaterials(); _decalMaterial = materialProperties.DecalMaterial; _previewMaterial = materialProperties.PreviewMaterial; if (!_isAttached) decalFrontTransform.GetComponent().material = _previewMaterial; } /// Update decal scale and projection protected void UpdateProjection() { // Update scale and depth scale = Mathf.Max(0.01f, scale); depth = Mathf.Max(0.01f, depth); var aspectRatio = Mathf.Max(0.01f, materialProperties.AspectRatio); Vector2 size; switch (scaleMode) { default: case DecalScaleMode.HEIGHT: size = new Vector2(scale / aspectRatio, scale); break; case DecalScaleMode.WIDTH: size = new Vector2(scale, scale * aspectRatio); break; case DecalScaleMode.AVERAGE: var width1 = 2 * scale / (1 + aspectRatio); size = new Vector2(width1, width1 * aspectRatio); break; case DecalScaleMode.AREA: var width2 = Mathf.Sqrt(scale / aspectRatio); size = new Vector2(width2, width2 * aspectRatio); break; case DecalScaleMode.MINIMUM: if (aspectRatio > 1) goto case DecalScaleMode.WIDTH; else goto case DecalScaleMode.HEIGHT; case DecalScaleMode.MAXIMUM: if (aspectRatio > 1) goto case DecalScaleMode.HEIGHT; else goto case DecalScaleMode.WIDTH; } // update material scale materialProperties.UpdateScale(size); decalProjectorTransform.localScale = new Vector3(size.x, size.y, depth); if (_isAttached) { // update orthogonal matrix _orthoMatrix = Matrix4x4.identity; _orthoMatrix[0, 3] = 0.5f; _orthoMatrix[1, 3] = 0.5f; var projectionBounds = _boundsRenderer.bounds; // disable all targets foreach (var target in _targets.Values) { target.enabled = false; } // collect list of potential targets IEnumerable targetParts; if (projectMultiple) { targetParts = HighLogic.LoadedSceneIsFlight ? part.vessel.parts : EditorLogic.fetch.ship.parts; } else { targetParts = new[] {part.parent}; } foreach (var targetPart in targetParts) { UpdatePartTarget(targetPart, projectionBounds); } } else { // rescale preview model decalModelTransform.localScale = new Vector3(size.x, size.y, (size.x + size.y) / 2); // update back material scale if (updateBackScale) { backMaterial.SetTextureScale(PropertyIDs._MainTex, new Vector2(size.x * backTextureBaseScale.x, size.y * backTextureBaseScale.y)); } } } protected void UpdatePartTarget(Part targetPart, Bounds projectionBounds) { if (targetPart.GetComponent() != null) return; // skip other decals if (!_targets.TryGetValue(targetPart, out var target)) { var rendererList = targetPart.FindModelComponents(); if (rendererList.Any(o => projectionBounds.Intersects(o.bounds))) { target = new ProjectionPartTarget(targetPart, useBaseNormal); _targets.Add(targetPart, target); } else { return; } } target.Project(_orthoMatrix, decalProjectorTransform, projectionBounds); } /// Render the decal public void Render(Camera camera) { if (!_isAttached) return; // render on each target object foreach (var target in _targets.Values) { target.Render(_decalMaterial, part.mpb, camera); } } } }