25 Commits

Author SHA1 Message Date
9295899787 Saving tests 2021-01-01 14:10:27 -08:00
e069f85e56 More tweaks 2021-01-01 14:02:46 -08:00
2f2b6fb692 Get multi-projection to work 2020-12-31 21:05:30 -08:00
169f2e9bf1 Add uint parsing function 2020-12-21 03:30:36 -08:00
eb033113b3 Merge branch 'main' into feature-multiProject 2020-12-20 01:46:16 -08:00
3173dd914a Update version and remove debug statement 2020-12-19 17:06:52 -08:00
5f6712f476 Revert changes to ModuleConformalDecal.cs 2020-12-19 16:58:15 -08:00
6b7996fdd7 Text render simplification and small optimizations 2020-12-19 16:11:56 -08:00
5feb16dcfb Only update text once per frame
Fixed text re-rendering several times in a single frame when pasting in text
2020-12-18 21:14:34 -08:00
bf8e98caf0 Fix text rendering for some non-ascii strings 2020-12-17 16:14:33 -08:00
b634eb1e8e Update version and changelog 2020-12-16 13:04:46 -08:00
dadf38acd5 go back to using temporary rendertexs 2020-12-16 01:28:57 -08:00
f42e0d78d6 Don't reuse textures, and don't keep them in RAM
Hopefully fixes #28
2020-12-16 01:11:32 -08:00
227c259e51 Cleanup and migrate to using VS 2020-12-08 16:06:01 -08:00
fc6820d73b make normal map gen less dumb 2020-12-04 17:12:41 -08:00
9dc98a6f9d Fix flag aspect ratio and font instantiation 2020-12-04 17:05:37 -08:00
35fce78616 fix flag decal NRE 2020-12-04 16:16:11 -08:00
84611a26e8 Remove workaround for outdated msbuild 2020-12-02 12:39:17 -08:00
a61a2b81a1 Cleanup refactor 2020-12-02 01:40:46 -08:00
e37cf03f7b Merge branch 'main' into feature-multiProject
# Conflicts:
#	GameData/ConformalDecals/Plugins/ConformalDecals.dll
#	Source/ConformalDecals/ModuleConformalDecal.cs
#	Source/ConformalDecals/ModuleConformalText.cs
2020-11-30 00:46:03 -08:00
e6288942ea Cleanup and fix onVariantApplied 2020-11-09 19:34:34 -08:00
62bdd151e1 Refactor to rearrange initialization code
• Add matrix parsing code
• Add target serialization
• Rearrange initialization to allow loading targets from config soon
2020-10-09 19:30:59 -07:00
f5af8a4d53 Merge branch 'develop' into feature-multiProject 2020-10-09 17:28:00 -07:00
df95601416 Allow toggling multiprojection in editor 2020-10-05 23:30:22 -07:00
596675ad8d Add naive implementation of multi-projection 2020-10-05 22:42:01 -07:00
29 changed files with 942 additions and 655 deletions

2
.gitignore vendored
View File

@ -49,5 +49,7 @@ Source/ConformalDecals/bin
.ds_store .ds_store
*.sublime* *.sublime*
.idea .idea
.vs
obj obj
*.swp *.swp
Source/.editorconfig

View File

@ -15,7 +15,7 @@ Localization
#LOC_ConformalDecals_gui-opacity = Opacity #LOC_ConformalDecals_gui-opacity = Opacity
#LOC_ConformalDecals_gui-cutoff = Cutoff #LOC_ConformalDecals_gui-cutoff = Cutoff
#LOC_ConformalDecals_gui-wear = Edge Wear #LOC_ConformalDecals_gui-wear = Edge Wear
#LOC_ConformalDecals_gui-aspectratio = Aspect Ratio #LOC_ConformalDecals_gui-multiproject = Project onto Multiple
#LOC_ConformalDecals_gui-select-flag = Select Flag #LOC_ConformalDecals_gui-select-flag = Select Flag
#LOC_ConformalDecals_gui-reset-flag = Reset Flag #LOC_ConformalDecals_gui-reset-flag = Reset Flag
#LOC_ConformalDecals_gui-set-text = Set Text #LOC_ConformalDecals_gui-set-text = Set Text

View File

@ -98,7 +98,7 @@ PART
MODULE { MODULE {
IDENTIFIER { name = ModuleConformalDecal } IDENTIFIER { name = ModuleConformalDecal }
DATA { DATA {
shader = ConformalDecals/Paint/DiffuseSDF KEYWORD { name = DECAL_SDF_ALPHA }
tile = 0, 2, 128, 112 tile = 0, 2, 128, 112
} }
} }

View File

@ -6,14 +6,14 @@
{ {
"MAJOR":0, "MAJOR":0,
"MINOR":2, "MINOR":2,
"PATCH":6, "PATCH":7,
"BUILD":1 "BUILD":0
}, },
"KSP_VERSION": "KSP_VERSION":
{ {
"MAJOR":1, "MAJOR":1,
"MINOR":10, "MINOR":11,
"PATCH":1 "PATCH":0
}, },
"KSP_VERSION_MIN":{ "KSP_VERSION_MIN":{
"MAJOR":1, "MAJOR":1,
@ -22,7 +22,7 @@
}, },
"KSP_VERSION_MAX":{ "KSP_VERSION_MAX":{
"MAJOR":1, "MAJOR":1,
"MINOR":10, "MINOR":11,
"PATCH":99 "PATCH":99
} }
} }

View File

