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