From d3a64c89a7b5dd9878a46ab6bceeab6362526be4 Mon Sep 17 00:00:00 2001 From: Andrew Opalach Date: Thu, 25 Jun 2026 21:36:21 -0400 Subject: LUTs wip Signed-off-by: Andrew Opalach --- LUT.cs | 207 ++++++++++++++++++++++++++++++++++++++++++++++++ Plugin.cs | 62 +++++++++++++-- Scripts/convert_luts.sh | 21 +++++ 3 files changed, 284 insertions(+), 6 deletions(-) create mode 100644 LUT.cs create mode 100755 Scripts/convert_luts.sh diff --git a/LUT.cs b/LUT.cs new file mode 100644 index 0000000..cb8975c --- /dev/null +++ b/LUT.cs @@ -0,0 +1,207 @@ +//#define TEXTURE_FEATURE + +using System.Text; +using System.Numerics; +using SharpPluginLoader.Core; +using SharpPluginLoader.Core.Rendering; + +namespace WorldTuningTool +{ + public class WorldLUT + { + private const string lutsPath = "nativePC/plugins/CSharp/LUT"; + + public WorldLUT() { } + + public string[] GetLUTs() + { + if (!Directory.Exists(lutsPath)) + { + return []; + } + return Directory.GetFiles(lutsPath); + } + + // https://github.com/AsteriskAmpersand/CrappyLUTStudio--CLUTS-/blob/5af10daaa5f295b106cc5db09530a6fe5a4320d4/LUT.py#L35 + public byte[]? parseWorldLUT(BinaryReader reader) + { + byte[] signature = reader.ReadBytes(4); + if (!(signature[0] == 0x54 && signature[1] == 0x45 && + (signature[2] == 0x58 && signature[3] == 0x0))) + { + return null; + } + ulong version = reader.ReadUInt64(); + uint datablock = reader.ReadUInt32(); + uint format = reader.ReadUInt32(); + uint mipCount = reader.ReadUInt32(); + uint width = reader.ReadUInt32(); + uint height = reader.ReadUInt32(); + uint mipListCount = reader.ReadUInt32(); + uint typeData = reader.ReadUInt32(); + uint depth = reader.ReadUInt32(); + byte[] NULL0 = reader.ReadBytes(12); + int NEG0 = reader.ReadInt32(); + byte[] NULL1 = reader.ReadBytes(8); + int Special = reader.ReadInt32(); + byte[] NULL2 = reader.ReadBytes(16); + byte[] NEG1 = reader.ReadBytes(32); + byte[] flags = reader.ReadBytes(32); + byte[] NULLX = reader.ReadBytes(32); + reader.BaseStream.Position = (long)reader.ReadUInt64(); + if (!(width == 32 && height == 32 && depth == 32)) + { + return null; + } + return reader.ReadBytes((int)(width * height * depth * 8)); + } + + // https://drive.google.com/file/d/143Eh08ZYncCAMwJ1q4gWxVOqR_OSWYvs/view + // When does DOMAIN_MIN/MAX apply? + public byte[]? parseCubeLUT(StreamReader reader) + { + string? line; + Vector3[,,]? parsed = null; + int dim = 0, last = 0; + int x = 0, y = 0, z = 0; + while ((line = reader.ReadLine()) != null) + { + if (line.StartsWith("LUT_3D_SIZE")) + { + try + { + dim = Convert.ToInt32(line.Split(' ').Last()); + } + catch (FormatException) + { + return null; + } + catch (OverflowException) + { + return null; + } + if (dim != 32) + { + return null; + } + parsed = new Vector3[dim,dim,dim]; + last = dim - 1; + while ((line = reader.ReadLine()) != null) + { + if (line == "") continue; + if (line[0] == '0' || line[0] == '1') + { + break; + } + } + if (line == null) + { + break; + } + } + if (parsed != null) + { + try + { + parsed[x,y,z] = Plugin.StringToVector3(line, ' '); + } + catch (FormatException) + { + return null; + } + catch (OverflowException) + { + return null; + } + if (x == last) + { + if (y == last) + { + z++; + y = 0; + } + else + { + y++; + } + x = 0; + } + else + { + x++; + } + } + } + if (parsed == null) + { + return null; + } + const int lutDataSize = 32 * 32 * 32 * 8; + byte[] data = new byte[lutDataSize], bytes; + float scale = MathF.Pow(2, 14) - 1; + int idx = 0; + for (z = 0; z < 32; z++) + { + for (y = 0; y < 32; y++) + { + for (x = 0; x < 32; x++) + { + Vector3 v = parsed[x,y,z]; + ushort normX = (ushort)Math.Round(v.X * scale); + ushort normY = (ushort)Math.Round(v.Y * scale); + ushort normZ = (ushort)Math.Round(v.Z * scale); + bytes = BitConverter.GetBytes(normX); + data[idx] = bytes[0]; + data[idx + 1] = bytes[1]; + bytes = BitConverter.GetBytes(normY); + data[idx + 2] = bytes[0]; + data[idx + 3] = bytes[1]; + bytes = BitConverter.GetBytes(normZ); + data[idx + 4] = bytes[0]; + data[idx + 5] = bytes[1]; + bytes = BitConverter.GetBytes(0x2B88); // Alpha constant. + data[idx + 6] = bytes[0]; + data[idx + 7] = bytes[1]; + idx += 8; + } + } + } + return data; + } + + public unsafe bool OverwriteFromFile(nint resource, string path) + { + using (FileStream stream = File.Open(path, FileMode.Open)) + { + byte[]? data = null; + string ext = path.Split('.').Last().ToLower(); + if (ext == "tex") + { + using (BinaryReader reader = new BinaryReader(stream, Encoding.ASCII, false)) + { + data = parseWorldLUT(reader); + } + } + else if (ext == "cube") + { + using (StreamReader reader = new StreamReader(stream)) + { + data = parseCubeLUT(reader); + } + } + if (data == null) + { + return false; + } + fixed (byte *dataPointer = (byte[])data) + { +#if TEXTURE_FEATURE + // DXGI_FORMAT_R16G16B16A16_FLOAT = 10 + Renderer.ReplaceTexture(resource, dataPointer, 10, 32, 32, 32); +#endif + } + return true; + } + } + } +} diff --git a/Plugin.cs b/Plugin.cs index 56be4f3..e79ba74 100755 --- a/Plugin.cs +++ b/Plugin.cs @@ -104,13 +104,13 @@ namespace WorldTuningTool private static string BooleanToString(bool b) { return b.ToString(CultureInfo.InvariantCulture); } private static string Int32ToString(int i, string? format = null) { return i.ToString(format, CultureInfo.InvariantCulture); } - private static Vector3 StringToVector3(string s) + public static Vector3 StringToVector3(string s, char delim = ',') { if (String.IsNullOrEmpty(s)) { throw new FormatException(); } - string[] vs = s.Split(',').Select(v => v.Trim()).ToArray(); + string[] vs = s.Split(delim).Select(v => v.Trim()).ToArray(); if (vs.Length != 3) { throw new FormatException(); @@ -1645,9 +1645,14 @@ namespace WorldTuningTool private Hook? createLightingObject; private Hook? destroyLightingObject; private nint lightingObject = 0x0; + private delegate void SetLightingParameters(nint lightingObjectInternal); + private Hook? setLightingParams; private delegate void UpdateLightingParameters(nint stackOffset, nint lightingObjectInternal); private Hook? updateLightingParams; private Hook? updateLutBlend; + private static WorldLUT lut = new WorldLUT(); + private static string selectedLut = ""; + private static bool selectedLutInavlid = false; private static Parameter[] lightingParameters = { newParameter("Light Tone Map Type", 0x16C, ParameterType.INT, pMinMaxStep(1, 6, 1)), newParameter("Light Compute Luminance", 0x170, ParameterType.BOOL), @@ -2157,6 +2162,7 @@ namespace WorldTuningTool createLightingObject = Hook.Create(0x1424CDE20, CreateLightingObjectHook); destroyLightingObject = Hook.Create(0x1424CE380, DestroyLightingObjectHook); // nint, int + setLightingParams = Hook.Create(0x1424CF1C0, SetLightingParametersHook); // nint updateLutBlend = Hook.Create(0x1416D8DF0, UpdateLutBlendHook); // nint, nint createBloomObject = Hook.Create(0x1424CB3C0, CreateBloomObjectHook); @@ -2563,10 +2569,7 @@ namespace WorldTuningTool { foreach (Parameter param in lightingParameters) { - if (!param.PerFrame) - { - param.Update(lightingObject); - } + param.Update(lightingObject); } } foreach (Parameter param in lightProbesParameters) @@ -2855,6 +2858,16 @@ namespace WorldTuningTool return destroyLightingObject!.Original(lightingObjectInternal, unknownInt); } + private void SetLightingParametersHook(nint lightingObjectInternal) + { + setLightingParams!.Original(lightingObjectInternal); + if (lightingObject == lightingObjectInternal) + { + // LUT texture should be set at this point. + selectedLut = ""; + } + } + private void UpdateLutBlendHook(nint stackOffset, nint lightingObjectInternal) { updateLutBlend!.Original(stackOffset, lightingObjectInternal); @@ -3789,6 +3802,43 @@ namespace WorldTuningTool { if (lightingObject != 0x0) { + nint lutTexture = MemoryUtil.Read(lightingObject + 0x1C0); + nint d3d12Resource = 0x0; + if (lutTexture != 0x0) + { + d3d12Resource = MemoryUtil.Read(lutTexture + 0x10); + } + if (d3d12Resource != 0x0) + { + ImGui.SetNextItemWidth(width * 0.65f); + bool prevLutWasInvalid = selectedLutInavlid; + if (prevLutWasInvalid) + { + ImGui.PushStyleColor(ImGuiCol.Text, 0xFF0000FF); + } + if (ImGui.BeginCombo("LUT", selectedLut, ImGuiComboFlags.HeightLarge)) + { + if (prevLutWasInvalid) + { + ImGui.PopStyleColor(); + } + foreach (string lutPath in lut.GetLUTs()) + { + string lutFile = Path.GetFileName(lutPath); + bool isSelected = selectedLut == lutFile; + if (ImGui.Selectable(lutFile, isSelected)) + { + selectedLutInavlid = !lut.OverwriteFromFile(d3d12Resource, lutPath); + selectedLut = lutFile; + } + } + ImGui.EndCombo(); + } + else if (prevLutWasInvalid) + { + ImGui.PopStyleColor(); + } + } ImGui.Text($"Address: 0x{lightingObject:X}"); foreach (Parameter param in lightingParameters) { diff --git a/Scripts/convert_luts.sh b/Scripts/convert_luts.sh new file mode 100755 index 0000000..4778b02 --- /dev/null +++ b/Scripts/convert_luts.sh @@ -0,0 +1,21 @@ +#! /usr/bin/env sh +# https://github.com/michelerenzullo/LUTify/blob/main/LUTify.py +# ----- +# diff --git a/LUTify.py b/LUTify.py +# index 71983ac..24020db 100644 +# --- a/LUTify.py +# +++ b/LUTify.py +# @@ -162,7 +162,8 @@ if args.input.lower().endswith((".cube",".png",".jpg",".jpeg",".tiff")) and args +# title = re.search("[^\\\/]+(?=\.[\w]+$)",args.input)[0] +# if args.input.lower().endswith(".cube"): +# file = open(args.input,'r').read() +# - o_array = np.array([i.lower().replace(',', '').split() for i in re.findall("\n[+-]?[0-9]*[.]?[0-9]+\s[+-]?[0-9]*[.]?[0-9]+\s[+-]?[0-9]*[.]?[0-9]+",file)],dtype=float).reshape(-1) +# + mfloat = "[+-]?[0-9]*[.]?[0-9]*[eE]?[+-]?[0-9]+" +# + o_array = np.array([i.lower().replace(',', '').split() for i in re.findall(f"\n{mfloat}\s{mfloat}\s{mfloat}",file)],dtype=float).reshape(-1) +# lutSize = int(re.search("_SIZE.*?(\d+)",file).group(1)) +# input_title = re.search("TITLE.?[\"'](.*?)[\"']",file) +# del file +# ----- +for var in "$@"; do + python3 LUTify.py -i "$var" -o "$var.32.cube" -s 32 -m tetrahedral +done -- cgit v1.2.3-101-g0448