diff --git a/Distribution/GameData/ConformalDecals/Parts/decal-text.cfg b/Distribution/GameData/ConformalDecals/Parts/decal-text.cfg new file mode 100644 index 0000000..8ff0413 --- /dev/null +++ b/Distribution/GameData/ConformalDecals/Parts/decal-text.cfg @@ -0,0 +1,57 @@ +PART +{ + name = conformaldecals-text + module = Part + author = Andrew Cassidy + MODEL + { + model = ConformalDecals/Assets/decal-blank + scale = 1.0, 1.0, 1.0 + } + rescaleFactor = 1 + + // Attachment + attachRules = 1,1,0,0,1 + node_attach = 0.0, 0.0, 0.1, 0.0, 0.0, -1.0 + + // Tech + TechRequired = start + + // Info + cost = 75 + category = Structural + + // CDL-F Flag Decal + title = Conformal Text + + // Peel-N-Stik Adhesive Decals + manufacturer = #LOC_ConformalDecals_agent-peel-n-stick_title + + // A simple switchable flag. Can either use the mission flag or select a specific flag to use. + description = #LOC_ConformalDecals_flag-description + + // conformal decal sticker flag + tags = #LOC_ConformalDecals_flag-tags + + bulkheadProfiles = srf + + // Parameters + mass = 0.0005 + dragModel = NONE + angularDrag = 0.0 + crashTolerance = 10 + maxTemp = 2000 + breakingForce = 350 + breakingTorque = 150 + physicalSignificance = NONE + + MODULE + { + name = ModuleConformalText + + useBaseNormal = true + + defaultDepth = 0.2 + defaultCutoff = 0 + } +} diff --git a/Distribution/GameData/ConformalDecals/Plugins/ConformalDecals.dll b/Distribution/GameData/ConformalDecals/Plugins/ConformalDecals.dll new file mode 100644 index 0000000..151c752 Binary files /dev/null and b/Distribution/GameData/ConformalDecals/Plugins/ConformalDecals.dll differ diff --git a/GameData/ConformalDecals/Plugins/ConformalDecals.dll b/GameData/ConformalDecals/Plugins/ConformalDecals.dll index 76e71ba..e9953b8 100644 Binary files a/GameData/ConformalDecals/Plugins/ConformalDecals.dll and b/GameData/ConformalDecals/Plugins/ConformalDecals.dll differ diff --git a/Source/ConformalDecals/ConformalDecals.csproj b/Source/ConformalDecals/ConformalDecals.csproj index 7cce8a8..13a5b8a 100644 --- a/Source/ConformalDecals/ConformalDecals.csproj +++ b/Source/ConformalDecals/ConformalDecals.csproj @@ -43,6 +43,9 @@ dlls\UnityEngine.dll + + dlls\UnityEngine.AssetBundleModule.dll + dlls\UnityEngine.CoreModule.dll @@ -60,9 +63,13 @@ + + + + diff --git a/Source/ConformalDecals/MaterialModifiers/MaterialTextureProperty.cs b/Source/ConformalDecals/MaterialModifiers/MaterialTextureProperty.cs index e6004bb..57cd291 100644 --- a/Source/ConformalDecals/MaterialModifiers/MaterialTextureProperty.cs +++ b/Source/ConformalDecals/MaterialModifiers/MaterialTextureProperty.cs @@ -18,7 +18,10 @@ namespace ConformalDecals.MaterialModifiers { [SerializeField] private Vector2 _textureOffset; [SerializeField] private Vector2 _textureScale = Vector2.one; - public Texture2D Texture => _texture; + public Texture2D Texture { + get => _texture; + set => _texture = value; + } public string TextureUrl { get => _textureUrl; diff --git a/Source/ConformalDecals/ModuleConformalText.cs b/Source/ConformalDecals/ModuleConformalText.cs new file mode 100644 index 0000000..322e4de --- /dev/null +++ b/Source/ConformalDecals/ModuleConformalText.cs @@ -0,0 +1,41 @@ +using ConformalDecals.Util; +using TMPro; +using UnityEngine; + +namespace ConformalDecals { + public class ModuleConformalText: ModuleConformalDecal { + private const string DefaultFlag = "Squad/Flags/default"; + + [KSPField(isPersistant = true)] public string text = "Hello World!"; + + public override void OnLoad(ConfigNode node) { + base.OnLoad(node); + + SetText(text); + } + + public override void OnStart(StartState state) { + base.OnStart(state); + + SetText(text); + } + + private void SetText(string newText) { + if (!HighLogic.LoadedSceneIsEditor) return; + + this.Log("Rendering text for part"); + var fonts = Resources.FindObjectsOfTypeAll(); + + foreach (var font in fonts) { + this.Log($"Font: {font.name}"); + foreach (var fallback in font.fallbackFontAssets) { + this.Log($" Fallback: {fallback.name}"); + } + } + + materialProperties.AddOrGetTextureProperty("_Decal", true).Texture = TextRenderer.RenderToTexture(fonts[0], newText); + + UpdateMaterials(); + } + } +} \ No newline at end of file diff --git a/Source/ConformalDecals/Text/DecalFont.cs b/Source/ConformalDecals/Text/DecalFont.cs new file mode 100644 index 0000000..b61d2bb --- /dev/null +++ b/Source/ConformalDecals/Text/DecalFont.cs @@ -0,0 +1,8 @@ +using UnityEngine; + +namespace ConformalDecals.Text { + public class DecalFont : ScriptableObject { + [SerializeField] public string foo1; + [SerializeField] public string foo2; + } +} \ No newline at end of file diff --git a/Source/ConformalDecals/Text/FontLoader.cs b/Source/ConformalDecals/Text/FontLoader.cs new file mode 100644 index 0000000..26f369e --- /dev/null +++ b/Source/ConformalDecals/Text/FontLoader.cs @@ -0,0 +1,36 @@ +using System.IO; +using System.Collections; +using System.Collections.Generic; +using TMPro; +using UnityEngine; + +namespace ConformalDecals.Text { + + [DatabaseLoaderAttrib(new[] {"kspfont"})] + public class FontLoader : DatabaseLoader { + public static List fonts; + + public override IEnumerator Load(UrlDir.UrlFile urlFile, FileInfo fileInfo) { + fonts ??= new List(); + + Debug.Log($"[ConformalDecals] '{urlFile.fullPath}'"); + var bundle = AssetBundle.LoadFromFile(urlFile.fullPath); + if (!bundle) { + Debug.Log($"[ConformalDecals] could not load font asset {urlFile.fullPath}"); + } + else { + var loadedFoo = bundle.LoadAllAssets(); + Debug.Log(loadedFoo[0].foo1); + Debug.Log(loadedFoo[0].foo2); + var loadedFonts = bundle.LoadAllAssets(); + foreach (var font in loadedFonts) { + Debug.Log($"[ConformalDecals] adding font {font.name}" ); + fonts.Add(font); + Debug.Log($"ConformalDecals] isReadable: {font.atlas.isReadable}"); + } + } + + yield break; + } + } +} \ No newline at end of file diff --git a/Source/ConformalDecals/Text/TextRenderer.cs b/Source/ConformalDecals/Text/TextRenderer.cs new file mode 100644 index 0000000..e9bfe89 --- /dev/null +++ b/Source/ConformalDecals/Text/TextRenderer.cs @@ -0,0 +1,146 @@ +using System; +using ConformalDecals.Util; +using TMPro; +using UnityEngine; + +namespace ConformalDecals { + public class TextRenderer { + private struct GlyphInfo { + public TMP_Glyph glyph; + public Vector2Int size; + public Vector2Int position; + public int fontIndex; + public bool needsResample; + } + + private struct FontInfo { + public TMP_FontAsset font; + public Texture2D fontAtlas; + public Color32[] fontAtlasColors; + } + + public static Texture2D RenderToTexture(TMP_FontAsset font, string text) { + Debug.Log($"Rendering text: {text}"); + var charArray = text.ToCharArray(); + var glyphInfoArray = new GlyphInfo[charArray.Length]; + var fontInfoArray = new FontInfo[charArray.Length]; + + var baseScale = font.fontInfo.Scale; + + var padding = (int) font.fontInfo.Padding; + var ascender = (int) font.fontInfo.Ascender; + var descender = (int) font.fontInfo.Descender; + var baseline = (int) baseScale * (descender + padding); + Debug.Log($"baseline: {baseline}"); + Debug.Log($"ascender: {ascender}"); + Debug.Log($"descender: {descender}"); + Debug.Log($"baseScale: {baseScale}"); + + fontInfoArray[0].font = font; + + int xAdvance = 0; + for (var i = 0; i < charArray.Length; i++) { + + var glyphFont = TMP_FontUtilities.SearchForGlyph(font, charArray[i], out var glyph); + + if (glyphFont == font) { + glyphInfoArray[i].fontIndex = 0; + } + else { + for (int f = 1; i < charArray.Length; i++) { + if (fontInfoArray[f].font == null) { + fontInfoArray[f].font = glyphFont; + glyphInfoArray[i].fontIndex = f; + break; + } + + if (fontInfoArray[f].font == glyphFont) { + glyphInfoArray[i].fontIndex = f; + break; + } + } + } + + Debug.Log($"getting font info for character: '{charArray[i]}'"); + Debug.Log($"character font: {glyphFont.name}"); + + glyphInfoArray[i].glyph = glyph; + glyphInfoArray[i].needsResample = false; + + float elementScale = glyph.scale; + + if (glyphFont == font) { + if (!Mathf.Approximately(elementScale, 1)) { + glyphInfoArray[i].needsResample = true; + } + + elementScale *= baseScale; + } + else { + var fontScale = glyphFont.fontInfo.Scale / glyphFont.fontInfo.PointSize; + if (!Mathf.Approximately(fontScale, baseScale)) { + glyphInfoArray[i].needsResample = true; + } + + elementScale *= fontScale; + } + + Debug.Log($"character scale: {glyphFont.fontInfo.Scale / glyphFont.fontInfo.PointSize}"); + Debug.Log($"character needs resampling: {glyphInfoArray[i].needsResample}"); + + glyphInfoArray[i].size.x = (int) ((glyph.width + (padding * 2)) * elementScale); + glyphInfoArray[i].size.y = (int) ((glyph.height + (padding * 2)) * elementScale); + glyphInfoArray[i].position.x = (int) ((xAdvance + glyph.xOffset - padding) * elementScale); + glyphInfoArray[i].position.y = (int) ((baseline + glyph.yOffset - padding) * elementScale); + + Debug.Log($"character size: {glyphInfoArray[i].size}"); + Debug.Log($"character position: {glyphInfoArray[i].position}"); + } + + // calculate texture bounds + int xOffset = glyphInfoArray[0].position.x; + var textureWidth = (glyphInfoArray[charArray.Length - 1].position.x + glyphInfoArray[charArray.Length - 1].size.x) - xOffset; + var textureHeight = (int) baseScale * (ascender + descender + padding * 2); + + // ensure texture sizes are powers of 2 + textureWidth = Mathf.NextPowerOfTwo(textureWidth); + textureHeight = Mathf.NextPowerOfTwo(textureHeight); + Debug.Log($"texture is {textureWidth} x {textureHeight}"); + + var texture = new Texture2D(textureWidth, textureHeight, TextureFormat.Alpha8, true); + + var colors = new Color32[textureWidth * textureHeight]; + + for (var i = 0; i < fontInfoArray.Length; i++) { + if (fontInfoArray[i].font == null) break; + fontInfoArray[i].fontAtlas = fontInfoArray[i].font.atlas; + fontInfoArray[i].fontAtlasColors = fontInfoArray[i].fontAtlas.GetPixels32(); + } + + for (int i = 0; i < charArray.Length; i++) { + var glyphInfo = glyphInfoArray[i]; + var glyph = glyphInfo.glyph; + var fontInfo = fontInfoArray[glyphInfo.fontIndex]; + + var srcPos = new Vector2Int((int) glyph.x, (int) glyph.y); + var dstPos = glyphInfo.position; + dstPos.x += xOffset; + var dstSize = glyphInfo.size; + + Debug.Log($"rendering character number {i}"); + + if (glyphInfo.needsResample) { + var srcSize = new Vector2(glyph.width, glyph.height); + TextureUtils.BlitRectBilinearAlpha(fontInfo.fontAtlas, srcPos, srcSize, texture, colors, dstPos, dstSize, TextureUtils.BlitMode.Add); + } + else { + TextureUtils.BlitRectAlpha(fontInfo.fontAtlas, fontInfo.fontAtlasColors, srcPos, texture, colors, dstPos, dstSize, TextureUtils.BlitMode.Add); + } + } + + texture.Apply(true); + + return texture; + } + } +} \ No newline at end of file diff --git a/Source/ConformalDecals/Util/TextureUtils.cs b/Source/ConformalDecals/Util/TextureUtils.cs index 0d86ae6..f835afe 100644 --- a/Source/ConformalDecals/Util/TextureUtils.cs +++ b/Source/ConformalDecals/Util/TextureUtils.cs @@ -2,11 +2,183 @@ using UnityEngine; namespace ConformalDecals.Util { public static class TextureUtils { + public enum BlitMode { + Set, + Add, + } + + public static Color32 AddColor32(Color32 color1, Color32 color2) { + return new Color32((byte) (color1.r + color2.r), (byte) (color1.g + color2.g), (byte) (color1.b + color2.b), (byte) (color1.a + color2.a)); + } + + public static Color32 AddColor32Clamped(Color32 color1, Color32 color2) { + var r = color1.r + color2.r; + var g = color1.g + color2.g; + var b = color1.b + color2.b; + var a = color1.a + color2.a; + if (r > byte.MaxValue) r = byte.MaxValue; + if (g > byte.MaxValue) g = byte.MaxValue; + if (b > byte.MaxValue) b = byte.MaxValue; + if (a > byte.MaxValue) a = byte.MaxValue; + + return new Color32((byte) r, (byte) g, (byte) b, (byte) a); + } + + public static void ClearTexture(Color32[] colors, Color32 clearColor = default) { + for (var i = 0; i < colors.Length; i++) { + colors[i] = clearColor; + } + } + + public static void BlitRectAlpha( + Texture2D src, Color32[] srcColors, Vector2Int srcPos, + Texture2D dst, Color32[] dstColors, Vector2Int dstPos, + Vector2Int size, BlitMode mode) { + + ClipRect(src, ref srcPos, dst, ref dstPos, ref size); + + if (size.x <= 0 || size.y <= 0) return; + + int srcIndex = srcPos.x + srcPos.y * src.width; + int dstIndex = dstPos.x + dstPos.y * dst.width; + + for (int dy = size.y - 1; dy >= 0; dy--) { + + for (int dx = size.x - 1; dx >= 0; dx--) { + switch (mode) { + case BlitMode.Set: + dstColors[dstIndex + dx].a = srcColors[srcIndex + dx].a; + break; + case BlitMode.Add: + var s = srcColors[srcIndex + dx].a; + var d = dstColors[dstIndex + dx].a; + var sum = s + d; + if (sum > byte.MaxValue) sum = byte.MaxValue; + dstColors[dstIndex + dx].a = (byte) sum; + break; + } + } + + srcIndex += src.width; + dstIndex += dst.width; + } + } + public static void BlitRect( Texture2D src, Color32[] srcColors, Vector2Int srcPos, Texture2D dst, Color32[] dstColors, Vector2Int dstPos, - Vector2Int size) { + Vector2Int size, BlitMode mode) { + + ClipRect(src, ref srcPos, dst, ref dstPos, ref size); + + if (size.x <= 0 || size.y <= 0) return; + int srcIndex = srcPos.x + srcPos.y * src.width; + int dstIndex = dstPos.x + dstPos.y * dst.width; + + for (int dy = 0; dy < size.y; dy++) { + + for (int dx = 0; dx < size.x; dx++) { + switch (mode) { + case BlitMode.Set: + dstColors[dstIndex + dx] = srcColors[srcIndex + dx]; + break; + case BlitMode.Add: + dstColors[dstIndex + dx] = AddColor32Clamped(srcColors[srcIndex + dx], dstColors[dstIndex + dx]); + break; + } + } + + srcIndex += src.width; + dstIndex += dst.width; + } + } + + public static void BlitRectBilinearAlpha( + Texture2D src, Vector2Int srcPos, Vector2 srcSize, + Texture2D dst, Color32[] dstColors, Vector2Int dstPos, Vector2Int dstSize, + BlitMode mode) { + + var sizeRatio = dstSize / srcSize; + + ClipRect(src, ref srcPos, dst, ref dstPos, ref srcSize, ref dstSize); + + if (dstSize.x <= 0 || dstSize.y <= 0) return; + + var srcPixel = new Vector2(1.0f / src.width, 1.0f / src.height); + var srcStart = (srcPos * srcPixel) + (srcPixel / 2); + var srcStep = sizeRatio * srcPixel; + var srcY = srcStart.y; + + int dstIndex = dstPos.x + dstPos.y * dst.width; + for (int dy = 0; + dy < dstSize.y; + dy++) { + var srcX = srcStart.x; + + for (int dx = 0; dx < dstSize.x; dx++) { + switch (mode) { + case BlitMode.Set: + dstColors[dstIndex + dx].a = (byte) (src.GetPixelBilinear(srcX, srcY).a * byte.MaxValue); + break; + case BlitMode.Add: + var s = (byte) (src.GetPixelBilinear(srcX, srcY).a * byte.MaxValue); + var d = dstColors[dstIndex + dx].a; + var sum = s + d; + if (sum > byte.MaxValue) sum = byte.MaxValue; + dstColors[dstIndex + dx].a = (byte) sum; + break; + } + + srcX += srcStep.x; + } + + srcY += srcStep.y; + dstIndex += dst.width; + } + } + + public static void BlitRectBilinear( + Texture2D src, Vector2Int srcPos, Vector2 srcSize, + Texture2D dst, Color32[] dstColors, Vector2Int dstPos, Vector2Int dstSize, + BlitMode mode) { + + var sizeRatio = dstSize / srcSize; + + ClipRect(src, ref srcPos, dst, ref dstPos, ref srcSize, ref dstSize); + + if (dstSize.x <= 0 || dstSize.y <= 0) return; + + var srcPixel = new Vector2(1.0f / src.width, 1.0f / src.height); + var srcStart = (srcPos * srcPixel) + (srcPixel / 2); + var srcStep = sizeRatio * srcPixel; + var srcY = srcStart.y; + + int dstIndex = dstPos.x + dstPos.y * dst.width; + for (int dy = 0; + dy < dstSize.y; + dy++) { + var srcX = srcStart.x; + + for (int dx = 0; dx < dstSize.x; dx++) { + switch (mode) { + case BlitMode.Set: + dstColors[dstIndex + dx] = src.GetPixelBilinear(srcX, srcY); + break; + case BlitMode.Add: + dstColors[dstIndex + dx] = AddColor32Clamped(src.GetPixelBilinear(srcX, srcY), dstColors[dstIndex + dx]); + break; + } + + srcX += srcStep.x; + } + + srcY += srcStep.y; + dstIndex += dst.width; + } + } + + private static void ClipRect(Texture2D src, ref Vector2Int srcPos, Texture2D dst, ref Vector2Int dstPos, ref Vector2Int size) { if (srcPos.x < 0) { size.x += srcPos.x; dstPos.x -= srcPos.x; @@ -35,30 +207,10 @@ namespace ConformalDecals.Util { if (srcPos.y + size.y > src.height) size.y = src.height - srcPos.y; if (dstPos.x + size.x > dst.width) size.x = dst.width - srcPos.x; if (dstPos.y + size.y > dst.height) size.y = dst.height - srcPos.y; - - if (size.x <= 0) return; - if (size.y <= 0) return; - - int srcIndex = srcPos.x + srcPos.y * src.width; - int dstIndex = dstPos.x + dstPos.y * dst.width; - - for (int dy = 0; dy < size.y; dy++) { - - for (int dx = 0; dx < size.x; dx++) { - dstColors[dstIndex + dx] = srcColors[srcIndex + dx]; - } - - srcIndex += src.width; - dstIndex += dst.width; - } } - public static void BlitRectBilinear( - Texture2D src, Vector2Int srcPos, Vector2 srcSize, - Texture2D dst, Color32[] dstColors, Vector2Int dstPos, Vector2Int dstSize) { - + private static void ClipRect(Texture2D src, ref Vector2Int srcPos, Texture2D dst, ref Vector2Int dstPos, ref Vector2 srcSize, ref Vector2Int dstSize) { var sizeRatio = dstSize / srcSize; - if (srcPos.x < 0) { dstSize.x += (int) (srcPos.x * sizeRatio.x); dstPos.x -= (int) (srcPos.x * sizeRatio.x); @@ -106,26 +258,6 @@ namespace ConformalDecals.Util { dstSize.y = dst.height - srcPos.y; srcSize.y = (int) (dstSize.y / sizeRatio.y); } - - var srcPixel = new Vector2(1.0f / src.width, 1.0f / src.height); - - var srcStart = (srcPos * srcPixel) + (srcPixel / 2); - var srcStep = sizeRatio * srcPixel; - - var srcY = srcStart.y; - int dstIndex = dstPos.x + dstPos.y * dst.width; - - for (int dy = 0; dy < dstSize.y; dy++) { - var srcX = srcStart.x; - - for (int dx = 0; dx < dstSize.x; dx++) { - dstColors[dstIndex + dx] = src.GetPixelBilinear(srcX, srcY); - srcX += srcStep.x; - } - - srcY += srcStep.y; - dstIndex += dst.width; - } } } } \ No newline at end of file