First working text rendering

feature-better-tweakables
Andrew Cassidy 4 years ago
parent f3698fc108
commit dbb3281e95

@ -1,4 +1,4 @@
Shader "ConformalDecals/TMP_Blit" Shader "ConformalDecals/Text Blit"
{ {
Properties Properties
{ {
@ -13,7 +13,7 @@ Shader "ConformalDecals/TMP_Blit"
Pass Pass
{ {
Blend One One BlendOp Max
CGPROGRAM CGPROGRAM
#pragma vertex vert #pragma vertex vert

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:282c893d34ca9c703aee8c1af30efe3fcf9fb38d28cee5097a470ddcc2eaaf7a oid sha256:fe7efdec2d12a1bad8b4661cbb53b092d8082c2a704bb7c2cf18e5c9ab555184
size 364831 size 364850

@ -88,6 +88,8 @@
<Compile Include="Text/TextRenderer.cs" /> <Compile Include="Text/TextRenderer.cs" />
<Compile Include="Text/DecalText.cs" /> <Compile Include="Text/DecalText.cs" />
<Compile Include="Test/TestLayers.cs" /> <Compile Include="Test/TestLayers.cs" />
<Compile Include="Text\DecalTextStyle.cs" />
<Compile Include="Text\RenderedText.cs" />
<Compile Include="UI/ColorPickerController.cs" /> <Compile Include="UI/ColorPickerController.cs" />
<Compile Include="UI/FontMenuController.cs" /> <Compile Include="UI/FontMenuController.cs" />
<Compile Include="UI/FontMenuItem.cs" /> <Compile Include="UI/FontMenuItem.cs" />

@ -1,20 +1,20 @@
using ConformalDecals.Text; using ConformalDecals.Text;
using ConformalDecals.UI; using ConformalDecals.UI;
using TMPro;
using UnityEngine; using UnityEngine;
namespace ConformalDecals { namespace ConformalDecals {
public class ModuleConformalText : ModuleConformalDecal { public class ModuleConformalText : ModuleConformalDecal {
[KSPField(isPersistant = true)] public string text = "Hello World!"; [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 int style;
[KSPField(isPersistant = true)] public bool vertical; [KSPField(isPersistant = true)] public bool vertical;
[KSPField(isPersistant = true)] public Color fillColor = Color.black; [KSPField(isPersistant = true)] public Color fillColor = Color.black;
[KSPField(isPersistant = true)] public Color outlineColor = Color.white; [KSPField(isPersistant = true)] public Color outlineColor = Color.white;
[KSPField(isPersistant = true)] public float outlineWidth; [KSPField(isPersistant = true)] public float outlineWidth;
private DecalText _text; private DecalTextStyle _style;
private DecalFont _font;
private TextEntryController _textEntryController; private TextEntryController _textEntryController;
private ColorPickerController _fillColorPickerController; private ColorPickerController _fillColorPickerController;
private ColorPickerController _outlineColorPickerCOntroller; private ColorPickerController _outlineColorPickerCOntroller;
@ -22,21 +22,21 @@ namespace ConformalDecals {
public override void OnStart(StartState state) { public override void OnStart(StartState state) {
base.OnStart(state); base.OnStart(state);
var decalFont = DecalConfig.GetFont(font); _font = DecalConfig.GetFont(fontName);
_style = new DecalTextStyle();
_text = new DecalText { var decalText = new DecalText("Hello World!", _font, _style);
text = text,
font = decalFont, TextRenderer.Instance.RenderText(decalText, out var texture, out var window);
style = (FontStyles) style, materialProperties.AddOrGetTextureProperty("_Decal", true).Texture = texture;
vertical = vertical, UpdateMaterials();
color = fillColor, UpdateScale();
outlineColor = outlineColor,
outlineWidth = outlineWidth
};
} }
public void OnTextUpdate(DecalText newText) { public void OnTextUpdate(string newText, DecalFont newFont, DecalTextStyle newStyle) {
_text = newText; text = newText;
_font = newFont;
_style = newStyle;
} }
public void OnFillColorUpdate(Color rgb, Util.ColorHSV hsv) { 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")] [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_ConformalDecals_gui-select-flag")]
public void SetText() { public void SetText() {
if (_textEntryController == null) { if (_textEntryController == null) {
_textEntryController = TextEntryController.Create(_text, OnTextUpdate); _textEntryController = TextEntryController.Create(text, _font, _style, OnTextUpdate);
} }
} }

@ -1,16 +1,54 @@
using System; using System;
using TMPro; using System.Text.RegularExpressions;
using UnityEngine;
namespace ConformalDecals.Text { namespace ConformalDecals.Text {
public struct DecalText { public class DecalText : IEquatable<DecalText> {
public string text; public string Text { get; }
public DecalFont font;
public FontStyles style; public DecalFont Font { get; }
public bool vertical;
public DecalTextStyle Style { get; }
public Color color;
public Color outlineColor; public string FormattedText {
public float outlineWidth; 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);
}
} }
} }

@ -0,0 +1,75 @@
using System;
using TMPro;
using UnityEngine;
namespace ConformalDecals.Text {
public struct DecalTextStyle : IEquatable<DecalTextStyle> {
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);
}
}
}

@ -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; }
}
}

@ -1,11 +1,12 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic;
using TMPro; using TMPro;
using UnityEngine; using UnityEngine;
using UnityEngine.Rendering; using UnityEngine.Rendering;
namespace ConformalDecals.Text { namespace ConformalDecals.Text {
[KSPAddon(KSPAddon.Startup.FlightAndEditor, true)] [KSPAddon(KSPAddon.Startup.Instantly, true)]
public class TextRenderer : MonoBehaviour { public class TextRenderer : MonoBehaviour {
public static TextRenderer Instance { public static TextRenderer Instance {
get { 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; 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 int MaxTextureSize = 4096;
private const float FontSize = 100;
private const float PixelDensity = 5;
private static TextRenderer _instance; private static TextRenderer _instance;
private bool _isSetup; private bool _isSetup;
private TextMeshPro _tmp; private TextMeshPro _tmp;
private GameObject _cameraObject;
private Camera _camera;
private Material _blitMaterial; private Material _blitMaterial;
private Dictionary<DecalText, RenderedText> _renderedTextures = new Dictionary<DecalText, RenderedText>();
private Texture2D _lastTexture; // to reduce the number of Texture2D objects created and destroyed, keep the last one on hand
private void Start() { private void Start() {
if (_instance._isSetup) { if (_instance != null) {
Debug.Log("[ConformalDecals] Duplicate TextRenderer created???"); Debug.Log("[ConformalDecals] Duplicate TextRenderer created???");
} }
@ -41,28 +45,102 @@ namespace ConformalDecals.Text {
DontDestroyOnLoad(gameObject); DontDestroyOnLoad(gameObject);
} }
public void Setup() { private void Setup() {
if (_isSetup) return; if (_isSetup) return;
Debug.Log("[ConformalDecals] Setting Up TextRenderer Object"); Debug.Log("[ConformalDecals] Setting Up TextRenderer Object");
_tmp = gameObject.AddComponent<TextMeshPro>(); _tmp = gameObject.AddComponent<TextMeshPro>();
_tmp.renderer.enabled = false; // dont automatically render _tmp.renderer.enabled = false; // dont automatically render
_cameraObject = new GameObject("ConformalDecals text camera"); var shader = Shabby.Shabby.FindShader(BlitShader);
_cameraObject.transform.parent = transform; if (shader == null) Debug.LogError($"[ConformalDecals] could not find text blit shader named '{shader}'");
_cameraObject.transform.SetPositionAndRotation(Vector3.back, Quaternion.identity); _blitMaterial = new Material(Shabby.Shabby.FindShader(BlitShader));
_camera = _cameraObject.AddComponent<Camera>();
_camera.enabled = false; // dont automatically render
_camera.orthographic = true;
_camera.depthTextureMode = DepthTextureMode.None;
_camera.nearClipPlane = 0.1f;
_camera.farClipPlane = 2f;
_isSetup = true; _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);
} }
} }
} }

@ -8,46 +8,48 @@ using UnityEngine.UI;
namespace ConformalDecals.UI { namespace ConformalDecals.UI {
public class TextEntryController : MonoBehaviour { public class TextEntryController : MonoBehaviour {
[Serializable] [Serializable]
public class TextUpdateEvent : UnityEvent<DecalText> { } public class TextUpdateEvent : UnityEvent<string, DecalFont, DecalTextStyle> { }
[SerializeField] public TextUpdateEvent onTextUpdate = new TextUpdateEvent(); [SerializeField] public TextUpdateEvent onValueChanged = new TextUpdateEvent();
[SerializeField] private Selectable _textBox; [SerializeField] private Selectable _textBox;
[SerializeField] private Button _fontButton; [SerializeField] private Button _fontButton;
[SerializeField] private Slider _outlineWidthSlider;
[SerializeField] private Toggle _boldButton; [SerializeField] private Toggle _boldButton;
[SerializeField] private Toggle _italicButton; [SerializeField] private Toggle _italicButton;
[SerializeField] private Toggle _underlineButton; [SerializeField] private Toggle _underlineButton;
[SerializeField] private Toggle _smallCapsButton; [SerializeField] private Toggle _smallCapsButton;
[SerializeField] private Toggle _verticalButton; [SerializeField] private Toggle _verticalButton;
private DecalText _decalText; private string _text;
private DecalFont _font;
private DecalTextStyle _style;
private FontMenuController _fontMenu; private FontMenuController _fontMenu;
public static TextEntryController Create(DecalText text, UnityAction<DecalText> textUpdateCallback) { public static TextEntryController Create(string text, DecalFont font, DecalTextStyle style, UnityAction<string, DecalFont, DecalTextStyle> textUpdateCallback) {
var window = Instantiate(UILoader.TextEntryPrefab, MainCanvasUtil.MainCanvas.transform, true); var window = Instantiate(UILoader.TextEntryPrefab, MainCanvasUtil.MainCanvas.transform, true);
window.AddComponent<DragPanel>(); window.AddComponent<DragPanel>();
MenuNavigation.SpawnMenuNavigation(window, Navigation.Mode.Automatic, true); MenuNavigation.SpawnMenuNavigation(window, Navigation.Mode.Automatic, true);
var controller = window.GetComponent<TextEntryController>(); var controller = window.GetComponent<TextEntryController>();
controller._decalText = text; controller._text = text;
controller.onTextUpdate.AddListener(textUpdateCallback); controller.onValueChanged.AddListener(textUpdateCallback);
return controller; return controller;
} }
private void Start() { private void Start() {
((TMP_InputField) _textBox).text = _decalText.text; ((TMP_InputField) _textBox).text = _text;
_decalText.font.SetupSample(_fontButton.GetComponentInChildren<TextMeshProUGUI>());
_outlineWidthSlider.value = _decalText.outlineWidth; _font.SetupSample(_fontButton.GetComponentInChildren<TextMeshProUGUI>());
_boldButton.isOn = (_decalText.style & FontStyles.Bold) != 0;
_italicButton.isOn = (_decalText.style & FontStyles.Italic) != 0; _boldButton.isOn = _style.Bold;
_underlineButton.isOn = (_decalText.style & FontStyles.Underline) != 0; _italicButton.isOn = _style.Italic;
_smallCapsButton.isOn = (_decalText.style & FontStyles.SmallCaps) != 0; _underlineButton.isOn = _style.Underline;
_verticalButton.isOn = _decalText.vertical; _smallCapsButton.isOn = _style.SmallCaps;
_verticalButton.isOn = _style.Vertical;
} }
@ -56,93 +58,58 @@ namespace ConformalDecals.UI {
Destroy(gameObject); Destroy(gameObject);
} }
public void OnAnyUpdate() { public void OnValueChanged() {
onTextUpdate.Invoke(_decalText); onValueChanged.Invoke(_text, _font, _style);
} }
public void OnTextUpdate(string newText) { public void OnTextUpdate(string newText) {
this._decalText.text = newText; this._text = newText;
OnAnyUpdate(); OnValueChanged();
} }
public void OnFontMenu() { 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) { public void OnFontUpdate(DecalFont font) {
_decalText.font = font; _font = font;
font.SetupSample(_fontButton.GetComponentInChildren<TextMeshProUGUI>()); font.SetupSample(_fontButton.GetComponentInChildren<TextMeshProUGUI>());
var textBox = ((TMP_InputField) _textBox); var textBox = ((TMP_InputField) _textBox);
textBox.textComponent.fontStyle = _decalText.style | _decalText.font.fontStyle; textBox.textComponent.fontStyle = _style.FontStyle | _font.fontStyle;
textBox.fontAsset = _decalText.font.fontAsset; textBox.fontAsset = _font.fontAsset;
OnAnyUpdate();
}
public void OnColorMenu() { }
public void OnColorUpdate(Color color) {
_decalText.color = color;
OnAnyUpdate();
}
public void OnOutlineColorMenu() { }
public void OnOutlineColorUpdate(Color color) { OnValueChanged();
_decalText.outlineColor = color;
OnAnyUpdate();
}
public void OnOutlineUpdate(float value) {
_decalText.outlineWidth = value;
OnAnyUpdate();
} }
public void OnBoldUpdate(bool state) { public void OnBoldUpdate(bool state) {
if (state) _decalText.style |= FontStyles.Bold; _style.Bold = state;
else _decalText.style &= ~FontStyles.Bold;
((TMP_InputField) _textBox).textComponent.fontStyle = _decalText.style | _decalText.font.fontStyle;
OnAnyUpdate();
OnValueChanged();
} }
public void OnItalicUpdate(bool state) { public void OnItalicUpdate(bool state) {
if (state) _decalText.style |= FontStyles.Italic; _style.Italic = state;
else _decalText.style &= ~FontStyles.Italic; OnValueChanged();
((TMP_InputField) _textBox).textComponent.fontStyle = _decalText.style | _decalText.font.fontStyle;
OnAnyUpdate();
} }
public void OnUnderlineUpdate(bool state) { public void OnUnderlineUpdate(bool state) {
if (state) _decalText.style |= FontStyles.Underline; _style.Underline = state;
else _decalText.style &= ~FontStyles.Underline; OnValueChanged();
((TMP_InputField) _textBox).textComponent.fontStyle = _decalText.style | _decalText.font.fontStyle;
OnAnyUpdate();
} }
public void OnSmallCapsUpdate(bool state) { public void OnSmallCapsUpdate(bool state) {
if (state) _decalText.style |= FontStyles.SmallCaps; _style.SmallCaps = state;
else _decalText.style &= ~FontStyles.SmallCaps; OnValueChanged();
((TMP_InputField) _textBox).textComponent.fontStyle = _decalText.style | _decalText.font.fontStyle;
OnAnyUpdate();
} }
public void OnVerticalUpdate(bool state) { public void OnVerticalUpdate(bool state) {
_decalText.vertical = state; _style.Vertical = state;
OnAnyUpdate(); OnValueChanged();
} }
} }
} }
Loading…
Cancel
Save