@ -1,5 +1,5 @@
# Conformal Decals v0.2.6 # Conformal Decals v0.2.7
[![Build Status](https://travis-ci.org/drewcassidy/KSP-Conformal-Decals.svg?branch=release)](https://travis-ci.org/drewcassidy/KSP-Conformal-Decals) [![Art: CC BY-SA 4.0](https://img.shields.io/badge/Art%20License-CC%20BY--SA%204.0-orange.svg)](https://creativecommons.org/licenses/by-sa/4.0/) [![Code: GPL v3](https://img.shields.io/badge/Code%20License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Build Status](https://travis-ci.com/drewcassidy/KSP-Conformal-Decals.svg?branch=release)](https://travis-ci.com/drewcassidy/KSP-Conformal-Decals) [![Art: CC BY-SA 4.0](https://img.shields.io/badge/Art%20License-CC%20BY--SA%204.0-orange.svg)](https://creativecommons.org/licenses/by-sa/4.0/) [![Code: GPL v3](https://img.shields.io/badge/Code%20License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
![Screenshot](http://pileof.rocks/KSP/images/ConformalDecalsHeader.png) ![Screenshot](http://pileof.rocks/KSP/images/ConformalDecalsHeader.png)

View File

@ -1,6 +1,12 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConformalDecals", "ConformalDecals/ConformalDecals.csproj", "{1EA983F9-42E5-494E-9683-FDAC9C9121F4}" # Visual Studio Version 16
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConformalDecals", "ConformalDecals\ConformalDecals.csproj", "{1EA983F9-42E5-494E-9683-FDAC9C9121F4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C2564402-B081-479B-B723-D5C065BC884E}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -13,4 +19,28 @@ Global
{1EA983F9-42E5-494E-9683-FDAC9C9121F4}.Release|Any CPU.ActiveCfg = Release|Any CPU {1EA983F9-42E5-494E-9683-FDAC9C9121F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1EA983F9-42E5-494E-9683-FDAC9C9121F4}.Release|Any CPU.Build.0 = Release|Any CPU {1EA983F9-42E5-494E-9683-FDAC9C9121F4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(MonoDevelopProperties) = preSolution
Policies = $0
$0.DotNetNamingPolicy = $1
$1.DirectoryNamespaceAssociation = PrefixedHierarchical
$0.TextStylePolicy = $2
$2.FileWidth = 80
$2.TabsToSpaces = True
$2.scope = text/x-csharp
$2.NoTabsAfterNonTabs = True
$2.EolMarker = Unix
$0.CSharpFormattingPolicy = $3
$3.NewLinesForBracesInTypes = False
$3.NewLinesForBracesInMethods = False
$3.NewLinesForBracesInProperties = False
$3.NewLinesForBracesInAccessors = False
$3.NewLinesForBracesInAnonymousMethods = False
$3.NewLinesForBracesInControlBlocks = False
$3.NewLinesForBracesInAnonymousTypes = False
$3.NewLinesForBracesInObjectCollectionArrayInitializers = False
$3.NewLinesForBracesInLambdaExpressionBody = False
$3.scope = text/x-csharp
$3.SpaceAfterCast = True
$0.StandardHeader = $4
EndGlobalSection
EndGlobal EndGlobal

View File

@ -5,6 +5,7 @@
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<PlatformTarget>x64</PlatformTarget> <PlatformTarget>x64</PlatformTarget>
<NoWarn>1701;1702;CS0649;CS1591</NoWarn> <NoWarn>1701;1702;CS0649;CS1591</NoWarn>
<AssemblyVersion>0.2.7</AssemblyVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -35,20 +36,22 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Remove="dlls\**"/> <Compile Remove="dlls\**" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Remove="dlls\**"/> <EmbeddedResource Remove="dlls\**" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="dlls\**"/> <None Remove="dlls\**" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="..\.editorconfig" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="/bin/cp -v '$(OutDir)ConformalDecals.dll' '$(SolutionDir)../GameData/ConformalDecals/Plugins'" IgnoreExitCode="true"/> <Exec Command="/bin/cp -v '$(OutDir)ConformalDecals.dll' '$(SolutionDir)../GameData/ConformalDecals/Plugins'" />
<!--Fuck you MSBuild stop trying to run CMD.exe on macOS-->
</Target> </Target>
</Project> </Project>

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using ConformalDecals.Text; using ConformalDecals.Text;
using ConformalDecals.Util; using ConformalDecals.Util;
using TMPro; using TMPro;
using UniLinq;
using UnityEngine; using UnityEngine;
namespace ConformalDecals { namespace ConformalDecals {
@ -51,8 +50,6 @@ namespace ConformalDecals {
public static IEnumerable<DecalFont> Fonts => _fontList.Values; public static IEnumerable<DecalFont> Fonts => _fontList.Values;
public static DecalFont FallbackFont { get; private set; }
public static bool IsBlacklisted(Shader shader) { public static bool IsBlacklisted(Shader shader) {
return IsBlacklisted(shader.name); return IsBlacklisted(shader.name);
} }
@ -97,7 +94,7 @@ namespace ConformalDecals {
foreach (var fontNode in node.GetNodes("FONT")) { foreach (var fontNode in node.GetNodes("FONT")) {
try { try {
var font = new DecalFont(fontNode, allFonts); var font = DecalFont.Parse(fontNode, allFonts);
_fontList.Add(font.Name, font); _fontList.Add(font.Name, font);
} }
catch (Exception e) { catch (Exception e) {
@ -114,12 +111,8 @@ namespace ConformalDecals {
var colors = new[] {color, color, color, color}; var colors = new[] {color, color, color, color};
var tex = new Texture2D(width, height, TextureFormat.RGBA32, false); var tex = new Texture2D(width, height, TextureFormat.RGBA32, false);
for (var x = 0; x <= width; x++) {
for (var y = 0; y < height; y++) {
tex.SetPixels32(colors);
}
}
tex.SetPixels32(colors);
tex.Apply(); tex.Apply();
return tex; return tex;
@ -133,7 +126,7 @@ namespace ConformalDecals {
var configs = GameDatabase.Instance.GetConfigs("CONFORMALDECALS"); var configs = GameDatabase.Instance.GetConfigs("CONFORMALDECALS");
if (configs.Length > 0) { if (configs.Length > 0) {
Logging.Log("loading config"); Logging.Log("Loading config");
foreach (var config in configs) { foreach (var config in configs) {
ParseConfig(config.config); ParseConfig(config.config);
} }

View File

@ -0,0 +1,9 @@
using UnityEngine;
namespace ConformalDecals {
public interface IProjectionTarget {
bool Project(Matrix4x4 orthoMatrix, Transform projector, Bounds projectionBounds);
void Render(Material decalMaterial, MaterialPropertyBlock partMPB, Camera camera);
ConfigNode Save();
}
}

View File

@ -8,7 +8,7 @@ namespace ConformalDecals.MaterialProperties {
public override void ParseNode(ConfigNode node) { public override void ParseNode(ConfigNode node) {
base.ParseNode(node); base.ParseNode(node);
ParseUtil.ParseBoolIndirect(ref value, node, "value"); value = ParseUtil.ParseBool(node, "value", true, true);
} }
public override void Modify(Material material) { public override void Modify(Material material) {

View File

@ -105,6 +105,28 @@ namespace ConformalDecals.MaterialProperties {
_materialProperties ??= new Dictionary<string, MaterialProperty>(); _materialProperties ??= new Dictionary<string, MaterialProperty>();
} }
public void Load(ConfigNode node) {
// add keyword nodes
foreach (var keywordNode in node.GetNodes("KEYWORD")) {
ParseProperty<MaterialKeywordProperty>(keywordNode);
}
// add texture nodes
foreach (var textureNode in node.GetNodes("TEXTURE")) {
ParseProperty<MaterialTextureProperty>(textureNode);
}
// add float nodes
foreach (var floatNode in node.GetNodes("FLOAT")) {
ParseProperty<MaterialTextureProperty>(floatNode);
}
// add color nodes
foreach (var colorNode in node.GetNodes("COLOR")) {
ParseProperty<MaterialColorProperty>(colorNode);
}
}
public void OnDestroy() { public void OnDestroy() {
if (_decalMaterial != null) Destroy(_decalMaterial); if (_decalMaterial != null) Destroy(_decalMaterial);
if (_previewMaterial != null) Destroy(_previewMaterial); if (_previewMaterial != null) Destroy(_previewMaterial);
@ -186,13 +208,14 @@ namespace ConformalDecals.MaterialProperties {
public T ParseProperty<T>(ConfigNode node) where T : MaterialProperty { public T ParseProperty<T>(ConfigNode node) where T : MaterialProperty {
string propertyName = ""; string propertyName = "";
if (!ParseUtil.ParseStringIndirect(ref propertyName, node, "name")) throw new ArgumentException("node has no name"); if (!ParseUtil.ParseStringIndirect(ref propertyName, node, "name")) throw new ArgumentException("node has no name");
Logging.Log($"Parsing material property {propertyName}");
if (ParseUtil.ParseBool(node, "remove", true)) RemoveProperty(propertyName); if (ParseUtil.ParseBool(node, "remove", true)) RemoveProperty(propertyName);
var newProperty = AddOrGetProperty<T>(propertyName); var newProperty = AddOrGetProperty<T>(propertyName);
newProperty.ParseNode(node); newProperty.ParseNode(node);
if (newProperty is MaterialTextureProperty textureProperty && textureProperty.isMain) { if (newProperty is MaterialTextureProperty {isMain: true} textureProperty) {
_mainTexture = textureProperty; _mainTexture = textureProperty;
} }

View File

@ -44,7 +44,7 @@ namespace ConformalDecals.MaterialProperties {
public float AspectRatio { public float AspectRatio {
get { get {
if (_texture == null) return 1; if (_texture == null) return 1;
if (_textureUrl?.Contains("Squad/Flags") == true) return 0.625f; if (_textureUrl?.Contains("Squad/Flags") == true) return 0.625f; // squad flags are slightly stretched, so unstretch them
return MaskedHeight / (float) MaskedWidth; return MaskedHeight / (float) MaskedWidth;
} }
} }

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using ConformalDecals.MaterialProperties; using ConformalDecals.MaterialProperties;
using ConformalDecals.Util; using ConformalDecals.Util;
using UniLinq;
using UnityEngine; using UnityEngine;
namespace ConformalDecals { namespace ConformalDecals {
@ -79,7 +80,9 @@ namespace ConformalDecals {
UI_FloatRange()] UI_FloatRange()]
public float wear = 100; public float wear = 100;
[KSPField(isPersistant = true)] public bool projectMultiple; // reserved for future features. do not modify [KSPField(guiName = "#LOC_ConformalDecals_gui-multiproject", guiActive = false, guiActiveEditor = true, isPersistant = true),
UI_Toggle()]
public bool projectMultiple = true;
[KSPField] public MaterialPropertyCollection materialProperties; [KSPField] public MaterialPropertyCollection materialProperties;
@ -96,7 +99,7 @@ namespace ConformalDecals {
private const int DecalQueueMax = 2400; private const int DecalQueueMax = 2400;
private static int _decalQueueCounter = -1; private static int _decalQueueCounter = -1;
private List<ProjectionTarget> _targets; private Dictionary<Part, ProjectionPartTarget> _targets = new Dictionary<Part, ProjectionPartTarget>();
private bool _isAttached; private bool _isAttached;
private Matrix4x4 _orthoMatrix; private Matrix4x4 _orthoMatrix;
@ -116,6 +119,8 @@ namespace ConformalDecals {
} }
} }
// EVENTS
/// <inheritdoc /> /// <inheritdoc />
public override void OnAwake() { public override void OnAwake() {
base.OnAwake(); base.OnAwake();
@ -130,8 +135,261 @@ namespace ConformalDecals {
/// <inheritdoc /> /// <inheritdoc />
public override void OnLoad(ConfigNode node) { public override void OnLoad(ConfigNode node) {
// Load
try { try {
// SETUP TRANSFORMS LoadDecal(node);
}
catch (Exception e) {
this.LogException("Error loading decal", e);
}
// Setup
try {
SetupDecal();
}
catch (Exception e) {
this.LogException("Error setting up decal", e);
}
}
/// <inheritdoc />
public override void OnSave(ConfigNode node) {
// SAVE TARGETS
if (HighLogic.LoadedSceneIsFlight) {
foreach (var partTarget in _targets.Values) {
if (partTarget.enabled) node.AddNode(partTarget.Save());
}
}
base.OnSave(node);
}
/// <inheritdoc />
public override void OnIconCreate() {
UpdateTextures();
UpdateProjection();
}
/// <inheritdoc />
public override void OnStart(StartState state) {
materialProperties.RenderQueue = DecalQueue;
_boundsRenderer = decalProjectorTransform.GetComponent<MeshRenderer>();
// 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
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<Collider>().enabled = false;
_boundsRenderer.enabled = false;
}
}
}
/// Called 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();
UpdateTargets();
foreach (var counterpart in part.symmetryCounterparts) {
var decal = counterpart.GetComponent<ModuleConformalDecal>();
decal.UpdateProjection();
decal.UpdateTargets();
}
}
/// Called when the decal's material is modified through a tweakable
protected void OnMaterialTweakEvent(BaseField field, object obj) {
materialProperties.SetOpacity(opacity);
materialProperties.SetCutoff(cutoff);
if (useBaseNormal) {
materialProperties.SetWear(wear);
}
foreach (var counterpart in part.symmetryCounterparts) {
var decal = counterpart.GetComponent<ModuleConformalDecal>();
decal.materialProperties.SetOpacity(opacity);
decal.materialProperties.SetCutoff(cutoff);
if (useBaseNormal) {
decal.materialProperties.SetWear(wear);
}
}
}
/// Called 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);
UpdateTargets();
}
}
/// Called 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) {
if (this.part == eventPart) {
UpdateProjection();
UpdateTargets();
}
else if (_isAttached && projectMultiple) {
UpdatePartTarget(eventPart, _boundsRenderer.bounds);
// recursively call for child parts
foreach (var child in eventPart.children) {
OnPartTransformed(child);
}
}
}
/// Called when a part is attached in the editor
protected void OnPartAttached(Part eventPart) {
if (this.part == eventPart) {
OnAttach();
}
else if (projectMultiple) {
UpdatePartTarget(eventPart, _boundsRenderer.bounds);
// recursively call for child parts
foreach (var child in eventPart.children) {
OnPartAttached(child);
}
}
}
/// Called when a part is detached in the editor
protected void OnPartDetached(Part eventPart) {
if (this.part == eventPart) {
OnDetach();
}
else if (projectMultiple) {
_targets.Remove(eventPart);
// recursively call for child parts
foreach (var child in eventPart.children) {
OnPartDetached(child);
}
}
}
/// Called when part `willDie` will be destroyed
protected void OnPartWillDie(Part willDie) {
if (willDie == part.parent) {
this.Log("Parent part about to be destroyed! Killing decal part.");
part.Die();
}
else if (projectMultiple) {
_targets.Remove(willDie);
}
}
/// Called when decal is attached to a new part
protected virtual void OnAttach() {
if (part.parent == null) {
this.LogError("Attach function called but part has no parent!");
_isAttached = false;
return;
}
_isAttached = true;
// hide model
decalModelTransform.gameObject.SetActive(false);
// unhide projector
decalProjectorTransform.gameObject.SetActive(true);
// add to preCull delegate
Camera.onPreCull += Render;
UpdateMaterials();
UpdateProjection();
UpdateTargets();
}
/// Called when decal is detached from its parent part
protected virtual void OnDetach() {
_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
decalFrontTransform = part.FindModelTransform(decalFront); decalFrontTransform = part.FindModelTransform(decalFront);
if (decalFrontTransform == null) throw new FormatException($"Could not find decalFront transform: '{decalFront}'."); if (decalFrontTransform == null) throw new FormatException($"Could not find decalFront transform: '{decalFront}'.");
@ -167,30 +425,10 @@ namespace ConformalDecals {
} }
// PARSE MATERIAL PROPERTIES // PARSE MATERIAL PROPERTIES
// set shader // set shader
materialProperties.SetShader(shader); materialProperties.SetShader(shader);
materialProperties.AddOrGetProperty<MaterialKeywordProperty>("DECAL_BASE_NORMAL").value = useBaseNormal; materialProperties.AddOrGetProperty<MaterialKeywordProperty>("DECAL_BASE_NORMAL").value = useBaseNormal;
materialProperties.Load(node);
// add keyword nodes
foreach (var keywordNode in node.GetNodes("KEYWORD")) {
materialProperties.ParseProperty<MaterialKeywordProperty>(keywordNode);
}
// add texture nodes
foreach (var textureNode in node.GetNodes("TEXTURE")) {
materialProperties.ParseProperty<MaterialTextureProperty>(textureNode);
}
// add float nodes
foreach (var floatNode in node.GetNodes("FLOAT")) {
materialProperties.ParseProperty<MaterialTextureProperty>(floatNode);
}
// add color nodes
foreach (var colorNode in node.GetNodes("COLOR")) {
materialProperties.ParseProperty<MaterialColorProperty>(colorNode);
}
// handle texture tiling parameters // handle texture tiling parameters
var tileString = node.GetValue("tile"); var tileString = node.GetValue("tile");
@ -205,23 +443,31 @@ namespace ConformalDecals {
else if (tileIndex >= 0) { else if (tileIndex >= 0) {
materialProperties.UpdateTile(tileIndex, tileSize); materialProperties.UpdateTile(tileIndex, tileSize);
} }
// PARSE TARGETS
if (HighLogic.LoadedSceneIsFlight) {
foreach (var partTargetNode in node.GetNodes(ProjectionPartTarget.NodeName)) {
try {
var partTarget = new ProjectionPartTarget(partTargetNode, part.vessel, useBaseNormal);
_targets.Add(partTarget.part, partTarget);
} }
catch (Exception e) { catch (Exception e) {
this.LogException("Exception parsing partmodule", e); this.LogWarning($"Encountered error while parsing part node: {e}");
} }
UpdateMaterials(); }
}
foreach (var keyword in _decalMaterial.shaderKeywords) {
this.Log($"keyword: {keyword}");
} }
/// Setup decal by calling update functions relevent for the current situation
protected virtual void SetupDecal() {
if (HighLogic.LoadedSceneIsEditor) { if (HighLogic.LoadedSceneIsEditor) {
// Update tweakables in editor mode
UpdateTweakables(); UpdateTweakables();
} }
if (HighLogic.LoadedSceneIsGame) { if (HighLogic.LoadedSceneIsGame) {
UpdateScale(); UpdateAll();
} }
else { else {
scale = defaultScale; scale = defaultScale;
@ -230,174 +476,122 @@ namespace ConformalDecals {
cutoff = defaultCutoff; cutoff = defaultCutoff;
wear = defaultWear; wear = defaultWear;
UpdateAll();
// QUEUE PART FOR ICON FIXING IN VAB // QUEUE PART FOR ICON FIXING IN VAB
DecalIconFixer.QueuePart(part.name); DecalIconFixer.QueuePart(part.name);
} }
} }
/// <inheritdoc /> /// Update decal editor tweakables
public override void OnIconCreate() { protected virtual void UpdateTweakables() {
UpdateScale(); // 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;
} }
/// <inheritdoc /> if (depthAdjustable) {
public override void OnStart(StartState state) { var minValue = Mathf.Max(Mathf.Epsilon, depthRange.x);
materialProperties.RenderQueue = DecalQueue; var maxValue = Mathf.Max(minValue, depthRange.y);
_boundsRenderer = decalProjectorTransform.GetComponent<MeshRenderer>(); var depthEditor = (UI_FloatRange) depthField.uiControlEditor;
depthEditor.minValue = minValue;
// handle tweakables depthEditor.maxValue = maxValue;
if (HighLogic.LoadedSceneIsEditor) { depthEditor.stepIncrement = 0.01f; //1cm
GameEvents.onEditorPartEvent.Add(OnEditorEvent); depthEditor.onFieldChanged = OnProjectionTweakEvent;
GameEvents.onVariantApplied.Add(OnVariantApplied);
UpdateTweakables();
}
} }
public override void OnStartFinished(StartState state) { if (opacityAdjustable) {
// handle game events var minValue = Mathf.Max(0, opacityRange.x);
if (HighLogic.LoadedSceneIsGame) { var maxValue = Mathf.Max(minValue, opacityRange.y);
// set initial attachment state maxValue = Mathf.Min(1, maxValue);
if (part.parent == null) {
OnDetach(); var opacityEditor = (UI_FloatRange) opacityField.uiControlEditor;
} opacityEditor.minValue = minValue;
else { opacityEditor.maxValue = maxValue;
OnAttach(); opacityEditor.stepIncrement = (maxValue - minValue) / steps;
} opacityEditor.onFieldChanged = OnMaterialTweakEvent;
} }
// handle flight events if (cutoffAdjustable) {
if (HighLogic.LoadedSceneIsFlight) { var minValue = Mathf.Max(0, cutoffRange.x);
GameEvents.onPartWillDie.Add(OnPartWillDie); var maxValue = Mathf.Max(minValue, cutoffRange.y);
maxValue = Mathf.Min(1, maxValue);
if (part.parent == null) part.explode(); var cutoffEditor = (UI_FloatRange) cutoffField.uiControlEditor;
cutoffEditor.minValue = minValue;
Part.layerMask |= 1 << DecalConfig.DecalLayer; cutoffEditor.maxValue = maxValue;
decalColliderTransform.gameObject.layer = DecalConfig.DecalLayer; cutoffEditor.stepIncrement = (maxValue - minValue) / steps;
cutoffEditor.onFieldChanged = OnMaterialTweakEvent;
if (!selectableInFlight || !DecalConfig.SelectableInFlight) {
decalColliderTransform.GetComponent<Collider>().enabled = false;
_boundsRenderer.enabled = false;
}
}
} }
public virtual void OnDestroy() { if (useBaseNormal) {
// remove GameEvents var minValue = Mathf.Max(0, wearRange.x);
if (HighLogic.LoadedSceneIsEditor) { var maxValue = Mathf.Max(minValue, wearRange.y);
GameEvents.onEditorPartEvent.Remove(OnEditorEvent);
GameEvents.onVariantApplied.Remove(OnVariantApplied); var wearEditor = (UI_FloatRange) wearField.uiControlEditor;
wearEditor.minValue = minValue;
wearEditor.maxValue = maxValue;
wearEditor.stepIncrement = (maxValue - minValue) / steps;
wearEditor.onFieldChanged = OnMaterialTweakEvent;
} }
if (HighLogic.LoadedSceneIsFlight) { var multiprojectEditor = (UI_Toggle) multiprojectField.uiControlEditor;
GameEvents.onPartWillDie.Remove(OnPartWillDie); multiprojectEditor.onFieldChanged = OnProjectionTweakEvent;
} }
// remove from preCull delegate /// Updates textures, materials, scale and targets
Camera.onPreCull -= Render; protected virtual void UpdateAll() {
UpdateTextures();
// destroy material properties object UpdateMaterials();
Destroy(materialProperties); UpdateProjection();
UpdateTargets();
} }
protected void OnSizeTweakEvent(BaseField field, object obj) { /// Update decal textures
// scale or depth values have been changed, so update scale protected virtual void UpdateTextures() { }
// and update projection matrices if attached
UpdateScale();
foreach (var counterpart in part.symmetryCounterparts) { /// Update decal materials
var decal = counterpart.GetComponent<ModuleConformalDecal>(); protected virtual void UpdateMaterials() {
decal.UpdateScale(); materialProperties.UpdateMaterials();
}
}
protected void OnMaterialTweakEvent(BaseField field, object obj) {
materialProperties.SetOpacity(opacity); materialProperties.SetOpacity(opacity);
materialProperties.SetCutoff(cutoff); materialProperties.SetCutoff(cutoff);
if (useBaseNormal) { if (useBaseNormal) {
materialProperties.SetWear(wear); materialProperties.SetWear(wear);
} }
foreach (var counterpart in part.symmetryCounterparts) { _decalMaterial = materialProperties.DecalMaterial;
var decal = counterpart.GetComponent<ModuleConformalDecal>(); _previewMaterial = materialProperties.PreviewMaterial;
decal.materialProperties.SetOpacity(opacity);
decal.materialProperties.SetCutoff(cutoff); if (!_isAttached) decalFrontTransform.GetComponent<MeshRenderer>().material = _previewMaterial;
if (useBaseNormal) {
decal.materialProperties.SetWear(wear);
}
}
} }
protected void OnVariantApplied(Part eventPart, PartVariant variant) { /// Update decal scale and projection
if (_isAttached && eventPart == part.parent) { protected void UpdateProjection() {
UpdateTargets();
}
}
protected void OnEditorEvent(ConstructionEventType eventType, Part eventPart) { // Update scale and depth
if (this.part != eventPart && !part.symmetryCounterparts.Contains(eventPart)) return;
switch (eventType) {
case ConstructionEventType.PartAttached:
OnAttach();
break;
case ConstructionEventType.PartDetached:
OnDetach();
break;
case ConstructionEventType.PartOffsetting:
case ConstructionEventType.PartRotating:
UpdateScale();
break;
}
}
protected void OnPartWillDie(Part willDie) {
if (willDie == part.parent) {
this.Log("Parent part about to be destroyed! Killing decal part.");
part.Die();
}
}
protected virtual void OnAttach() {
if (part.parent == null) {
this.LogError("Attach function called but part has no parent!");
_isAttached = false;
return;
}
_isAttached = true;
// hide model
decalModelTransform.gameObject.SetActive(false);
// unhide projector
decalProjectorTransform.gameObject.SetActive(true);
// add to preCull delegate
Camera.onPreCull += Render;
UpdateMaterials();
UpdateTargets();
UpdateScale();
}
protected virtual void OnDetach() {
_isAttached = false;
// unhide model
decalModelTransform.gameObject.SetActive(true);
// hide projector
decalProjectorTransform.gameObject.SetActive(false);
// remove from preCull delegate
Camera.onPreCull -= Render;
UpdateMaterials();
UpdateScale();
}
protected void UpdateScale() {
scale = Mathf.Max(0.01f, scale); scale = Mathf.Max(0.01f, scale);
depth = Mathf.Max(0.01f, depth); depth = Mathf.Max(0.01f, depth);
var aspectRatio = Mathf.Max(0.01f, materialProperties.AspectRatio); var aspectRatio = Mathf.Max(0.01f, materialProperties.AspectRatio);
@ -437,11 +631,6 @@ namespace ConformalDecals {
_orthoMatrix[1, 3] = 0.5f; _orthoMatrix[1, 3] = 0.5f;
decalProjectorTransform.localScale = new Vector3(size.x, size.y, depth); decalProjectorTransform.localScale = new Vector3(size.x, size.y, depth);
// update projection
foreach (var target in _targets) {
target.Project(_orthoMatrix, decalProjectorTransform, _boundsRenderer.bounds, useBaseNormal);
}
} }
else { else {
// rescale preview model // rescale preview model
@ -454,129 +643,54 @@ namespace ConformalDecals {
} }
} }
protected virtual void UpdateMaterials() { /// Called when updating decal targets
materialProperties.UpdateMaterials();
materialProperties.SetOpacity(opacity);
materialProperties.SetCutoff(cutoff);
if (useBaseNormal) {
materialProperties.SetWear(wear);
}
_decalMaterial = materialProperties.DecalMaterial;
_previewMaterial = materialProperties.PreviewMaterial;
if (!_isAttached) decalFrontTransform.GetComponent<MeshRenderer>().material = _previewMaterial;
}
protected void UpdateTargets() { protected void UpdateTargets() {
if (_targets == null) { if (!_isAttached) return;
_targets = new List<ProjectionTarget>();
var projectionBounds = _boundsRenderer.bounds;
// collect list of potential targets
IEnumerable<Part> targetParts;
if (projectMultiple) {
targetParts = HighLogic.LoadedSceneIsFlight ? part.vessel.parts : EditorLogic.fetch.ship.parts;
} }
else { else {
_targets.Clear(); targetParts = new[] {part.parent};
} }
// find all valid renderers foreach (var targetPart in targetParts) {
var renderers = part.parent.FindModelComponents<MeshRenderer>(); UpdatePartTarget(targetPart, projectionBounds);
foreach (var renderer in renderers) {
// skip disabled renderers
if (renderer.gameObject.activeInHierarchy == false) continue;
// skip blacklisted shaders
if (DecalConfig.IsBlacklisted(renderer.material.shader)) continue;
var meshFilter = renderer.GetComponent<MeshFilter>();
if (meshFilter == null) continue; // object has a meshRenderer with no filter, invalid
var mesh = meshFilter.mesh;
if (mesh == null) continue; // object has a null mesh, invalid
// create new ProjectionTarget to represent the renderer
var target = new ProjectionTarget(renderer, mesh);
// add the target to the list
_targets.Add(target);
} }
} }
protected virtual void UpdateTweakables() { protected void UpdatePartTarget(Part targetPart, Bounds projectionBounds) {
// setup tweakable fields if (targetPart.GetComponent<ModuleConformalDecal>() != null) return; // skip other decals
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)];
scaleField.guiActiveEditor = scaleAdjustable; this.Log($"Updating projection onto part {targetPart.name}");
depthField.guiActiveEditor = depthAdjustable;
opacityField.guiActiveEditor = opacityAdjustable;
cutoffField.guiActiveEditor = cutoffAdjustable;
wearField.guiActiveEditor = useBaseNormal;
var steps = 20; if (!_targets.TryGetValue(targetPart, out var target)) {
var rendererList = targetPart.FindModelComponents<MeshRenderer>();
if (scaleAdjustable) { if (rendererList.Any(o => projectionBounds.Intersects(o.bounds))) {
var minValue = Mathf.Max(Mathf.Epsilon, scaleRange.x); target = new ProjectionPartTarget(targetPart, useBaseNormal);
var maxValue = Mathf.Max(minValue, scaleRange.y); _targets.Add(targetPart, target);
var scaleEditor = (UI_FloatRange) scaleField.uiControlEditor;
scaleEditor.minValue = minValue;
scaleEditor.maxValue = maxValue;
scaleEditor.stepIncrement = 0.01f; //1cm
scaleEditor.onFieldChanged = OnSizeTweakEvent;
} }
else {
if (depthAdjustable) { return;
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 = OnSizeTweakEvent;
}
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;
} }
} }
this.Log($"valid target: {targetPart.name}");
target.Project(_orthoMatrix, decalProjectorTransform, projectionBounds);
}
/// Render the decal
public void Render(Camera camera) { public void Render(Camera camera) {
if (!_isAttached) return; if (!_isAttached) return;
// render on each target object // render on each target object
foreach (var target in _targets) { foreach (var target in _targets.Values) {
target.Render(_decalMaterial, part.mpb, camera); target.Render(_decalMaterial, part.mpb, camera);
} }
} }

View File

@ -1,4 +1,6 @@
using ConformalDecals.MaterialProperties;
using ConformalDecals.Util; using ConformalDecals.Util;
using UniLinq;
using UnityEngine; using UnityEngine;
namespace ConformalDecals { namespace ConformalDecals {
@ -9,6 +11,8 @@ namespace ConformalDecals {
[KSPField(isPersistant = true)] public bool useCustomFlag; [KSPField(isPersistant = true)] public bool useCustomFlag;
private MaterialTextureProperty _flagTextureProperty;
public string MissionFlagUrl { public string MissionFlagUrl {
get { get {
if (HighLogic.LoadedSceneIsEditor) { if (HighLogic.LoadedSceneIsEditor) {
@ -23,17 +27,6 @@ namespace ConformalDecals {
} }
} }
public override void OnLoad(ConfigNode node) {
base.OnLoad(node);
if (useCustomFlag) {
SetFlag(flagUrl);
}
else {
SetFlag(MissionFlagUrl);
}
}
public override void OnStart(StartState state) { public override void OnStart(StartState state) {
base.OnStart(state); base.OnStart(state);
@ -44,17 +37,10 @@ namespace ConformalDecals {
if (HighLogic.LoadedSceneIsEditor) { if (HighLogic.LoadedSceneIsEditor) {
Events[nameof(ResetFlag)].guiActiveEditor = useCustomFlag; Events[nameof(ResetFlag)].guiActiveEditor = useCustomFlag;
} }
if (useCustomFlag) {
SetFlag(flagUrl);
}
else {
SetFlag(MissionFlagUrl);
}
} }
public override void OnDestroy() { public override void OnDestroy() {
GameEvents.onMissionFlagSelect.Remove(SetFlag); GameEvents.onMissionFlagSelect.Remove(OnEditorFlagSelected);
base.OnDestroy(); base.OnDestroy();
} }
@ -66,44 +52,45 @@ namespace ConformalDecals {
[KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_ConformalDecals_gui-reset-flag")] [KSPEvent(guiActive = false, guiActiveEditor = true, guiName = "#LOC_ConformalDecals_gui-reset-flag")]
public void ResetFlag() { public void ResetFlag() {
SetFlag(MissionFlagUrl);
SetFlagSymmetryCounterparts(MissionFlagUrl);
useCustomFlag = false;
Events[nameof(ResetFlag)].guiActiveEditor = false; Events[nameof(ResetFlag)].guiActiveEditor = false;
flagUrl = MissionFlagUrl;
useCustomFlag = false;
UpdateAll();
foreach (var decal in part.symmetryCounterparts.Select(o => o.GetComponent<ModuleConformalFlag>())) {
decal.Events[nameof(ResetFlag)].guiActiveEditor = false;
decal.flagUrl = flagUrl;
decal.useCustomFlag = false;
decal.UpdateAll();
}
} }
private void OnCustomFlagSelected(FlagBrowser.FlagEntry newFlagEntry) { private void OnCustomFlagSelected(FlagBrowser.FlagEntry newFlagEntry) {
SetFlag(newFlagEntry.textureInfo.name);
SetFlagSymmetryCounterparts(newFlagEntry.textureInfo.name);
useCustomFlag = true;
Events[nameof(ResetFlag)].guiActiveEditor = true; Events[nameof(ResetFlag)].guiActiveEditor = true;
flagUrl = newFlagEntry.textureInfo.name;
useCustomFlag = true;
UpdateAll();
foreach (var decal in part.symmetryCounterparts.Select(o => o.GetComponent<ModuleConformalFlag>())) {
decal.Events[nameof(ResetFlag)].guiActiveEditor = true;
decal.flagUrl = flagUrl;
decal.useCustomFlag = true;
decal.UpdateAll();
}
} }
private void OnEditorFlagSelected(string newFlagUrl) { private void OnEditorFlagSelected(string newFlagUrl) {
if (!useCustomFlag) { if (!useCustomFlag) UpdateAll();
SetFlag(newFlagUrl);
SetFlagSymmetryCounterparts(newFlagUrl);
}
} }
private void SetFlag(string newFlagUrl) { protected override void UpdateTextures() {
this.Log($"Loading flag texture '{newFlagUrl}'."); _flagTextureProperty ??= materialProperties.AddOrGetTextureProperty("_Decal", true);
flagUrl = newFlagUrl; base.UpdateTextures();
materialProperties.AddOrGetTextureProperty("_Decal", true).TextureUrl = newFlagUrl; if (useCustomFlag) {
_flagTextureProperty.TextureUrl = flagUrl;
UpdateMaterials();
UpdateScale();
} }
else {
private void SetFlagSymmetryCounterparts(string newFlagUrl) { _flagTextureProperty.TextureUrl = MissionFlagUrl;
foreach (var counterpart in part.symmetryCounterparts) {
var decal = counterpart.GetComponent<ModuleConformalFlag>();
decal.SetFlag(newFlagUrl);
decal.useCustomFlag = useCustomFlag;
} }
} }
} }

View File

@ -5,6 +5,7 @@ using ConformalDecals.Text;
using ConformalDecals.UI; using ConformalDecals.UI;
using ConformalDecals.Util; using ConformalDecals.Util;
using TMPro; using TMPro;
using UniLinq;
using UnityEngine; using UnityEngine;
namespace ConformalDecals { namespace ConformalDecals {
@ -89,42 +90,11 @@ namespace ConformalDecals {
private MaterialColorProperty _outlineColorProperty; private MaterialColorProperty _outlineColorProperty;
private MaterialFloatProperty _outlineWidthProperty; private MaterialFloatProperty _outlineWidthProperty;
private TextRenderJob _currentJob;
private DecalText _currentText; private DecalText _currentText;
public override void OnLoad(ConfigNode node) { // EVENTS
base.OnLoad(node);
string textRaw = "";
if (ParseUtil.ParseStringIndirect(ref textRaw, node, "text")) {
text = WebUtility.UrlDecode(textRaw);
}
string fontName = "";
if (ParseUtil.ParseStringIndirect(ref fontName, node, "fontName")) {
font = DecalConfig.GetFont(fontName);
}
else if (font == null) font = DecalConfig.GetFont("Calibri SDF");
int styleInt = 0;
if (ParseUtil.ParseIntIndirect(ref styleInt, node, "style")) {
style = (FontStyles) styleInt;
}
ParseUtil.ParseColor32Indirect(ref fillColor, node, "fillColor");
ParseUtil.ParseColor32Indirect(ref outlineColor, node, "outlineColor");
if (HighLogic.LoadedSceneIsGame) {
// For some reason, rendering doesnt work right on the first frame a scene is loaded
// So delay any rendering until the next frame when called in OnLoad
// This is probably a problem with Unity, not KSP
StartCoroutine(UpdateTextLate());
}
else {
UpdateText();
}
}
/// <inheritdoc />
public override void OnSave(ConfigNode node) { public override void OnSave(ConfigNode node) {
node.AddValue("text", WebUtility.UrlEncode(text)); node.AddValue("text", WebUtility.UrlEncode(text));
node.AddValue("fontName", font.Name); node.AddValue("fontName", font.Name);
@ -148,13 +118,23 @@ namespace ConformalDecals {
} }
public void OnTextUpdate(string newText, DecalFont newFont, FontStyles newStyle, bool newVertical, float newLineSpacing, float newCharSpacing) { public void OnTextUpdate(string newText, DecalFont newFont, FontStyles newStyle, bool newVertical, float newLineSpacing, float newCharSpacing) {
this.text = newText; text = newText;
this.font = newFont; font = newFont;
this.style = newStyle; style = newStyle;
this.vertical = newVertical; vertical = newVertical;
this.lineSpacing = newLineSpacing; lineSpacing = newLineSpacing;
this.charSpacing = newCharSpacing; charSpacing = newCharSpacing;
UpdateTextRecursive(); UpdateAll();
foreach (var decal in part.symmetryCounterparts.Select(o => o.GetComponent<ModuleConformalText>())) {
decal.text = newText;
decal.font = newFont;
decal.style = newStyle;
decal.vertical = newVertical;
decal.lineSpacing = newLineSpacing;
decal.charSpacing = newCharSpacing;
decal.UpdateAll();
}
} }
public void OnFillColorUpdate(Color rgb, Util.ColorHSV hsv) { public void OnFillColorUpdate(Color rgb, Util.ColorHSV hsv) {
@ -241,49 +221,70 @@ namespace ConformalDecals {
base.OnDetach(); base.OnDetach();
} }
private void UpdateTextRecursive() { // FUNCTIONS
UpdateText();
foreach (var counterpart in part.symmetryCounterparts) { protected override void LoadDecal(ConfigNode node) {
var decal = counterpart.GetComponent<ModuleConformalText>(); base.LoadDecal(node);
decal.text = text;
decal.font = font;
decal.style = style;
decal.vertical = vertical;
decal.charSpacing = charSpacing;
decal.lineSpacing = lineSpacing;
decal._currentJob = _currentJob; string textRaw = "";
decal._currentText = _currentText; if (ParseUtil.ParseStringIndirect(ref textRaw, node, "text")) {
decal.UpdateText(); text = WebUtility.UrlDecode(textRaw);
}
string fontName = "";
if (ParseUtil.ParseStringIndirect(ref fontName, node, "fontName")) {
font = DecalConfig.GetFont(fontName);
}
else if (font == null) font = DecalConfig.GetFont("Calibri SDF");
int styleInt = 0;
if (ParseUtil.ParseIntIndirect(ref styleInt, node, "style")) {
style = (FontStyles) styleInt;
}
ParseUtil.ParseColor32Indirect(ref fillColor, node, "fillColor");
ParseUtil.ParseColor32Indirect(ref outlineColor, node, "outlineColor");
}
protected override void SetupDecal() {
if (HighLogic.LoadedSceneIsEditor) {
// Update tweakables in editor mode
UpdateTweakables();
}
if (HighLogic.LoadedSceneIsGame) {
// For some reason text rendering fails on the first frame of a scene, so this is my workaround
StartCoroutine(UpdateTextLate());
}
else {
scale = defaultScale;
depth = defaultDepth;
opacity = defaultOpacity;
cutoff = defaultCutoff;
wear = defaultWear;
UpdateTextures();
UpdateMaterials();
UpdateProjection();
// QUEUE PART FOR ICON FIXING IN VAB
DecalIconFixer.QueuePart(part.name);
} }
} }
private IEnumerator UpdateTextLate() { private IEnumerator UpdateTextLate() {
yield return null; yield return null;
UpdateText(); UpdateAll();
} }
private void UpdateText() { protected override void UpdateTextures() {
// Render text // Render text
var newText = new DecalText(text, font, style, vertical, lineSpacing, charSpacing); var newText = new DecalText(text, font, style, vertical, lineSpacing, charSpacing);
var output = TextRenderer.UpdateTextNow(_currentText, newText); var output = TextRenderer.UpdateText(_currentText, newText);
_currentText = newText; _currentText = newText;
UpdateTexture(output);
// TODO: ASYNC RENDERING
// var newText = new DecalText(text, _font, _style);
// _currentJob = TextRenderer.UpdateText(_currentText, newText, UpdateTexture);
// _currentText = newText;
}
public void UpdateTexture(TextRenderOutput output) {
_decalTextureProperty.Texture = output.Texture; _decalTextureProperty.Texture = output.Texture;
_decalTextureProperty.SetTile(output.Window); _decalTextureProperty.SetTile(output.Window);
UpdateMaterials();
UpdateScale();
} }
protected override void UpdateMaterials() { protected override void UpdateMaterials() {

View File

@ -0,0 +1,167 @@
using System;
using System.Text;
using ConformalDecals.Util;
using UniLinq;
using UnityEngine;
using UnityEngine.Rendering;
namespace ConformalDecals {
public class ProjectionMeshTarget : IProjectionTarget {
public const string NodeName = "MESH_TARGET";
// enabled flag
public bool enabled;
// Target object data
public readonly Transform target;
public readonly Transform root;
public readonly Mesh mesh;
public readonly MeshRenderer renderer;
// Projection data
private Matrix4x4 _decalMatrix;
private Vector3 _decalNormal;
private Vector3 _decalTangent;
// property block
private readonly MaterialPropertyBlock _decalMPB;
public ProjectionMeshTarget(Transform target, Transform root, MeshRenderer renderer, Mesh mesh, bool useBaseNormal) {
this.root = root;
this.target = target;
this.renderer = renderer;
this.mesh = mesh;
_decalMPB = new MaterialPropertyBlock();
SetNormalMap(renderer.sharedMaterial, useBaseNormal);
}
public ProjectionMeshTarget(ConfigNode node, Transform root, bool useBaseNormal) {
if (node == null) throw new ArgumentNullException(nameof(node));
if (root == null) throw new ArgumentNullException(nameof(root));
enabled = true;
var targetPath = ParseUtil.ParseString(node, "targetPath");
var targetName = ParseUtil.ParseString(node, "targetName");
_decalMatrix = ParseUtil.ParseMatrix4x4(node, "decalMatrix");
_decalNormal = ParseUtil.ParseVector3(node, "decalNormal");
_decalTangent = ParseUtil.ParseVector3(node, "decalTangent");
_decalMPB = new MaterialPropertyBlock();
target = LoadTransformPath(targetPath, root);
if (target.name != targetName) throw new FormatException("Target name does not match");
renderer = target.GetComponent<MeshRenderer>();
var filter = target.GetComponent<MeshFilter>();
if (!ValidateTarget(target, renderer, filter)) throw new FormatException("Invalid target");
mesh = filter.sharedMesh;
SetNormalMap(renderer.sharedMaterial, useBaseNormal);
_decalMPB.SetMatrix(DecalPropertyIDs._ProjectionMatrix, _decalMatrix);
_decalMPB.SetVector(DecalPropertyIDs._DecalNormal, _decalNormal);
_decalMPB.SetVector(DecalPropertyIDs._DecalTangent, _decalTangent);
}
private void SetNormalMap(Material targetMaterial, bool useBaseNormal) {
if (useBaseNormal && targetMaterial.HasProperty(DecalPropertyIDs._BumpMap)) {
_decalMPB.SetTexture(DecalPropertyIDs._BumpMap, targetMaterial.GetTexture(DecalPropertyIDs._BumpMap));
var normalScale = targetMaterial.GetTextureScale(DecalPropertyIDs._BumpMap);
var normalOffset = targetMaterial.GetTextureOffset(DecalPropertyIDs._BumpMap);
_decalMPB.SetVector(DecalPropertyIDs._BumpMap_ST, new Vector4(normalScale.x, normalScale.y, normalOffset.x, normalOffset.y));
}
else {
_decalMPB.SetTexture(DecalPropertyIDs._BumpMap, DecalConfig.BlankNormal);
}
}
public bool Project(Matrix4x4 orthoMatrix, Transform projector, Bounds projectionBounds) {
if (projectionBounds.Intersects(renderer.bounds)) {
enabled = true;
var projectorToTargetMatrix = target.worldToLocalMatrix * projector.localToWorldMatrix;
_decalMatrix = orthoMatrix * projectorToTargetMatrix.inverse;
_decalNormal = projectorToTargetMatrix.MultiplyVector(Vector3.back).normalized;
_decalTangent = projectorToTargetMatrix.MultiplyVector(Vector3.right).normalized;
_decalMPB.SetMatrix(DecalPropertyIDs._ProjectionMatrix, _decalMatrix);
_decalMPB.SetVector(DecalPropertyIDs._DecalNormal, _decalNormal);
_decalMPB.SetVector(DecalPropertyIDs._DecalTangent, _decalTangent);
}
else {
enabled = false;
}
return enabled;
}
public void Render(Material decalMaterial, MaterialPropertyBlock partMPB, Camera camera) {
if (!enabled) return;
_decalMPB.SetFloat(PropertyIDs._RimFalloff, partMPB.GetFloat(PropertyIDs._RimFalloff));
_decalMPB.SetColor(PropertyIDs._RimColor, partMPB.GetColor(PropertyIDs._RimColor));
Graphics.DrawMesh(mesh, target.localToWorldMatrix, decalMaterial, 0, camera, 0, _decalMPB, ShadowCastingMode.Off, true);
}
public ConfigNode Save() {
var node = new ConfigNode(NodeName);
node.AddValue("decalMatrix", _decalMatrix);
node.AddValue("decalNormal", _decalNormal);
node.AddValue("decalTangent", _decalTangent);
node.AddValue("targetPath", SaveTransformPath(target, root)); // used to find the target transform
node.AddValue("targetName", target.name); // used to validate the mesh has not changed since last load
return node;
}
public static bool ValidateTarget(Transform target, MeshRenderer renderer, MeshFilter filter) {
if (renderer == null) return false;
if (filter == null) return false;
if (!target.gameObject.activeInHierarchy) return false;
var material = renderer.material;
if (material == null) return false;
if (DecalConfig.IsBlacklisted(material.shader)) return false;
if (filter.sharedMesh == null) return false;
return true;
}
private static string SaveTransformPath(Transform leaf, Transform root) {
var builder = new StringBuilder($"{leaf.GetSiblingIndex()}");
var current = leaf.parent;
while (current != root) {
builder.Insert(0, "/");
builder.Insert(0, current.GetSiblingIndex());
current = current.parent;
if (current == null) throw new FormatException("Leaf does not exist as a child of root");
}
return builder.ToString();
}
private static Transform LoadTransformPath(string path, Transform root) {
var indices = path.Split('/').Select(int.Parse);
var current = root;
Logging.Log($"root transform: {current.name}");
foreach (var index in indices) {
if (index > current.childCount) throw new FormatException("Child index path is invalid");
current = current.GetChild(index);
Logging.Log($"found child {current.name} at index {index}");
}
return current;
}
}
}

View File

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using ConformalDecals.Util;
using UnityEngine;
namespace ConformalDecals {
public class ProjectionPartTarget : IProjectionTarget {
public const string NodeName = "PART_TARGET";
// enabled flag
public bool enabled;
// locked flag, to prevent re-projection of loaded targets
public readonly bool locked;
public readonly Part part;
public readonly List<ProjectionMeshTarget> meshTargets = new List<ProjectionMeshTarget>();
public ProjectionPartTarget(Part part, bool useBaseNormal) {
this.part = part;
locked = false;
foreach (var renderer in part.FindModelComponents<MeshRenderer>()) {
var target = renderer.transform;
var filter = target.GetComponent<MeshFilter>();
// check if the target has any missing data
if (!ProjectionMeshTarget.ValidateTarget(target, renderer, filter)) continue;
// create new ProjectionTarget to represent the renderer
var projectionTarget = new ProjectionMeshTarget(target, part.transform, renderer, filter.sharedMesh, useBaseNormal);
// add the target to the list
meshTargets.Add(projectionTarget);
}
}
public ProjectionPartTarget(ConfigNode node, Vessel vessel, bool useBaseNormal) {
if (node == null) throw new ArgumentNullException(nameof(node));
locked = true;
enabled = true;
var flightID = ParseUtil.ParseUint(node, "part");
part = vessel[flightID];
if (part == null) throw new IndexOutOfRangeException("Vessel returned null part, part must be destroyed or detached");
var root = part.transform;
foreach (var meshTargetNode in node.GetNodes(ProjectionMeshTarget.NodeName)) {
meshTargets.Add(new ProjectionMeshTarget(meshTargetNode, root, useBaseNormal));
}
Logging.Log($"Loaded target for part {part.name}");
}
public bool Project(Matrix4x4 orthoMatrix, Transform projector, Bounds projectionBounds) {
if (locked) return true; // dont overwrite saved targets in flight mode
enabled = false;
foreach (var meshTarget in meshTargets) {
enabled |= meshTarget.Project(orthoMatrix, projector, projectionBounds);
}
return enabled;
}
public void Render(Material decalMaterial, MaterialPropertyBlock partMPB, Camera camera) {
foreach (var target in meshTargets) {
target.Render(decalMaterial, partMPB, camera);
}
}
public ConfigNode Save() {
var node = new ConfigNode(NodeName);
node.AddValue("part", part.flightID);
foreach (var meshTarget in meshTargets) {
if (meshTarget.enabled) node.AddNode(meshTarget.Save());
}
Logging.Log($"Saved target for part {part.name}");
return node;
}
}
}

View File

@ -1,68 +0,0 @@
using UnityEngine;
using UnityEngine.Rendering;
namespace ConformalDecals {
public class ProjectionTarget {
// Target object data
public readonly Transform target;
private readonly Renderer _targetRenderer;
private readonly Mesh _targetMesh;
private bool _projectionEnabled;
// property block
private readonly MaterialPropertyBlock _decalMPB;
public ProjectionTarget(MeshRenderer targetRenderer, Mesh targetMesh) {
target = targetRenderer.transform;
_targetRenderer = targetRenderer;
_targetMesh = targetMesh;
_decalMPB = new MaterialPropertyBlock();
}
public void Project(Matrix4x4 orthoMatrix, Transform projector, Bounds projectorBounds, bool useBaseNormal) {
if (projectorBounds.Intersects(_targetRenderer.bounds)) {
_projectionEnabled = true;
var targetMaterial = _targetRenderer.sharedMaterial;
var projectorToTargetMatrix = target.worldToLocalMatrix * projector.localToWorldMatrix;
var projectionMatrix = orthoMatrix * projectorToTargetMatrix.inverse;
var decalNormal = projectorToTargetMatrix.MultiplyVector(Vector3.back).normalized;
var decalTangent = projectorToTargetMatrix.MultiplyVector(Vector3.right).normalized;
_decalMPB.SetMatrix(DecalPropertyIDs._ProjectionMatrix, projectionMatrix);
_decalMPB.SetVector(DecalPropertyIDs._DecalNormal, decalNormal);
_decalMPB.SetVector(DecalPropertyIDs._DecalTangent, decalTangent);
if (useBaseNormal && targetMaterial.HasProperty(DecalPropertyIDs._BumpMap)) {
_decalMPB.SetTexture(DecalPropertyIDs._BumpMap, targetMaterial.GetTexture(DecalPropertyIDs._BumpMap));
var normalScale = targetMaterial.GetTextureScale(DecalPropertyIDs._BumpMap);
var normalOffset = targetMaterial.GetTextureOffset(DecalPropertyIDs._BumpMap);
_decalMPB.SetVector(DecalPropertyIDs._BumpMap_ST, new Vector4(normalScale.x, normalScale.y, normalOffset.x, normalOffset.y));
}
else {
_decalMPB.SetTexture(DecalPropertyIDs._BumpMap, DecalConfig.BlankNormal);
}
}
else {
_projectionEnabled = false;
}
}
public bool Render(Material decalMaterial, MaterialPropertyBlock partMPB, Camera camera) {
if (_projectionEnabled) {
_decalMPB.SetFloat(PropertyIDs._RimFalloff, partMPB.GetFloat(PropertyIDs._RimFalloff));
_decalMPB.SetColor(PropertyIDs._RimColor, partMPB.GetColor(PropertyIDs._RimColor));
Graphics.DrawMesh(_targetMesh, target.localToWorldMatrix, decalMaterial, 0, camera, 0, _decalMPB, ShadowCastingMode.Off, true);
return true;
}
return false;
}
}
}

View File

@ -1 +1 @@
[assembly: KSPAssembly("ConformalDecals", 0, 2, 6)] [assembly: KSPAssembly("ConformalDecals", 0, 2)]

View File

@ -46,21 +46,25 @@ namespace ConformalDecals.Text {
public bool SmallCapsMask => (_fontStyleMask & FontStyles.SmallCaps) != 0; public bool SmallCapsMask => (_fontStyleMask & FontStyles.SmallCaps) != 0;
public DecalFont(ConfigNode node, IEnumerable<TMP_FontAsset> fontAssets) { public static DecalFont Parse(ConfigNode node, IEnumerable<TMP_FontAsset> fontAssets) {
if (node == null) throw new ArgumentNullException(nameof(node)); if (node == null) throw new ArgumentNullException(nameof(node));
if (fontAssets == null) throw new ArgumentNullException(nameof(fontAssets)); if (fontAssets == null) throw new ArgumentNullException(nameof(fontAssets));
var font = ScriptableObject.CreateInstance<DecalFont>();
var name = ParseUtil.ParseString(node, "name"); var name = ParseUtil.ParseString(node, "name");
_fontAsset = fontAssets.First(o => o.name == name); var fontAsset = fontAssets.First(o => o.name == name);
if (FontAsset == null) { if (fontAsset == null) {
throw new FormatException($"Could not find font asset named {name}"); throw new FormatException($"Could not find font asset named {name}");
} }
_title = ParseUtil.ParseString(node, "title", true, name); font._fontAsset = fontAsset;
_fontStyle = (FontStyles) ParseUtil.ParseInt(node, "style", true); font._title = ParseUtil.ParseString(node, "title", true, name);
_fontStyleMask = (FontStyles) ParseUtil.ParseInt(node, "styleMask", true); font._fontStyle = (FontStyles) ParseUtil.ParseInt(node, "style", true);
} font._fontStyleMask = (FontStyles) ParseUtil.ParseInt(node, "styleMask", true);
return font;
}
public void SetupSample(TMP_Text tmp) { public void SetupSample(TMP_Text tmp) {
if (tmp == null) throw new ArgumentNullException(nameof(tmp)); if (tmp == null) throw new ArgumentNullException(nameof(tmp));
@ -105,5 +109,9 @@ namespace ConformalDecals.Text {
public void OnBeforeSerialize() { } public void OnBeforeSerialize() { }
public void OnAfterDeserialize() { } public void OnAfterDeserialize() { }
public override string ToString() {
return _title;
}
} }
} }

View File

@ -86,5 +86,9 @@ namespace ConformalDecals.Text {
public static bool operator !=(DecalText left, DecalText right) { public static bool operator !=(DecalText left, DecalText right) {
return !Equals(left, right); return !Equals(left, right);
} }
public override string ToString() {
return $"{nameof(_text)}: {_text}, {nameof(_font)}: {_font}, {nameof(_style)}: {_style}, {nameof(_vertical)}: {_vertical}, {nameof(_lineSpacing)}: {_lineSpacing}, {nameof(_charSpacing)}: {_charSpacing}";
}
} }
} }

View File

@ -1,6 +1,5 @@
using System.IO; using System.IO;
using System.Collections; using System.Collections;
using System.Collections.Generic;
using ConformalDecals.Util; using ConformalDecals.Util;
using TMPro; using TMPro;
using UniLinq; using UniLinq;

View File

@ -1,35 +0,0 @@
using System;
using UnityEngine.Events;
namespace ConformalDecals.Text {
public class TextRenderJob {
public DecalText OldText { get; }
public DecalText NewText { get; }
public bool Needed { get; private set; }
public bool IsStarted { get; private set; }
public bool IsDone { get; private set; }
public readonly TextRenderer.TextRenderEvent onRenderFinished = new TextRenderer.TextRenderEvent();
public TextRenderJob(DecalText oldText, DecalText newText, UnityAction<TextRenderOutput> renderFinishedCallback) {
OldText = oldText;
NewText = newText ?? throw new ArgumentNullException(nameof(newText));
Needed = true;
if (renderFinishedCallback != null) onRenderFinished.AddListener(renderFinishedCallback);
}
public void Cancel() {
Needed = false;
}
public void Start() {
IsStarted = true;
}
public void Finish(TextRenderOutput output) {
IsDone = true;
onRenderFinished.Invoke(output);
}
}
}

View File

@ -9,7 +9,7 @@ namespace ConformalDecals.Text {
/// The rectangle that the rendered text takes up within the texture /// The rectangle that the rendered text takes up within the texture
public Rect Window { get; private set; } public Rect Window { get; private set; }
/// The number of users for this render output. If 0, it can be discarded from the cache and the texture reused /// The number of users for this render output. If 0, it can be discarded from the cache
public int UserCount { get; set; } public int UserCount { get; set; }
public TextRenderOutput(Texture2D texture, Rect window) { public TextRenderOutput(Texture2D texture, Rect window) {

View File

@ -3,16 +3,12 @@ using System.Collections.Generic;
using ConformalDecals.Util; using ConformalDecals.Util;
using TMPro; using TMPro;
using UnityEngine; using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Rendering; using UnityEngine.Rendering;
using Object = UnityEngine.Object;
namespace ConformalDecals.Text { namespace ConformalDecals.Text {
// TODO: Testing shows the job system is unnecessary, so remove job system code.
/// Class handing text rendering. /// Class handing text rendering.
/// Is a singleton referencing a single gameobject in the scene which contains the TextMeshPro component public static class TextRenderer {
[KSPAddon(KSPAddon.Startup.Instantly, true)]
public class TextRenderer : MonoBehaviour {
/// Texture format used for returned textures. /// Texture format used for returned textures.
/// Unfortunately due to how Unity textures work, this cannot be R8 or Alpha8, /// Unfortunately due to how Unity textures work, this cannot be R8 or Alpha8,
/// so theres always a superfluous green channel using memory /// so theres always a superfluous green channel using memory
@ -22,163 +18,73 @@ namespace ConformalDecals.Text {
/// Overriden below to be ARGB32 on DirectX because DirectX is dumb /// Overriden below to be ARGB32 on DirectX because DirectX is dumb
public static RenderTextureFormat textRenderTextureFormat = RenderTextureFormat.R8; public static RenderTextureFormat textRenderTextureFormat = RenderTextureFormat.R8;
/// The text renderer object within the scene which contains the TextMeshPro component used for rendering.
public static TextRenderer Instance {
get {
if (!_instance._isSetup) {
_instance.Setup();
}
return _instance;
}
}
/// Text Render unityevent, used with the job system to signal render completion
[Serializable]
public class TextRenderEvent : UnityEvent<TextRenderOutput> { }
private const string ShaderName = "ConformalDecals/Text Blit"; private const string ShaderName = "ConformalDecals/Text Blit";
private const int MaxTextureSize = 4096; private const int MaxTextureSize = 4096;
private const float FontSize = 100; private const float FontSize = 100;
private const float PixelDensity = 5; private const float PixelDensity = 5;
private static TextRenderer _instance; private static Shader _blitShader;
private static Texture2D _blankTexture;
private bool _isSetup;
private TextMeshPro _tmp;
private Shader _blitShader;
private static readonly Dictionary<DecalText, TextRenderOutput> RenderCache = new Dictionary<DecalText, TextRenderOutput>(); private static readonly Dictionary<DecalText, TextRenderOutput> RenderCache = new Dictionary<DecalText, TextRenderOutput>();
private static readonly Queue<TextRenderJob> RenderJobs = new Queue<TextRenderJob>();
/// Update text using the job queue
public static TextRenderJob UpdateText(DecalText oldText, DecalText newText, UnityAction<TextRenderOutput> renderFinishedCallback) {
if (newText == null) throw new ArgumentNullException(nameof(newText));
var job = new TextRenderJob(oldText, newText, renderFinishedCallback);
RenderJobs.Enqueue(job);
return job;
}
/// Update text immediately without using job queue /// Update text immediately without using job queue
public static TextRenderOutput UpdateTextNow(DecalText oldText, DecalText newText) { public static TextRenderOutput UpdateText(DecalText oldText, DecalText newText) {
if (newText == null) throw new ArgumentNullException(nameof(newText)); if (newText == null) throw new ArgumentNullException(nameof(newText));
return Instance.RunJob(new TextRenderJob(oldText, newText, null), out _); if (!(oldText is null)) UnregisterText(oldText);
// now that all old references are handled, begin rendering the new output
if (!RenderCache.TryGetValue(newText, out var renderOutput)) {
renderOutput = RenderText(newText);
RenderCache.Add(newText, renderOutput);
}
renderOutput.UserCount++;
return renderOutput;
} }
/// Unregister a user of a piece of text /// Unregister a user of a piece of text
public static void UnregisterText(DecalText text) { public static void UnregisterText(DecalText text) {
if (text == null) throw new ArgumentNullException(nameof(text));
if (RenderCache.TryGetValue(text, out var renderedText)) { if (RenderCache.TryGetValue(text, out var renderedText)) {
renderedText.UserCount--; renderedText.UserCount--;
if (renderedText.UserCount <= 0) { if (renderedText.UserCount <= 0) {
RenderCache.Remove(text); RenderCache.Remove(text);
Destroy(renderedText.Texture); var texture = renderedText.Texture;
if (texture != _blankTexture) Object.Destroy(texture);
} }
} }
} }
private void Start() {
if (_instance != null) {
Logging.LogError("Duplicate TextRenderer created???");
}
Logging.Log("Creating TextRenderer Object");
_instance = this;
DontDestroyOnLoad(gameObject);
if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.Direct3D11 || SystemInfo.graphicsDeviceType == GraphicsDeviceType.Direct3D12) {
textRenderTextureFormat = RenderTextureFormat.ARGB32; // DirectX is dumb
}
if (!SystemInfo.SupportsTextureFormat(textTextureFormat)) {
Logging.LogError($"Text texture format {textTextureFormat} not supported on this platform.");
}
if (!SystemInfo.SupportsRenderTextureFormat(textRenderTextureFormat)) {
Logging.LogError($"Text texture format {textRenderTextureFormat} not supported on this platform.");
}
}
/// Setup this text renderer instance for rendering
private void Setup() {
if (_isSetup) return;
Logging.Log("Setting Up TextRenderer Object");
_tmp = gameObject.AddComponent<TextMeshPro>();
_tmp.renderer.enabled = false; // dont automatically render
_blitShader = Shabby.Shabby.FindShader(ShaderName);
if (_blitShader == null) Logging.LogError($"Could not find text blit shader named '{ShaderName}'");
_isSetup = true;
}
/// Run a text render job
private TextRenderOutput RunJob(TextRenderJob job, out bool renderNeeded) {
if (!job.Needed) {
renderNeeded = false;
return null;
}
job.Start();
Texture2D texture = null;
if (job.OldText != null && RenderCache.TryGetValue(job.OldText, out var oldRender)) {
// old output still exists
oldRender.UserCount--;
if (oldRender.UserCount <= 0) {
// this is the only usage of this output, so we are free to re-render into the texture
texture = oldRender.Texture;
RenderCache.Remove(job.OldText);
}
}
// now that all old references are handled, begin rendering the new output
if (RenderCache.TryGetValue(job.NewText, out var renderOutput)) {
renderNeeded = false;
}
else {
renderNeeded = true;
renderOutput = RenderText(job.NewText, texture);
RenderCache.Add(job.NewText, renderOutput);
}
renderOutput.UserCount++;
job.Finish(renderOutput);
return renderOutput;
}
/// Render a piece of text to a given texture /// Render a piece of text to a given texture
public TextRenderOutput RenderText(DecalText text, Texture2D texture) { public static TextRenderOutput RenderText(DecalText text) {
if (text == null) throw new ArgumentNullException(nameof(text)); if (text == null) throw new ArgumentNullException(nameof(text));
if (_tmp == null) throw new InvalidOperationException("TextMeshPro object not yet created.");
var tmpObject = new GameObject("Text Mesh Pro renderer");
var tmp = tmpObject.AddComponent<TextMeshPro>();
// SETUP TMP OBJECT FOR RENDERING // SETUP TMP OBJECT FOR RENDERING
_tmp.text = text.FormattedText; tmp.text = text.FormattedText;
_tmp.font = text.Font.FontAsset; tmp.font = text.Font.FontAsset;
_tmp.fontStyle = text.Style | text.Font.FontStyle; tmp.fontStyle = text.Style | text.Font.FontStyle;
_tmp.lineSpacing = text.LineSpacing; tmp.lineSpacing = text.LineSpacing;
_tmp.characterSpacing = text.CharSpacing; tmp.characterSpacing = text.CharSpacing;
_tmp.extraPadding = true; tmp.extraPadding = true;
_tmp.enableKerning = true; tmp.enableKerning = true;
_tmp.enableWordWrapping = false; tmp.enableWordWrapping = false;
_tmp.overflowMode = TextOverflowModes.Overflow; tmp.overflowMode = TextOverflowModes.Overflow;
_tmp.alignment = TextAlignmentOptions.Center; tmp.alignment = TextAlignmentOptions.Center;
_tmp.fontSize = FontSize; tmp.fontSize = FontSize;
// GENERATE MESH // GENERATE MESH
_tmp.ClearMesh(false); tmp.ClearMesh(false);
_tmp.ForceMeshUpdate(); tmp.ForceMeshUpdate();
var meshFilters = gameObject.GetComponentsInChildren<MeshFilter>(); var meshFilters = tmpObject.GetComponentsInChildren<MeshFilter>();
var meshes = new Mesh[meshFilters.Length]; var meshes = new Mesh[meshFilters.Length];
var materials = new Material[meshFilters.Length]; var materials = new Material[meshFilters.Length];
@ -189,9 +95,9 @@ namespace ConformalDecals.Text {
var renderer = meshFilters[i].gameObject.GetComponent<MeshRenderer>(); var renderer = meshFilters[i].gameObject.GetComponent<MeshRenderer>();
meshes[i] = meshFilters[i].mesh; meshes[i] = meshFilters[i].mesh;
if (i == 0) meshes[i] = _tmp.mesh; if (i == 0) meshes[i] = tmp.mesh;
materials[i] = Instantiate(renderer.material); materials[i] = Object.Instantiate(renderer.material);
materials[i].shader = _blitShader; materials[i].shader = _blitShader;
if (renderer == null) throw new FormatException($"Object {meshFilters[i].gameObject.name} has filter but no renderer"); if (renderer == null) throw new FormatException($"Object {meshFilters[i].gameObject.name} has filter but no renderer");
@ -216,8 +122,9 @@ namespace ConformalDecals.Text {
}; };
if (textureSize.x == 0 || textureSize.y == 0) { if (textureSize.x == 0 || textureSize.y == 0) {
Logging.LogWarning("No text present or error in texture size calculation. Aborting."); Logging.LogError("No text present or error in texture size calculation. Aborting.");
return new TextRenderOutput(Texture2D.blackTexture, Rect.zero); Object.Destroy(tmpObject);
return new TextRenderOutput(_blankTexture, Rect.zero);
} }
// make sure texture isnt too big, scale it down if it is // make sure texture isnt too big, scale it down if it is
@ -242,12 +149,7 @@ namespace ConformalDecals.Text {
}; };
// SETUP TEXTURE // SETUP TEXTURE
if (texture == null) { var texture = new Texture2D(textureSize.x, textureSize.y, textTextureFormat, false);
texture = new Texture2D(textureSize.x, textureSize.y, textTextureFormat, true);
}
else if (texture.width != textureSize.x || texture.height != textureSize.y || texture.format != textTextureFormat) {
texture.Resize(textureSize.x, textureSize.y, textTextureFormat, true);
}
// GENERATE PROJECTION MATRIX // GENERATE PROJECTION MATRIX
var halfSize = (Vector2) textureSize / PixelDensity / 2 / sizeRatio; var halfSize = (Vector2) textureSize / PixelDensity / 2 / sizeRatio;
@ -255,7 +157,7 @@ namespace ConformalDecals.Text {
bounds.center.y - halfSize.y, bounds.center.y + halfSize.y, -1, 1); bounds.center.y - halfSize.y, bounds.center.y + halfSize.y, -1, 1);
// GET RENDERTEX // GET RENDERTEX
var renderTex = new RenderTexture(textureSize.x, textureSize.y, 0, textRenderTextureFormat, RenderTextureReadWrite.Linear) {autoGenerateMips = false}; var renderTex = RenderTexture.GetTemporary(textureSize.x, textureSize.y, 0, textRenderTextureFormat, RenderTextureReadWrite.Linear);
// RENDER // RENDER
Graphics.SetRenderTarget(renderTex); Graphics.SetRenderTarget(renderTex);
@ -271,28 +173,42 @@ namespace ConformalDecals.Text {
} }
} }
// COPY TEXTURE BACK INTO RAM // COPY RENDERTEX INTO TEXTURE
var prevRT = RenderTexture.active; var prevRT = RenderTexture.active;
RenderTexture.active = renderTex; RenderTexture.active = renderTex;
texture.ReadPixels(new Rect(0, 0, textureSize.x, textureSize.y), 0, 0, true); texture.ReadPixels(new Rect(0, 0, textureSize.x, textureSize.y), 0, 0, false);
texture.Apply(); texture.Apply(false, true);
RenderTexture.active = prevRT; RenderTexture.active = prevRT;
GL.PopMatrix(); GL.PopMatrix();
// RELEASE RENDERTEX // RELEASE RENDERTEX
renderTex.Release(); RenderTexture.ReleaseTemporary(renderTex);
RenderTexture.Destroy(renderTex);
// CLEAR SUBMESHES // DESTROY THE RENDERER OBJECT
_tmp.text = ""; Object.Destroy(tmpObject);
for (int i = 0; i < transform.childCount; i++) {
var child = transform.GetChild(i);
Destroy(child.gameObject);
}
return new TextRenderOutput(texture, window); return new TextRenderOutput(texture, window);
} }
/// Setup shader and texture
public static void ModuleManagerPostLoad() {
if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.Direct3D11 || SystemInfo.graphicsDeviceType == GraphicsDeviceType.Direct3D12) {
textRenderTextureFormat = RenderTextureFormat.ARGB32; // DirectX is dumb
}
if (!SystemInfo.SupportsTextureFormat(textTextureFormat)) {
Logging.LogError($"Text texture format {textTextureFormat} not supported on this platform.");
}
if (!SystemInfo.SupportsRenderTextureFormat(textRenderTextureFormat)) {
Logging.LogError($"Text texture format {textRenderTextureFormat} not supported on this platform.");
}
_blankTexture = Texture2D.blackTexture;
_blitShader = Shabby.Shabby.FindShader(ShaderName);
if (_blitShader == null) Logging.LogError($"Could not find text blit shader named '{ShaderName}'");
}
} }
} }

View File

@ -3,7 +3,6 @@ using ConformalDecals.Text;
using ConformalDecals.Util; using ConformalDecals.Util;
using TMPro; using TMPro;
using UnityEngine; using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI; using UnityEngine.UI;
namespace ConformalDecals.UI { namespace ConformalDecals.UI {
@ -35,14 +34,15 @@ namespace ConformalDecals.UI {
private Vector2 _lineSpacingRange; private Vector2 _lineSpacingRange;
private Vector2 _charSpacingRange; private Vector2 _charSpacingRange;
private TMP_InputField _textBoxTMP; private TMP_InputField _textBoxTMP;
private FontMenuController _fontMenu;
private TextUpdateDelegate _onValueChanged; private TextUpdateDelegate _onValueChanged;
private FontMenuController _fontMenu; private static int _lockCounter;
private bool _ignoreUpdates;
private bool _isLocked; private bool _isLocked;
private string _lockString; private string _lockString;
private static int _lockCounter; private bool _ignoreUpdates;
private bool _textUpdated;
public static TextEntryController Create( public static TextEntryController Create(
string text, DecalFont font, FontStyles style, bool vertical, float linespacing, float charspacing, string text, DecalFont font, FontStyles style, bool vertical, float linespacing, float charspacing,
@ -74,7 +74,7 @@ namespace ConformalDecals.UI {
public void SetControlLock(string value = null) { public void SetControlLock(string value = null) {
if (_isLocked) return; if (_isLocked) return;
InputLockManager.SetControlLock(_lockString); InputLockManager.SetControlLock(ControlTypes.EDITOR_UI, _lockString);
_isLocked = true; _isLocked = true;
} }
@ -86,8 +86,7 @@ namespace ConformalDecals.UI {
public void OnTextUpdate(string newText) { public void OnTextUpdate(string newText) {
this._text = newText; this._text = newText;
_textUpdated = true;
OnValueChanged();
} }
public void OnFontMenu() { public void OnFontMenu() {
@ -105,7 +104,7 @@ namespace ConformalDecals.UI {
_textBoxTMP.fontAsset = _font.FontAsset; _textBoxTMP.fontAsset = _font.FontAsset;
UpdateStyleButtons(); UpdateStyleButtons();
OnValueChanged(); _textUpdated = true;
} }
public void OnLineSpacingUpdate(float value) { public void OnLineSpacingUpdate(float value) {
@ -114,7 +113,7 @@ namespace ConformalDecals.UI {
_lineSpacing = Mathf.Lerp(_lineSpacingRange.x, _lineSpacingRange.y, value); _lineSpacing = Mathf.Lerp(_lineSpacingRange.x, _lineSpacingRange.y, value);
UpdateLineSpacing(); UpdateLineSpacing();
OnValueChanged(); _textUpdated = true;
} }
public void OnLineSpacingUpdate(string text) { public void OnLineSpacingUpdate(string text) {
@ -128,7 +127,7 @@ namespace ConformalDecals.UI {
} }
UpdateLineSpacing(); UpdateLineSpacing();
OnValueChanged(); _textUpdated = true;
} }
public void OnCharSpacingUpdate(float value) { public void OnCharSpacingUpdate(float value) {
@ -137,7 +136,7 @@ namespace ConformalDecals.UI {
_charSpacing = Mathf.Lerp(_charSpacingRange.x, _charSpacingRange.y, value); _charSpacing = Mathf.Lerp(_charSpacingRange.x, _charSpacingRange.y, value);
UpdateCharSpacing(); UpdateCharSpacing();
OnValueChanged(); _textUpdated = true;
} }
public void OnCharSpacingUpdate(string text) { public void OnCharSpacingUpdate(string text) {
@ -151,7 +150,7 @@ namespace ConformalDecals.UI {
} }
UpdateCharSpacing(); UpdateCharSpacing();
OnValueChanged(); _textUpdated = true;
} }
public void OnBoldUpdate(bool state) { public void OnBoldUpdate(bool state) {
@ -163,7 +162,7 @@ namespace ConformalDecals.UI {
_style &= ~FontStyles.Bold; _style &= ~FontStyles.Bold;
_textBoxTMP.textComponent.fontStyle = _style | _font.FontStyle & ~_font.FontStyleMask; _textBoxTMP.textComponent.fontStyle = _style | _font.FontStyle & ~_font.FontStyleMask;
OnValueChanged(); _textUpdated = true;
} }
public void OnItalicUpdate(bool state) { public void OnItalicUpdate(bool state) {
@ -175,7 +174,7 @@ namespace ConformalDecals.UI {
_style &= ~FontStyles.Italic; _style &= ~FontStyles.Italic;
_textBoxTMP.textComponent.fontStyle = _style | _font.FontStyle & ~_font.FontStyleMask; _textBoxTMP.textComponent.fontStyle = _style | _font.FontStyle & ~_font.FontStyleMask;
OnValueChanged(); _textUpdated = true;
} }
public void OnUnderlineUpdate(bool state) { public void OnUnderlineUpdate(bool state) {
@ -187,7 +186,7 @@ namespace ConformalDecals.UI {
_style &= ~FontStyles.Underline; _style &= ~FontStyles.Underline;
_textBoxTMP.textComponent.fontStyle = _style | _font.FontStyle & ~_font.FontStyleMask; _textBoxTMP.textComponent.fontStyle = _style | _font.FontStyle & ~_font.FontStyleMask;
OnValueChanged(); _textUpdated = true;
} }
public void OnSmallCapsUpdate(bool state) { public void OnSmallCapsUpdate(bool state) {
@ -199,14 +198,14 @@ namespace ConformalDecals.UI {
_style &= ~FontStyles.SmallCaps; _style &= ~FontStyles.SmallCaps;
_textBoxTMP.textComponent.fontStyle = _style | _font.FontStyle & ~_font.FontStyleMask; _textBoxTMP.textComponent.fontStyle = _style | _font.FontStyle & ~_font.FontStyleMask;
OnValueChanged(); _textUpdated = true;
} }
public void OnVerticalUpdate(bool state) { public void OnVerticalUpdate(bool state) {
if (_ignoreUpdates) return; if (_ignoreUpdates) return;
_vertical = state; _vertical = state;
OnValueChanged(); _textUpdated = true;
} }
private void Start() { private void Start() {
@ -230,8 +229,11 @@ namespace ConformalDecals.UI {
RemoveControlLock(); RemoveControlLock();
} }
private void OnValueChanged() { private void LateUpdate() {
if (_textUpdated) {
_onValueChanged(_text, _font, _style, _vertical, _lineSpacing, _charSpacing); _onValueChanged(_text, _font, _style, _vertical, _lineSpacing, _charSpacing);
_textUpdated = false;
}
} }
private void UpdateStyleButtons() { private void UpdateStyleButtons() {

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using UniLinq;
using UnityEngine; using UnityEngine;
namespace ConformalDecals.Util { namespace ConformalDecals.Util {
@ -74,6 +75,14 @@ namespace ConformalDecals.Util {
return ParseValueIndirect(ref value, node, valueName, int.TryParse); return ParseValueIndirect(ref value, node, valueName, int.TryParse);
} }
public static uint ParseUint(ConfigNode node, string valueName, bool isOptional = false, uint defaultValue = 0) {
return ParseValue(node, valueName, uint.TryParse, isOptional, defaultValue);
}
public static bool ParseUintIndirect(ref uint value, ConfigNode node, string valueName) {
return ParseValueIndirect(ref value, node, valueName, uint.TryParse);
}
public static Color32 ParseColor32(ConfigNode node, string valueName, bool isOptional = false, Color32 defaultValue = default) { public static Color32 ParseColor32(ConfigNode node, string valueName, bool isOptional = false, Color32 defaultValue = default) {
return ParseValue(node, valueName, TryParseColor32, isOptional, defaultValue); return ParseValue(node, valueName, TryParseColor32, isOptional, defaultValue);
} }
@ -106,6 +115,15 @@ namespace ConformalDecals.Util {
return ParseValueIndirect(ref value, node, valueName, ParseExtensions.TryParseVector3); return ParseValueIndirect(ref value, node, valueName, ParseExtensions.TryParseVector3);
} }
public static Matrix4x4 ParseMatrix4x4(ConfigNode node, string valueName, bool isOptional = false, Matrix4x4 defaultValue = default) {
return ParseValue(node, valueName, ParseUtil.TryParseMatrix4x4, isOptional, defaultValue);
}
public static bool ParseMatrix4x4Indirect(ref Matrix4x4 value, ConfigNode node, string valueName) {
return ParseValueIndirect(ref value, node, valueName, ParseUtil.TryParseMatrix4x4);
}
public static T ParseValue<T>(ConfigNode node, string valueName, TryParseDelegate<T> tryParse, bool isOptional = false, T defaultValue = default) { public static T ParseValue<T>(ConfigNode node, string valueName, TryParseDelegate<T> tryParse, bool isOptional = false, T defaultValue = default) {
string valueString = node.GetValue(valueName); string valueString = node.GetValue(valueName);
@ -145,6 +163,26 @@ namespace ConformalDecals.Util {
throw new FormatException($"Improperly formatted {typeof(T)} value for {valueName} : '{valueString}"); throw new FormatException($"Improperly formatted {typeof(T)} value for {valueName} : '{valueString}");
} }
public static bool TryParseMatrix4x4(string valueString, out Matrix4x4 value) {
value = new Matrix4x4();
var split = valueString.Split(Separator, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < split.Length; i++) {
split[i] = split[i].Trim();
}
if (split.Length != 16) return false;
int index = 0;
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 4; col++) {
if (!float.TryParse(split[index], out float component)) return false;
value[row, col] = component;
}
}
return true;
}
public static bool TryParseHexColor(string valueString, out Color32 value) { public static bool TryParseHexColor(string valueString, out Color32 value) {
value = new Color32(0, 0, 0, byte.MaxValue); value = new Color32(0, 0, 0, byte.MaxValue);

View File

@ -1,3 +1,12 @@
v0.2.7
------
- Supported KSP versions: 1.8.x to 1.11.x
- Notes:
- Attaching decal parts in flight using engineer kerbals is not supported.
- Fixes:
- Fixed certain non-ascii strings not rendering correctly under certain circumstances.
- Yet another attempted fix for the planet text glitch.
v0.2.6 v0.2.6
------ ------
- Fixes: - Fixes: