Refactor and implement img to text block importing
This commit is contained in:
parent
6acf1b7d4e
commit
2e314595ac
8 changed files with 450 additions and 270 deletions
17
Pixi/Common/BlockInfo.cs
Normal file
17
Pixi/Common/BlockInfo.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
using GamecraftModdingAPI.Blocks;
|
||||||
|
|
||||||
|
namespace Pixi.Common
|
||||||
|
{
|
||||||
|
public struct BlockInfo
|
||||||
|
{
|
||||||
|
public BlockIDs block;
|
||||||
|
|
||||||
|
public BlockColors color;
|
||||||
|
|
||||||
|
public byte darkness;
|
||||||
|
|
||||||
|
public bool visible;
|
||||||
|
}
|
||||||
|
}
|
214
Pixi/Images/ImageCommands.cs
Normal file
214
Pixi/Images/ImageCommands.cs
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
using UnityEngine;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
using Svelto.ECS.Experimental;
|
||||||
|
using Svelto.ECS;
|
||||||
|
|
||||||
|
using GamecraftModdingAPI.Blocks;
|
||||||
|
using GamecraftModdingAPI.Commands;
|
||||||
|
using GamecraftModdingAPI.Players;
|
||||||
|
using GamecraftModdingAPI.Utility;
|
||||||
|
using GamecraftModdingAPI;
|
||||||
|
|
||||||
|
using Pixi.Common;
|
||||||
|
|
||||||
|
namespace Pixi.Images
|
||||||
|
{
|
||||||
|
public static class ImageCommands
|
||||||
|
{
|
||||||
|
public const uint PIXEL_WARNING_THRESHOLD = 25_000;
|
||||||
|
// hash length to display after Pixi in text block id field
|
||||||
|
public const uint HASH_LENGTH = 6;
|
||||||
|
|
||||||
|
private static double blockSize = 0.2;
|
||||||
|
|
||||||
|
private static uint thiccness = 1;
|
||||||
|
|
||||||
|
public static void CreateThiccCommand()
|
||||||
|
{
|
||||||
|
CommandBuilder.Builder()
|
||||||
|
.Name("PixiThicc")
|
||||||
|
.Description("Set the image thickness for Pixi2D. Use this if you'd like add depth to a 2D image after importing. (Pixi)")
|
||||||
|
.Action<int>((d) => {
|
||||||
|
if (d > 0)
|
||||||
|
{
|
||||||
|
thiccness = (uint)d;
|
||||||
|
}
|
||||||
|
else Logging.CommandLogError("");
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CreateImportCommand()
|
||||||
|
{
|
||||||
|
CommandBuilder.Builder()
|
||||||
|
.Name("Pixi2D")
|
||||||
|
.Description("Converts an image to blocks. Larger images will freeze your game until conversion completes. (Pixi)")
|
||||||
|
.Action<string>(Pixelate2DFile)
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CreateTextCommand()
|
||||||
|
{
|
||||||
|
CommandBuilder.Builder()
|
||||||
|
.Name("PixiText")
|
||||||
|
.Description("Converts an image to coloured text in a new text block. Larger images may cause save issues. (Pixi)")
|
||||||
|
.Action<string>(Pixelate2DFileToTextBlock)
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CreateTextConsoleCommand()
|
||||||
|
{
|
||||||
|
CommandBuilder.Builder()
|
||||||
|
.Name("PixiConsole")
|
||||||
|
.Description("Converts an image to a ChangeTextBlockCommand in a new console block. The first parameter is the image filepath and the second parameter is the text block id. Larger images may cause save issues. (Pixi)")
|
||||||
|
.Action<string, string>(Pixelate2DFileToCommand)
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Pixelate2DFile(string filepath)
|
||||||
|
{
|
||||||
|
// Load image file and convert to Gamecraft blocks
|
||||||
|
Texture2D img = new Texture2D(64, 64);
|
||||||
|
// load file into texture
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] imgData = File.ReadAllBytes(filepath);
|
||||||
|
img.LoadImage(imgData);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
|
||||||
|
Logging.MetaLog(e.Message + "\n" + e.StackTrace);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Logging.CommandLog($"Image size: {img.width}x{img.height}");
|
||||||
|
float3 position = new Player(PlayerType.Local).Position;
|
||||||
|
uint blockCount = 0;
|
||||||
|
position.x += 1f;
|
||||||
|
position.y += (float)blockSize;
|
||||||
|
float zero_y = position.y;
|
||||||
|
// convert the image to blocks
|
||||||
|
// this groups same-colored pixels in the same column into a single block to reduce the block count
|
||||||
|
// any further pixel-grouping optimisations (eg 2D grouping) risk increasing conversion time higher than O(x*y)
|
||||||
|
for (int x = 0; x < img.width; x++)
|
||||||
|
{
|
||||||
|
BlockInfo qVoxel = new BlockInfo
|
||||||
|
{
|
||||||
|
block = BlockIDs.AbsoluteMathsBlock, // impossible canvas block
|
||||||
|
color = BlockColors.Default,
|
||||||
|
darkness = 10,
|
||||||
|
visible = false,
|
||||||
|
};
|
||||||
|
float3 scale = new float3(1, 1, thiccness);
|
||||||
|
position.x += (float)(blockSize);
|
||||||
|
for (int y = 0; y < img.height; y++)
|
||||||
|
{
|
||||||
|
//position.y += (float)blockSize;
|
||||||
|
Color pixel = img.GetPixel(x, y);
|
||||||
|
BlockInfo qPixel = PixelUtility.QuantizePixel(pixel);
|
||||||
|
if (qPixel.darkness != qVoxel.darkness
|
||||||
|
|| qPixel.color != qVoxel.color
|
||||||
|
|| qPixel.visible != qVoxel.visible
|
||||||
|
|| qPixel.block != qVoxel.block)
|
||||||
|
{
|
||||||
|
if (y != 0)
|
||||||
|
{
|
||||||
|
if (qVoxel.visible)
|
||||||
|
{
|
||||||
|
position.y = zero_y + (float)((y * blockSize + (y - scale.y) * blockSize) / 2);
|
||||||
|
Block.PlaceNew(qVoxel.block, position, color: qVoxel.color, darkness: qVoxel.darkness, scale: scale);
|
||||||
|
blockCount++;
|
||||||
|
}
|
||||||
|
scale = new float3(1, 1, thiccness);
|
||||||
|
}
|
||||||
|
qVoxel = qPixel;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
scale.y += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if (qVoxel.visible)
|
||||||
|
{
|
||||||
|
position.y = zero_y + (float)((img.height * blockSize + (img.height - scale.y) * blockSize) / 2);
|
||||||
|
Block.PlaceNew(qVoxel.block, position, color: qVoxel.color, darkness: qVoxel.darkness, scale: scale);
|
||||||
|
blockCount++;
|
||||||
|
}
|
||||||
|
//position.y = zero_y;
|
||||||
|
}
|
||||||
|
Logging.CommandLog($"Placed {img.width}x{img.height} image beside you ({blockCount} blocks total)");
|
||||||
|
Logging.MetaLog($"Saved {(img.width * img.height) - blockCount} blocks ({img.width * img.height / blockCount}x) while placing {filepath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Pixelate2DFileToTextBlock(string filepath)
|
||||||
|
{
|
||||||
|
// Thanks to TheGreenGoblin for the idea (and the working Python implementation for reference)
|
||||||
|
// Load image file and convert to Gamecraft blocks
|
||||||
|
Texture2D img = new Texture2D(64, 64);
|
||||||
|
// load file into texture
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] imgData = File.ReadAllBytes(filepath);
|
||||||
|
img.LoadImage(imgData);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
|
||||||
|
Logging.MetaLog(e.Message + "\n" + e.StackTrace);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float3 position = new Player(PlayerType.Local).Position;
|
||||||
|
position.x += 1f;
|
||||||
|
position.y += (float)blockSize;
|
||||||
|
string text = PixelUtility.TextureToString(img);
|
||||||
|
TextBlock textBlock = TextBlock.PlaceNew(position, scale: new float3(Mathf.Ceil(img.width / 16), Mathf.Ceil(img.height / 16), 1));
|
||||||
|
textBlock.Text = text;
|
||||||
|
byte[] textHash;
|
||||||
|
using (HashAlgorithm hasher = SHA256.Create())
|
||||||
|
textHash = hasher.ComputeHash(Encoding.UTF8.GetBytes(text));
|
||||||
|
string textId = "Pixi_";
|
||||||
|
// every byte converts to 2 hexadecimal characters so hash length needs to be halved
|
||||||
|
for (int i = 0; i < HASH_LENGTH/2 && i < textHash.Length; i++)
|
||||||
|
{
|
||||||
|
textId += textHash[i].ToString("X2");
|
||||||
|
}
|
||||||
|
textBlock.TextBlockId = textId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Pixelate2DFileToCommand(string filepath, string textBlockId)
|
||||||
|
{
|
||||||
|
// Thanks to Nullpersonan for the idea
|
||||||
|
// Load image file and convert to Gamecraft blocks
|
||||||
|
Texture2D img = new Texture2D(64, 64);
|
||||||
|
// load file into texture
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] imgData = File.ReadAllBytes(filepath);
|
||||||
|
img.LoadImage(imgData);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
|
||||||
|
Logging.MetaLog(e.Message + "\n" + e.StackTrace);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float3 position = new Player(PlayerType.Local).Position;
|
||||||
|
position.x += 1f;
|
||||||
|
position.y += (float)blockSize;
|
||||||
|
float zero_y = position.y;
|
||||||
|
string text = PixelUtility.TextureToString(img); // conversion
|
||||||
|
ConsoleBlock console = ConsoleBlock.PlaceNew(position);
|
||||||
|
// set console's command
|
||||||
|
console.Command = "ChangeTextBlockCommand";
|
||||||
|
console.Arg1 = textBlockId;
|
||||||
|
console.Arg2 = text;
|
||||||
|
console.Arg3 = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
168
Pixi/Images/PixelUtility.cs
Normal file
168
Pixi/Images/PixelUtility.cs
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
using GamecraftModdingAPI.Blocks;
|
||||||
|
using GamecraftModdingAPI.Utility;
|
||||||
|
|
||||||
|
using Pixi.Common;
|
||||||
|
|
||||||
|
namespace Pixi.Images
|
||||||
|
{
|
||||||
|
public static class PixelUtility
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static BlockInfo QuantizePixel(Color pixel)
|
||||||
|
{
|
||||||
|
BlockColors color = BlockColors.Default;
|
||||||
|
int darkness = 0;
|
||||||
|
bool force = false;
|
||||||
|
#if DEBUG
|
||||||
|
Logging.MetaLog($"Color (r:{pixel.r}, g:{pixel.g}, b:{pixel.b})");
|
||||||
|
#endif
|
||||||
|
if (Mathf.Abs(pixel.r - pixel.g) <= pixel.r * 0.1f && Mathf.Abs(pixel.r - pixel.b) <= pixel.r * 0.1f)
|
||||||
|
{
|
||||||
|
color = BlockColors.White;
|
||||||
|
darkness = (int)(10 - ((pixel.r + pixel.g + pixel.b) * 3.5));
|
||||||
|
//Logging.MetaDebugLog($"Color (r:{pixel.r}, g:{pixel.g}, b:{pixel.b})");
|
||||||
|
}
|
||||||
|
else if (pixel.r >= pixel.g && pixel.r >= pixel.b)
|
||||||
|
{
|
||||||
|
// Red is highest
|
||||||
|
if ((pixel.r - pixel.g) > pixel.r * 0.65 && (pixel.r - pixel.b) > pixel.r * 0.55)
|
||||||
|
{
|
||||||
|
// Red is much higher than other pixels
|
||||||
|
darkness = (int)(9 - (pixel.r * 8.01));
|
||||||
|
color = BlockColors.Red;
|
||||||
|
}
|
||||||
|
else if ((pixel.g - pixel.b) > pixel.g * 0.25)
|
||||||
|
{
|
||||||
|
// Green is much higher than blue
|
||||||
|
if ((pixel.r - pixel.g) < pixel.r * 0.8)
|
||||||
|
{
|
||||||
|
darkness = (int)(10 - ((pixel.r * 2.1 + pixel.g) * 2.1));
|
||||||
|
color = BlockColors.Orange;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
darkness = (int)(10 - ((pixel.r * 2.1 + pixel.g) * 2.2));
|
||||||
|
color = BlockColors.Yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else if ((pixel.b - pixel.g) > pixel.b * 0.3)
|
||||||
|
{
|
||||||
|
// Blue is much higher than green
|
||||||
|
darkness = (int)(10 - ((pixel.r + pixel.b) * 5.0));
|
||||||
|
color = BlockColors.Purple;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Green is close strength to blue
|
||||||
|
darkness = (int)(10 - ((pixel.r * 2.1 + pixel.g + pixel.b) * 2.5));
|
||||||
|
color = darkness < 6 ? BlockColors.Pink : BlockColors.Orange;
|
||||||
|
force = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (pixel.g >= pixel.r && pixel.g >= pixel.b)
|
||||||
|
{
|
||||||
|
// Green is highest
|
||||||
|
if ((pixel.g - pixel.r) > pixel.g * 0.6 && (pixel.g - pixel.b) > pixel.g * 0.48)
|
||||||
|
{
|
||||||
|
// Green is much higher than other pixels
|
||||||
|
darkness = (int)(10 - (pixel.g * 10.1));
|
||||||
|
color = BlockColors.Green;
|
||||||
|
}
|
||||||
|
else if ((pixel.r - pixel.b) > pixel.r * 0.3)
|
||||||
|
{
|
||||||
|
// Red is much higher than blue
|
||||||
|
darkness = (int)(10 - ((pixel.r + pixel.g) * 5.1));
|
||||||
|
color = BlockColors.Yellow;
|
||||||
|
}
|
||||||
|
else if ((pixel.b - pixel.r) > pixel.b * 0.2)
|
||||||
|
{
|
||||||
|
// Blue is much higher than red
|
||||||
|
darkness = (int)(9 - ((pixel.g + pixel.b) * 5.1));
|
||||||
|
color = BlockColors.Aqua;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Red is close strength to blue
|
||||||
|
darkness = (int)(10 - ((pixel.r + pixel.g * 2.2 + pixel.b) * 2.9));
|
||||||
|
color = BlockColors.Lime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (pixel.b >= pixel.g && pixel.b >= pixel.r)
|
||||||
|
{
|
||||||
|
// Blue is highest
|
||||||
|
if ((pixel.b - pixel.g) > pixel.b * 0.6 && (pixel.b - pixel.r) > pixel.b * 0.6)
|
||||||
|
{
|
||||||
|
// Blue is much higher than other pixels
|
||||||
|
darkness = (int)(10 - (pixel.b * 10.1));
|
||||||
|
color = BlockColors.Blue;
|
||||||
|
}
|
||||||
|
else if ((pixel.g - pixel.r) > pixel.g * 0.3)
|
||||||
|
{
|
||||||
|
// Green is much higher than red
|
||||||
|
darkness = (int)(10 - ((pixel.g + pixel.b) * 5.1));
|
||||||
|
if (darkness == 4 || darkness == 5) darkness = 0;
|
||||||
|
else if (darkness < 3) darkness = 4;
|
||||||
|
color = BlockColors.Aqua;
|
||||||
|
}
|
||||||
|
else if ((pixel.r - pixel.g) > pixel.r * 0.3)
|
||||||
|
{
|
||||||
|
// Red is much higher than green
|
||||||
|
darkness = (int)(10 - ((pixel.r + pixel.b) * 5.0));
|
||||||
|
color = BlockColors.Purple;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Green is close strength to red
|
||||||
|
darkness = (int)(10 - ((pixel.r + pixel.g + pixel.b * 2.2) * 3.0));
|
||||||
|
color = BlockColors.Aqua;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// level 9 is not darker than lvl 8
|
||||||
|
if (darkness > 8 && !force) darkness = 8;
|
||||||
|
// darkness 0 is the most saturated (it's not just the lightest)
|
||||||
|
if (darkness < 0) darkness = 0;
|
||||||
|
|
||||||
|
BlockInfo result = new BlockInfo
|
||||||
|
{
|
||||||
|
block = pixel.a > 0.75 ? BlockIDs.AluminiumCube : BlockIDs.GlassCube,
|
||||||
|
color = color,
|
||||||
|
darkness = (byte)darkness,
|
||||||
|
visible = pixel.a > 0.5f,
|
||||||
|
};
|
||||||
|
#if DEBUG
|
||||||
|
Logging.MetaLog($"Quantized {color} (b:{result.block} d:{result.darkness} v:{result.visible})");
|
||||||
|
#endif
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string HexPixel(Color pixel)
|
||||||
|
{
|
||||||
|
return "#"+ColorUtility.ToHtmlStringRGBA(pixel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string TextureToString(Texture2D img)
|
||||||
|
{
|
||||||
|
StringBuilder imgString = new StringBuilder("<cspace=-0.1em><line-height=42%>");
|
||||||
|
for (int y = img.height-1; y >= 0 ; y--) // text origin is top right, but img origin is bottom right
|
||||||
|
{
|
||||||
|
for (int x = 0; x < img.width; x++)
|
||||||
|
{
|
||||||
|
Color pixel = img.GetPixel(x, y);
|
||||||
|
imgString.Append("<color=");
|
||||||
|
imgString.Append(HexPixel(pixel));
|
||||||
|
imgString.Append(">\u25a0</color>");
|
||||||
|
}
|
||||||
|
imgString.Append("<br>");
|
||||||
|
}
|
||||||
|
imgString.Append("</cspace></line-height>");
|
||||||
|
return imgString.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,17 +3,13 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net472</TargetFramework>
|
<TargetFramework>net472</TargetFramework>
|
||||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
<Version>0.2.0</Version>
|
<Version>0.3.0</Version>
|
||||||
<Authors>NGnius</Authors>
|
<Authors>NGnius</Authors>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
<PackageProjectUrl>https://git.exmods.org/NGnius/Pixi</PackageProjectUrl>
|
<PackageProjectUrl>https://git.exmods.org/NGnius/Pixi</PackageProjectUrl>
|
||||||
<NeutralLanguage>en-CA</NeutralLanguage>
|
<NeutralLanguage>en-CA</NeutralLanguage>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Lib.Harmony" Version="1.2.0.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!--Start Dependencies-->
|
<!--Start Dependencies-->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="GamecraftModdingAPI">
|
<Reference Include="GamecraftModdingAPI">
|
||||||
|
@ -781,7 +777,33 @@
|
||||||
<HintPath>..\..\ref\Gamecraft_Data\Managed\VisualProfiler.dll</HintPath>
|
<HintPath>..\..\ref\Gamecraft_Data\Managed\VisualProfiler.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Microsoft.CSharp" />
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
<Reference Include="GamecraftModdingAPI">
|
||||||
|
<HintPath>..\..\ref\Plugins\GamecraftModdingAPI.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="GamecraftModdingAPI">
|
||||||
|
<HintPath>..\..\ref\Plugins\GamecraftModdingAPI.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="GamecraftModdingAPI">
|
||||||
|
<HintPath>..\..\ref\Plugins\GamecraftModdingAPI.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="GamecraftModdingAPI">
|
||||||
|
<HintPath>..\..\ref\Plugins\GamecraftModdingAPI.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="GamecraftModdingAPI">
|
||||||
|
<HintPath>..\..\ref\Plugins\GamecraftModdingAPI.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="GamecraftModdingAPI">
|
||||||
|
<HintPath>..\..\ref\Plugins\GamecraftModdingAPI.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="GamecraftModdingAPI">
|
||||||
|
<HintPath>..\..\ref\Plugins\GamecraftModdingAPI.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="GamecraftModdingAPI">
|
||||||
|
<HintPath>..\..\ref\Plugins\GamecraftModdingAPI.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="GamecraftModdingAPI">
|
||||||
|
<HintPath>..\..\ref\Plugins\GamecraftModdingAPI.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!--End Dependencies-->
|
<!--End Dependencies-->
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -13,23 +13,18 @@ using GamecraftModdingAPI.Utility;
|
||||||
using GamecraftModdingAPI.Blocks;
|
using GamecraftModdingAPI.Blocks;
|
||||||
using GamecraftModdingAPI.Players;
|
using GamecraftModdingAPI.Players;
|
||||||
|
|
||||||
|
using Pixi.Images;
|
||||||
|
|
||||||
namespace Pixi
|
namespace Pixi
|
||||||
{
|
{
|
||||||
public class PixiPlugin : IPlugin // the Illusion Plugin Architecture (IPA) will ignore classes that don't implement IPlugin'
|
public class PixiPlugin : IPlugin // the Illusion Plugin Architecture (IPA) will ignore classes that don't implement IPlugin'
|
||||||
{
|
{
|
||||||
private const uint PIXEL_WARNING_THRESHOLD = 25_000;
|
|
||||||
|
|
||||||
public string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name; // Pixi
|
public string Name { get; } = Assembly.GetExecutingAssembly().GetName().Name; // Pixi
|
||||||
// To change the name, change the project's name
|
// To change the name, change the project's name
|
||||||
|
|
||||||
public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString(); // 0.1.0 (for now)
|
public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString(); // 0.1.0 (for now)
|
||||||
// To change the version, change <Version>#.#.#</Version> in Pixi.csproj
|
// To change the version, change <Version>#.#.#</Version> in Pixi.csproj
|
||||||
|
|
||||||
private uint width = 64;
|
|
||||||
private uint height = 64;
|
|
||||||
|
|
||||||
private double blockSize = 0.2;
|
|
||||||
|
|
||||||
// called when Gamecraft shuts down
|
// called when Gamecraft shuts down
|
||||||
public void OnApplicationQuit()
|
public void OnApplicationQuit()
|
||||||
{
|
{
|
||||||
|
@ -48,28 +43,11 @@ namespace Pixi
|
||||||
// check out the modding API docs here: https://mod.exmods.org/
|
// check out the modding API docs here: https://mod.exmods.org/
|
||||||
|
|
||||||
// Initialize Pixi mod
|
// Initialize Pixi mod
|
||||||
// create SimpleCustomCommandEngine for 2D image importing
|
// 2D image functionality
|
||||||
SimpleCustomCommandEngine<string> pixelate2DCommand = new SimpleCustomCommandEngine<string>(
|
ImageCommands.CreateThiccCommand();
|
||||||
pixelate2DFile, // command action
|
ImageCommands.CreateImportCommand();
|
||||||
"Pixi2D", // command name (used to invoke it in the console)
|
ImageCommands.CreateTextCommand();
|
||||||
"Converts an image to blocks.\nLarger images will freeze your game until conversion completes. (Pixi)" // command description (displayed when help command is executed)
|
ImageCommands.CreateTextConsoleCommand();
|
||||||
);
|
|
||||||
|
|
||||||
SimpleCustomCommandEngine<string> pixelate3DCommand = new SimpleCustomCommandEngine<string>(
|
|
||||||
pixelate3DFile, // command action
|
|
||||||
"Pixi3D", // command name (used to invoke it in the console)
|
|
||||||
"Converts a 3D model to blocks.\nLarger models will freeze your game until conversion completes. (Pixi)" // command description (displayed when help command is executed)
|
|
||||||
);
|
|
||||||
|
|
||||||
SimpleCustomCommandEngine<uint, uint> scaleCommand = new SimpleCustomCommandEngine<uint, uint>(
|
|
||||||
setScale, // command action
|
|
||||||
"PixiScale", // command name (used to invoke it in the console)
|
|
||||||
"Sets the image scale factor for Pixi2D.\nBigger images take longer to convert. (Pixi)" // command description (displayed when help command is executed)
|
|
||||||
);
|
|
||||||
|
|
||||||
// register commands so the modding API knows about it
|
|
||||||
CommandManager.AddCommand(pixelate2DCommand);
|
|
||||||
CommandManager.AddCommand(scaleCommand);
|
|
||||||
|
|
||||||
GamecraftModdingAPI.Utility.Logging.LogDebug($"{Name} has started up");
|
GamecraftModdingAPI.Utility.Logging.LogDebug($"{Name} has started up");
|
||||||
}
|
}
|
||||||
|
@ -83,239 +61,5 @@ namespace Pixi
|
||||||
public void OnLevelWasLoaded(int level) { } // called after a level is loaded
|
public void OnLevelWasLoaded(int level) { } // called after a level is loaded
|
||||||
|
|
||||||
public void OnUpdate() { } // called once per rendered frame (frame update)
|
public void OnUpdate() { } // called once per rendered frame (frame update)
|
||||||
|
|
||||||
// pixelation methods
|
|
||||||
|
|
||||||
private void pixelate2DFile(string filepath)
|
|
||||||
{
|
|
||||||
// Load image file and convert to Gamecraft blocks
|
|
||||||
Texture2D img = new Texture2D((int)width, (int)height);
|
|
||||||
// load file into texture
|
|
||||||
try
|
|
||||||
{
|
|
||||||
byte[] imgData = File.ReadAllBytes(filepath);
|
|
||||||
img.LoadImage(imgData);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logging.CommandLogError($"Failed to load picture data. Reason: {e.Message}");
|
|
||||||
Logging.MetaLog(e.Message + "\n" + e.StackTrace);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
float3 position = new Player(PlayerType.Local).Position;
|
|
||||||
uint blockCount = 0;
|
|
||||||
position.x += 1f;
|
|
||||||
//position.y += 1f;
|
|
||||||
float zero_y = position.y;
|
|
||||||
// convert the image to blocks
|
|
||||||
// this groups same-colored pixels in the same column into a single block to reduce the block count
|
|
||||||
// any further pixel-grouping optimisations (eg 2D grouping) risk increasing conversion time higher than O(x*y)
|
|
||||||
for (int x = 0; x < width; x++)
|
|
||||||
{
|
|
||||||
QuantizedPixel qVoxel = new QuantizedPixel
|
|
||||||
{
|
|
||||||
block = BlockIDs.AbsoluteMathsBlock, // impossible canvas block
|
|
||||||
color = BlockColors.Default,
|
|
||||||
darkness = 10,
|
|
||||||
visible = false,
|
|
||||||
};
|
|
||||||
float3 scale = new float3(1, 1, 1);
|
|
||||||
position.x += (float)(blockSize);
|
|
||||||
for (int y = 0; y < height; y++)
|
|
||||||
{
|
|
||||||
//position.y += (float)blockSize;
|
|
||||||
Color pixel = img.GetPixel(x, y);
|
|
||||||
QuantizedPixel qPixel = quantizeColor(pixel);
|
|
||||||
if (qPixel.darkness != qVoxel.darkness
|
|
||||||
|| qPixel.color != qVoxel.color
|
|
||||||
|| qPixel.visible != qVoxel.visible
|
|
||||||
|| qPixel.block != qVoxel.block)
|
|
||||||
{
|
|
||||||
if (y != 0)
|
|
||||||
{
|
|
||||||
if (qVoxel.visible)
|
|
||||||
{
|
|
||||||
position.y = zero_y + (float)((y * blockSize + (y - scale.y) * blockSize) / 2);
|
|
||||||
Block.PlaceNew(qVoxel.block, position, color: qVoxel.color, darkness: qVoxel.darkness, scale: scale);
|
|
||||||
blockCount++;
|
|
||||||
}
|
|
||||||
scale = new float3(1, 1, 1);
|
|
||||||
}
|
|
||||||
qVoxel = qPixel;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
scale.y += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
if (qVoxel.visible)
|
|
||||||
{
|
|
||||||
position.y = zero_y + (float)((height * blockSize + (height - scale.y) * blockSize) / 2);
|
|
||||||
Block.PlaceNew(qVoxel.block, position, color: qVoxel.color, darkness: qVoxel.darkness, scale: scale);
|
|
||||||
blockCount++;
|
|
||||||
}
|
|
||||||
//position.y = zero_y;
|
|
||||||
}
|
|
||||||
Logging.CommandLog($"Placed {width}x{height} image beside you ({blockCount} blocks total)");
|
|
||||||
Logging.MetaLog($"Saved {(width * height) - blockCount} blocks ({width * height / blockCount}x) while placing {filepath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setScale(uint _width, uint _height)
|
|
||||||
{
|
|
||||||
width = _width;
|
|
||||||
height = _height;
|
|
||||||
if (width * height > PIXEL_WARNING_THRESHOLD)
|
|
||||||
{
|
|
||||||
Logging.CommandLogWarning($"That's a lot of pixels ({width * height}px)!\nImporting large images may freeze your game for a long time.");
|
|
||||||
}
|
|
||||||
Logging.CommandLog($"Pixi image size set to {width}x{height}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void pixelate3DFile(string filepath)
|
|
||||||
{
|
|
||||||
// TODO?
|
|
||||||
Logging.CommandLogError("Oh no you found this command!\nCommand functionality not implemented (yet)");
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private QuantizedPixel quantizeColor(Color pixel)
|
|
||||||
{
|
|
||||||
BlockColors color = BlockColors.Default;
|
|
||||||
int darkness = 0;
|
|
||||||
bool force = false;
|
|
||||||
#if DEBUG
|
|
||||||
Logging.MetaLog($"Color (r:{pixel.r}, g:{pixel.g}, b:{pixel.b})");
|
|
||||||
#endif
|
|
||||||
if (Mathf.Abs(pixel.r - pixel.g) <= pixel.r * 0.1f && Mathf.Abs(pixel.r - pixel.b) <= pixel.r * 0.1f)
|
|
||||||
{
|
|
||||||
color = BlockColors.White;
|
|
||||||
darkness = (int)(10 - ((pixel.r + pixel.g + pixel.b) * 3.5));
|
|
||||||
//Logging.MetaDebugLog($"Color (r:{pixel.r}, g:{pixel.g}, b:{pixel.b})");
|
|
||||||
}
|
|
||||||
else if (pixel.r >= pixel.g && pixel.r >= pixel.b)
|
|
||||||
{
|
|
||||||
// Red is highest
|
|
||||||
if ((pixel.r - pixel.g) > pixel.r * 0.65 && (pixel.r - pixel.b) > pixel.r * 0.55)
|
|
||||||
{
|
|
||||||
// Red is much higher than other pixels
|
|
||||||
darkness = (int)(9 - (pixel.r * 8.01));
|
|
||||||
color = BlockColors.Red;
|
|
||||||
}
|
|
||||||
else if ((pixel.g - pixel.b) > pixel.g * 0.25)
|
|
||||||
{
|
|
||||||
// Green is much higher than blue
|
|
||||||
if ((pixel.r - pixel.g) < pixel.r * 0.8)
|
|
||||||
{
|
|
||||||
darkness = (int)(10 - ((pixel.r * 2.1 + pixel.g) * 2.1));
|
|
||||||
color = BlockColors.Orange;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
darkness = (int)(10 - ((pixel.r * 2.1 + pixel.g) * 2.2));
|
|
||||||
color = BlockColors.Yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else if ((pixel.b - pixel.g) > pixel.b * 0.3)
|
|
||||||
{
|
|
||||||
// Blue is much higher than green
|
|
||||||
darkness = (int)(10 - ((pixel.r + pixel.b) * 5.0));
|
|
||||||
color = BlockColors.Purple;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Green is close strength to blue
|
|
||||||
darkness = (int)(10 - ((pixel.r * 2.1 + pixel.g + pixel.b) * 2.5));
|
|
||||||
color = darkness < 6 ? BlockColors.Pink : BlockColors.Orange;
|
|
||||||
force = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (pixel.g >= pixel.r && pixel.g >= pixel.b)
|
|
||||||
{
|
|
||||||
// Green is highest
|
|
||||||
if ((pixel.g - pixel.r) > pixel.g * 0.6 && (pixel.g - pixel.b) > pixel.g * 0.48)
|
|
||||||
{
|
|
||||||
// Green is much higher than other pixels
|
|
||||||
darkness = (int)(10 - (pixel.g * 10.1));
|
|
||||||
color = BlockColors.Green;
|
|
||||||
}
|
|
||||||
else if ((pixel.r - pixel.b) > pixel.r * 0.3)
|
|
||||||
{
|
|
||||||
// Red is much higher than blue
|
|
||||||
darkness = (int)(10 - ((pixel.r + pixel.g) * 5.1));
|
|
||||||
color = BlockColors.Yellow;
|
|
||||||
}
|
|
||||||
else if ((pixel.b - pixel.r) > pixel.b * 0.2)
|
|
||||||
{
|
|
||||||
// Blue is much higher than red
|
|
||||||
darkness = (int)(9 - ((pixel.g + pixel.b) * 5.1));
|
|
||||||
color = BlockColors.Aqua;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Red is close strength to blue
|
|
||||||
darkness = (int)(10 - ((pixel.r + pixel.g * 2.2 + pixel.b) * 2.9));
|
|
||||||
color = BlockColors.Lime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (pixel.b >= pixel.g && pixel.b >= pixel.r)
|
|
||||||
{
|
|
||||||
// Blue is highest
|
|
||||||
if ((pixel.b - pixel.g) > pixel.b * 0.6 && (pixel.b - pixel.r) > pixel.b * 0.6)
|
|
||||||
{
|
|
||||||
// Blue is much higher than other pixels
|
|
||||||
darkness = (int)(10 - (pixel.b * 10.1));
|
|
||||||
color = BlockColors.Blue;
|
|
||||||
}
|
|
||||||
else if ((pixel.g - pixel.r) > pixel.g * 0.3)
|
|
||||||
{
|
|
||||||
// Green is much higher than red
|
|
||||||
darkness = (int)(10 - ((pixel.g + pixel.b) * 5.1));
|
|
||||||
if (darkness == 4 || darkness == 5) darkness = 0;
|
|
||||||
else if (darkness < 3) darkness = 4;
|
|
||||||
color = BlockColors.Aqua;
|
|
||||||
}
|
|
||||||
else if ((pixel.r - pixel.g) > pixel.r * 0.3)
|
|
||||||
{
|
|
||||||
// Red is much higher than green
|
|
||||||
darkness = (int)(10 - ((pixel.r + pixel.b) * 5.0));
|
|
||||||
color = BlockColors.Purple;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Green is close strength to red
|
|
||||||
darkness = (int)(10 - ((pixel.r + pixel.g + pixel.b * 2.2) * 3.0));
|
|
||||||
color = BlockColors.Aqua;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// level 9 is not darker than lvl 8
|
|
||||||
if (darkness > 8 && !force) darkness = 8;
|
|
||||||
// darkness 0 is the most saturated (it's not just the lightest)
|
|
||||||
if (darkness < 0) darkness = 0;
|
|
||||||
|
|
||||||
QuantizedPixel result = new QuantizedPixel
|
|
||||||
{
|
|
||||||
block = pixel.a > 0.75 ? BlockIDs.AluminiumCube : BlockIDs.GlassCube,
|
|
||||||
color = color,
|
|
||||||
darkness = (byte)darkness,
|
|
||||||
visible = pixel.a > 0.5f,
|
|
||||||
};
|
|
||||||
#if DEBUG
|
|
||||||
Logging.MetaLog($"Quantized {color} (b:{result.block} d:{result.darkness} v:{result.visible})");
|
|
||||||
#endif
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal struct QuantizedPixel
|
|
||||||
{
|
|
||||||
public BlockIDs block;
|
|
||||||
|
|
||||||
public BlockColors color;
|
|
||||||
|
|
||||||
public byte darkness;
|
|
||||||
|
|
||||||
public bool visible;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
7
Pixi/Robots/CubeUtility.cs
Normal file
7
Pixi/Robots/CubeUtility.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
using System;
|
||||||
|
namespace Pixi.Robots
|
||||||
|
{
|
||||||
|
public static class CubeUtility
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
7
Pixi/Robots/RobotCommands.cs
Normal file
7
Pixi/Robots/RobotCommands.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
using System;
|
||||||
|
namespace Pixi.Robots
|
||||||
|
{
|
||||||
|
public static class RobotCommands
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,8 @@ Think of it like automatic pixel art.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
To install the Pixi mod, copy the build's `Pixi.dll` into the `Plugins` folder in Gamecraft's main folder.
|
To install the Pixi mod, copy `Pixi.dll` (from the latest release) into the `Plugins` folder in Gamecraft's main folder.
|
||||||
|
You'll also need [GamecraftModdingAPI](https://git.exmods.org/modtainers/GamecraftModdingAPI) installed and Gamecraft patched with [GCIPA](https://git.exmods.org/modtainers/GCIPA/releases).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue