diff --git a/Assets/Shaders/TMPBlit.shader b/Assets/Shaders/TextBlit.shader similarity index 93% rename from Assets/Shaders/TMPBlit.shader rename to Assets/Shaders/TextBlit.shader index 1ef6391..0352834 100644 --- a/Assets/Shaders/TMPBlit.shader +++ b/Assets/Shaders/TextBlit.shader @@ -1,4 +1,4 @@ -Shader "ConformalDecals/TMP_Blit" +Shader "ConformalDecals/Text Blit" { Properties { @@ -13,7 +13,7 @@ Shader "ConformalDecals/TMP_Blit" Pass { - Blend One One + BlendOp Max CGPROGRAM #pragma vertex vert diff --git a/GameData/ConformalDecals/Plugins/ConformalDecals.dll b/GameData/ConformalDecals/Plugins/ConformalDecals.dll index e7466e5..1ac867a 100644 Binary files a/GameData/ConformalDecals/Plugins/ConformalDecals.dll and b/GameData/ConformalDecals/Plugins/ConformalDecals.dll differ diff --git a/GameData/ConformalDecals/Resources/conformaldecals.kspfont b/GameData/ConformalDecals/Resources/conformaldecals.kspfont index 09a558e..e591a61 100644 --- a/GameData/ConformalDecals/Resources/conformaldecals.kspfont +++ b/GameData/ConformalDecals/Resources/conformaldecals.kspfont @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:282c893d34ca9c703aee8c1af30efe3fcf9fb38d28cee5097a470ddcc2eaaf7a -size 364831 +oid sha256:fe7efdec2d12a1bad8b4661cbb53b092d8082c2a704bb7c2cf18e5c9ab555184 +size 364850 diff --git a/GameData/ConformalDecals/Resources/conformaldecals.shab b/GameData/ConformalDecals/Resources/conformaldecals.shab index ebcd050..3ee78d4 100644 Binary files a/GameData/ConformalDecals/Resources/conformaldecals.shab and b/GameData/ConformalDecals/Resources/conformaldecals.shab differ diff --git a/Source/ConformalDecals/ConformalDecals.csproj b/Source/ConformalDecals/ConformalDecals.csproj index e1fea8a..f4c8b7a 100644 --- a/Source/ConformalDecals/ConformalDecals.csproj +++ b/Source/ConformalDecals/ConformalDecals.csproj @@ -88,6 +88,8 @@ + + diff --git a/Source/ConformalDecals/ModuleConformalText.cs b/Source/ConformalDecals/ModuleConformalText.cs index 95e8bcd..a851536 100644 --- a/Source/ConformalDecals/ModuleConformalText.cs +++ b/Source/ConformalDecals/ModuleConformalText.cs @@ -1,20 +1,20 @@ using ConformalDecals.Text; using ConformalDecals.UI; -using TMPro; using UnityEngine; namespace ConformalDecals { public class ModuleConformalText : ModuleConformalDecal { [KSPField(isPersistant = true)] public string text = "Hello World!"; - [KSPField(isPersistant = true)] public string font = "Calibri SDF"; + [KSPField(isPersistant = true)] public string fontName = "Calibri SDF"; [KSPField(isPersistant = true)] public int style; [KSPField(isPersistant = true)] public bool vertical; [KSPField(isPersistant = true)] public Color fillColor = Color.black; [KSPField(isPersistant = true)] public Color outlineColor = Color.white; [KSPField(isPersistant = true)] public float outlineWidth; - private DecalText _text; - + private DecalTextStyle _style; + private DecalFont _font; + private TextEntryController _textEntryController; private ColorPickerController _fillColorPickerController; private ColorPickerController _outlineColorPickerCOntroller; @@ -22,21 +22,21 @@ namespace ConformalDecals { public override void OnStart(StartState state) { base.OnStart(state); - var decalFont = DecalConfig.GetFont(font); + _font = DecalConfig.GetFont(fontName); + _style = new DecalTextStyle(); - _text = new DecalText { - text = text, - font = decalFont, - style = (FontStyles) style, - vertical = vertical, - color = fillColor, - outlineColor = outlineColor, - outlineWidth = outlineWidth - }; + var decalText = new DecalText("Hello World!", _font, _style); + + TextRenderer.Instance.RenderText(decalText, out var texture, out var window); + materialProperties.AddOrGetTextureProperty("_Decal", true).Texture = texture; + UpdateMaterials(); + UpdateScale(); } - public void OnTextUpdate(DecalText newText) { - _text = newText; + public void OnTextUpdate(string newText, DecalFont newFont, DecalTextStyle newStyle) { + text = newText; + _font = newFont; + _style = newStyle; } public void OnFillColorUpdate(Color rgb, Util.ColorHSV hsv) { @@ -50,7 +50,7 @@ namespace ConformalDecals { [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_ConformalDecals_gui-select-flag")] public void SetText() { if (_textEntryController == null) { - _textEntryController = TextEntryController.Create(_text, OnTextUpdate); + _textEntryController = TextEntryController.Create(text, _font, _style, OnTextUpdate); } } diff --git a/Source/ConformalDecals/Text/DecalText.cs b/Source/ConformalDecals/Text/DecalText.cs index aa59d21..f2f2792 100644 --- a/Source/ConformalDecals/Text/DecalText.cs +++ b/Source/ConformalDecals/Text/DecalText.cs @@ -1,16 +1,54 @@ using System; -using TMPro; -using UnityEngine; +using System.Text.RegularExpressions; namespace ConformalDecals.Text { - public struct DecalText { - public string text; - public DecalFont font; - public FontStyles style; - public bool vertical; - - public Color color; - public Color outlineColor; - public float outlineWidth; + public class DecalText : IEquatable { + public string Text { get; } + + public DecalFont Font { get; } + + public DecalTextStyle Style { get; } + + public string FormattedText { + get { + if (Style.Vertical) { + return Regex.Replace(Text, @"(.)", "$1\n"); + } + else { + return Text; + } + } + } + + public DecalText(string text, DecalFont font, DecalTextStyle style) { + Text = text; + Font = font; + Style = style; + } + + public bool Equals(DecalText other) { + return other != null && (Text == other.Text && Equals(Font, other.Font) && Style.Equals(other.Style)); + } + + public override bool Equals(object obj) { + return obj is DecalText other && Equals(other); + } + + public override int GetHashCode() { + unchecked { + var hashCode = (Text != null ? Text.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (Font != null ? Font.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ Style.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(DecalText left, DecalText right) { + return left != null && left.Equals(right); + } + + public static bool operator !=(DecalText left, DecalText right) { + return left != null && !left.Equals(right); + } } } \ No newline at end of file diff --git a/Source/ConformalDecals/Text/DecalTextStyle.cs b/Source/ConformalDecals/Text/DecalTextStyle.cs new file mode 100644 index 0000000..459fed2 --- /dev/null +++ b/Source/ConformalDecals/Text/DecalTextStyle.cs @@ -0,0 +1,75 @@ +using System; +using TMPro; +using UnityEngine; + +namespace ConformalDecals.Text { + public struct DecalTextStyle : IEquatable { + public FontStyles FontStyle { get; set; } + + public bool Bold { + get => (FontStyle & FontStyles.Bold) != 0; + set { + if (value) FontStyle |= FontStyles.Bold; + else FontStyle &= ~FontStyles.Bold; + } + } + + public bool Italic { + get => (FontStyle & FontStyles.Italic) != 0; + set { + if (value) FontStyle |= FontStyles.Italic; + else FontStyle &= ~FontStyles.Italic; + } + } + + public bool Underline { + get => (FontStyle & FontStyles.Underline) != 0; + set { + if (value) FontStyle |= FontStyles.Underline; + else FontStyle &= ~FontStyles.Underline; + } + } + + public bool SmallCaps { + get => (FontStyle & FontStyles.SmallCaps) != 0; + set { + if (value) FontStyle |= FontStyles.SmallCaps; + else FontStyle &= ~FontStyles.SmallCaps; + } + } + + public bool Vertical { get; set; } + + public float LineSpacing { get; set; } + + public float CharacterSpacing { get; set; } + + public bool Equals(DecalTextStyle other) { + return FontStyle == other.FontStyle && Vertical == other.Vertical && + Mathf.Approximately(LineSpacing, other.LineSpacing) && + Mathf.Approximately(CharacterSpacing, other.CharacterSpacing); + } + + public override bool Equals(object obj) { + return obj is DecalTextStyle other && Equals(other); + } + + public override int GetHashCode() { + unchecked { + var hashCode = (int) FontStyle; + hashCode = (hashCode * 397) ^ Vertical.GetHashCode(); + hashCode = (hashCode * 397) ^ LineSpacing.GetHashCode(); + hashCode = (hashCode * 397) ^ CharacterSpacing.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(DecalTextStyle left, DecalTextStyle right) { + return left.Equals(right); + } + + public static bool operator !=(DecalTextStyle left, DecalTextStyle right) { + return !left.Equals(right); + } + } +} \ No newline at end of file diff --git a/Source/ConformalDecals/Text/RenderedText.cs b/Source/ConformalDecals/Text/RenderedText.cs new file mode 100644 index 0000000..d00c293 --- /dev/null +++ b/Source/ConformalDecals/Text/RenderedText.cs @@ -0,0 +1,11 @@ +using UnityEngine; + +namespace ConformalDecals.Text { + public class RenderedText : ScriptableObject { + public Texture2D Texture { get; private set; } + + public Rect Window { get; private set; } + + public int UserCount { get; private set; } + } +} \ No newline at end of file diff --git a/Source/ConformalDecals/Text/TextRenderer.cs b/Source/ConformalDecals/Text/TextRenderer.cs index 5d42a67..e717f65 100644 --- a/Source/ConformalDecals/Text/TextRenderer.cs +++ b/Source/ConformalDecals/Text/TextRenderer.cs @@ -1,11 +1,12 @@ using System; using System.Collections; +using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.Rendering; namespace ConformalDecals.Text { - [KSPAddon(KSPAddon.Startup.FlightAndEditor, true)] + [KSPAddon(KSPAddon.Startup.Instantly, true)] public class TextRenderer : MonoBehaviour { public static TextRenderer Instance { get { @@ -17,22 +18,25 @@ namespace ConformalDecals.Text { } } - public const TextureFormat TextTextureFormat = TextureFormat.Alpha8; + public const TextureFormat TextTextureFormat = TextureFormat.RG16; public const RenderTextureFormat TextRenderTextureFormat = RenderTextureFormat.R8; - private const string BlitShader = "ConformalDecals/TMP_Blit"; + private const string BlitShader = "ConformalDecals/Text Blit"; private const int MaxTextureSize = 4096; + private const float FontSize = 100; + private const float PixelDensity = 5; private static TextRenderer _instance; private bool _isSetup; private TextMeshPro _tmp; - private GameObject _cameraObject; - private Camera _camera; private Material _blitMaterial; + private Dictionary _renderedTextures = new Dictionary(); + private Texture2D _lastTexture; // to reduce the number of Texture2D objects created and destroyed, keep the last one on hand + private void Start() { - if (_instance._isSetup) { + if (_instance != null) { Debug.Log("[ConformalDecals] Duplicate TextRenderer created???"); } @@ -41,28 +45,102 @@ namespace ConformalDecals.Text { DontDestroyOnLoad(gameObject); } - public void Setup() { + private void Setup() { if (_isSetup) return; - Debug.Log("[ConformalDecals] Setting Up TextRenderer Object"); _tmp = gameObject.AddComponent(); _tmp.renderer.enabled = false; // dont automatically render - _cameraObject = new GameObject("ConformalDecals text camera"); - _cameraObject.transform.parent = transform; - _cameraObject.transform.SetPositionAndRotation(Vector3.back, Quaternion.identity); + var shader = Shabby.Shabby.FindShader(BlitShader); + if (shader == null) Debug.LogError($"[ConformalDecals] could not find text blit shader named '{shader}'"); + _blitMaterial = new Material(Shabby.Shabby.FindShader(BlitShader)); - _camera = _cameraObject.AddComponent(); - _camera.enabled = false; // dont automatically render - _camera.orthographic = true; - _camera.depthTextureMode = DepthTextureMode.None; - _camera.nearClipPlane = 0.1f; - _camera.farClipPlane = 2f; _isSetup = true; + } - _blitMaterial = new Material(Shabby.Shabby.FindShader(BlitShader)); + public void RenderText(DecalText text, out Texture2D texture, out Rect window) { + // Setup TMP object for rendering + _tmp.text = text.FormattedText; + _tmp.font = text.Font.fontAsset; + _tmp.fontStyle = text.Style.FontStyle | text.Font.fontStyle; + _tmp.lineSpacing = text.Style.LineSpacing; + _tmp.characterSpacing = text.Style.CharacterSpacing; + + _tmp.enableKerning = true; + _tmp.enableWordWrapping = false; + _tmp.overflowMode = TextOverflowModes.Overflow; + _tmp.alignment = TextAlignmentOptions.Center | TextAlignmentOptions.Baseline; + _tmp.fontSize = FontSize; + + // Setup blit material + _blitMaterial.SetTexture(PropertyIDs._MainTex, text.Font.fontAsset.atlas); + + // Generate Mesh + _tmp.ForceMeshUpdate(); + var mesh = _tmp.mesh; + mesh.RecalculateBounds(); + var bounds = mesh.bounds; + + // Calculate Sizes + var size = bounds.size * PixelDensity; + + var textureSize = new Vector2Int { + x = Mathf.NextPowerOfTwo((int) size.x), + y = Mathf.NextPowerOfTwo((int) size.y) + }; + + if (textureSize.x > MaxTextureSize) { + textureSize.x /= textureSize.x / MaxTextureSize; + textureSize.y /= textureSize.x / MaxTextureSize; + } + + if (textureSize.y > MaxTextureSize) { + textureSize.x /= textureSize.y / MaxTextureSize; + textureSize.y /= textureSize.y / MaxTextureSize; + } + + float sizeRatio = Mathf.Min(textureSize.x / size.x, textureSize.y, size.y); + + window = new Rect { + size = size * sizeRatio, + center = (Vector2) textureSize / 2 + }; + + // Get Texture + if (_lastTexture != null) { + texture = _lastTexture; + texture.Resize(textureSize.x, textureSize.y, TextTextureFormat, false); + _lastTexture = null; + } + else { + texture = new Texture2D(textureSize.x, textureSize.y, TextTextureFormat, false); + } + + // Generate Projection Matrix + var halfSize = window.size / PixelDensity / 2; + var matrix = Matrix4x4.Ortho(bounds.center.x - halfSize.x, bounds.center.x + halfSize.x, + bounds.center.y - halfSize.y, bounds.center.y + halfSize.y, -1, 1); + + // Get Rendertex + var renderTex = RenderTexture.GetTemporary(textureSize.x, textureSize.y, 0, TextRenderTextureFormat, RenderTextureReadWrite.Linear, 1); + renderTex.autoGenerateMips = false; + + // Render + Graphics.SetRenderTarget(renderTex); + GL.PushMatrix(); + GL.LoadProjectionMatrix(matrix); + _blitMaterial.SetPass(0); + Graphics.DrawMeshNow(mesh, Matrix4x4.identity); + GL.PopMatrix(); + + // Copy texture back into RAM + RenderTexture.active = renderTex; + texture.ReadPixels(new Rect(0, 0, textureSize.x, textureSize.y), 0, 0, false); + texture.Apply(); + + RenderTexture.ReleaseTemporary(renderTex); } } } \ No newline at end of file diff --git a/Source/ConformalDecals/UI/TextEntryController.cs b/Source/ConformalDecals/UI/TextEntryController.cs index 86dcbc7..0f06d18 100644 --- a/Source/ConformalDecals/UI/TextEntryController.cs +++ b/Source/ConformalDecals/UI/TextEntryController.cs @@ -8,46 +8,48 @@ using UnityEngine.UI; namespace ConformalDecals.UI { public class TextEntryController : MonoBehaviour { [Serializable] - public class TextUpdateEvent : UnityEvent { } + public class TextUpdateEvent : UnityEvent { } - [SerializeField] public TextUpdateEvent onTextUpdate = new TextUpdateEvent(); + [SerializeField] public TextUpdateEvent onValueChanged = new TextUpdateEvent(); [SerializeField] private Selectable _textBox; [SerializeField] private Button _fontButton; - [SerializeField] private Slider _outlineWidthSlider; [SerializeField] private Toggle _boldButton; [SerializeField] private Toggle _italicButton; [SerializeField] private Toggle _underlineButton; [SerializeField] private Toggle _smallCapsButton; [SerializeField] private Toggle _verticalButton; - - private DecalText _decalText; + + private string _text; + private DecalFont _font; + private DecalTextStyle _style; + private FontMenuController _fontMenu; - public static TextEntryController Create(DecalText text, UnityAction textUpdateCallback) { + public static TextEntryController Create(string text, DecalFont font, DecalTextStyle style, UnityAction textUpdateCallback) { + var window = Instantiate(UILoader.TextEntryPrefab, MainCanvasUtil.MainCanvas.transform, true); window.AddComponent(); MenuNavigation.SpawnMenuNavigation(window, Navigation.Mode.Automatic, true); var controller = window.GetComponent(); - controller._decalText = text; - controller.onTextUpdate.AddListener(textUpdateCallback); + controller._text = text; + controller.onValueChanged.AddListener(textUpdateCallback); return controller; } private void Start() { - ((TMP_InputField) _textBox).text = _decalText.text; - - _decalText.font.SetupSample(_fontButton.GetComponentInChildren()); + ((TMP_InputField) _textBox).text = _text; - _outlineWidthSlider.value = _decalText.outlineWidth; - _boldButton.isOn = (_decalText.style & FontStyles.Bold) != 0; - _italicButton.isOn = (_decalText.style & FontStyles.Italic) != 0; - _underlineButton.isOn = (_decalText.style & FontStyles.Underline) != 0; - _smallCapsButton.isOn = (_decalText.style & FontStyles.SmallCaps) != 0; - _verticalButton.isOn = _decalText.vertical; + _font.SetupSample(_fontButton.GetComponentInChildren()); + + _boldButton.isOn = _style.Bold; + _italicButton.isOn = _style.Italic; + _underlineButton.isOn = _style.Underline; + _smallCapsButton.isOn = _style.SmallCaps; + _verticalButton.isOn = _style.Vertical; } @@ -56,93 +58,58 @@ namespace ConformalDecals.UI { Destroy(gameObject); } - public void OnAnyUpdate() { - onTextUpdate.Invoke(_decalText); + public void OnValueChanged() { + onValueChanged.Invoke(_text, _font, _style); } public void OnTextUpdate(string newText) { - this._decalText.text = newText; + this._text = newText; - OnAnyUpdate(); + OnValueChanged(); } public void OnFontMenu() { - if (_fontMenu == null) _fontMenu = FontMenuController.Create(DecalConfig.Fonts, _decalText.font, OnFontUpdate); + if (_fontMenu == null) _fontMenu = FontMenuController.Create(DecalConfig.Fonts, _font, OnFontUpdate); } public void OnFontUpdate(DecalFont font) { - _decalText.font = font; + _font = font; font.SetupSample(_fontButton.GetComponentInChildren()); var textBox = ((TMP_InputField) _textBox); - textBox.textComponent.fontStyle = _decalText.style | _decalText.font.fontStyle; - textBox.fontAsset = _decalText.font.fontAsset; - - OnAnyUpdate(); - } - - public void OnColorMenu() { } - - public void OnColorUpdate(Color color) { - _decalText.color = color; - OnAnyUpdate(); - } - - public void OnOutlineColorMenu() { } + textBox.textComponent.fontStyle = _style.FontStyle | _font.fontStyle; + textBox.fontAsset = _font.fontAsset; - public void OnOutlineColorUpdate(Color color) { - _decalText.outlineColor = color; - OnAnyUpdate(); - } - - public void OnOutlineUpdate(float value) { - _decalText.outlineWidth = value; - OnAnyUpdate(); + OnValueChanged(); } public void OnBoldUpdate(bool state) { - if (state) _decalText.style |= FontStyles.Bold; - else _decalText.style &= ~FontStyles.Bold; - - ((TMP_InputField) _textBox).textComponent.fontStyle = _decalText.style | _decalText.font.fontStyle; - - OnAnyUpdate(); + _style.Bold = state; + OnValueChanged(); } public void OnItalicUpdate(bool state) { - if (state) _decalText.style |= FontStyles.Italic; - else _decalText.style &= ~FontStyles.Italic; - - ((TMP_InputField) _textBox).textComponent.fontStyle = _decalText.style | _decalText.font.fontStyle; - - OnAnyUpdate(); + _style.Italic = state; + OnValueChanged(); } public void OnUnderlineUpdate(bool state) { - if (state) _decalText.style |= FontStyles.Underline; - else _decalText.style &= ~FontStyles.Underline; - - ((TMP_InputField) _textBox).textComponent.fontStyle = _decalText.style | _decalText.font.fontStyle; - - OnAnyUpdate(); + _style.Underline = state; + OnValueChanged(); } public void OnSmallCapsUpdate(bool state) { - if (state) _decalText.style |= FontStyles.SmallCaps; - else _decalText.style &= ~FontStyles.SmallCaps; - - ((TMP_InputField) _textBox).textComponent.fontStyle = _decalText.style | _decalText.font.fontStyle; - - OnAnyUpdate(); + _style.SmallCaps = state; + OnValueChanged(); } public void OnVerticalUpdate(bool state) { - _decalText.vertical = state; - OnAnyUpdate(); + _style.Vertical = state; + OnValueChanged(); } } } \ No newline at end of file