The MaterialPresets class:
using System;
using System.Runtime.CompilerServices;
using static Embodiment.Types.Material;
using Category = Embodiment.Types.Enums.MaterialCategory;
namespace Embodiment.Types
{
public static class MaterialPresets
{
private const int VS = 0; // Viscosity value for static materials. It is ignored since they won't move.
private const int SL = 0;
public const int MAX_MATERIALS = 256;
private static ByteTypeButCantTakeSameValueTwice id = new ByteTypeButCantTakeSameValueTwice(0);
public static readonly Material[] materials;// = new Material[MAX_MATERIALS];
public static readonly Material_Unmanaged[] materialsUnmanaged; // An copy of `materials` without managed types like `string`.
public static readonly Material AIR;
public static readonly Material SAND;
public static readonly Material WOOD_WEAK;
public static readonly Material SOIL_WEAK;
public static readonly Material ROCK_WEAK;
public static readonly Material ICE_SOLID;
public static readonly Material IRON_RUSTY;
public static readonly Material SOIL;
static MaterialPresets()
{
materials = new Material[MAX_MATERIALS];
materialsUnmanaged = new Material_Unmanaged[MAX_MATERIALS];
// PRESET ID NAME TYPE IS STATIC? DENSITY VISCOSITY R G B A
AIR = materials[id] = new Material(id, "Air", Category.Gas, false, 0, 0.0f, 0, 0, 0, 0, StrengthPresets.NA);
id = 0x40; SAND = materials[id] = new Material(id, "Sand", Category.Powdered, false, 100, 0.2f, 216, 216, 0, 255, StrengthPresets.NA);
id = 0x60; WOOD_WEAK = materials[id] = new Material(id, "Wood", Category.Solid, true, SL, VS, 128, 90, 30, 255, StrengthPresets.WOOD_WEAK);
id = 0x70; SOIL_WEAK = materials[id] = new Material(id, "Soil", Category.Solid, true, SL, VS, 60, 30, 30, 255, StrengthPresets.SOIL_WEAK);
id = 0x71; SOIL = materials[id] = new Material(id, "Soil", Category.Solid, true, SL, VS, 40, 10, 40, 255, StrengthPresets.NA);
id = 0x80; ROCK_WEAK = materials[id] = new Material(id, "Rock", Category.Solid, true, SL, VS, 128, 128, 128, 255, StrengthPresets.ROCK_WEAK);
id = 0x90; ICE_SOLID = materials[id] = new Material(id, "Ice", Category.Solid, true, SL, VS, 190, 226, 222, 200, StrengthPresets.ICE_SOLID);
id = 0xA0; IRON_RUSTY = materials[id] = new Material(id, "Rusty Iron", Category.Solid, true, SL, VS, 64, 30, 40, 255, StrengthPresets.IRON_RUSTY);
InitializeUnmanagedCopy();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static UInt32 GetColor(in byte materialId)
{
return materials[materialId].color;
}
private static void InitializeUnmanagedCopy()
{
for (int i=0; i<MAX_MATERIALS; i++)
{
materialsUnmanaged[i] = new Material_Unmanaged( materials[i].id, //byte id,
//in string name,
materials[i].category, //MaterialCategory category,
materials[i].isStatic, //bool isStatic),
materials[i].density, //int density,
materials[i].viscosity, //float viscosity,
materials[i].color); //UInt32 color,
//in Durability durability);
}
}
}
}
A colored image (The RGBA values in the PNG are the material colors in the MaterialPresets):
A resulting image (the RGBA values in the colored version transform into material IDs in the green channel):
By the way, I updated the script a little. This is the final one which runs perfectly fine:
using Embodiment.Types;
using Godot;
using System.Collections.Generic;
using System.IO;
namespace Embodiment.EditorScripts
{
public class PatternFileConverter
{
public void RunConversion_ColoredToMaterialId()
{
string rootDirectoryPath = ProjectSettings.GlobalizePath("res://Game Data/Patterns");
if (!Directory.Exists(rootDirectoryPath))
{
GD.PrintErr($"PatternFileConverter: Directory not found at OS path: {rootDirectoryPath}");
return;
}
Dictionary<uint, byte> colorToMaterialId = new Dictionary<uint, byte>();
for (int i = 0; i < MaterialPresets.MAX_MATERIALS; i++)
{
var mat = MaterialPresets.materialsUnmanaged[i];
if (mat.id != 0 || i == 0) // Skip empty slots, but keep Air (ID 0)
{
colorToMaterialId[mat.color] = mat.id;
}
}
string[] filesToProcess = Directory.GetFiles(rootDirectoryPath, "*_Colored.png", SearchOption.AllDirectories);
foreach (string file in filesToProcess)
{
ProcessImage_ToMaterialId(file, colorToMaterialId);
}
GD.Print($"Colored -> Material ID complete! Processed {filesToProcess.Length} files.");
}
private void ProcessImage_ToMaterialId(string inputPath, Dictionary<uint, byte> lookup)
{
using Image img = Image.LoadFromFile(inputPath);
if (img.GetFormat() != Image.Format.Rgba8) img.Convert(Image.Format.Rgba8);
int width = img.GetWidth();
int height = img.GetHeight();
byte[] sourceData = img.GetData();
byte[] targetData;
string outputPath = inputPath.Replace("_Colored.png", ".png");
bool fileExists = File.Exists(outputPath);
if (fileExists)
{
// Load the existing raw data file to preserve the WIMB entities in the R and B channels
using Image existingImg = Image.LoadFromFile(outputPath);
if (existingImg.GetFormat() != Image.Format.Rgba8) existingImg.Convert(Image.Format.Rgba8);
targetData = existingImg.GetData();
}
else
{
// Create a fresh array if the file doesn't exist yet
targetData = new byte[sourceData.Length];
}
for (int i = 0; i < sourceData.Length; i += 4)
{
byte r = sourceData[i];
byte g = sourceData[i + 1];
byte b = sourceData[i + 2];
byte a = sourceData[i + 3];
uint pixelColorPacked = (uint)((a << 24) | (b << 16) | (g << 8) | (r << 0));
byte materialId = lookup.TryGetValue(pixelColorPacked, out byte id) ? id : (byte)0;
if (fileExists)
{
// ONLY overwrite the Green channel. Leave R, B, and A completely untouched.
targetData[i + 1] = materialId;
}
else
{
targetData[i] = 0; // R (Empty WIMB slot)
targetData[i + 1] = materialId; // G (Material ID)
targetData[i + 2] = 0; // B (Empty WIMB metadata)
targetData[i + 3] = 255; // A (Fully Opaque)
}
}
using Image dataImage = Image.CreateFromData(width, height, false, Image.Format.Rgba8, targetData);
dataImage.SavePng(outputPath);
}
public void RunConversion_MaterialIdToColored()
{
string rootDirectoryPath = ProjectSettings.GlobalizePath("res://Game Data/Patterns");
if (!Directory.Exists(rootDirectoryPath))
{
GD.PrintErr($"PatternFileConverter: Directory not found at OS path: {rootDirectoryPath}");
return;
}
// 1. Build Reverse Dictionary: Material ID (Green Channel) -> Packed Natural Color (UInt32)
Dictionary<byte, uint> materialIdToColor = new Dictionary<byte, uint>();
for (int i = 0; i < MaterialPresets.MAX_MATERIALS; i++)
{
var mat = MaterialPresets.materialsUnmanaged[i];
if (mat.id != 0 || i == 0)
{
materialIdToColor[mat.id] = mat.color;
}
}
// 2. Fetch all PNGs, but filter out the ones already marked "_Colored.png"
string[] allPngs = Directory.GetFiles(rootDirectoryPath, "*.png", SearchOption.AllDirectories);
List<string> filesToProcess = new List<string>();
foreach(string file in allPngs)
{
if (!file.EndsWith("_Colored.png"))
{
filesToProcess.Add(file);
}
}
// 3. Process every collected data file
foreach (string file in filesToProcess)
{
ProcessImage_ToColored(file, materialIdToColor);
}
GD.Print($"Material ID -> Colored complete! Processed {filesToProcess.Count} files.");
}
private void ProcessImage_ToColored(string inputPath, Dictionary<byte, uint> lookup)
{
using Image img = Image.LoadFromFile(inputPath);
if (img.GetFormat() != Image.Format.Rgba8) img.Convert(Image.Format.Rgba8);
int width = img.GetWidth();
int height = img.GetHeight();
byte[] sourceData = img.GetData();
byte[] targetData = new byte[sourceData.Length];
for (int i = 0; i < sourceData.Length; i += 4)
{
// The Green channel holds the material ID
byte materialId = sourceData[i + 1];
if (!lookup.TryGetValue(materialId, out uint naturalColor))
{
naturalColor = lookup[0]; // Fallback to Air (0) if the ID is missing/corrupted
}
// Unpack the natural color exactly how MakeColor packed it
targetData[i] = (byte)( naturalColor & 0xFF); // R
targetData[i + 1] = (byte)((naturalColor >> 8) & 0xFF); // G
targetData[i + 2] = (byte)((naturalColor >> 16) & 0xFF); // B
targetData[i + 3] = (byte)((naturalColor >> 24) & 0xFF); // A
}
using Image dataImage = Image.CreateFromData(width, height, false, Image.Format.Rgba8, targetData);
string outputPath = inputPath.Replace(".png", "_Colored.png");
dataImage.SavePng(outputPath);
}
}
}