From 10b77ba61dd5caedf96b7c8dbce9939d72587b8c Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Mon, 2 Sep 2024 00:58:44 +1000 Subject: [PATCH 01/44] Material properties init - init lightshaft support - use vertex colours for bg/bgprop - (needs testing) add prop for exporting bitangent data - better methods for getting material constants - add more material constants (for lightshaft) - init work on LightLayoutInstance and Light drawobjects --- .../Models/Composer/InstanceComposer.cs | 290 ++++++++++++++++-- .../Models/Structs/LightInstance.cs | 29 ++ .../Meddle.Plugin/Services/LayoutService.cs | 15 +- Meddle/Meddle.Utils/Export/Material.cs | 10 +- Meddle/Meddle.Utils/Export/ShaderPackage.cs | 19 +- Meddle/Meddle.Utils/ImageUtils.cs | 10 +- .../Meddle.Utils/Materials/MaterialUtility.cs | 2 + Meddle/Meddle.Utils/Materials/Other.cs | 32 +- .../Materials/XIVMaterialBuilder.cs | 13 + Meddle/Meddle.Utils/MeshBuilder.cs | 228 ++++++++++++-- 10 files changed, 572 insertions(+), 76 deletions(-) create mode 100644 Meddle/Meddle.Plugin/Models/Structs/LightInstance.cs create mode 100644 Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs diff --git a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs index 5b39282..c687b71 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs @@ -1,6 +1,8 @@ using System.Collections.Concurrent; using System.Numerics; using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Nodes; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Meddle.Plugin.Models.Layout; using Meddle.Plugin.Utils; @@ -50,7 +52,11 @@ public InstanceComposer(ILogger log, SqPack manager, Configuration config, Parse public void Compose(SceneBuilder scene) { progress?.Invoke(new ProgressEvent("Export", 0, count)); - Parallel.ForEach(instances, instance => + Parallel.ForEach(instances, new ParallelOptions + { + CancellationToken = cancellationToken, + MaxDegreeOfParallelism = Math.Max(Environment.ProcessorCount / 2, 1) + }, instance => { try { @@ -286,7 +292,7 @@ private void ComposeCharacterInstance(ParsedCharacterInstance characterInstance, MaterialUtility.BuildIris(material, name, cubeMapTex, customizeParams, customizeData), "water.shpk" => MaterialUtility.BuildWater(material, name), "lightshaft.shpk" => MaterialUtility.BuildLightShaft(material, name), - _ => ComposeGenericMaterial(materialInfo.Path) + _ => ComposeMaterial(materialInfo.Path) }; materialBuilders.Add(builder); @@ -382,7 +388,7 @@ private void ComposeTerrainInstance(ParsedTerrainInstance terrainInstance, Scene var materialBuilders = new List(); foreach (var mtrlPath in materials) { - var materialBuilder = ComposeGenericMaterial(mtrlPath); + var materialBuilder = ComposeMaterial(mtrlPath); materialBuilders.Add(materialBuilder); } @@ -415,7 +421,7 @@ private void ComposeTerrainInstance(ParsedTerrainInstance terrainInstance, Scene var materialBuilders = new List(); foreach (var mtrlPath in materials) { - var output = ComposeGenericMaterial(mtrlPath); + var output = ComposeMaterial(mtrlPath); materialBuilders.Add(output); } @@ -423,8 +429,8 @@ private void ComposeTerrainInstance(ParsedTerrainInstance terrainInstance, Scene var meshes = ModelBuilder.BuildMeshes(model, materialBuilders, [], null); return meshes; } - - private MaterialBuilder ComposeGenericMaterial(string path) + + private MaterialBuilder ComposeMaterial(string path) { if (mtrlCache.TryGetValue(path, out var cached)) { @@ -434,10 +440,10 @@ private MaterialBuilder ComposeGenericMaterial(string path) var mtrlData = dataManager.GetFileOrReadFromDisk(path); if (mtrlData == null) throw new Exception($"Failed to load material file: {path}"); log.LogInformation("Loaded material {path}", path); - + var mtrlFile = new MtrlFile(mtrlData); - var texturePaths = mtrlFile.GetTexturePaths(); - var shpkPath = $"shader/sm5/shpk/{mtrlFile.GetShaderPackageName()}"; + var shpkName = mtrlFile.GetShaderPackageName(); + var shpkPath = $"shader/sm5/shpk/{shpkName}"; if (!shpkCache.TryGetValue(shpkPath, out var shader)) { var shpkData = dataManager.GetFileOrReadFromDisk(shpkPath); @@ -448,16 +454,107 @@ private MaterialBuilder ComposeGenericMaterial(string path) shpkCache.TryAdd(shpkPath, shader); log.LogInformation("Loaded shader package {shpkPath}", shpkPath); } - var material = new MaterialSet(mtrlFile, shader.File); - var output = new MaterialBuilder(Path.GetFileNameWithoutExtension(path)) + var material = new MaterialSet(mtrlFile, shader.File, shpkName); + + if (shpkName == "lightshaft.shpk") + { + return ComposeLightshaft(path, material); + } + + return ComposeGenericMaterial(path, material); + } + + private MaterialBuilder ComposeLightshaft(string path, MaterialSet materialSet) + { + var output = new XivMaterialBuilder(path, "lightshaft.shpk"); + + var sampler0 = materialSet.TextureUsageDict[TextureUsage.g_Sampler0]; + var sampler1 = materialSet.TextureUsageDict[TextureUsage.g_Sampler1]; + var texture0 = dataManager.GetFileOrReadFromDisk(sampler0); + var texture1 = dataManager.GetFileOrReadFromDisk(sampler1); + if (texture0 == null || texture1 == null) + { + log.LogWarning("Failed to load lightshaft textures {sampler0} {sampler1}", sampler0, sampler1); + return output; + } + + var tex0 = new TexFile(texture0); + var tex1 = new TexFile(texture1); + + Vector2 size = Vector2.Max(new Vector2(tex0.Header.Width, tex0.Header.Height), new Vector2(tex1.Header.Width, tex1.Header.Height)); + + var res0 = Texture.GetResource(tex0).ToTexture(size); + var res1 = Texture.GetResource(tex1).ToTexture(size); + + + var outTexture = new SKTexture((int)size.X, (int)size.Y); + materialSet.TryGetConstant(MaterialConstant.g_Color, out Vector3 colorv3); + for (var x = 0; x < outTexture.Width; x++) + for (var y = 0; y < outTexture.Height; y++) + { + var tex0Color = res0[x, y].ToVector4(); + var tex1Color = res1[x, y].ToVector4(); + var outColor = new Vector4(colorv3, 1); + + outTexture[x, y] = (outColor * tex0Color * tex1Color).ToSkColor(); + } + + // cache texture + var fileName = $"{Path.GetFileNameWithoutExtension(path)}_computed_lightshaft"; + var tempPath = Path.Combine(CacheDir, $"{fileName}.png"); + var diffuseImage = CacheTexture(outTexture, tempPath); + output.WithBaseColor(diffuseImage); + + + if (materialSet.TryGetConstant(MaterialConstant.g_AlphaThreshold, out float alphaThreshold)) + { + output.WithAlpha(AlphaMode.MASK, alphaThreshold); + } + + materialSet.TryGetConstant(MaterialConstant.g_Ray, out float[] ray); + materialSet.TryGetConstant(MaterialConstant.g_TexU, out float[] texU); + materialSet.TryGetConstant(MaterialConstant.g_TexV, out float[] texV); + materialSet.TryGetConstant(MaterialConstant.g_TexAnim, out float[] texAnim); + materialSet.TryGetConstant(MaterialConstant.g_ShadowAlphaThreshold, out float[] shadowAlphaThreshold); + materialSet.TryGetConstant(MaterialConstant.g_NearClip, out float[] nearClip); + materialSet.TryGetConstant(MaterialConstant.g_AngleClip, out float[] angleClip); + + + var extrasDict = new Dictionary + { + {"Sampler0", sampler0}, + {"Sampler1", sampler1}, + {"AlphaThreshold", alphaThreshold}, + {"Ray", ray}, + {"TexU", texU}, + {"TexV", texV}, + {"TexAnim", texAnim}, + {"ShadowAlphaThreshold", shadowAlphaThreshold}, + {"NearClip", nearClip}, + {"AngleClip", angleClip}, + {"Color", colorv3} + }; + + output.Extras = JsonNode.Parse(JsonSerializer.Serialize(extrasDict, new JsonSerializerOptions + { + IncludeFields = true + })); + return output; + } + + private MaterialBuilder ComposeGenericMaterial(string path, MaterialSet materialSet) + { + var materialName = $"{Path.GetFileNameWithoutExtension(path)}_{materialSet.ShpkName}"; + var output = new XivMaterialBuilder(materialName, materialSet.ShpkName) .WithMetallicRoughnessShader() .WithBaseColor(Vector4.One); - var alphaThreshold = material.GetConstantOrDefault(MaterialConstant.g_AlphaThreshold, 0.0f); + var alphaThreshold = materialSet.GetConstantOrDefault(MaterialConstant.g_AlphaThreshold, 0.0f); if (alphaThreshold > 0) output.WithAlpha(AlphaMode.MASK, alphaThreshold); // Initialize texture in cache + var texturePaths = materialSet.File.GetTexturePaths(); foreach (var (offset, texPath) in texturePaths) { if (imageCache.ContainsKey(texPath)) continue; @@ -465,15 +562,15 @@ private MaterialBuilder ComposeGenericMaterial(string path) } var setTypes = new HashSet(); - foreach (var sampler in mtrlFile.Samplers) + foreach (var sampler in materialSet.File.Samplers) { if (sampler.TextureIndex == byte.MaxValue) continue; - var textureInfo = mtrlFile.TextureOffsets[sampler.TextureIndex]; + var textureInfo = materialSet.File.TextureOffsets[sampler.TextureIndex]; var texturePath = texturePaths[textureInfo.Offset]; if (!imageCache.TryGetValue(texturePath, out var tex)) continue; // bg textures can have additional textures, which may be dummy textures, ignore them if (texturePath.Contains("dummy_")) continue; - if (!shader.Package.TextureLookup.TryGetValue(sampler.SamplerId, out var usage)) + if (!materialSet.Package.TextureLookup.TryGetValue(sampler.SamplerId, out var usage)) { log.LogWarning("Unknown texture usage for texture {texturePath} ({textureUsage})", texturePath, (TextureUsage)sampler.SamplerId); continue; @@ -482,7 +579,7 @@ private MaterialBuilder ComposeGenericMaterial(string path) var channel = MaterialUtility.MapTextureUsageToChannel(usage); if (channel != null && setTypes.Add(usage)) { - var fileName = $"{Path.GetFileNameWithoutExtension(texturePath)}_{usage}_{shader.Package.Name}"; + var fileName = $"{Path.GetFileNameWithoutExtension(texturePath)}_{usage}_{materialSet.ShpkName}"; var imageBuilder = ImageBuilder.From(tex.MemoryImage, fileName); imageBuilder.AlternateWriteFileName = $"{fileName}.*"; output.WithChannelImage(channel.Value, imageBuilder); @@ -503,49 +600,144 @@ private MaterialBuilder ComposeGenericMaterial(string path) public class MaterialSet { - private readonly MtrlFile file; - private readonly ShpkFile shpk; - private readonly ShaderKey[] shaderKeys; - private readonly Dictionary materialConstantDict; + public readonly MtrlFile File; + public readonly ShpkFile Shpk; + public readonly string ShpkName; + public readonly ShaderPackage Package; + public readonly ShaderKey[] ShaderKeys; + public readonly Dictionary MaterialConstantDict; + public readonly Dictionary TextureUsageDict; + + public bool TryGetConstant(MaterialConstant id, out float[] value) + { + if (MaterialConstantDict.TryGetValue(id, out var values)) + { + value = values; + return true; + } + + if (Package.MaterialConstants.TryGetValue(id, out var constant)) + { + value = constant; + return true; + } + + value = []; + return false; + } + + public bool TryGetConstant(MaterialConstant id, out float value) + { + if (MaterialConstantDict.TryGetValue(id, out var values)) + { + value = values[0]; + return true; + } + + if (Package.MaterialConstants.TryGetValue(id, out var constant)) + { + value = constant[0]; + return true; + } + + value = 0; + return false; + } + + public bool TryGetConstant(MaterialConstant id, out Vector2 value) + { + if (MaterialConstantDict.TryGetValue(id, out var values)) + { + value = new Vector2(values[0], values[1]); + return true; + } + + if (Package.MaterialConstants.TryGetValue(id, out var constant)) + { + value = new Vector2(constant[0], constant[1]); + return true; + } + + value = Vector2.Zero; + return false; + } + + public bool TryGetConstant(MaterialConstant id, out Vector3 value) + { + if (MaterialConstantDict.TryGetValue(id, out var values)) + { + value = new Vector3(values[0], values[1], values[2]); + return true; + } + + if (Package.MaterialConstants.TryGetValue(id, out var constant)) + { + value = new Vector3(constant[0], constant[1], constant[2]); + return true; + } + + value = Vector3.Zero; + return false; + } + + public bool TryGetConstant(MaterialConstant id, out Vector4 value) + { + if (MaterialConstantDict.TryGetValue(id, out var values)) + { + value = new Vector4(values[0], values[1], values[2], values[3]); + return true; + } + if (Package.MaterialConstants.TryGetValue(id, out var constant)) + { + value = new Vector4(constant[0], constant[1], constant[2], constant[3]); + return true; + } + + value = Vector4.Zero; + return false; + } + public float GetConstantOrDefault(MaterialConstant id, float @default) { - return materialConstantDict.TryGetValue(id, out var values) ? values[0] : @default; + return MaterialConstantDict.TryGetValue(id, out var values) ? values[0] : @default; } public Vector2 GetConstantOrDefault(MaterialConstant id, Vector2 @default) { - return materialConstantDict.TryGetValue(id, out var values) ? new Vector2(values[0], values[1]) : @default; + return MaterialConstantDict.TryGetValue(id, out var values) ? new Vector2(values[0], values[1]) : @default; } public Vector3 GetConstantOrDefault(MaterialConstant id, Vector3 @default) { - return materialConstantDict.TryGetValue(id, out var values) ? new Vector3(values[0], values[1], values[2]) : @default; + return MaterialConstantDict.TryGetValue(id, out var values) ? new Vector3(values[0], values[1], values[2]) : @default; } public Vector4 GetConstantOrDefault(MaterialConstant id, Vector4 @default) { - return materialConstantDict.TryGetValue(id, out var values) + return MaterialConstantDict.TryGetValue(id, out var values) ? new Vector4(values[0], values[1], values[2], values[3]) : @default; } - public MaterialSet(MtrlFile file, ShpkFile shpk) + public MaterialSet(MtrlFile file, ShpkFile shpk, string shpkName) { - this.file = file; - this.shpk = shpk; + this.File = file; + this.Shpk = shpk; + this.ShpkName = shpkName; + this.Package = new ShaderPackage(shpk, shpkName); - shaderKeys = new ShaderKey[file.ShaderKeys.Length]; + ShaderKeys = new ShaderKey[file.ShaderKeys.Length]; for (var i = 0; i < file.ShaderKeys.Length; i++) { - shaderKeys[i] = new ShaderKey + ShaderKeys[i] = new ShaderKey { Category = file.ShaderKeys[i].Category, Value = file.ShaderKeys[i].Value }; } - materialConstantDict = new Dictionary(); + MaterialConstantDict = new Dictionary(); foreach (var constant in file.Constants) { var index = constant.ValueOffset / 4; @@ -567,9 +759,45 @@ public MaterialSet(MtrlFile file, ShpkFile shpk) // even if duplicate, last probably takes precedence var id = (MaterialConstant)constant.ConstantId; - materialConstantDict[id] = values; + MaterialConstantDict[id] = values; } + + TextureUsageDict = new Dictionary(); + var texturePaths = file.GetTexturePaths(); + foreach (var sampler in file.Samplers) + { + if (sampler.TextureIndex == byte.MaxValue) continue; + var textureInfo = file.TextureOffsets[sampler.TextureIndex]; + var texturePath = texturePaths[textureInfo.Offset]; + if (!Package.TextureLookup.TryGetValue(sampler.SamplerId, out var usage)) + { + continue; + } + TextureUsageDict[usage] = texturePath; + } + } + } + + private MemoryImage CacheTexture(SKTexture texture, string texPath) + { + byte[] textureBytes; + using (var memoryStream = new MemoryStream()) + { + texture.Bitmap.Encode(memoryStream, SKEncodedImageFormat.Png, 100); + textureBytes = memoryStream.ToArray(); } + + var dirPath = Path.GetDirectoryName(texPath); + if (!string.IsNullOrEmpty(dirPath) && !Directory.Exists(dirPath)) + { + Directory.CreateDirectory(dirPath); + } + + File.WriteAllBytes(texPath, textureBytes); + + var outImage = new MemoryImage(() => File.ReadAllBytes(texPath)); + imageCache.TryAdd(texPath, (texPath, outImage)); + return outImage; } private void CacheTexture(string texPath) diff --git a/Meddle/Meddle.Plugin/Models/Structs/LightInstance.cs b/Meddle/Meddle.Plugin/Models/Structs/LightInstance.cs new file mode 100644 index 0000000..bbba8b9 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Structs/LightInstance.cs @@ -0,0 +1,29 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.LayoutEngine; +using FFXIVClientStructs.FFXIV.Client.LayoutEngine.Layer; + +namespace Meddle.Plugin.Models.Structs; + +[StructLayout(LayoutKind.Explicit, Size = 0x50)] +public unsafe struct LightLayoutInstance +{ + [FieldOffset(0x08)] public LayerManager* LayerManager; + [FieldOffset(0x10)] public LayoutManager* LayoutManager; + [FieldOffset(0x30)] public Light* LightPtr; +} + +[StructLayout(LayoutKind.Explicit, Size = 0x98)] +public unsafe struct Light +{ + [FieldOffset(0x00)] public DrawObject DrawObject; + [FieldOffset(0x90)] public LightItem* LightItem; +} + +// at least 0x90, now sure if this is correct +[StructLayout(LayoutKind.Explicit, Size = 0x90)] +public unsafe struct LightItem +{ + [FieldOffset(0x20)] public Transform* Transform; + [FieldOffset(0x8C)] public float UnkFloat; +} diff --git a/Meddle/Meddle.Plugin/Services/LayoutService.cs b/Meddle/Meddle.Plugin/Services/LayoutService.cs index fa64618..1d93651 100644 --- a/Meddle/Meddle.Plugin/Services/LayoutService.cs +++ b/Meddle/Meddle.Plugin/Services/LayoutService.cs @@ -208,7 +208,8 @@ private unsafe ParsedInstanceSet[] Parse(LayoutManager* activeLayout, ParseCtx c } case InstanceType.Light: { - return new ParsedLightInstance((nint)instanceLayout, new Transform(*instanceLayout->GetTransformImpl())); + var light = ParsedLightInstance(instanceLayout); + return light; } default: { @@ -226,6 +227,18 @@ private unsafe ParsedInstanceSet[] Parse(LayoutManager* activeLayout, ParseCtx c } } } + + private unsafe ParsedLightInstance? ParsedLightInstance(Pointer lightPtr) + { + if (lightPtr == null || lightPtr.Value == null) + return null; + + var light = lightPtr.Value; + if (light->Id.Type != InstanceType.Light) + return null; + + return new ParsedLightInstance((nint)light, new Transform(*light->GetTransformImpl())); + } private unsafe ParsedInstance? ParseSharedGroup(Pointer sharedGroupPtr, ParseCtx ctx) { diff --git a/Meddle/Meddle.Utils/Export/Material.cs b/Meddle/Meddle.Utils/Export/Material.cs index 9c8e13d..5d689c1 100644 --- a/Meddle/Meddle.Utils/Export/Material.cs +++ b/Meddle/Meddle.Utils/Export/Material.cs @@ -87,7 +87,15 @@ public enum MaterialConstant : uint g_GlassThicknessMax = 0xC4647F37, g_TextureMipBias = 0x39551220, g_OutlineColor = 0x623CC4FE, - g_OutlineWidth = 0x8870C938 + g_OutlineWidth = 0x8870C938, + g_Ray = 0x827BDD09, + g_TexU = 0x5926A043, + g_TexV = 0xC02FF1F9, + g_TexAnim = 0x14D8E13D, + g_Color = 0xD27C58B9, + g_ShadowAlphaThreshold = 0xD925FF32, + g_NearClip = 0x17A52926, + g_AngleClip = 0x71DBDA81 } public class Material diff --git a/Meddle/Meddle.Utils/Export/ShaderPackage.cs b/Meddle/Meddle.Utils/Export/ShaderPackage.cs index 15c5a38..d410b3d 100644 --- a/Meddle/Meddle.Utils/Export/ShaderPackage.cs +++ b/Meddle/Meddle.Utils/Export/ShaderPackage.cs @@ -6,8 +6,9 @@ namespace Meddle.Utils.Export; public unsafe class ShaderPackage { public string Name { get; } - public IReadOnlyDictionary TextureLookup { get; } - public IReadOnlyDictionary? ResourceKeys { get; } + public Dictionary TextureLookup { get; } + public Dictionary MaterialConstants { get; } + public Dictionary? ResourceKeys { get; } public ShaderPackage(ShpkFile file, string name) { @@ -58,6 +59,20 @@ public ShaderPackage(ShpkFile file, string name) resourceKeys[uav.Id] = resName; } + var materialConstantDict = new Dictionary(); + var orderedMaterialParams = file.MaterialParams.Select((x, idx) => (x, idx)).OrderBy(x => x.x.ByteOffset).ToArray(); + foreach (var (materialParam, i) in orderedMaterialParams) + { + // get defaults from byteoffset -> byteoffset + bytesize + var defaults = file.MaterialParamDefaults + .Skip(materialParam.ByteOffset / 4) + .Take(materialParam.ByteSize / 4).ToArray(); + var defaultCopy = new float[defaults.Length]; + Array.Copy(defaults, defaultCopy, defaults.Length); + materialConstantDict[(MaterialConstant)materialParam.Id] = defaultCopy; + } + + MaterialConstants = materialConstantDict; TextureLookup = textureUsages; ResourceKeys = resourceKeys; } diff --git a/Meddle/Meddle.Utils/ImageUtils.cs b/Meddle/Meddle.Utils/ImageUtils.cs index b04414f..13977d2 100644 --- a/Meddle/Meddle.Utils/ImageUtils.cs +++ b/Meddle/Meddle.Utils/ImageUtils.cs @@ -1,4 +1,5 @@ -using Meddle.Utils.Export; +using System.Numerics; +using Meddle.Utils.Export; using Meddle.Utils.Files; using Meddle.Utils.Models; using OtterTex; @@ -167,6 +168,13 @@ public static unsafe SKTexture ToTexture(this Image img, (int width, int height) return new SKTexture(bitmap); } + public static SKTexture ToTexture(this TextureResource resource, Vector2 size) + { + var bitmap = resource.ToBitmap(); + bitmap = bitmap.Resize(new SKImageInfo((int)size.X, (int)size.Y, SKColorType.Rgba8888, SKAlphaType.Unpremul), SKFilterQuality.High); + return new SKTexture(bitmap); + } + public static SKTexture ToTexture(this TextureResource resource, (int width, int height)? resize = null) { var bitmap = resource.ToBitmap(); diff --git a/Meddle/Meddle.Utils/Materials/MaterialUtility.cs b/Meddle/Meddle.Utils/Materials/MaterialUtility.cs index 281c97a..7a22a6f 100644 --- a/Meddle/Meddle.Utils/Materials/MaterialUtility.cs +++ b/Meddle/Meddle.Utils/Materials/MaterialUtility.cs @@ -1,4 +1,6 @@ using System.Numerics; +using System.Reflection.Metadata; +using FFXIVClientStructs.FFXIV.Shader; using Meddle.Utils.Export; using Meddle.Utils.Models; using SharpGLTF.Materials; diff --git a/Meddle/Meddle.Utils/Materials/Other.cs b/Meddle/Meddle.Utils/Materials/Other.cs index cce1dbd..c23488b 100644 --- a/Meddle/Meddle.Utils/Materials/Other.cs +++ b/Meddle/Meddle.Utils/Materials/Other.cs @@ -1,5 +1,6 @@ using System.Numerics; using Meddle.Utils.Export; +using Meddle.Utils.Models; using SharpGLTF.Materials; namespace Meddle.Utils.Materials; @@ -10,20 +11,37 @@ public static MaterialBuilder BuildWater(Material material, string name) { // TODO: Wavemap stuff maybe? not sure if I want to compute that since its dynamic var output = new MaterialBuilder(name) - .WithDoubleSide(material.RenderBackfaces) - .WithAlpha(AlphaMode.BLEND, 0.5f) - .WithBaseColor(new Vector4(1, 1, 1, 0f));; - + .WithDoubleSide(material.RenderBackfaces) + .WithAlpha(AlphaMode.BLEND, 0.5f) + .WithBaseColor(new Vector4(1, 1, 1, 0f)); + return output; } public static MaterialBuilder BuildLightShaft(Material material, string name) { var output = new MaterialBuilder(name) - .WithDoubleSide(material.RenderBackfaces) - .WithAlpha(AlphaMode.BLEND, 0.5f) - .WithBaseColor(new Vector4(1, 1, 1, 0f));; + .WithDoubleSide(material.RenderBackfaces); + + var sampler0 = material.GetTexture(TextureUsage.g_Sampler0); + var sampler1 = material.GetTexture(TextureUsage.g_Sampler1); + + var texture0 = sampler0.ToTexture(); + var texture1 = sampler1.ToTexture(); + var outTexture = new SKTexture(texture0.Width, texture0.Height); + + for (var x = 0; x < outTexture.Width; x++) + for (var y = 0; y < outTexture.Height; y++) + { + var tex0 = texture0[x, y].ToVector4(); + var tex1 = texture1[x, y].ToVector4(); + + outTexture[x, y] = (tex0 * tex1).ToSkColor(); + } + + output.WithBaseColor(BuildImage(outTexture, name, "base")); + return output; } } diff --git a/Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs b/Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs new file mode 100644 index 0000000..ed344cc --- /dev/null +++ b/Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs @@ -0,0 +1,13 @@ +using SharpGLTF.Materials; + +namespace Meddle.Utils.Materials; + +public class XivMaterialBuilder : MaterialBuilder +{ + public string Shpk; + + public XivMaterialBuilder(string name, string shpk) : base(name) + { + this.Shpk = shpk; + } +} diff --git a/Meddle/Meddle.Utils/MeshBuilder.cs b/Meddle/Meddle.Utils/MeshBuilder.cs index ad41ef9..4f8f200 100644 --- a/Meddle/Meddle.Utils/MeshBuilder.cs +++ b/Meddle/Meddle.Utils/MeshBuilder.cs @@ -3,9 +3,13 @@ using System.Text.Json.Nodes; using Meddle.Utils.Export; using Meddle.Utils.Files; +using Meddle.Utils.Materials; using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.Materials; +using SharpGLTF.Memory; +using SharpGLTF.Schema2; +using Mesh = Meddle.Utils.Export.Mesh; namespace Meddle.Utils; @@ -176,30 +180,7 @@ private IVertexBuilder BuildVertex(Vertex vertex) if (boneWeight == 0) continue; - var indices = vertex.BlendIndices?.Select(x => (int)x).ToArray(); - - var serializedVertexData = new - { - Vertex = new - { - vertex.Position, - vertex.BlendWeights, - BlendIndices = indices, - vertex.Normal, - vertex.UV, - vertex.Color, - vertex.Tangent2, - vertex.Tangent1 - }, - JoinLutSize = JointLut.Length, - JointLut = JointLut, - BoneIndex = boneIndex, - BoneWeight = boneWeight - }; - - var json = JsonSerializer.Serialize(serializedVertexData, new JsonSerializerOptions { WriteIndented = true, IncludeFields = true}); - - throw new InvalidOperationException($"Bone index {boneIndex} is out of bounds! Vertex data: {json}"); + throw new InvalidOperationException($"Bone index {boneIndex} is out of bounds!"); } var mappedBoneIndex = JointLut[boneIndex]; @@ -228,22 +209,44 @@ private IVertexBuilder BuildVertex(Vertex vertex) currentPos = deformedPos; } } - + geometryParamCache.Add(currentPos); // Means it's either VertexPositionNormal or VertexPositionNormalTangent; both have Normal if (GeometryT != typeof(VertexPosition)) geometryParamCache.Add(vertex.Normal!.Value); // Tangent W should be 1 or -1, but sometimes XIV has their -1 as 0? + // ReSharper disable CompareOfFloatsByEqualityOperator if (GeometryT == typeof(VertexPositionNormalTangent)) { - // ReSharper disable once CompareOfFloatsByEqualityOperator geometryParamCache.Add(vertex.Tangent1!.Value with { W = vertex.Tangent1.Value.W == 1 ? 1 : -1 }); } + if (GeometryT == typeof(VertexPositionNormalTangent2)) + { + geometryParamCache.Add(vertex.Tangent1!.Value with { W = vertex.Tangent1.Value.W == 1 ? 1 : -1 }); + geometryParamCache.Add(vertex.Tangent2!.Value with { W = vertex.Tangent2.Value.W == 1 ? 1 : -1 }); + } + // ReSharper restore CompareOfFloatsByEqualityOperator // AKA: Has "Color1" component - //if( _materialT != typeof( VertexTexture2 ) ) _materialParamCache.Insert( 0, vertex.Color!.Value ); - if (MaterialT != typeof(VertexTexture2)) materialParamCache.Insert(0, new Vector4(255, 255, 255, 255)); + if (MaterialT != typeof(VertexTexture2)) + { + Vector4 vertexColor = new Vector4(1, 1, 1, 1); + if (MaterialBuilder is XivMaterialBuilder xivMaterialBuilder) + { + vertexColor = xivMaterialBuilder.Shpk switch + { + "bg.shpk" => vertex.Color!.Value, + "bgprop.shpk" => vertex.Color!.Value, + _ => new Vector4(1, 1, 1, 1) + }; + } + + materialParamCache.Insert(0, vertexColor); + } + + //if( MaterialT != typeof( VertexTexture2 ) ) materialParamCache.Insert( 0, vertex.Color!.Value ); + //if (MaterialT != typeof(VertexTexture2)) materialParamCache.Insert(0, new Vector4(1, 1, 1, 1)); // AKA: Has "TextureN" component if (MaterialT != typeof(VertexColor1)) @@ -271,6 +274,11 @@ private static Type GetVertexGeometryType(IReadOnlyList vertex) { return typeof(VertexPosition); } + + if (vertex[0].Tangent2 != null && vertex[0].Tangent1 != null) + { + return typeof(VertexPositionNormalTangent2); + } if (vertex[0].Tangent1 != null) { @@ -284,6 +292,31 @@ private static Type GetVertexGeometryType(IReadOnlyList vertex) return typeof(VertexPosition); } + + private static IVertexGeometry CreateGeometryParamCache(Vertex vertex, Type type) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + switch (type) + { + case not null when type == typeof(VertexPosition): + return new VertexPosition(vertex.Position!.Value); + case not null when type == typeof(VertexPositionNormal): + return new VertexPositionNormal(vertex.Position!.Value, vertex.Normal!.Value); + case not null when type == typeof(VertexPositionNormalTangent): + // Tangent W should be 1 or -1, but sometimes XIV has their -1 as 0? + return new VertexPositionNormalTangent(vertex.Position!.Value, + vertex.Normal!.Value, + vertex.Tangent1!.Value with { W = vertex.Tangent1.Value.W == 1 ? 1 : -1 }); + case not null when type == typeof(VertexPositionNormalTangent2): + return new VertexPositionNormalTangent2(vertex.Position!.Value, + vertex.Normal!.Value, + vertex.Tangent1!.Value with { W = vertex.Tangent1.Value.W == 1 ? 1 : -1 }, + vertex.Tangent2!.Value with { W = vertex.Tangent2.Value.W == 1 ? 1 : -1 }); + default: + return new VertexPosition(vertex.Position!.Value); + } + // ReSharper restore CompareOfFloatsByEqualityOperator + } /// Obtain the correct material type for a set of vertices. private static Type GetVertexMaterialType(IReadOnlyList vertex) @@ -296,12 +329,45 @@ private static Type GetVertexMaterialType(IReadOnlyList vertex) var hasColor = vertex[0].Color != null; var hasUv = vertex[0].UV != null; - return hasColor switch + if (hasColor && hasUv) { - true when hasUv => typeof(VertexColor1Texture2), - false when hasUv => typeof(VertexTexture2), - _ => typeof(VertexColor1), - }; + return typeof(VertexColor1Texture2); + } + + if (hasColor) + { + return typeof(VertexColor1); + } + + if (hasUv) + { + return typeof(VertexTexture2); + } + + return typeof(VertexColor1); + } + + private static IVertexMaterial CreateMaterialParamCache(Vertex vertex, Type type) + { + switch (type) + { + case not null when type == typeof(VertexColor1): + { + return new VertexColor1(vertex.Color!.Value); + } + case not null when type == typeof(VertexTexture2): + { + var (xy, zw) = ToVec2(vertex.UV!.Value); + return new VertexTexture2(xy, zw); + } + case not null when type == typeof(VertexColor1Texture2): + { + var (xy, zw) = ToVec2(vertex.UV!.Value); + return new VertexColor1Texture2(vertex.Color!.Value, xy, zw); + } + default: + return new VertexEmpty(); + } } private static Type GetVertexSkinningType(IReadOnlyList vertex, bool isSkinned) @@ -323,3 +389,99 @@ private static Type GetVertexSkinningType(IReadOnlyList vertex, bool isS private static Vector3 ToVec3(Vector4 v) => new(v.X, v.Y, v.Z); private static (Vector2 XY, Vector2 ZW) ToVec2(Vector4 v) => (new(v.X, v.Y), new(v.Z, v.W)); } + +public struct VertexPositionNormalTangent2 : IVertexGeometry, IEquatable +{ + public VertexPositionNormalTangent2(in Vector3 p, in Vector3 n, in Vector4 t, in Vector4 t2) + { + this.Position = p; + this.Normal = n; + this.Tangent = t; + this.Tangent2 = t2; + } + + public static implicit operator VertexPositionNormalTangent2(in (Vector3 Pos, Vector3 Nrm, Vector4 Tgt, Vector4 Tgt2) tuple) + { + return new VertexPositionNormalTangent2(tuple.Pos, tuple.Nrm, tuple.Tgt, tuple.Tgt2); + } + + #region data + + public Vector3 Position; + public Vector3 Normal; + public Vector4 Tangent; + public Vector4 Tangent2; + + IEnumerable> IVertexReflection.GetEncodingAttributes() + { + yield return new KeyValuePair("POSITION", new AttributeFormat(DimensionType.VEC3)); + yield return new KeyValuePair("NORMAL", new AttributeFormat(DimensionType.VEC3)); + yield return new KeyValuePair("TANGENT", new AttributeFormat(DimensionType.VEC4)); + yield return new KeyValuePair("TANGENT2", new AttributeFormat(DimensionType.VEC4)); + } + + public override readonly int GetHashCode() { return Position.GetHashCode(); } + + /// + public override readonly bool Equals(object obj) { return obj is VertexPositionNormalTangent2 other && AreEqual(this, other); } + + /// + public readonly bool Equals(VertexPositionNormalTangent2 other) { return AreEqual(this, other); } + public static bool operator ==(in VertexPositionNormalTangent2 a, in VertexPositionNormalTangent2 b) { return AreEqual(a, b); } + public static bool operator !=(in VertexPositionNormalTangent2 a, in VertexPositionNormalTangent2 b) { return !AreEqual(a, b); } + public static bool AreEqual(in VertexPositionNormalTangent2 a, in VertexPositionNormalTangent2 b) + { + return a.Position == b.Position && a.Normal == b.Normal && a.Tangent == b.Tangent && a.Tangent2 == b.Tangent2; + } + + #endregion + + #region API + + void IVertexGeometry.SetPosition(in Vector3 position) { this.Position = position; } + + void IVertexGeometry.SetNormal(in Vector3 normal) { this.Normal = normal; } + + void IVertexGeometry.SetTangent(in Vector4 tangent) { this.Tangent = tangent; } + + void SetTangent2(in Vector4 tangent2) { this.Tangent2 = tangent2; } + + /// + public readonly VertexGeometryDelta Subtract(IVertexGeometry baseValue) + { + var baseVertex = (VertexPositionNormalTangent2)baseValue; + var tangentDelta = this.Tangent - baseVertex.Tangent; + + return new VertexGeometryDelta( + this.Position - baseVertex.Position, + this.Normal - baseVertex.Normal, + new Vector3(tangentDelta.X, tangentDelta.Y, tangentDelta.Z)); + } + + public void Add(in VertexGeometryDelta delta) + { + this.Position += delta.PositionDelta; + this.Normal += delta.NormalDelta; + this.Tangent += new Vector4(delta.TangentDelta, 0); + } + + public readonly Vector3 GetPosition() { return this.Position; } + public readonly bool TryGetNormal(out Vector3 normal) { normal = this.Normal; return true; } + public readonly bool TryGetTangent(out Vector4 tangent) { tangent = this.Tangent; return true; } + public readonly bool TryGetTangent2(out Vector4 tangent2) { tangent2 = this.Tangent2; return true; } + + /// + public void ApplyTransform(in Matrix4x4 xform) + { + Position = Vector3.Transform(Position, xform); + Normal = Vector3.Normalize(Vector3.TransformNormal(Normal, xform)); + + var txyz = Vector3.Normalize(Vector3.TransformNormal(new Vector3(Tangent.X, Tangent.Y, Tangent.Z), xform)); + Tangent = new Vector4(txyz, Tangent.W); + + var t2xyz = Vector3.Normalize(Vector3.TransformNormal(new Vector3(Tangent2.X, Tangent2.Y, Tangent2.Z), xform)); + Tangent2 = new Vector4(t2xyz, Tangent2.W); + } + + #endregion +} From 6680584c67d8af5fd2bc9196d636b7bbfa570621 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Mon, 2 Sep 2024 17:04:37 +1000 Subject: [PATCH 02/44] UI Updates + lightshaft and bg init work --- .../Models/Composer/BgMaterialBuilder.cs | 234 +++++++++++++++ .../Models/Composer/InstanceComposer.cs | 283 ++---------------- .../Composer/InstanceMaterialBuilder.cs | 17 ++ .../Composer/LightshaftMaterialBuilder.cs | 68 +++++ .../Models/Composer/MaterialSet.cs | 273 +++++++++++++++++ Meddle/Meddle.Plugin/Services/ParseService.cs | 2 +- Meddle/Meddle.UI/Windows/SqPackWindow.cs | 2 +- Meddle/Meddle.UI/Windows/Views/ExportView.cs | 4 +- Meddle/Meddle.UI/Windows/Views/MdlView.cs | 27 +- Meddle/Meddle.UI/Windows/Views/MtrlView.cs | 120 ++++++-- Meddle/Meddle.UI/Windows/Views/ShpkView.cs | 43 ++- Meddle/Meddle.UI/Windows/Views/TeraView.cs | 26 ++ Meddle/Meddle.Utils/Export/Material.cs | 71 ++++- Meddle/Meddle.Utils/Export/Texture.cs | 18 -- Meddle/Meddle.Utils/Export/TextureResource.cs | 5 +- Meddle/Meddle.Utils/ImageUtils.cs | 17 ++ .../Materials/XIVMaterialBuilder.cs | 5 + Meddle/Meddle.Utils/MeshBuilder.cs | 9 +- 18 files changed, 899 insertions(+), 325 deletions(-) create mode 100644 Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs create mode 100644 Meddle/Meddle.Plugin/Models/Composer/InstanceMaterialBuilder.cs create mode 100644 Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs create mode 100644 Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs diff --git a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs new file mode 100644 index 0000000..981d171 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs @@ -0,0 +1,234 @@ +using System.Numerics; +using Meddle.Utils; +using Meddle.Utils.Export; +using Meddle.Utils.Files; +using Meddle.Utils.Materials; +using Meddle.Utils.Models; +using SharpGLTF.Materials; +using SharpGLTF.Memory; + +namespace Meddle.Plugin.Models.Composer; + +public class BgMaterialBuilder : InstanceMaterialBuilder, IVertexPaintMaterialBuilder +{ + private readonly MaterialSet set; + private const uint BGVertexPaintKey = 0x4F4F0636; + private const uint BGVertexPaintValue = 0xBD94649A; + private const uint DiffuseAlphaKey = 0xA9A3EE25; + private const uint DiffuseAlphaValue = 0x72AAA9AE; // if present, alpha channel on diffuse texture is used?? and should respect g_AlphaThreshold + + public BgMaterialBuilder(string name, MaterialSet set, Func lookupFunc, Func cacheFunc) : base(name, "bg.shpk", lookupFunc, cacheFunc) + { + this.set = set; + } + + + public BgMaterialBuilder WithBg() + { + // samplers + var colorMap0 = set.TextureUsageDict[TextureUsage.g_SamplerColorMap0]; + var specularMap0 = set.TextureUsageDict[TextureUsage.g_SamplerSpecularMap0]; + var normalMap0 = set.TextureUsageDict[TextureUsage.g_SamplerNormalMap0]; + var colorMap0Texture = LookupFunc(colorMap0); + var specularMap0Texture = LookupFunc(specularMap0); + var normalMap0Texture = LookupFunc(normalMap0); + if (colorMap0Texture == null || specularMap0Texture == null || normalMap0Texture == null) + { + return this; + } + + var colorRes0 = new TexFile(colorMap0Texture).ToResource(); + var specularRes0 = new TexFile(specularMap0Texture).ToResource(); + var normalRes0 = new TexFile(normalMap0Texture).ToResource(); + var sizes = new List + { + colorRes0.Size, + specularRes0.Size, + normalRes0.Size + }; + + TextureResource? colorRes1 = null; + if (set.TextureUsageDict.TryGetValue(TextureUsage.g_SamplerColorMap1, out var colorMap1)) + { + var colorMap1Texture = LookupFunc(colorMap1); + if (colorMap1Texture != null) + { + colorRes1 = new TexFile(colorMap1Texture).ToResource(); + sizes.Add(colorRes1.Value.Size); + } + } + + TextureResource? specularRes1 = null; + if (set.TextureUsageDict.TryGetValue(TextureUsage.g_SamplerSpecularMap1, out var specularMap1)) + { + var specularMap1Texture = LookupFunc(specularMap1); + if (specularMap1Texture != null) + { + specularRes1 = new TexFile(specularMap1Texture).ToResource(); + sizes.Add(specularRes1.Value.Size); + } + } + + TextureResource? normalRes1 = null; + if (set.TextureUsageDict.TryGetValue(TextureUsage.g_SamplerNormalMap1, out var normalMap1)) + { + var normalMap1Texture = LookupFunc(normalMap1); + if (normalMap1Texture != null) + { + normalRes1 = new TexFile(normalMap1Texture).ToResource(); + sizes.Add(normalRes1.Value.Size); + } + } + + var size = Max(sizes); + var colorTex0 = colorRes0.ToTexture(size); + var specularTex0 = specularRes0.ToTexture(size); + var normalTex0 = normalRes0.ToTexture(size); + var colorTex1 = colorRes1?.ToTexture(size); + var specularTex1 = specularRes1?.ToTexture(size); + var normalTex1 = normalRes1?.ToTexture(size); + + var useAlpha = set.ShaderKeys.Any(x => x is {Category: DiffuseAlphaKey, Value: DiffuseAlphaValue}); + if (useAlpha && set.TryGetConstant(MaterialConstant.g_AlphaThreshold, out float alphaThreshold)) + { + WithAlpha(AlphaMode.MASK, alphaThreshold); + } + + Vector3 tmp; + Vector3 diffuseColor = Vector3.One; + if (set.TryGetConstant(MaterialConstant.g_DiffuseColor, out tmp)) + { + diffuseColor = tmp; + } + Vector3 specularColor = Vector3.One; + if (set.TryGetConstant(MaterialConstant.g_SpecularColor, out tmp)) + { + specularColor = tmp; + } + Vector3 emissiveColor = Vector3.Zero; + if (set.TryGetConstant(MaterialConstant.g_EmissiveColor, out tmp)) + { + emissiveColor = tmp; + } + + var diffuseColor0 = new Vector4(diffuseColor, 1); + var specularColor0 = new Vector4(specularColor, 1); + SKTexture diffuse = new SKTexture((int)size.X, (int)size.Y); + //SKTexture specular = new SKTexture((int)size.X, (int)size.Y); + //SKTexture emmissive = new SKTexture((int)size.X, (int)size.Y); + SKTexture normal = new SKTexture((int)size.X, (int)size.Y); + for (int x = 0; x < diffuse.Width; x++) + for (int y = 0; y < diffuse.Height; y++) + { + var color0 = colorTex0[x, y].ToVector4(); + var specular0 = specularTex0[x, y].ToVector4(); + var normal0 = normalTex0[x, y].ToVector4(); + var color1 = colorTex1?[x, y].ToVector4() ?? Vector4.One; + var specular1 = specularTex1?[x, y].ToVector4() ?? Vector4.One; + var normal1 = normalTex1?[x, y].ToVector4() ?? Vector4.One; + + + var outDiffuse = (diffuseColor0 * color0 * color1).ToSkColor(); + + var specularData = (specularColor0 * specular0 * specular1); + var outNormal = (normal0 * normal1).ToSkColor(); + + diffuse[x, y] = outDiffuse; + //emmissive[x, y] = Vector4.Lerp(Vector4.Zero, new Vector4(emissiveColor, 1), specularData.X).ToSkColor(); + //specular[x, y] = specularData.ToSkColor(); + normal[x, y] = outNormal; + } + + VertexPaint = set.ShaderKeys.Any(x => x is {Category: BGVertexPaintKey, Value: BGVertexPaintValue}); + Extras = set.ComposeExtrasNode(); + + if (set.TryGetConstant(MaterialConstant.g_NormalScale, out float normalScale)) + { + WithNormal(CacheFunc(normal, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_computed_bg_normal"), normalScale); + } + else + { + WithNormal(CacheFunc(normal, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_computed_bg_normal")); + } + + WithBaseColor(CacheFunc(diffuse, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_computed_bg_diffuse")); + + // should not be applied uniformly. strength is probably dictated using one of the spec channels + // WithEmissive(CacheFunc(emmissive, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_emissive")); + + CacheFunc(colorTex0, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_color0"); + CacheFunc(specularTex0, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_specular0"); + CacheFunc(normalTex0, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_normal0"); + if (colorTex1 != null) + { + CacheFunc(colorTex1, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_color1"); + } + if (specularTex1 != null) + { + CacheFunc(specularTex1, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_specular1"); + } + if (normalTex1 != null) + { + CacheFunc(normalTex1, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_normal1"); + } + + return this; + } + + public bool VertexPaint { get; private set; } + + private Vector2 Max(IEnumerable vectors) + { + var max = new Vector2(float.MinValue); + foreach (var vector in vectors) + { + max = Vector2.Max(max, vector); + } + + return max; + } + + /* +g_AlphaThreshold = [0] +g_ShadowAlphaThreshold = [0.5] +g_ShaderID = [0] +g_DiffuseColor = [1, 1, 1] +g_MultiDiffuseColor = [1, 1, 1] +g_SpecularColor = [1, 1, 1] +g_MultiSpecularColor = [1, 1, 1] +g_EmissiveColor = [0, 0, 0] +g_MultiEmissiveColor = [0, 0, 0] +g_NormalScale = [1] +g_MultiNormalScale = [1] +g_HeightScale = [0.015] +g_MultiHeightScale = [0.015] +g_SSAOMask = [1] +g_MultiSSAOMask = [1] +0xBFE9D12D = [1] +0x093084AD = [1] +g_InclusionAperture = [1] +0x5106E045 = [0] +g_ColorUVScale = [1, 1, 1, 1] +g_SpecularUVScale = [1, 1, 1, 1] +g_NormalUVScale = [1, 1, 1, 1] +g_AlphaMultiParam = [0, 0, 0, 0] +g_DetailID = [0] +0xAC156136 = [0] +g_DetailColor = [0.5, 0.5, 0.5] +g_MultiDetailColor = [0.5, 0.5, 0.5] +0xF769298E = [0.3, 0.3, 0.3, 0.3] +g_DetailNormalScale = [1] +0xA83DBDF1 = [1] +g_DetailNormalUvScale = [4, 4, 4, 4] +g_DetailColorUvScale = [4, 4, 4, 4] +0xB8ACCE58 = [50, 100, 50, 100] +0xD67F62C8 = [1] +0x12F6AB51 = [3] +0x236EE793 = [0, 0] +0xF3F28C58 = [0, 0] +0x756DFE22 = [0, 0] +0xB10AF2DA = [0, 0] +0x9A696A17 = [10, 10, 10, 10] +g_EnvMapPower = [0.85] + */ +} diff --git a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs index c687b71..0d43a0b 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs @@ -454,92 +454,22 @@ private MaterialBuilder ComposeMaterial(string path) shpkCache.TryAdd(shpkPath, shader); log.LogInformation("Loaded shader package {shpkPath}", shpkPath); } - var material = new MaterialSet(mtrlFile, shader.File, shpkName); + var material = new MaterialSet(mtrlFile, path, shader.File, shpkName); + var materialName = $"{Path.GetFileNameWithoutExtension(path)}_{Path.GetFileNameWithoutExtension(shpkName)}"; if (shpkName == "lightshaft.shpk") { - return ComposeLightshaft(path, material); + return new LightshaftMaterialBuilder(materialName, material, dataManager.GetFileOrReadFromDisk, CacheComputedTexture) + .WithLightShaft(); } - - return ComposeGenericMaterial(path, material); - } - - private MaterialBuilder ComposeLightshaft(string path, MaterialSet materialSet) - { - var output = new XivMaterialBuilder(path, "lightshaft.shpk"); - - var sampler0 = materialSet.TextureUsageDict[TextureUsage.g_Sampler0]; - var sampler1 = materialSet.TextureUsageDict[TextureUsage.g_Sampler1]; - var texture0 = dataManager.GetFileOrReadFromDisk(sampler0); - var texture1 = dataManager.GetFileOrReadFromDisk(sampler1); - if (texture0 == null || texture1 == null) - { - log.LogWarning("Failed to load lightshaft textures {sampler0} {sampler1}", sampler0, sampler1); - return output; - } - - var tex0 = new TexFile(texture0); - var tex1 = new TexFile(texture1); - - Vector2 size = Vector2.Max(new Vector2(tex0.Header.Width, tex0.Header.Height), new Vector2(tex1.Header.Width, tex1.Header.Height)); - var res0 = Texture.GetResource(tex0).ToTexture(size); - var res1 = Texture.GetResource(tex1).ToTexture(size); - - - var outTexture = new SKTexture((int)size.X, (int)size.Y); - materialSet.TryGetConstant(MaterialConstant.g_Color, out Vector3 colorv3); - for (var x = 0; x < outTexture.Width; x++) - for (var y = 0; y < outTexture.Height; y++) - { - var tex0Color = res0[x, y].ToVector4(); - var tex1Color = res1[x, y].ToVector4(); - var outColor = new Vector4(colorv3, 1); - - outTexture[x, y] = (outColor * tex0Color * tex1Color).ToSkColor(); - } - - // cache texture - var fileName = $"{Path.GetFileNameWithoutExtension(path)}_computed_lightshaft"; - var tempPath = Path.Combine(CacheDir, $"{fileName}.png"); - var diffuseImage = CacheTexture(outTexture, tempPath); - output.WithBaseColor(diffuseImage); - - - if (materialSet.TryGetConstant(MaterialConstant.g_AlphaThreshold, out float alphaThreshold)) + if (shpkName == "bg.shpk") { - output.WithAlpha(AlphaMode.MASK, alphaThreshold); + return new BgMaterialBuilder(materialName, material, dataManager.GetFileOrReadFromDisk, CacheComputedTexture) + .WithBg(); } - - materialSet.TryGetConstant(MaterialConstant.g_Ray, out float[] ray); - materialSet.TryGetConstant(MaterialConstant.g_TexU, out float[] texU); - materialSet.TryGetConstant(MaterialConstant.g_TexV, out float[] texV); - materialSet.TryGetConstant(MaterialConstant.g_TexAnim, out float[] texAnim); - materialSet.TryGetConstant(MaterialConstant.g_ShadowAlphaThreshold, out float[] shadowAlphaThreshold); - materialSet.TryGetConstant(MaterialConstant.g_NearClip, out float[] nearClip); - materialSet.TryGetConstant(MaterialConstant.g_AngleClip, out float[] angleClip); - - var extrasDict = new Dictionary - { - {"Sampler0", sampler0}, - {"Sampler1", sampler1}, - {"AlphaThreshold", alphaThreshold}, - {"Ray", ray}, - {"TexU", texU}, - {"TexV", texV}, - {"TexAnim", texAnim}, - {"ShadowAlphaThreshold", shadowAlphaThreshold}, - {"NearClip", nearClip}, - {"AngleClip", angleClip}, - {"Color", colorv3} - }; - - output.Extras = JsonNode.Parse(JsonSerializer.Serialize(extrasDict, new JsonSerializerOptions - { - IncludeFields = true - })); - return output; + return ComposeGenericMaterial(path, material); } private MaterialBuilder ComposeGenericMaterial(string path, MaterialSet materialSet) @@ -597,188 +527,8 @@ private MaterialBuilder ComposeGenericMaterial(string path, MaterialSet material mtrlCache.TryAdd(path, output); return output; } - - public class MaterialSet - { - public readonly MtrlFile File; - public readonly ShpkFile Shpk; - public readonly string ShpkName; - public readonly ShaderPackage Package; - public readonly ShaderKey[] ShaderKeys; - public readonly Dictionary MaterialConstantDict; - public readonly Dictionary TextureUsageDict; - - public bool TryGetConstant(MaterialConstant id, out float[] value) - { - if (MaterialConstantDict.TryGetValue(id, out var values)) - { - value = values; - return true; - } - - if (Package.MaterialConstants.TryGetValue(id, out var constant)) - { - value = constant; - return true; - } - - value = []; - return false; - } - - public bool TryGetConstant(MaterialConstant id, out float value) - { - if (MaterialConstantDict.TryGetValue(id, out var values)) - { - value = values[0]; - return true; - } - - if (Package.MaterialConstants.TryGetValue(id, out var constant)) - { - value = constant[0]; - return true; - } - - value = 0; - return false; - } - - public bool TryGetConstant(MaterialConstant id, out Vector2 value) - { - if (MaterialConstantDict.TryGetValue(id, out var values)) - { - value = new Vector2(values[0], values[1]); - return true; - } - - if (Package.MaterialConstants.TryGetValue(id, out var constant)) - { - value = new Vector2(constant[0], constant[1]); - return true; - } - - value = Vector2.Zero; - return false; - } - - public bool TryGetConstant(MaterialConstant id, out Vector3 value) - { - if (MaterialConstantDict.TryGetValue(id, out var values)) - { - value = new Vector3(values[0], values[1], values[2]); - return true; - } - - if (Package.MaterialConstants.TryGetValue(id, out var constant)) - { - value = new Vector3(constant[0], constant[1], constant[2]); - return true; - } - - value = Vector3.Zero; - return false; - } - - public bool TryGetConstant(MaterialConstant id, out Vector4 value) - { - if (MaterialConstantDict.TryGetValue(id, out var values)) - { - value = new Vector4(values[0], values[1], values[2], values[3]); - return true; - } - - if (Package.MaterialConstants.TryGetValue(id, out var constant)) - { - value = new Vector4(constant[0], constant[1], constant[2], constant[3]); - return true; - } - - value = Vector4.Zero; - return false; - } - - public float GetConstantOrDefault(MaterialConstant id, float @default) - { - return MaterialConstantDict.TryGetValue(id, out var values) ? values[0] : @default; - } - - public Vector2 GetConstantOrDefault(MaterialConstant id, Vector2 @default) - { - return MaterialConstantDict.TryGetValue(id, out var values) ? new Vector2(values[0], values[1]) : @default; - } - - public Vector3 GetConstantOrDefault(MaterialConstant id, Vector3 @default) - { - return MaterialConstantDict.TryGetValue(id, out var values) ? new Vector3(values[0], values[1], values[2]) : @default; - } - - public Vector4 GetConstantOrDefault(MaterialConstant id, Vector4 @default) - { - return MaterialConstantDict.TryGetValue(id, out var values) - ? new Vector4(values[0], values[1], values[2], values[3]) - : @default; - } - - public MaterialSet(MtrlFile file, ShpkFile shpk, string shpkName) - { - this.File = file; - this.Shpk = shpk; - this.ShpkName = shpkName; - this.Package = new ShaderPackage(shpk, shpkName); - - ShaderKeys = new ShaderKey[file.ShaderKeys.Length]; - for (var i = 0; i < file.ShaderKeys.Length; i++) - { - ShaderKeys[i] = new ShaderKey - { - Category = file.ShaderKeys[i].Category, - Value = file.ShaderKeys[i].Value - }; - } - - MaterialConstantDict = new Dictionary(); - foreach (var constant in file.Constants) - { - var index = constant.ValueOffset / 4; - var count = constant.ValueSize / 4; - var buf = new List(128); - for (var j = 0; j < count; j++) - { - var value = file.ShaderValues[index + j]; - var bytes = BitConverter.GetBytes(value); - buf.AddRange(bytes); - } - - var floats = MemoryMarshal.Cast(buf.ToArray()); - var values = new float[count]; - for (var j = 0; j < count; j++) - { - values[j] = floats[j]; - } - - // even if duplicate, last probably takes precedence - var id = (MaterialConstant)constant.ConstantId; - MaterialConstantDict[id] = values; - } - - TextureUsageDict = new Dictionary(); - var texturePaths = file.GetTexturePaths(); - foreach (var sampler in file.Samplers) - { - if (sampler.TextureIndex == byte.MaxValue) continue; - var textureInfo = file.TextureOffsets[sampler.TextureIndex]; - var texturePath = texturePaths[textureInfo.Offset]; - if (!Package.TextureLookup.TryGetValue(sampler.SamplerId, out var usage)) - { - continue; - } - TextureUsageDict[usage] = texturePath; - } - } - } - private MemoryImage CacheTexture(SKTexture texture, string texPath) + private MemoryImage CacheComputedTexture(SKTexture texture, string texName) { byte[] textureBytes; using (var memoryStream = new MemoryStream()) @@ -787,16 +537,17 @@ private MemoryImage CacheTexture(SKTexture texture, string texPath) textureBytes = memoryStream.ToArray(); } - var dirPath = Path.GetDirectoryName(texPath); - if (!string.IsNullOrEmpty(dirPath) && !Directory.Exists(dirPath)) + var outPath = Path.Combine(CacheDir, "Computed", $"{texName}.png"); + var outDir = Path.GetDirectoryName(outPath); + if (!string.IsNullOrEmpty(outDir) && !Directory.Exists(outDir)) { - Directory.CreateDirectory(dirPath); + Directory.CreateDirectory(outDir); } - File.WriteAllBytes(texPath, textureBytes); + File.WriteAllBytes(outPath, textureBytes); - var outImage = new MemoryImage(() => File.ReadAllBytes(texPath)); - imageCache.TryAdd(texPath, (texPath, outImage)); + var outImage = new MemoryImage(() => File.ReadAllBytes(outPath)); + imageCache.TryAdd(texName, (outPath, outImage)); return outImage; } @@ -808,7 +559,7 @@ private void CacheTexture(string texPath) var texFile = new TexFile(texData); var diskPath = Path.Combine(CacheDir, Path.GetDirectoryName(texPath) ?? "", Path.GetFileNameWithoutExtension(texPath)) + ".png"; - var texture = Texture.GetResource(texFile).ToTexture(); + var texture = texFile.ToResource().ToTexture(); byte[] textureBytes; using (var memoryStream = new MemoryStream()) { diff --git a/Meddle/Meddle.Plugin/Models/Composer/InstanceMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/InstanceMaterialBuilder.cs new file mode 100644 index 0000000..8143f35 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/InstanceMaterialBuilder.cs @@ -0,0 +1,17 @@ +using Meddle.Utils.Materials; +using Meddle.Utils.Models; +using SharpGLTF.Memory; + +namespace Meddle.Plugin.Models.Composer; + +public abstract class InstanceMaterialBuilder : XivMaterialBuilder +{ + protected readonly Func LookupFunc; + protected readonly Func CacheFunc; + + public InstanceMaterialBuilder(string name, string shpk, Func lookupFunc, Func cacheFunc) : base(name, shpk) + { + this.LookupFunc = lookupFunc; + this.CacheFunc = cacheFunc; + } +} diff --git a/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs new file mode 100644 index 0000000..bd1d8cf --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs @@ -0,0 +1,68 @@ +using System.Numerics; +using System.Text.Json; +using System.Text.Json.Nodes; +using Meddle.Utils; +using Meddle.Utils.Export; +using Meddle.Utils.Files; +using Meddle.Utils.Materials; +using Meddle.Utils.Models; +using SharpGLTF.Materials; +using SharpGLTF.Memory; + +namespace Meddle.Plugin.Models.Composer; + +public class LightshaftMaterialBuilder : InstanceMaterialBuilder +{ + private readonly MaterialSet set; + + public LightshaftMaterialBuilder(string name, MaterialSet set, Func lookupFunc, Func cacheFunc) : base(name, "lightshaft.shpk", lookupFunc, cacheFunc) + { + this.set = set; + } + + public LightshaftMaterialBuilder WithLightShaft() + { + var sampler0 = set.TextureUsageDict[TextureUsage.g_Sampler0]; + var sampler1 = set.TextureUsageDict[TextureUsage.g_Sampler1]; + var texture0 = LookupFunc(sampler0); + var texture1 = LookupFunc(sampler1); + if (texture0 == null || texture1 == null) + { + return this; + } + + var tex0 = new TexFile(texture0).ToResource(); + var tex1 = new TexFile(texture1).ToResource(); + + var size = Vector2.Max(tex0.Size, tex1.Size); + var res0 = tex0.ToTexture(size); + var res1 = tex1.ToTexture(size); + + + var outTexture = new SKTexture((int)size.X, (int)size.Y); + set.TryGetConstant(MaterialConstant.g_Color, out Vector3 color); + for (var x = 0; x < outTexture.Width; x++) + for (var y = 0; y < outTexture.Height; y++) + { + var tex0Color = res0[x, y].ToVector4(); + var tex1Color = res1[x, y].ToVector4(); + var outColor = new Vector4(color, 1); + + outTexture[x, y] = (outColor * tex0Color * tex1Color).ToSkColor(); + } + + var fileName = $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_computed_lightshaft_diffuse"; + var diffuseImage = CacheFunc(outTexture, fileName); + this.WithBaseColor(diffuseImage); + + + if (set.TryGetConstant(MaterialConstant.g_AlphaThreshold, out float alphaThreshold)) + { + this.WithAlpha(AlphaMode.MASK, alphaThreshold); + } + + Extras = set.ComposeExtrasNode(); + + return this; + } +} diff --git a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs new file mode 100644 index 0000000..2b67e4e --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs @@ -0,0 +1,273 @@ +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Nodes; +using Meddle.Utils.Export; +using Meddle.Utils.Files; +using Meddle.Utils.Models; + +namespace Meddle.Plugin.Models.Composer; + +public class MaterialSet +{ + public readonly string MtrlPath; + public readonly MtrlFile File; + public readonly ShpkFile Shpk; + public readonly string ShpkName; + public readonly ShaderPackage Package; + public readonly ShaderKey[] ShaderKeys; + public readonly Dictionary MaterialConstantDict; + public readonly Dictionary TextureUsageDict; + + public Dictionary GetConstants() + { + var dict = new Dictionary(); + foreach (var (key, value) in Package.MaterialConstants) + { + var values = new float[value.Length]; + value.CopyTo(values, 0); + dict[key] = values; + } + + foreach (var (key, value) in MaterialConstantDict) + { + var values = new float[value.Length]; + value.CopyTo(values, 0); + dict[key] = values; + } + + return dict; + } + + public bool TryGetConstant(MaterialConstant id, out float[] value) + { + if (MaterialConstantDict.TryGetValue(id, out var values)) + { + value = values; + return true; + } + + if (Package.MaterialConstants.TryGetValue(id, out var constant)) + { + value = constant; + return true; + } + + value = []; + return false; + } + + public bool TryGetConstant(MaterialConstant id, out float value) + { + if (MaterialConstantDict.TryGetValue(id, out var values)) + { + value = values[0]; + return true; + } + + if (Package.MaterialConstants.TryGetValue(id, out var constant)) + { + value = constant[0]; + return true; + } + + value = 0; + return false; + } + + public bool TryGetConstant(MaterialConstant id, out Vector2 value) + { + if (MaterialConstantDict.TryGetValue(id, out var values)) + { + value = new Vector2(values[0], values[1]); + return true; + } + + if (Package.MaterialConstants.TryGetValue(id, out var constant)) + { + value = new Vector2(constant[0], constant[1]); + return true; + } + + value = Vector2.Zero; + return false; + } + + public bool TryGetConstant(MaterialConstant id, out Vector3 value) + { + if (MaterialConstantDict.TryGetValue(id, out var values)) + { + value = new Vector3(values[0], values[1], values[2]); + return true; + } + + if (Package.MaterialConstants.TryGetValue(id, out var constant)) + { + value = new Vector3(constant[0], constant[1], constant[2]); + return true; + } + + value = Vector3.Zero; + return false; + } + + public bool TryGetConstant(MaterialConstant id, out Vector4 value) + { + if (MaterialConstantDict.TryGetValue(id, out var values)) + { + value = new Vector4(values[0], values[1], values[2], values[3]); + return true; + } + + if (Package.MaterialConstants.TryGetValue(id, out var constant)) + { + value = new Vector4(constant[0], constant[1], constant[2], constant[3]); + return true; + } + + value = Vector4.Zero; + return false; + } + + public float GetConstantOrDefault(MaterialConstant id, float @default) + { + return MaterialConstantDict.TryGetValue(id, out var values) ? values[0] : @default; + } + + public Vector2 GetConstantOrDefault(MaterialConstant id, Vector2 @default) + { + return MaterialConstantDict.TryGetValue(id, out var values) ? new Vector2(values[0], values[1]) : @default; + } + + public Vector3 GetConstantOrDefault(MaterialConstant id, Vector3 @default) + { + return MaterialConstantDict.TryGetValue(id, out var values) + ? new Vector3(values[0], values[1], values[2]) + : @default; + } + + public Vector4 GetConstantOrDefault(MaterialConstant id, Vector4 @default) + { + return MaterialConstantDict.TryGetValue(id, out var values) + ? new Vector4(values[0], values[1], values[2], values[3]) + : @default; + } + + public MaterialSet(MtrlFile file, string mtrlPath, ShpkFile shpk, string shpkName) + { + this.MtrlPath = mtrlPath; + this.File = file; + this.Shpk = shpk; + this.ShpkName = shpkName; + this.Package = new ShaderPackage(shpk, shpkName); + + ShaderKeys = new ShaderKey[file.ShaderKeys.Length]; + for (var i = 0; i < file.ShaderKeys.Length; i++) + { + ShaderKeys[i] = new ShaderKey + { + Category = file.ShaderKeys[i].Category, + Value = file.ShaderKeys[i].Value + }; + } + + MaterialConstantDict = new Dictionary(); + foreach (var constant in file.Constants) + { + var index = constant.ValueOffset / 4; + var count = constant.ValueSize / 4; + var buf = new List(128); + for (var j = 0; j < count; j++) + { + var value = file.ShaderValues[index + j]; + var bytes = BitConverter.GetBytes(value); + buf.AddRange(bytes); + } + + var floats = MemoryMarshal.Cast(buf.ToArray()); + var values = new float[count]; + for (var j = 0; j < count; j++) + { + values[j] = floats[j]; + } + + // even if duplicate, last probably takes precedence + var id = (MaterialConstant)constant.ConstantId; + MaterialConstantDict[id] = values; + } + + TextureUsageDict = new Dictionary(); + var texturePaths = file.GetTexturePaths(); + foreach (var sampler in file.Samplers) + { + if (sampler.TextureIndex == byte.MaxValue) continue; + var textureInfo = file.TextureOffsets[sampler.TextureIndex]; + var texturePath = texturePaths[textureInfo.Offset]; + if (!Package.TextureLookup.TryGetValue(sampler.SamplerId, out var usage)) + { + continue; + } + + TextureUsageDict[usage] = texturePath; + } + } + + + + private static JsonSerializerOptions JsonOptions => new() + { + IncludeFields = true + }; + public JsonNode ComposeExtrasNode() + { + var extrasDict = ComposeExtras(); + return JsonNode.Parse(JsonSerializer.Serialize(extrasDict, JsonOptions))!; + } + public Dictionary ComposeExtras() + { + var extrasDict = new Dictionary + { + {"ShaderPackage", ShpkName}, + {"Material", MtrlPath} + }; + AddConstants(); + AddSamplers(); + AddShaderKeys(); + + return extrasDict; + + void AddShaderKeys() + { + foreach (var key in ShaderKeys) + { + var category = key.Category; + var value = key.Value; + extrasDict[$"0x{category:X8}"] = $"0x{value:X8}"; + } + } + + void AddSamplers() + { + foreach (var (usage, path) in TextureUsageDict) + { + extrasDict[usage.ToString()] = path; + } + } + + void AddConstants() + { + foreach (var (constant, value) in GetConstants()) + { + if (Enum.IsDefined(typeof(MaterialConstant), constant)) + { + extrasDict[constant.ToString()] = value; + } + else + { + var key = $"0x{(uint)constant:X8}"; + extrasDict[key] = value; + } + } + } + } +} diff --git a/Meddle/Meddle.Plugin/Services/ParseService.cs b/Meddle/Meddle.Plugin/Services/ParseService.cs index bb5e289..1f9612e 100644 --- a/Meddle/Meddle.Plugin/Services/ParseService.cs +++ b/Meddle/Meddle.Plugin/Services/ParseService.cs @@ -257,7 +257,7 @@ public Task ParseFromPath(string mdlPath) TexCache[texturePath] = texFile; } - var texRes = Meddle.Utils.Export.Texture.GetResource(texFile); + var texRes = texFile.ToResource(); var texGroup = new TexResourceGroup(texturePath, texturePath, texRes); texGroups.Add(texGroup); } diff --git a/Meddle/Meddle.UI/Windows/SqPackWindow.cs b/Meddle/Meddle.UI/Windows/SqPackWindow.cs index a896713..30b2aed 100644 --- a/Meddle/Meddle.UI/Windows/SqPackWindow.cs +++ b/Meddle/Meddle.UI/Windows/SqPackWindow.cs @@ -556,7 +556,7 @@ private void DrawFileView(IndexHashTableEntry hash, SqPackFile file, SelectedFil { SelectedFileType.Texture => view ?? new TexView(new TexFile(file.RawData), imageHandler, path), SelectedFileType.Material => view ?? new MtrlView(new MtrlFile(file.RawData), sqPack, imageHandler), - SelectedFileType.Model => view ?? new MdlView(new MdlFile(file.RawData), path), + SelectedFileType.Model => view ?? new MdlView(new MdlFile(file.RawData), path, sqPack, imageHandler), SelectedFileType.Sklb => view ?? new SklbView(new SklbFile(file.RawData), config), SelectedFileType.Shpk => view ?? new ShpkView(new ShpkFile(file.RawData), path), SelectedFileType.Pbd => view ?? new PbdView(new PbdFile(file.RawData)), diff --git a/Meddle/Meddle.UI/Windows/Views/ExportView.cs b/Meddle/Meddle.UI/Windows/Views/ExportView.cs index bf02a8b..5333920 100644 --- a/Meddle/Meddle.UI/Windows/Views/ExportView.cs +++ b/Meddle/Meddle.UI/Windows/Views/ExportView.cs @@ -194,7 +194,7 @@ public void DrawFiles() ImGui.SeparatorText("Model Info"); if (!views.TryGetValue(path, out var mdlView)) { - views[path] = mdlView = new MdlView(mdlGroup.MdlFile, path); + views[path] = mdlView = new MdlView(mdlGroup.MdlFile, path, pack, imageHandler); } mdlView.Draw(); @@ -423,7 +423,7 @@ private void RunExport(Dictionary modelDict, Dictionary mtrlFiles = new(); public void Draw() { ImGui.Text($"Version: {mdlFile.FileHeader.Version}"); @@ -48,8 +51,26 @@ public void Draw() for (var i = 0; i < mdlFile.MaterialNameOffsets.Length; i++) { + ImGui.PushID(i); var material = materialNames[(int)mdlFile.MaterialNameOffsets[i]]; - ImGui.Text($"Material {i}: {material}"); + if (ImGui.CollapsingHeader($"Material {i}: {material}")) + { + if (!mtrlFiles.ContainsKey(material)) + { + if (!material.StartsWith("/")) + { + var data = sqPack.GetFile(material); + var mtrlFile = new MtrlFile(data!.Value.file.RawData); + mtrlFiles[material] = (mtrlFile, new MtrlView(mtrlFile, sqPack, imageHandler)); + } + else + { + continue; + } + } + mtrlFiles[material].Item2.Draw(); + } + ImGui.PopID(); } } diff --git a/Meddle/Meddle.UI/Windows/Views/MtrlView.cs b/Meddle/Meddle.UI/Windows/Views/MtrlView.cs index 11769a7..e56fa9e 100644 --- a/Meddle/Meddle.UI/Windows/Views/MtrlView.cs +++ b/Meddle/Meddle.UI/Windows/Views/MtrlView.cs @@ -1,4 +1,5 @@ using System.Numerics; +using System.Runtime.InteropServices; using ImGuiNET; using Meddle.UI.Models; using Meddle.Utils.Export; @@ -135,23 +136,62 @@ public void Draw() if (ImGui.CollapsingHeader("Shader Values")) { ImGui.Text($"Shader Keys [{file.ShaderKeys.Length}]"); + ImGui.BeginTable("ShaderKeys", 4, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable); + ImGui.TableSetupColumn("Index"); + ImGui.TableSetupColumn("Name"); + ImGui.TableSetupColumn("Category"); + ImGui.TableSetupColumn("Value"); + ImGui.TableHeadersRow(); + for (var i = 0; i < file.ShaderKeys.Length; i++) { var key = file.ShaderKeys[i]; - + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.Text($"{i}"); + ImGui.TableSetColumnIndex(1); if (Enum.IsDefined((ShaderCategory)key.Category)) { - ImGui.Text($"[{i}][{key.Category:X4}] {((ShaderCategory)key.Category).ToString()} {key.Value:X4}"); + ImGui.Text($"{((ShaderCategory)key.Category).ToString()}"); } - else + ImGui.TableSetColumnIndex(2); + ImGui.Text($"{key.Category:X4}"); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.Text($"0x{key.Category:X4}"); + ImGui.EndTooltip(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + ImGui.SetClipboardText($"0x{key.Category:X4}"); + } + ImGui.TableSetColumnIndex(3); + ImGui.Text($"0x{key.Value:X4}"); + + if (ImGui.IsItemHovered()) { - ImGui.Text($"[{i}][{key.Category:X4}] {key.Value:X4}"); + ImGui.BeginTooltip(); + ImGui.Text($"0x{key.Value:X4}"); + ImGui.EndTooltip(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + ImGui.SetClipboardText($"0x{key.Value:X4}"); } } - + ImGui.EndTable(); + ImGui.Text($"Constants [{file.Constants.Length}]"); + ImGui.BeginTable("Constants", 4, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable); + ImGui.TableSetupColumn("Index"); + ImGui.TableSetupColumn("Name"); + ImGui.TableSetupColumn("Key"); + ImGui.TableSetupColumn("Value"); + ImGui.TableHeadersRow(); for (var i = 0; i < file.Constants.Length; i++) { + ImGui.TableNextRow(); var constant = file.Constants[i]; var index = constant.ValueOffset / 4; var count = constant.ValueSize / 4; @@ -162,42 +202,76 @@ public void Draw() var bytes = BitConverter.GetBytes(value); buf.AddRange(bytes); } + var floatBuf = MemoryMarshal.Cast(buf.ToArray()).ToArray(); - //ImGui.Text( - // $"[{i}][{constant.ConstantId:X4}|{constant.ConstantId}] off:{constant.ValueOffset:X2} size:{constant.ValueSize:X2} [{BitConverter.ToString(buf.ToArray())}]"); + ImGui.TableSetColumnIndex(0); + ImGui.Text($"{i}"); + ImGui.TableSetColumnIndex(1); if (Enum.IsDefined((MaterialConstant)constant.ConstantId)) { - ImGui.Text( - $"[{i}][{constant.ConstantId:X4}|{constant.ConstantId}] {((MaterialConstant)constant.ConstantId).ToString()} [{BitConverter.ToString(buf.ToArray())}]"); + ImGui.Text($"{((MaterialConstant)constant.ConstantId).ToString()}"); } - else + ImGui.TableSetColumnIndex(2); + ImGui.Text($"{constant.ConstantId:X4}"); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.Text($"0x{constant.ConstantId:X4}"); + ImGui.EndTooltip(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { - ImGui.Text( - $"[{i}][{constant.ConstantId:X4}|{constant.ConstantId}] Unknown [{BitConverter.ToString(buf.ToArray())}]"); + ImGui.SetClipboardText($"0x{constant.ConstantId:X4}"); } + ImGui.TableSetColumnIndex(3); + ImGui.Text($"{string.Join(", ", floatBuf)}"); } + ImGui.EndTable(); ImGui.Text($"Samplers [{file.Samplers.Length}]"); + ImGui.BeginTable("Samplers", 4, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable); + ImGui.TableSetupColumn("Index"); + ImGui.TableSetupColumn("Name"); + ImGui.TableSetupColumn("Id"); + ImGui.TableSetupColumn("Flags"); + ImGui.TableHeadersRow(); for (var i = 0; i < file.Samplers.Length; i++) { + ImGui.TableNextRow(); var sampler = file.Samplers[i]; + ImGui.TableSetColumnIndex(0); + ImGui.Text($"{i}"); + ImGui.TableSetColumnIndex(1); if (Enum.IsDefined((TextureUsage)sampler.SamplerId)) { - ImGui.Text( - $"[{i}][{sampler.SamplerId:X4}] {((TextureUsage)sampler.SamplerId).ToString()} {sampler.Flags:X4}"); + ImGui.Text($"{((TextureUsage)sampler.SamplerId).ToString()}"); } - else + ImGui.TableSetColumnIndex(2); + ImGui.Text($"{sampler.SamplerId:X4}"); + if (ImGui.IsItemHovered()) { - ImGui.Text($"[{i}][{sampler.SamplerId:X4}] Unknown {sampler.Flags:X4}"); + ImGui.BeginTooltip(); + ImGui.Text($"0x{sampler.SamplerId:X4}"); + ImGui.EndTooltip(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + ImGui.SetClipboardText($"0x{sampler.SamplerId:X4}"); + } + ImGui.TableSetColumnIndex(3); + ImGui.Text($"0x{sampler.Flags:X4}"); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.Text($"0x{sampler.Flags:X4}"); + ImGui.EndTooltip(); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + ImGui.SetClipboardText($"0x{sampler.Flags:X4}"); } } - - ImGui.Text($"Shader Values [{file.ShaderValues.Length}]"); - for (var i = 0; i < file.ShaderValues.Length; i++) - { - var value = file.ShaderValues[i]; - ImGui.Text($"[{i}]{value:X8}"); - } + ImGui.EndTable(); } if (ImGui.CollapsingHeader("Color Table")) diff --git a/Meddle/Meddle.UI/Windows/Views/ShpkView.cs b/Meddle/Meddle.UI/Windows/Views/ShpkView.cs index 1f56fe9..b6a8c33 100644 --- a/Meddle/Meddle.UI/Windows/Views/ShpkView.cs +++ b/Meddle/Meddle.UI/Windows/Views/ShpkView.cs @@ -324,15 +324,52 @@ public void Draw() if (ImGui.CollapsingHeader("Material Params")) { - if (ImGui.BeginTable("MaterialParams", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable | ImGuiTableFlags.SizingFixedFit)) + if (ImGui.Button("Copy")) { - ImGui.TableSetupColumn("Idx", ImGuiTableColumnFlags.WidthFixed); + var sb = new StringBuilder(); + foreach (var materialParam in file.MaterialParams) + { + var idString = Enum.IsDefined((MaterialConstant)materialParam.Id) ? $"{(MaterialConstant)materialParam.Id}" : $"0x{materialParam.Id:X8}"; + var defaults = file.MaterialParamDefaults + .Skip(materialParam.ByteOffset / 4).Take(materialParam.ByteSize / 4) + .ToArray(); + var msg = $"[{string.Join(", ", defaults.Select(x => $"{x}"))}]"; + sb.AppendLine($"{idString} = {msg}"); + } + ImGui.SetClipboardText(sb.ToString()); + } + + if (ImGui.BeginTable("MaterialParams", 5, ImGuiTableFlags.Sortable | ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable | ImGuiTableFlags.SizingFixedFit)) + { + ImGui.TableSetupColumn("Idx", ImGuiTableColumnFlags.WidthFixed, 0.0f, 0); ImGui.TableSetupColumn("Id", ImGuiTableColumnFlags.WidthFixed); ImGui.TableSetupColumn("ByteOffset", ImGuiTableColumnFlags.WidthFixed); ImGui.TableSetupColumn("ByteSize", ImGuiTableColumnFlags.WidthFixed); ImGui.TableSetupColumn("Defaults", ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); - var orderedMaterialParams = file.MaterialParams.Select((x, idx) => (x, idx)).OrderBy(x => x.x.ByteOffset).ToArray(); + + var orderedMaterialParams = file.MaterialParams.Select((x, idx) => (x, idx)).ToArray(); + var sortSpecs = ImGui.TableGetSortSpecs(); + var spec = sortSpecs.Specs; + var column = spec.ColumnIndex; + var descending = spec.SortDirection == ImGuiSortDirection.Descending; + orderedMaterialParams = column switch + { + 0 => descending + ? orderedMaterialParams.OrderByDescending(x => x.idx).ToArray() + : orderedMaterialParams.OrderBy(x => x.idx).ToArray(), + 1 => descending + ? orderedMaterialParams.OrderByDescending(x => x.x.Id).ToArray() + : orderedMaterialParams.OrderBy(x => x.x.Id).ToArray(), + 2 => descending + ? orderedMaterialParams.OrderByDescending(x => x.x.ByteOffset).ToArray() + : orderedMaterialParams.OrderBy(x => x.x.ByteOffset).ToArray(), + 3 => descending + ? orderedMaterialParams.OrderByDescending(x => x.x.ByteSize).ToArray() + : orderedMaterialParams.OrderBy(x => x.x.ByteSize).ToArray(), + _ => orderedMaterialParams + }; + foreach (var (materialParam, i) in orderedMaterialParams) { // get defaults from byteoffset -> byteoffset + bytesize diff --git a/Meddle/Meddle.UI/Windows/Views/TeraView.cs b/Meddle/Meddle.UI/Windows/Views/TeraView.cs index ad6deda..4a67067 100644 --- a/Meddle/Meddle.UI/Windows/Views/TeraView.cs +++ b/Meddle/Meddle.UI/Windows/Views/TeraView.cs @@ -23,6 +23,7 @@ public class TeraView : IView private readonly PathManager pathManager; private readonly List<(string Path, LgbFile.Group.InstanceObject ObjectInfo)> bgObjects = new(); private readonly HexView hexView; + private readonly List<(string Path, Vector2 Position, MdlFile file, MdlView view)> mdlFiles = new(); public TeraView( TeraFile teraFile, string? handlePath, SqPack sqPack, Configuration config, ImageHandler imageHandler, @@ -57,6 +58,19 @@ public TeraView( } } } + var mdlRoot = handlePath.Replace("/terrain.tera", ""); + for (int i = 0; i < teraFile.Header.PlateCount; i++) + { + var pos = teraFile.GetPlatePosition(i); + var mdlPath = $"{mdlRoot}/{i:D4}.mdl"; + var mdlData = sqPack.GetFile(mdlPath); + if (mdlData != null) + { + var mdlFile = new MdlFile(mdlData.Value.file.RawData); + var view = new MdlView(mdlFile, mdlPath, sqPack, this.imageHandler); + mdlFiles.Add((mdlPath, pos, mdlFile, view)); + } + } } } @@ -214,6 +228,18 @@ private void RunExport() public void Draw() { + foreach (var (mdlPath, pos, mdlFile, view) in mdlFiles) + { + ImGui.PushID(mdlPath); + if (ImGui.TreeNode(mdlPath)) + { + ImGui.Text($"Position: {pos}"); + view.Draw(); + ImGui.TreePop(); + } + ImGui.PopID(); + } + hexView.DrawHexDump(); ImGui.BeginDisabled(!exportTask.IsCompleted); diff --git a/Meddle/Meddle.Utils/Export/Material.cs b/Meddle/Meddle.Utils/Export/Material.cs index 5d689c1..a4aa946 100644 --- a/Meddle/Meddle.Utils/Export/Material.cs +++ b/Meddle/Meddle.Utils/Export/Material.cs @@ -95,7 +95,74 @@ public enum MaterialConstant : uint g_Color = 0xD27C58B9, g_ShadowAlphaThreshold = 0xD925FF32, g_NearClip = 0x17A52926, - g_AngleClip = 0x71DBDA81 + g_AngleClip = 0x71DBDA81, + g_CausticsReflectionPowerBright = 0x0CC09E67, // 213950055 + g_CausticsReflectionPowerDark = 0xC295EA6C, // 3264604780 + g_HeightMapScale = 0xA320B199, // 2736828825 + g_HeightMapUVScale = 0x5B99505D, // 1536774237 + g_MultiWaveScale = 0x37363FDD, // 926302173 + g_WaveSpeed = 0xE4C68FF3, // 3838218227 + g_WaveTime = 0x8EB9D2A6, // 2394542758 + g_AlphaMultiParam = 0x07EDA444, // 133014596 + g_AmbientOcclusionMask = 0x575ABFB2, // 1465565106 + g_ColorUVScale = 0xA5D02C52, // 2781883474 + g_DetailColorUvScale = 0xC63D9716, // 3325925142 + g_DetailID = 0x8981D4D9, // 2306987225 + g_DetailNormalScale = 0x9F42EDA2, // 2671963554 + g_EnvMapPower = 0xEEF5665F, // 4009059935 + g_FresnelValue0 = 0x62E44A4F, // 1659128399 + g_HeightScale = 0x8F8B0070, // 2408251504 + g_InclusionAperture = 0xBCA22FD4, // 3164745684 + g_IrisRingForceColor = 0x58DE06E2, // 1490945762 + g_LayerDepth = 0xA9295FEF, // 2838061039 + g_LayerIrregularity = 0x0A00B0A1, // 167817377 + g_LayerScale = 0xBFCC6602, // 3217843714 + g_LayerVelocity = 0x72181E22, // 1914183202 + g_LipFresnelValue0 = 0x174BB64E, // 390837838 + g_LipShininess = 0x878B272C, // 2274043692 + g_MultiDetailColor = 0x11FD4221, // 301810209 + g_MultiDiffuseColor = 0x3F8AC211, // 1066058257 + g_MultiEmissiveColor = 0xAA676D0F, // 2858904847 + g_MultiHeightScale = 0x43E59A68, // 1139120744 + g_MultiNormalScale = 0x793AC5A3, // 2033894819 + g_MultiSpecularColor = 0x86D60CB8, // 2262174904 + g_MultiSSAOMask = 0x926E860D, // 2456716813 + g_MultiWhitecapDistortion = 0x93504F3B, // 2471513915 + g_MultiWhitecapScale = 0x312B69C1, // 824928705 + g_NormalScale1 = 0x0DD83E61, // 232275553 + g_NormalUVScale = 0xBB99CF76, // 3147419510 + g_PrefersFailure = 0x5394405B, // 1402224731 + g_ReflectionPower = 0x223A3329, // 574239529 + g_ScatteringLevel = 0xB500BB24, // 3036724004 + g_ShadowOffset = 0x96D2B53D, // 2530391357 + g_ShadowPosOffset = 0x5351646E, // 1397843054 + g_SpecularMask = 0x36080AD0, // 906496720 + g_SpecularPower = 0xD9CB6B9C, // 3653987228 + g_SpecularUVScale = 0x8D03A782, // 2365826946 + g_ToonIndex = 0xDF15112D, // 3742699821 + g_ToonLightScale = 0x3CCE9E4C, // 1020173900 + g_ToonReflectionScale = 0xD96FAF7A, // 3647975290 + g_ToonSpecIndex = 0x00A680BC, // 10911932 + g_TransparencyDistance = 0x1624F841, // 371521601 + g_WaveletDistortion = 0x3439B378, // 876196728 + g_WaveletNoiseParam = 0x1279815C, // 309952860 + g_WaveletOffset = 0x9BE8354A, // 2615686474 + g_WaveletScale = 0xD62C681E, // 3593234462 + g_WaveTime1 = 0x6EE5BF35, // 1860550453 + g_WhitecapDistance = 0x5D26B262, // 1562817122 + g_WhitecapDistortion = 0x61053025, // 1627729957 + g_WhitecapNoiseScale = 0x0FF95B0C, // 268000012 + g_WhitecapScale = 0xA3EA47AC, // 2750039980 + g_WhitecapSpeed = 0x408A9CDE, // 1082825950 + g_Fresnel = 0xE3AA427A, // 3819586170 + g_Gradation = 0x94B40EEE, // 2494828270 + g_Intensity = 0xBCBA70E1, // 3166335201 + g_Shininess = 0x992869AB, // 2569562539 + g_DetailColor = 0xDD93D839, // 3717453881 + g_LayerColor = 0x35DC0B6F, // 903613295 + g_RefractionColor = 0xBA163700, // 3122018048 + g_WhitecapColor = 0x29FA2AC1, // 704260801 + g_DetailNormalUvScale = 0x025A9BEE, // 39492590 } public class Material @@ -111,7 +178,7 @@ public Material(string path, MtrlFile file, Dictionary texFiles { HandlePath = path; InitFromFile(file); - InitTextures(file, texFiles.ToDictionary(x => x.Key, x => Texture.GetResource(x.Value)), shpkFile); + InitTextures(file, texFiles.ToDictionary(x => x.Key, x => x.Value.ToResource()), shpkFile); } private void InitTextures(MtrlFile file, Dictionary texFiles, ShpkFile shpkFile) diff --git a/Meddle/Meddle.Utils/Export/Texture.cs b/Meddle/Meddle.Utils/Export/Texture.cs index 503a8d7..a440ae1 100644 --- a/Meddle/Meddle.Utils/Export/Texture.cs +++ b/Meddle/Meddle.Utils/Export/Texture.cs @@ -66,22 +66,4 @@ public Texture(TextureResource resource, string path, uint? samplerFlags, uint? Usage = usage; } } - - - public static TextureResource GetResource(TexFile file) - { - var h = file.Header; - D3DResourceMiscFlags flags = 0; - if (h.Type.HasFlag(TexFile.Attribute.TextureTypeCube)) - flags |= D3DResourceMiscFlags.TextureCube; - return new TextureResource( - TexFile.GetDxgiFormatFromTextureFormat(h.Format), - h.Width, - h.Height, - h.CalculatedMips, - h.CalculatedArraySize, - TexFile.GetTexDimensionFromAttribute(h.Type), - flags, - file.TextureBuffer); - } } diff --git a/Meddle/Meddle.Utils/Export/TextureResource.cs b/Meddle/Meddle.Utils/Export/TextureResource.cs index 892a324..8b798c3 100644 --- a/Meddle/Meddle.Utils/Export/TextureResource.cs +++ b/Meddle/Meddle.Utils/Export/TextureResource.cs @@ -1,4 +1,5 @@ -using OtterTex; +using System.Numerics; +using OtterTex; namespace Meddle.Utils.Export; @@ -7,6 +8,8 @@ public readonly struct TextureResource(DXGIFormat format, int width, int height, public DXGIFormat Format { get; init; } = format; public int Width { get; init; } = width; public int Height { get; init; } = height; + public Vector2 Size => new(Width, Height); + public int MipLevels { get; init; } = mipLevels; public int ArraySize { get; init; } = arraySize; public TexDimension Dimension { get; init; } = dimension; diff --git a/Meddle/Meddle.Utils/ImageUtils.cs b/Meddle/Meddle.Utils/ImageUtils.cs index 13977d2..0921ec3 100644 --- a/Meddle/Meddle.Utils/ImageUtils.cs +++ b/Meddle/Meddle.Utils/ImageUtils.cs @@ -22,6 +22,23 @@ public static int GetStride(this TexFile.TextureFormat format, int width) }; } + public static TextureResource ToResource(this TexFile file) + { + var h = file.Header; + D3DResourceMiscFlags flags = 0; + if (h.Type.HasFlag(TexFile.Attribute.TextureTypeCube)) + flags |= D3DResourceMiscFlags.TextureCube; + return new TextureResource( + TexFile.GetDxgiFormatFromTextureFormat(h.Format), + h.Width, + h.Height, + h.CalculatedMips, + h.CalculatedArraySize, + TexFile.GetTexDimensionFromAttribute(h.Type), + flags, + file.TextureBuffer); + } + public static ReadOnlySpan ImageAsPng(Image image) { unsafe diff --git a/Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs b/Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs index ed344cc..0c1467a 100644 --- a/Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs +++ b/Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs @@ -11,3 +11,8 @@ public XivMaterialBuilder(string name, string shpk) : base(name) this.Shpk = shpk; } } + +public interface IVertexPaintMaterialBuilder +{ + public bool VertexPaint { get; } +} diff --git a/Meddle/Meddle.Utils/MeshBuilder.cs b/Meddle/Meddle.Utils/MeshBuilder.cs index 4f8f200..c8daf5b 100644 --- a/Meddle/Meddle.Utils/MeshBuilder.cs +++ b/Meddle/Meddle.Utils/MeshBuilder.cs @@ -232,13 +232,12 @@ private IVertexBuilder BuildVertex(Vertex vertex) if (MaterialT != typeof(VertexTexture2)) { Vector4 vertexColor = new Vector4(1, 1, 1, 1); - if (MaterialBuilder is XivMaterialBuilder xivMaterialBuilder) + if (MaterialBuilder is IVertexPaintMaterialBuilder paintBuilder) { - vertexColor = xivMaterialBuilder.Shpk switch + vertexColor = paintBuilder.VertexPaint switch { - "bg.shpk" => vertex.Color!.Value, - "bgprop.shpk" => vertex.Color!.Value, - _ => new Vector4(1, 1, 1, 1) + true => vertex.Color!.Value, + false => new Vector4(1, 1, 1, 1) }; } From 1ce93f32c5fe20537c0af8a1e4db556dcf8557b6 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Mon, 2 Sep 2024 23:28:16 +1000 Subject: [PATCH 03/44] BgColorChange & various other material things - --- .../Models/Composer/BgMaterialBuilder.cs | 361 ++++++++++++------ .../Models/Composer/GenericMaterialBuilder.cs | 53 +++ .../Models/Composer/InstanceComposer.cs | 196 ++++------ .../Composer/InstanceMaterialBuilder.cs | 5 +- .../Composer/LightshaftMaterialBuilder.cs | 15 +- .../Models/Composer/MaterialSet.cs | 2 +- .../Models/Layout/ParsedInstance.cs | 12 +- .../Meddle.Plugin/Services/LayoutService.cs | 20 +- Meddle/Meddle.Plugin/UI/Layout/Config.cs | 11 + .../Meddle.Plugin/UI/Layout/LayoutWindow.cs | 9 +- Meddle/Meddle.Utils/ImageUtils.cs | 4 +- .../Materials/XIVMaterialBuilder.cs | 8 +- Meddle/Meddle.Utils/ModelBuilder.cs | 2 +- Meddle/Meddle.Utils/Models/SKTexture.cs | 1 + 14 files changed, 444 insertions(+), 255 deletions(-) create mode 100644 Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs diff --git a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs index 981d171..11ecf02 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs @@ -1,4 +1,6 @@ using System.Numerics; +using System.Text.Json; +using System.Text.Json.Nodes; using Meddle.Utils; using Meddle.Utils.Export; using Meddle.Utils.Files; @@ -16,26 +18,111 @@ public class BgMaterialBuilder : InstanceMaterialBuilder, IVertexPaintMaterialBu private const uint BGVertexPaintValue = 0xBD94649A; private const uint DiffuseAlphaKey = 0xA9A3EE25; private const uint DiffuseAlphaValue = 0x72AAA9AE; // if present, alpha channel on diffuse texture is used?? and should respect g_AlphaThreshold - - public BgMaterialBuilder(string name, MaterialSet set, Func lookupFunc, Func cacheFunc) : base(name, "bg.shpk", lookupFunc, cacheFunc) + private readonly string shpkSuffix; + public BgMaterialBuilder(string name, string shpkName, MaterialSet set, Func lookupFunc, Func cacheFunc) : base(name, shpkName, lookupFunc, cacheFunc) { this.set = set; + shpkSuffix = Path.GetExtension(shpkName); } + private record TextureSet(SKTexture Color0, SKTexture Specular0, SKTexture Normal0, SKTexture? Color1, SKTexture? Specular1, SKTexture? Normal1); - public BgMaterialBuilder WithBg() + // DetailID = + // bgcommon/nature/detail/texture/detail_d_array.tex + // bgcommon/nature/detail/texture/detail_n_array.tex + private static TexFile? DetailDArray; + private static TexFile? DetailNArray; + private static readonly object DetailLock = new(); + private record DetailSet(SKTexture Diffuse, SKTexture Normal, Vector3 DetailColor, float DetailNormalScale, Vector4 DetailColorUvScale, Vector4 DetailNormalUvScale); + + private DetailSet GetDetail(int detailId, Vector2 size) + { + lock (DetailLock) + { + const int maxDetailId = 32; + if (detailId < 0 || detailId >= maxDetailId) + { + throw new ArgumentOutOfRangeException(nameof(detailId), + $"Detail ID must be between 0 and {maxDetailId - 1}"); + } + + if (DetailDArray == null) + { + var detailDArray = LookupFunc("bgcommon/nature/detail/texture/detail_d_array.tex"); + if (detailDArray == null) + { + throw new Exception("Detail D array texture not found"); + } + + DetailDArray = new TexFile(detailDArray); + } + + if (DetailNArray == null) + { + var detailNArray = LookupFunc("bgcommon/nature/detail/texture/detail_n_array.tex"); + if (detailNArray == null) + { + throw new Exception("Detail N array texture not found"); + } + + DetailNArray = new TexFile(detailNArray); + } + + var detailD = ImageUtils.GetTexData(DetailDArray, detailId, 0, 0).ToTexture(size); + var detailN = ImageUtils.GetTexData(DetailNArray, detailId, 0, 0).ToTexture(size); + + if (!set.TryGetConstant(MaterialConstant.g_DetailColor, out Vector3 detailColor)) + { + detailColor = Vector3.One; + } + + if (!set.TryGetConstant(MaterialConstant.g_DetailNormalScale, out float detailNormalScale)) + { + detailNormalScale = 1; + } + + if (!set.TryGetConstant(MaterialConstant.g_DetailColorUvScale, out Vector4 detailColorUvScale)) + { + detailColorUvScale = new Vector4(4); + } + + if (!set.TryGetConstant(MaterialConstant.g_DetailNormalUvScale, out Vector4 detailNormalUvScale)) + { + detailNormalUvScale = new Vector4(4); + } + + return new DetailSet(detailD, detailN, detailColor, detailNormalScale, detailColorUvScale, detailNormalUvScale); + } + } + + private TextureResource? GetTextureResourceOrNull(TextureUsage usage, ref List sizes) + { + if (set.TextureUsageDict.TryGetValue(usage, out var texture)) + { + if (texture.Contains("_dummy")) + { + return null; + } + var textureResource = LookupFunc(texture); + if (textureResource != null) + { + var resource = new TexFile(textureResource).ToResource(); + sizes.Add(resource.Size); + return resource; + } + } + + return null; + } + + private TextureSet GetTextureSet() { - // samplers var colorMap0 = set.TextureUsageDict[TextureUsage.g_SamplerColorMap0]; var specularMap0 = set.TextureUsageDict[TextureUsage.g_SamplerSpecularMap0]; var normalMap0 = set.TextureUsageDict[TextureUsage.g_SamplerNormalMap0]; - var colorMap0Texture = LookupFunc(colorMap0); - var specularMap0Texture = LookupFunc(specularMap0); - var normalMap0Texture = LookupFunc(normalMap0); - if (colorMap0Texture == null || specularMap0Texture == null || normalMap0Texture == null) - { - return this; - } + var colorMap0Texture = LookupFunc(colorMap0) ?? throw new Exception("ColorMap0 texture not found"); + var specularMap0Texture = LookupFunc(specularMap0) ?? throw new Exception("SpecularMap0 texture not found"); + var normalMap0Texture = LookupFunc(normalMap0) ?? throw new Exception("NormalMap0 texture not found"); var colorRes0 = new TexFile(colorMap0Texture).ToResource(); var specularRes0 = new TexFile(specularMap0Texture).ToResource(); @@ -47,38 +134,9 @@ public BgMaterialBuilder WithBg() normalRes0.Size }; - TextureResource? colorRes1 = null; - if (set.TextureUsageDict.TryGetValue(TextureUsage.g_SamplerColorMap1, out var colorMap1)) - { - var colorMap1Texture = LookupFunc(colorMap1); - if (colorMap1Texture != null) - { - colorRes1 = new TexFile(colorMap1Texture).ToResource(); - sizes.Add(colorRes1.Value.Size); - } - } - - TextureResource? specularRes1 = null; - if (set.TextureUsageDict.TryGetValue(TextureUsage.g_SamplerSpecularMap1, out var specularMap1)) - { - var specularMap1Texture = LookupFunc(specularMap1); - if (specularMap1Texture != null) - { - specularRes1 = new TexFile(specularMap1Texture).ToResource(); - sizes.Add(specularRes1.Value.Size); - } - } - - TextureResource? normalRes1 = null; - if (set.TextureUsageDict.TryGetValue(TextureUsage.g_SamplerNormalMap1, out var normalMap1)) - { - var normalMap1Texture = LookupFunc(normalMap1); - if (normalMap1Texture != null) - { - normalRes1 = new TexFile(normalMap1Texture).ToResource(); - sizes.Add(normalRes1.Value.Size); - } - } + var colorRes1 = GetTextureResourceOrNull(TextureUsage.g_SamplerColorMap1, ref sizes); + var specularRes1 = GetTextureResourceOrNull(TextureUsage.g_SamplerSpecularMap1, ref sizes); + var normalRes1 = GetTextureResourceOrNull(TextureUsage.g_SamplerNormalMap1, ref sizes); var size = Max(sizes); var colorTex0 = colorRes0.ToTexture(size); @@ -88,104 +146,179 @@ public BgMaterialBuilder WithBg() var specularTex1 = specularRes1?.ToTexture(size); var normalTex1 = normalRes1?.ToTexture(size); + return new TextureSet(colorTex0, specularTex0, normalTex0, colorTex1, specularTex1, normalTex1); + } + + + public BgMaterialBuilder WithBgColorChange(Vector4? stainColor) + { + Apply(stainColor); + return this; + } + + private void SaveAllTextures() + { + foreach (var (usage, path) in set.TextureUsageDict) + { + var texture = LookupFunc(path); + if (texture == null) + { + continue; + } + var tex = new TexFile(texture).ToResource().ToTexture(); + CacheFunc(tex, $"Debug/{Path.GetFileNameWithoutExtension(path)}"); + } + } + + + public BgMaterialBuilder WithBg() + { + Apply(); + return this; + } + + public bool VertexPaint { get; private set; } + public Vector4 TangentMultiplier { get; private set; } + + private Vector2 Max(IEnumerable vectors) + { + var max = new Vector2(float.MinValue); + foreach (var vector in vectors) + { + max = Vector2.Max(max, vector); + } + + return max; + } + + private void Apply(Vector4? stainColor = null) + { + var textureSet = GetTextureSet(); var useAlpha = set.ShaderKeys.Any(x => x is {Category: DiffuseAlphaKey, Value: DiffuseAlphaValue}); if (useAlpha && set.TryGetConstant(MaterialConstant.g_AlphaThreshold, out float alphaThreshold)) { WithAlpha(AlphaMode.MASK, alphaThreshold); } - Vector3 tmp; - Vector3 diffuseColor = Vector3.One; - if (set.TryGetConstant(MaterialConstant.g_DiffuseColor, out tmp)) + if (!set.TryGetConstant(MaterialConstant.g_DiffuseColor, out Vector3 diffuseColor)) { - diffuseColor = tmp; + diffuseColor = Vector3.One; } - Vector3 specularColor = Vector3.One; - if (set.TryGetConstant(MaterialConstant.g_SpecularColor, out tmp)) + + // if (!set.TryGetConstant(MaterialConstant.g_SpecularColor, out Vector3 specularColor)) + // { + // specularColor = Vector3.One; + // } + + // Vector3 emmissiveColor; + // if (!set.TryGetConstant(MaterialConstant.g_EmissiveColor, out emmissiveColor)) + // { + // emmissiveColor = Vector3.Zero; + // } + + Vector4 diffuseColor0; + if (stainColor != null && stainColor != Vector4.Zero) { - specularColor = tmp; + diffuseColor0 = stainColor.Value; } - Vector3 emissiveColor = Vector3.Zero; - if (set.TryGetConstant(MaterialConstant.g_EmissiveColor, out tmp)) + else { - emissiveColor = tmp; + diffuseColor0 = new Vector4(diffuseColor, 1); } - - var diffuseColor0 = new Vector4(diffuseColor, 1); - var specularColor0 = new Vector4(specularColor, 1); - SKTexture diffuse = new SKTexture((int)size.X, (int)size.Y); - //SKTexture specular = new SKTexture((int)size.X, (int)size.Y); - //SKTexture emmissive = new SKTexture((int)size.X, (int)size.Y); - SKTexture normal = new SKTexture((int)size.X, (int)size.Y); - for (int x = 0; x < diffuse.Width; x++) - for (int y = 0; y < diffuse.Height; y++) + + if (!set.TryGetConstant(MaterialConstant.g_DetailID, out float detailId)) { - var color0 = colorTex0[x, y].ToVector4(); - var specular0 = specularTex0[x, y].ToVector4(); - var normal0 = normalTex0[x, y].ToVector4(); - var color1 = colorTex1?[x, y].ToVector4() ?? Vector4.One; - var specular1 = specularTex1?[x, y].ToVector4() ?? Vector4.One; - var normal1 = normalTex1?[x, y].ToVector4() ?? Vector4.One; - - - var outDiffuse = (diffuseColor0 * color0 * color1).ToSkColor(); - - var specularData = (specularColor0 * specular0 * specular1); - var outNormal = (normal0 * normal1).ToSkColor(); - - diffuse[x, y] = outDiffuse; - //emmissive[x, y] = Vector4.Lerp(Vector4.Zero, new Vector4(emissiveColor, 1), specularData.X).ToSkColor(); - //specular[x, y] = specularData.ToSkColor(); - normal[x, y] = outNormal; + detailId = 0; } - - VertexPaint = set.ShaderKeys.Any(x => x is {Category: BGVertexPaintKey, Value: BGVertexPaintValue}); - Extras = set.ComposeExtrasNode(); - - if (set.TryGetConstant(MaterialConstant.g_NormalScale, out float normalScale)) + + if (!set.TryGetConstant(MaterialConstant.g_NormalScale, out float normalScale)) { - WithNormal(CacheFunc(normal, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_computed_bg_normal"), normalScale); + normalScale = 1; } - else + + if (!set.TryGetConstant(MaterialConstant.g_ColorUVScale, out Vector4 colorUvScale)) { - WithNormal(CacheFunc(normal, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_computed_bg_normal")); + colorUvScale = new Vector4(1); } - WithBaseColor(CacheFunc(diffuse, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_computed_bg_diffuse")); + //var detail = GetDetail((int)detailId, textureSet.Color0.Size); + //var detailColor0 = new Vector4(detail.DetailColor, 1); - // should not be applied uniformly. strength is probably dictated using one of the spec channels - // WithEmissive(CacheFunc(emmissive, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_emissive")); - - CacheFunc(colorTex0, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_color0"); - CacheFunc(specularTex0, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_specular0"); - CacheFunc(normalTex0, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_normal0"); - if (colorTex1 != null) - { - CacheFunc(colorTex1, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_color1"); - } - if (specularTex1 != null) + //var specularColor0 = new Vector4(specularColor, 1); + SKTexture diffuse = new SKTexture(textureSet.Color0.Width, textureSet.Color0.Height); + SKTexture normal = new SKTexture(textureSet.Color0.Width, textureSet.Color0.Height); + //for (int x = 0; x < diffuse.Width; x++) + //for (int y = 0; y < diffuse.Height; y++) + Parallel.For(0, diffuse.Width, x => { - CacheFunc(specularTex1, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_specular1"); - } - if (normalTex1 != null) + for (int y = 0; y < diffuse.Height; y++) + { + var color0 = textureSet.Color0[x, y].ToVector4(); + var normal0 = textureSet.Normal0[x, y].ToVector4(); + var color1 = textureSet.Color1?[x, y].ToVector4() ?? Vector4.One; + var normal1 = textureSet.Normal1?[x, y].ToVector4() ?? Vector4.One; + + // idk about this one tbh + //var specular0 = textureSet.Specular0[x, y].ToVector4(); + //var specular1 = textureSet.Specular1?[x, y].ToVector4() ?? Vector4.One; + //var specularData = (specularColor0 * specular0 * specular1); + + // var detailColorMask = detail.Diffuse[x, y].ToVector4(); + // var detailNormalMap = detail.Normal[x, y].ToVector4(); + + var outDiffuse = color0 * color1; + //var color = Vector4.Lerp(diffuseColor0, detailColor0, detailColorMask.Z); + // diffuse alpha is a mask for stain color + if (Shpk == "bgcolorchange.shpk") + { + outDiffuse = Vector4.Lerp(outDiffuse, diffuseColor0, outDiffuse.W); + outDiffuse.W = 1; + } + + // normal blue is weight for the detail normal map? + var outNormal = (normal0 * normal1 * normalScale); + //outNormal = Vector4.Lerp(outNormal, detailNormalMap * detail.DetailNormalScale, detailColorMask.Z); + // maybe? + outNormal *= outNormal.Z; + outNormal.W = 1; + + + diffuse[x, y] = outDiffuse.ToSkColor(); + //emmissive[x, y] = Vector4.Lerp(Vector4.Zero, new Vector4(emissiveColor, 1), specularData.X).ToSkColor(); + //specular[x, y] = specularData.ToSkColor(); + normal[x, y] = outNormal.ToSkColor(); + } + }); + + VertexPaint = set.ShaderKeys.Any(x => x is {Category: BGVertexPaintKey, Value: BGVertexPaintValue}); + Extras = set.ComposeExtrasNode(); + + var extrasDict = set.ComposeExtras(); + var stainString = ""; + if (Shpk == "bgcolorchange.shpk") { - CacheFunc(normalTex1, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_normal1"); + stainString = $"_{ToHex(diffuseColor0)}"; + extrasDict["stainColor"] = ToHex(diffuseColor0); } + Extras = JsonNode.Parse(JsonSerializer.Serialize(extrasDict, MaterialSet.JsonOptions))!; - return this; + WithNormal(CacheFunc(normal, $"Computed/{Path.GetFileNameWithoutExtension(set.MtrlPath)}_{shpkSuffix}_normal")); + WithBaseColor(CacheFunc(diffuse, $"Computed/{Path.GetFileNameWithoutExtension(set.MtrlPath)}_{shpkSuffix}{stainString}_diffuse")); + + // should not be applied uniformly. strength is probably dictated using one of the spec channels + // WithEmissive(CacheFunc(emmissive, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_emissive")); + //CacheFunc(detail.Diffuse, $"Debug/{Path.GetFileNameWithoutExtension(set.MtrlPath)}_{shpkSuffix}_detail{(int)detailId}_diffuse"); + //CacheFunc(detail.Normal, $"Debug/{Path.GetFileNameWithoutExtension(set.MtrlPath)}_{shpkSuffix}_detail{(int)detailId:D}_normal"); + SaveAllTextures(); } - - public bool VertexPaint { get; private set; } - private Vector2 Max(IEnumerable vectors) + private string ToHex(Vector4 color) { - var max = new Vector2(float.MinValue); - foreach (var vector in vectors) - { - max = Vector2.Max(max, vector); - } - - return max; + var r = (byte)(color.X * 255); + var g = (byte)(color.Y * 255); + var b = (byte)(color.Z * 255); + var a = (byte)(color.W * 255); + return $"{r:X2}{g:X2}{b:X2}{a:X2}"; } /* diff --git a/Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs new file mode 100644 index 0000000..f2d842f --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs @@ -0,0 +1,53 @@ +using Meddle.Utils; +using Meddle.Utils.Export; +using Meddle.Utils.Files; +using Meddle.Utils.Materials; +using Meddle.Utils.Models; +using SharpGLTF.Materials; + +namespace Meddle.Plugin.Models.Composer; + +public class GenericMaterialBuilder : InstanceMaterialBuilder +{ + private readonly MaterialSet set; + + public GenericMaterialBuilder(string name, MaterialSet set, Func lookupFunc, Func cacheFunc) : base(name, set.ShpkName, lookupFunc, cacheFunc) + { + this.set = set; + } + + public GenericMaterialBuilder WithGeneric() + { + var alphaThreshold = set.GetConstantOrDefault(MaterialConstant.g_AlphaThreshold, 0.0f); + if (alphaThreshold > 0) + WithAlpha(AlphaMode.MASK, alphaThreshold); + + var texturePaths = set.File.GetTexturePaths(); + var setTypes = new HashSet(); + foreach (var sampler in set.File.Samplers) + { + if (sampler.TextureIndex == byte.MaxValue) continue; + var textureInfo = set.File.TextureOffsets[sampler.TextureIndex]; + var texturePath = texturePaths[textureInfo.Offset]; + // bg textures can have additional textures, which may be dummy textures, ignore them + if (texturePath.Contains("dummy_")) continue; + if (!set.Package.TextureLookup.TryGetValue(sampler.SamplerId, out var usage)) + { + continue; + } + var texData = LookupFunc(texturePath); + if (texData == null) continue; + var texture = new TexFile(texData).ToResource().ToTexture(); + var tex = CacheFunc(texture, texturePath); + var channel = MaterialUtility.MapTextureUsageToChannel(usage); + if (channel != null && setTypes.Add(usage)) + { + WithChannelImage(channel.Value, tex); + } + } + + Extras = set.ComposeExtrasNode(); + + return this; + } +} diff --git a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs index 0d43a0b..5000907 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs @@ -1,8 +1,5 @@ using System.Collections.Concurrent; using System.Numerics; -using System.Runtime.InteropServices; -using System.Text.Json; -using System.Text.Json.Nodes; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Meddle.Plugin.Models.Layout; using Meddle.Plugin.Utils; @@ -20,10 +17,17 @@ namespace Meddle.Plugin.Models.Composer; -public class InstanceComposer +public class InstanceComposer : IDisposable { - public InstanceComposer(ILogger log, SqPack manager, Configuration config, ParsedInstance[] instances, string? cacheDir = null, - Action? progress = null, CancellationToken cancellationToken = default) + + + public InstanceComposer(ILogger log, SqPack manager, + Configuration config, + ParsedInstance[] instances, + string? cacheDir = null, + Action? progress = null, + bool bakeMaterials = true, + CancellationToken cancellationToken = default) { CacheDir = cacheDir ?? Path.GetTempPath(); Directory.CreateDirectory(CacheDir); @@ -32,6 +36,7 @@ public InstanceComposer(ILogger log, SqPack manager, Configuration config, Parse this.dataManager = manager; this.config = config; this.progress = progress; + this.bakeMaterials = bakeMaterials; this.cancellationToken = cancellationToken; this.count = instances.Length; } @@ -40,6 +45,7 @@ public InstanceComposer(ILogger log, SqPack manager, Configuration config, Parse private readonly SqPack dataManager; private readonly Configuration config; private readonly Action? progress; + private readonly bool bakeMaterials; private readonly CancellationToken cancellationToken; private readonly int count; private int countProgress; @@ -48,34 +54,30 @@ public InstanceComposer(ILogger log, SqPack manager, Configuration config, Parse private readonly ConcurrentDictionary imageCache = new(); private readonly ConcurrentDictionary shpkCache = new(); private readonly ConcurrentDictionary mtrlCache = new(); - - public void Compose(SceneBuilder scene) + + private void Iterate(Action action, bool parallel) { - progress?.Invoke(new ProgressEvent("Export", 0, count)); - Parallel.ForEach(instances, new ParallelOptions + if (parallel) { - CancellationToken = cancellationToken, - MaxDegreeOfParallelism = Math.Max(Environment.ProcessorCount / 2, 1) - }, instance => + Parallel.ForEach(instances, new ParallelOptions + { + CancellationToken = cancellationToken, + MaxDegreeOfParallelism = Math.Max(Environment.ProcessorCount / 2, 1) + }, action); + } + else { - try + foreach (var instance in instances) { - var node = ComposeInstance(scene, instance); - if (node != null) - { - scene.AddNode(node); - } + action(instance); } - catch (Exception ex) - { - log.LogError(ex, "Failed to compose instance {instanceId} {instanceType}", instance.Id, instance.Type); - } - - //countProgress++; - Interlocked.Increment(ref countProgress); - progress?.Invoke(new ProgressEvent("Export", countProgress, count)); - }); - /*foreach (var instance in instances) + } + } + + public void Compose(SceneBuilder scene) + { + progress?.Invoke(new ProgressEvent("Export", 0, count)); + Iterate(instance => { try { @@ -90,9 +92,10 @@ public void Compose(SceneBuilder scene) log.LogError(ex, "Failed to compose instance {instanceId} {instanceType}", instance.Id, instance.Type); } - countProgress++; + //countProgress++; + Interlocked.Increment(ref countProgress); progress?.Invoke(new ProgressEvent("Export", countProgress, count)); - }*/ + }, false); } public NodeBuilder? ComposeInstance(SceneBuilder scene, ParsedInstance parsedInstance) @@ -292,7 +295,7 @@ private void ComposeCharacterInstance(ParsedCharacterInstance characterInstance, MaterialUtility.BuildIris(material, name, cubeMapTex, customizeParams, customizeData), "water.shpk" => MaterialUtility.BuildWater(material, name), "lightshaft.shpk" => MaterialUtility.BuildLightShaft(material, name), - _ => ComposeMaterial(materialInfo.Path) + _ => ComposeMaterial(materialInfo.Path, characterInstance) }; materialBuilders.Add(builder); @@ -388,7 +391,7 @@ private void ComposeTerrainInstance(ParsedTerrainInstance terrainInstance, Scene var materialBuilders = new List(); foreach (var mtrlPath in materials) { - var materialBuilder = ComposeMaterial(mtrlPath); + var materialBuilder = ComposeMaterial(mtrlPath, terrainInstance); materialBuilders.Add(materialBuilder); } @@ -421,7 +424,7 @@ private void ComposeTerrainInstance(ParsedTerrainInstance terrainInstance, Scene var materialBuilders = new List(); foreach (var mtrlPath in materials) { - var output = ComposeMaterial(mtrlPath); + var output = ComposeMaterial(mtrlPath, bgPartsInstance); materialBuilders.Add(output); } @@ -430,7 +433,7 @@ private void ComposeTerrainInstance(ParsedTerrainInstance terrainInstance, Scene return meshes; } - private MaterialBuilder ComposeMaterial(string path) + private MaterialBuilder ComposeMaterial(string path, ParsedInstance instance) { if (mtrlCache.TryGetValue(path, out var cached)) { @@ -457,78 +460,44 @@ private MaterialBuilder ComposeMaterial(string path) var material = new MaterialSet(mtrlFile, path, shader.File, shpkName); var materialName = $"{Path.GetFileNameWithoutExtension(path)}_{Path.GetFileNameWithoutExtension(shpkName)}"; - if (shpkName == "lightshaft.shpk") - { - return new LightshaftMaterialBuilder(materialName, material, dataManager.GetFileOrReadFromDisk, CacheComputedTexture) - .WithLightShaft(); - } - - if (shpkName == "bg.shpk") - { - return new BgMaterialBuilder(materialName, material, dataManager.GetFileOrReadFromDisk, CacheComputedTexture) - .WithBg(); - } - - return ComposeGenericMaterial(path, material); - } - - private MaterialBuilder ComposeGenericMaterial(string path, MaterialSet materialSet) - { - var materialName = $"{Path.GetFileNameWithoutExtension(path)}_{materialSet.ShpkName}"; - var output = new XivMaterialBuilder(materialName, materialSet.ShpkName) - .WithMetallicRoughnessShader() - .WithBaseColor(Vector4.One); - - var alphaThreshold = materialSet.GetConstantOrDefault(MaterialConstant.g_AlphaThreshold, 0.0f); - if (alphaThreshold > 0) - output.WithAlpha(AlphaMode.MASK, alphaThreshold); - - // Initialize texture in cache - var texturePaths = materialSet.File.GetTexturePaths(); - foreach (var (offset, texPath) in texturePaths) - { - if (imageCache.ContainsKey(texPath)) continue; - CacheTexture(texPath); - } - var setTypes = new HashSet(); - foreach (var sampler in materialSet.File.Samplers) + if (bakeMaterials) { - if (sampler.TextureIndex == byte.MaxValue) continue; - var textureInfo = materialSet.File.TextureOffsets[sampler.TextureIndex]; - var texturePath = texturePaths[textureInfo.Offset]; - if (!imageCache.TryGetValue(texturePath, out var tex)) continue; - // bg textures can have additional textures, which may be dummy textures, ignore them - if (texturePath.Contains("dummy_")) continue; - if (!materialSet.Package.TextureLookup.TryGetValue(sampler.SamplerId, out var usage)) + if (shpkName == "lightshaft.shpk") { - log.LogWarning("Unknown texture usage for texture {texturePath} ({textureUsage})", texturePath, (TextureUsage)sampler.SamplerId); - continue; + return new LightshaftMaterialBuilder(materialName, + material, + dataManager.GetFileOrReadFromDisk, + CacheTexture) + .WithLightShaft(); } - - var channel = MaterialUtility.MapTextureUsageToChannel(usage); - if (channel != null && setTypes.Add(usage)) - { - var fileName = $"{Path.GetFileNameWithoutExtension(texturePath)}_{usage}_{materialSet.ShpkName}"; - var imageBuilder = ImageBuilder.From(tex.MemoryImage, fileName); - imageBuilder.AlternateWriteFileName = $"{fileName}.*"; - output.WithChannelImage(channel.Value, imageBuilder); - } - else if (channel != null) + + if (shpkName is "bg.shpk" or "bgprop.shpk") { - log.LogDebug("Duplicate texture {texturePath} with usage {usage}", texturePath, usage); + return new BgMaterialBuilder(materialName, shpkName, material, dataManager.GetFileOrReadFromDisk, + CacheTexture) + .WithBg(); } - else + + if (shpkName == "bgcolorchange.shpk") { - log.LogDebug("Unknown texture usage {usage} for texture {texturePath}", usage, texturePath); + Vector4? stainColor = instance switch + { + IStainableInstance stainable => stainable.StainColor, + _ => null + }; + + return new BgMaterialBuilder(materialName, shpkName, material, dataManager.GetFileOrReadFromDisk, + CacheTexture) + .WithBgColorChange(stainColor); } } - - mtrlCache.TryAdd(path, output); - return output; + + return new GenericMaterialBuilder(materialName, material, dataManager.GetFileOrReadFromDisk, CacheTexture) + .WithGeneric(); } - private MemoryImage CacheComputedTexture(SKTexture texture, string texName) + private ImageBuilder CacheTexture(SKTexture texture, string texName) { byte[] textureBytes; using (var memoryStream = new MemoryStream()) @@ -537,7 +506,7 @@ private MemoryImage CacheComputedTexture(SKTexture texture, string texName) textureBytes = memoryStream.ToArray(); } - var outPath = Path.Combine(CacheDir, "Computed", $"{texName}.png"); + var outPath = Path.Combine(CacheDir, $"{texName}.png"); var outDir = Path.GetDirectoryName(outPath); if (!string.IsNullOrEmpty(outDir) && !Directory.Exists(outDir)) { @@ -548,32 +517,15 @@ private MemoryImage CacheComputedTexture(SKTexture texture, string texName) var outImage = new MemoryImage(() => File.ReadAllBytes(outPath)); imageCache.TryAdd(texName, (outPath, outImage)); - return outImage; + + var name = Path.GetFileNameWithoutExtension(texName.Replace('.', '_')); + var builder = ImageBuilder.From(outImage, name); + builder.AlternateWriteFileName = $"{name}.*"; + return builder; } - - private void CacheTexture(string texPath) - { - var texData = dataManager.GetFileOrReadFromDisk(texPath); - if (texData == null) throw new Exception($"Failed to load texture file: {texPath}"); - log.LogInformation("Loaded texture {texPath}", texPath); - var texFile = new TexFile(texData); - var diskPath = Path.Combine(CacheDir, Path.GetDirectoryName(texPath) ?? "", - Path.GetFileNameWithoutExtension(texPath)) + ".png"; - var texture = texFile.ToResource().ToTexture(); - byte[] textureBytes; - using (var memoryStream = new MemoryStream()) - { - texture.Bitmap.Encode(memoryStream, SKEncodedImageFormat.Png, 100); - textureBytes = memoryStream.ToArray(); - } - var dirPath = Path.GetDirectoryName(diskPath); - if (!string.IsNullOrEmpty(dirPath) && !Directory.Exists(dirPath)) - { - Directory.CreateDirectory(dirPath); - } - - File.WriteAllBytes(diskPath, textureBytes); - imageCache.TryAdd(texPath, (diskPath, new MemoryImage(() => File.ReadAllBytes(diskPath)))); + public void Dispose() + { + cancellationToken.ThrowIfCancellationRequested(); } } diff --git a/Meddle/Meddle.Plugin/Models/Composer/InstanceMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/InstanceMaterialBuilder.cs index 8143f35..1af07e2 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/InstanceMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/InstanceMaterialBuilder.cs @@ -1,5 +1,6 @@ using Meddle.Utils.Materials; using Meddle.Utils.Models; +using SharpGLTF.Materials; using SharpGLTF.Memory; namespace Meddle.Plugin.Models.Composer; @@ -7,9 +8,9 @@ namespace Meddle.Plugin.Models.Composer; public abstract class InstanceMaterialBuilder : XivMaterialBuilder { protected readonly Func LookupFunc; - protected readonly Func CacheFunc; + protected readonly Func CacheFunc; - public InstanceMaterialBuilder(string name, string shpk, Func lookupFunc, Func cacheFunc) : base(name, shpk) + public InstanceMaterialBuilder(string name, string shpk, Func lookupFunc, Func cacheFunc) : base(name, shpk) { this.LookupFunc = lookupFunc; this.CacheFunc = cacheFunc; diff --git a/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs index bd1d8cf..6d614a4 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs @@ -15,7 +15,7 @@ public class LightshaftMaterialBuilder : InstanceMaterialBuilder { private readonly MaterialSet set; - public LightshaftMaterialBuilder(string name, MaterialSet set, Func lookupFunc, Func cacheFunc) : base(name, "lightshaft.shpk", lookupFunc, cacheFunc) + public LightshaftMaterialBuilder(string name, MaterialSet set, Func lookupFunc, Func cacheFunc) : base(name, "lightshaft.shpk", lookupFunc, cacheFunc) { this.set = set; } @@ -51,15 +51,20 @@ public LightshaftMaterialBuilder WithLightShaft() outTexture[x, y] = (outColor * tex0Color * tex1Color).ToSkColor(); } - var fileName = $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_computed_lightshaft_diffuse"; + var fileName = $"Computed/{Path.GetFileNameWithoutExtension(set.MtrlPath)}_lightshaft_diffuse"; var diffuseImage = CacheFunc(outTexture, fileName); - this.WithBaseColor(diffuseImage); + //this.WithBaseColor(diffuseImage); + this.WithBaseColor(new Vector4(1, 1, 1, 0)); + this.WithEmissive(diffuseImage); - if (set.TryGetConstant(MaterialConstant.g_AlphaThreshold, out float alphaThreshold)) + + if (!set.TryGetConstant(MaterialConstant.g_AlphaThreshold, out float alphaThreshold)) { - this.WithAlpha(AlphaMode.MASK, alphaThreshold); + alphaThreshold = 0.5f; } + + this.WithAlpha(AlphaMode.MASK, alphaThreshold); Extras = set.ComposeExtrasNode(); diff --git a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs index 2b67e4e..a8a8a37 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs @@ -214,7 +214,7 @@ public MaterialSet(MtrlFile file, string mtrlPath, ShpkFile shpk, string shpkNam - private static JsonSerializerOptions JsonOptions => new() + public static JsonSerializerOptions JsonOptions => new() { IncludeFields = true }; diff --git a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs index 7077338..227422e 100644 --- a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs +++ b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs @@ -1,4 +1,5 @@ -using FFXIVClientStructs.FFXIV.Client.Game.Object; +using System.Numerics; +using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.LayoutEngine; using Lumina.Excel.GeneratedSheets; using Meddle.Plugin.Models.Skeletons; @@ -112,7 +113,7 @@ public ParsedHousingInstance(nint id, Transform transform, string path, string n public ObjectKind Kind { get; } } -public class ParsedBgPartsInstance : ParsedInstance, IPathInstance +public class ParsedBgPartsInstance : ParsedInstance, IPathInstance, IStainableInstance { public string Path { get; } @@ -120,6 +121,13 @@ public ParsedBgPartsInstance(nint id, Transform transform, string path) : base(i { Path = path; } + + public Vector4? StainColor { get; set; } +} + +public interface IStainableInstance +{ + public Vector4? StainColor { get; set; } } public class ParsedLightInstance : ParsedInstance diff --git a/Meddle/Meddle.Plugin/Services/LayoutService.cs b/Meddle/Meddle.Plugin/Services/LayoutService.cs index 1d93651..912a556 100644 --- a/Meddle/Meddle.Plugin/Services/LayoutService.cs +++ b/Meddle/Meddle.Plugin/Services/LayoutService.cs @@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.LayoutEngine.Layer; using FFXIVClientStructs.FFXIV.Client.LayoutEngine.Terrain; using FFXIVClientStructs.Interop; +using ImGuiNET; using Lumina.Excel.GeneratedSheets; using Meddle.Plugin.Models; using Meddle.Plugin.Models.Layout; @@ -280,11 +281,24 @@ private unsafe ParsedInstanceSet[] Parse(LayoutManager* activeLayout, ParseCtx c var furnitureMatch = ctx.HousingItems.FirstOrDefault(item => item.LayoutInstance == sharedGroupPtr); if (furnitureMatch is not null) { - return new ParsedHousingInstance((nint)sharedGroup, new Transform(*sharedGroup->GetTransformImpl()), path, + // TODO: Kinda messy + var stain = stainDict.GetValueOrDefault(furnitureMatch.HousingFurniture.Stain); + var item = itemDict.GetValueOrDefault(furnitureMatch.HousingFurniture.Id); + + var housing = new ParsedHousingInstance((nint)sharedGroup, new Transform(*sharedGroup->GetTransformImpl()), path, furnitureMatch.GameObject->NameString, furnitureMatch.GameObject->ObjectKind, - stainDict.GetValueOrDefault(furnitureMatch.HousingFurniture.Stain), - itemDict.GetValueOrDefault(furnitureMatch.HousingFurniture.Id), children); + stain, + item, children); + foreach (var child in housing.Flatten()) + { + if (child is ParsedBgPartsInstance parsedBgPartsInstance) + { + parsedBgPartsInstance.StainColor = stain?.Color != null ? ImGui.ColorConvertU32ToFloat4(stain.Color) : null; + } + } + + return housing; } return new ParsedSharedInstance((nint)sharedGroup, new Transform(*sharedGroup->GetTransformImpl()), path, children); diff --git a/Meddle/Meddle.Plugin/UI/Layout/Config.cs b/Meddle/Meddle.Plugin/UI/Layout/Config.cs index f9d72bd..aff5893 100644 --- a/Meddle/Meddle.Plugin/UI/Layout/Config.cs +++ b/Meddle/Meddle.Plugin/UI/Layout/Config.cs @@ -25,6 +25,7 @@ public enum ExportType private bool traceToHovered = true; private bool hideOffscreenCharacters = true; private int maxItemCount = 100; + private bool bakeTextures = true; private void DrawOptions() { @@ -47,7 +48,17 @@ private void DrawOptions() ImGui.Checkbox("Order by Distance", ref orderByDistance); ImGui.Checkbox("Trace to Hovered", ref traceToHovered); ImGui.Checkbox("Hide Offscreen Characters", ref hideOffscreenCharacters); + ImGui.Checkbox("Bake Textures", ref bakeTextures); + ImGui.SameLine(); + ImGui.TextDisabled("(?)"); + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Computes some properties of textures before exporting them, will increase export time significantly"); + } + ImGui.DragInt("Max Item Count", ref maxItemCount, 1, 1, 50000); + ImGui.SameLine(); + ImGui.TextDisabled("(?)"); if (ImGui.IsItemHovered()) { ImGui.SetTooltip("The maximum number of items to draw in the layout window, does not affect exports"); diff --git a/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs b/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs index 64d74bb..d677429 100644 --- a/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs +++ b/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs @@ -16,7 +16,7 @@ namespace Meddle.Plugin.UI.Layout; -public partial class LayoutWindow : Window +public partial class LayoutWindow : Window, IDisposable { private readonly LayoutService layoutService; private readonly Configuration config; @@ -281,7 +281,7 @@ private void InstanceExport(ParsedInstance[] instances) Directory.CreateDirectory(cacheDir); var instanceSet = new InstanceComposer(log, dataManager, config, instances, cacheDir, - x => progress = x, cancelToken.Token); + x => progress = x, bakeTextures, cancelToken.Token); var scene = new SceneBuilder(); instanceSet.Compose(scene); var gltf = scene.ToGltf2(); @@ -304,4 +304,9 @@ private void InstanceExport(ParsedInstance[] instances) }, cancelToken.Token); }, Plugin.TempDirectory); } + + public void Dispose() + { + cancelToken.Cancel(); + } } diff --git a/Meddle/Meddle.Utils/ImageUtils.cs b/Meddle/Meddle.Utils/ImageUtils.cs index 0921ec3..8081e13 100644 --- a/Meddle/Meddle.Utils/ImageUtils.cs +++ b/Meddle/Meddle.Utils/ImageUtils.cs @@ -165,7 +165,7 @@ public static Image GetTexData(TexFile tex, int arrayLevel, int mipLevel, int sl return img; } - public static unsafe SKTexture ToTexture(this Image img, (int width, int height)? resize = null) + public static unsafe SKTexture ToTexture(this Image img, Vector2? resize = null) { if (img.Format != DXGIFormat.R8G8B8A8UNorm) throw new ArgumentException("Image must be in RGBA format.", nameof(img)); @@ -179,7 +179,7 @@ public static unsafe SKTexture ToTexture(this Image img, (int width, int height) if (resize != null) { - bitmap = bitmap.Resize(new SKImageInfo(resize.Value.width, resize.Value.height, SKColorType.Rgba8888, SKAlphaType.Unpremul), SKFilterQuality.High); + bitmap = bitmap.Resize(new SKImageInfo((int)resize.Value.X, (int)resize.Value.Y, SKColorType.Rgba8888, SKAlphaType.Unpremul), SKFilterQuality.High); } return new SKTexture(bitmap); diff --git a/Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs b/Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs index 0c1467a..a59bc07 100644 --- a/Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs +++ b/Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs @@ -1,4 +1,5 @@ -using SharpGLTF.Materials; +using System.Numerics; +using SharpGLTF.Materials; namespace Meddle.Utils.Materials; @@ -16,3 +17,8 @@ public interface IVertexPaintMaterialBuilder { public bool VertexPaint { get; } } + +public interface ITangentMuliplierMaterialBuilder +{ + public Vector4 TangentMultiplier { get; } +} diff --git a/Meddle/Meddle.Utils/ModelBuilder.cs b/Meddle/Meddle.Utils/ModelBuilder.cs index 169c008..6cd076d 100644 --- a/Meddle/Meddle.Utils/ModelBuilder.cs +++ b/Meddle/Meddle.Utils/ModelBuilder.cs @@ -1,4 +1,5 @@ using Meddle.Utils.Export; +using Meddle.Utils.Materials; using SharpGLTF.Geometry; using SharpGLTF.Materials; @@ -39,7 +40,6 @@ public static IReadOnlyList BuildMeshes( meshBuilder = new MeshBuilder(mesh, null, material, raceDeformer); } - meshBuilder.BuildVertices(); var modelPathName = Path.GetFileNameWithoutExtension(model.Path); if (mesh.SubMeshes.Count == 0) diff --git a/Meddle/Meddle.Utils/Models/SKTexture.cs b/Meddle/Meddle.Utils/Models/SKTexture.cs index 7e97609..0c6b12a 100644 --- a/Meddle/Meddle.Utils/Models/SKTexture.cs +++ b/Meddle/Meddle.Utils/Models/SKTexture.cs @@ -10,6 +10,7 @@ public sealed class SKTexture public int Width { get; } public int Height { get; } + public Vector2 Size => new(Width, Height); public SKBitmap Bitmap { From bd6d1df2408918bb7c590f95181bb25f24be36e1 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Wed, 4 Sep 2024 21:01:24 +1000 Subject: [PATCH 04/44] wip --- .../Models/Composer/BgMaterialBuilder.cs | 271 +++--------- .../Models/Composer/CharacterComposer.cs | 183 ++++++++ .../Composer/CharacterMaterialBuilder.cs | 97 +++++ .../CharacterOcclusionMaterialBuilder.cs | 26 ++ .../CharacterTattooMaterialBuilder.cs | 69 +++ .../Models/Composer/DataProvider.cs | 152 +++++++ .../Models/Composer/GenericMaterialBuilder.cs | 61 ++- .../Models/Composer/InstanceComposer.cs | 407 ++++-------------- .../Composer/InstanceMaterialBuilder.cs | 18 - .../Composer/LightshaftMaterialBuilder.cs | 61 +-- .../Models/Composer/MaterialSet.cs | 311 ++++++++++--- .../Models/Composer/MeddleMaterialBuilder.cs | 25 ++ .../Models/Composer/SkinMaterialBuilder.cs | 112 +++++ .../Models/Layout/ParsedInstance.cs | 38 +- Meddle/Meddle.Plugin/UI/Layout/Config.cs | 8 - .../Meddle.Plugin/UI/Layout/LayoutWindow.cs | 2 +- Meddle/Meddle.Plugin/Utils/PathUtil.cs | 26 +- .../Meddle.Utils/Export/CustomizeParameter.cs | 26 ++ Meddle/Meddle.Utils/ImageUtils.cs | 5 + .../Meddle.Utils/Materials/MaterialUtility.cs | 28 +- .../Materials/XIVMaterialBuilder.cs | 20 +- Meddle/Meddle.Utils/MeshBuilder.cs | 5 +- 22 files changed, 1197 insertions(+), 754 deletions(-) create mode 100644 Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs create mode 100644 Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs create mode 100644 Meddle/Meddle.Plugin/Models/Composer/CharacterOcclusionMaterialBuilder.cs create mode 100644 Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs create mode 100644 Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs delete mode 100644 Meddle/Meddle.Plugin/Models/Composer/InstanceMaterialBuilder.cs create mode 100644 Meddle/Meddle.Plugin/Models/Composer/MeddleMaterialBuilder.cs create mode 100644 Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs diff --git a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs index 11ecf02..ef85d6a 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs @@ -7,22 +7,39 @@ using Meddle.Utils.Materials; using Meddle.Utils.Models; using SharpGLTF.Materials; -using SharpGLTF.Memory; namespace Meddle.Plugin.Models.Composer; -public class BgMaterialBuilder : InstanceMaterialBuilder, IVertexPaintMaterialBuilder + +public interface IBgMaterialBuilderParams; + +public record BgColorChangeParams : IBgMaterialBuilderParams +{ + public Vector4? StainColor { get; init; } + + public BgColorChangeParams(Vector4? stainColor) + { + StainColor = stainColor; + } +} + +public record BgParams : IBgMaterialBuilderParams; + + +public class BgMaterialBuilder : MeddleMaterialBuilder, IVertexPaintMaterialBuilder { + private readonly IBgMaterialBuilderParams bgParams; private readonly MaterialSet set; - private const uint BGVertexPaintKey = 0x4F4F0636; - private const uint BGVertexPaintValue = 0xBD94649A; + private readonly DataProvider dataProvider; + private const uint BgVertexPaintKey = 0x4F4F0636; + private const uint BgVertexPaintValue = 0xBD94649A; private const uint DiffuseAlphaKey = 0xA9A3EE25; private const uint DiffuseAlphaValue = 0x72AAA9AE; // if present, alpha channel on diffuse texture is used?? and should respect g_AlphaThreshold - private readonly string shpkSuffix; - public BgMaterialBuilder(string name, string shpkName, MaterialSet set, Func lookupFunc, Func cacheFunc) : base(name, shpkName, lookupFunc, cacheFunc) + public BgMaterialBuilder(string name, IBgMaterialBuilderParams bgParams, MaterialSet set, DataProvider dataProvider) : base(name) { + this.bgParams = bgParams; this.set = set; - shpkSuffix = Path.GetExtension(shpkName); + this.dataProvider = dataProvider; } private record TextureSet(SKTexture Color0, SKTexture Specular0, SKTexture Normal0, SKTexture? Color1, SKTexture? Specular1, SKTexture? Normal1); @@ -48,7 +65,7 @@ private DetailSet GetDetail(int detailId, Vector2 size) if (DetailDArray == null) { - var detailDArray = LookupFunc("bgcommon/nature/detail/texture/detail_d_array.tex"); + var detailDArray = dataProvider.LookupData("bgcommon/nature/detail/texture/detail_d_array.tex"); if (detailDArray == null) { throw new Exception("Detail D array texture not found"); @@ -59,7 +76,7 @@ private DetailSet GetDetail(int detailId, Vector2 size) if (DetailNArray == null) { - var detailNArray = LookupFunc("bgcommon/nature/detail/texture/detail_n_array.tex"); + var detailNArray = dataProvider.LookupData("bgcommon/nature/detail/texture/detail_n_array.tex"); if (detailNArray == null) { throw new Exception("Detail N array texture not found"); @@ -99,11 +116,12 @@ private DetailSet GetDetail(int detailId, Vector2 size) { if (set.TextureUsageDict.TryGetValue(usage, out var texture)) { - if (texture.Contains("_dummy")) + if (texture.Contains("dummy_")) { return null; } - var textureResource = LookupFunc(texture); + var mappedPath = set.MapTexturePath(texture); + var textureResource = dataProvider.LookupData(mappedPath); if (textureResource != null) { var resource = new TexFile(textureResource).ToResource(); @@ -117,16 +135,13 @@ private DetailSet GetDetail(int detailId, Vector2 size) private TextureSet GetTextureSet() { - var colorMap0 = set.TextureUsageDict[TextureUsage.g_SamplerColorMap0]; - var specularMap0 = set.TextureUsageDict[TextureUsage.g_SamplerSpecularMap0]; - var normalMap0 = set.TextureUsageDict[TextureUsage.g_SamplerNormalMap0]; - var colorMap0Texture = LookupFunc(colorMap0) ?? throw new Exception("ColorMap0 texture not found"); - var specularMap0Texture = LookupFunc(specularMap0) ?? throw new Exception("SpecularMap0 texture not found"); - var normalMap0Texture = LookupFunc(normalMap0) ?? throw new Exception("NormalMap0 texture not found"); + var colorMap0Texture = set.GetTexture(dataProvider, TextureUsage.g_SamplerColorMap0) ?? throw new Exception("ColorMap0 texture not found"); + var specularMap0Texture = set.GetTexture(dataProvider, TextureUsage.g_SamplerSpecularMap0) ?? throw new Exception("SpecularMap0 texture not found"); + var normalMap0Texture = set.GetTexture(dataProvider, TextureUsage.g_SamplerNormalMap0) ?? throw new Exception("NormalMap0 texture not found"); - var colorRes0 = new TexFile(colorMap0Texture).ToResource(); - var specularRes0 = new TexFile(specularMap0Texture).ToResource(); - var normalRes0 = new TexFile(normalMap0Texture).ToResource(); + var colorRes0 = colorMap0Texture.ToResource(); + var specularRes0 = specularMap0Texture.ToResource(); + var normalRes0 = normalMap0Texture.ToResource(); var sizes = new List { colorRes0.Size, @@ -138,7 +153,7 @@ private TextureSet GetTextureSet() var specularRes1 = GetTextureResourceOrNull(TextureUsage.g_SamplerSpecularMap1, ref sizes); var normalRes1 = GetTextureResourceOrNull(TextureUsage.g_SamplerNormalMap1, ref sizes); - var size = Max(sizes); + var size = sizes.MaxBy(x => x.X * x.Y); var colorTex0 = colorRes0.ToTexture(size); var specularTex0 = specularRes0.ToTexture(size); var normalTex0 = normalRes0.ToTexture(size); @@ -148,220 +163,58 @@ private TextureSet GetTextureSet() return new TextureSet(colorTex0, specularTex0, normalTex0, colorTex1, specularTex1, normalTex1); } - - - public BgMaterialBuilder WithBgColorChange(Vector4? stainColor) - { - Apply(stainColor); - return this; - } - private void SaveAllTextures() + public bool VertexPaint { get; private set; } + + private bool UseAlpha(out float alphaThreshold) { - foreach (var (usage, path) in set.TextureUsageDict) + var useAlpha = set.ShaderKeys.Any(x => x is {Category: DiffuseAlphaKey, Value: DiffuseAlphaValue}); + alphaThreshold = 0; + if (useAlpha) { - var texture = LookupFunc(path); - if (texture == null) - { - continue; - } - var tex = new TexFile(texture).ToResource().ToTexture(); - CacheFunc(tex, $"Debug/{Path.GetFileNameWithoutExtension(path)}"); + set.TryGetConstant(MaterialConstant.g_AlphaThreshold, out alphaThreshold); } - } - - public BgMaterialBuilder WithBg() - { - Apply(); - return this; + return useAlpha; } - - public bool VertexPaint { get; private set; } - public Vector4 TangentMultiplier { get; private set; } - private Vector2 Max(IEnumerable vectors) + private bool GetDiffuseColor(out Vector4 diffuseColor) { - var max = new Vector2(float.MinValue); - foreach (var vector in vectors) + if (!set.TryGetConstant(MaterialConstant.g_DiffuseColor, out Vector3 diffuseColor3)) { - max = Vector2.Max(max, vector); + diffuseColor = Vector4.One; + return false; } - return max; + diffuseColor = new Vector4(diffuseColor3, 1); + return true; } - private void Apply(Vector4? stainColor = null) + public override MeddleMaterialBuilder Apply() { + var extrasDict = set.ComposeExtras(); var textureSet = GetTextureSet(); - var useAlpha = set.ShaderKeys.Any(x => x is {Category: DiffuseAlphaKey, Value: DiffuseAlphaValue}); - if (useAlpha && set.TryGetConstant(MaterialConstant.g_AlphaThreshold, out float alphaThreshold)) + if (UseAlpha(out var alphaThreshold)) { WithAlpha(AlphaMode.MASK, alphaThreshold); } - if (!set.TryGetConstant(MaterialConstant.g_DiffuseColor, out Vector3 diffuseColor)) - { - diffuseColor = Vector3.One; - } - - // if (!set.TryGetConstant(MaterialConstant.g_SpecularColor, out Vector3 specularColor)) - // { - // specularColor = Vector3.One; - // } - - // Vector3 emmissiveColor; - // if (!set.TryGetConstant(MaterialConstant.g_EmissiveColor, out emmissiveColor)) - // { - // emmissiveColor = Vector3.Zero; - // } - - Vector4 diffuseColor0; - if (stainColor != null && stainColor != Vector4.Zero) - { - diffuseColor0 = stainColor.Value; - } - else - { - diffuseColor0 = new Vector4(diffuseColor, 1); - } - - if (!set.TryGetConstant(MaterialConstant.g_DetailID, out float detailId)) - { - detailId = 0; - } - - if (!set.TryGetConstant(MaterialConstant.g_NormalScale, out float normalScale)) + if (bgParams is BgColorChangeParams bgColorChangeParams && GetDiffuseColor(out var bgColorChangeDiffuseColor)) { - normalScale = 1; + var diffuseColor = bgColorChangeParams.StainColor ?? bgColorChangeDiffuseColor; + extrasDict["stainColor"] = diffuseColor.AsFloatArray(); } - - if (!set.TryGetConstant(MaterialConstant.g_ColorUVScale, out Vector4 colorUvScale)) - { - colorUvScale = new Vector4(1); - } - - //var detail = GetDetail((int)detailId, textureSet.Color0.Size); - //var detailColor0 = new Vector4(detail.DetailColor, 1); - - //var specularColor0 = new Vector4(specularColor, 1); - SKTexture diffuse = new SKTexture(textureSet.Color0.Width, textureSet.Color0.Height); - SKTexture normal = new SKTexture(textureSet.Color0.Width, textureSet.Color0.Height); - //for (int x = 0; x < diffuse.Width; x++) - //for (int y = 0; y < diffuse.Height; y++) - Parallel.For(0, diffuse.Width, x => - { - for (int y = 0; y < diffuse.Height; y++) - { - var color0 = textureSet.Color0[x, y].ToVector4(); - var normal0 = textureSet.Normal0[x, y].ToVector4(); - var color1 = textureSet.Color1?[x, y].ToVector4() ?? Vector4.One; - var normal1 = textureSet.Normal1?[x, y].ToVector4() ?? Vector4.One; - - // idk about this one tbh - //var specular0 = textureSet.Specular0[x, y].ToVector4(); - //var specular1 = textureSet.Specular1?[x, y].ToVector4() ?? Vector4.One; - //var specularData = (specularColor0 * specular0 * specular1); - // var detailColorMask = detail.Diffuse[x, y].ToVector4(); - // var detailNormalMap = detail.Normal[x, y].ToVector4(); - - var outDiffuse = color0 * color1; - //var color = Vector4.Lerp(diffuseColor0, detailColor0, detailColorMask.Z); - // diffuse alpha is a mask for stain color - if (Shpk == "bgcolorchange.shpk") - { - outDiffuse = Vector4.Lerp(outDiffuse, diffuseColor0, outDiffuse.W); - outDiffuse.W = 1; - } - - // normal blue is weight for the detail normal map? - var outNormal = (normal0 * normal1 * normalScale); - //outNormal = Vector4.Lerp(outNormal, detailNormalMap * detail.DetailNormalScale, detailColorMask.Z); - // maybe? - outNormal *= outNormal.Z; - outNormal.W = 1; - - - diffuse[x, y] = outDiffuse.ToSkColor(); - //emmissive[x, y] = Vector4.Lerp(Vector4.Zero, new Vector4(emissiveColor, 1), specularData.X).ToSkColor(); - //specular[x, y] = specularData.ToSkColor(); - normal[x, y] = outNormal.ToSkColor(); - } - }); - - VertexPaint = set.ShaderKeys.Any(x => x is {Category: BGVertexPaintKey, Value: BGVertexPaintValue}); + VertexPaint = set.ShaderKeys.Any(x => x is {Category: BgVertexPaintKey, Value: BgVertexPaintValue}); Extras = set.ComposeExtrasNode(); - var extrasDict = set.ComposeExtras(); - var stainString = ""; - if (Shpk == "bgcolorchange.shpk") - { - stainString = $"_{ToHex(diffuseColor0)}"; - extrasDict["stainColor"] = ToHex(diffuseColor0); - } Extras = JsonNode.Parse(JsonSerializer.Serialize(extrasDict, MaterialSet.JsonOptions))!; - WithNormal(CacheFunc(normal, $"Computed/{Path.GetFileNameWithoutExtension(set.MtrlPath)}_{shpkSuffix}_normal")); - WithBaseColor(CacheFunc(diffuse, $"Computed/{Path.GetFileNameWithoutExtension(set.MtrlPath)}_{shpkSuffix}{stainString}_diffuse")); + // Stub + WithNormal(dataProvider.CacheTexture(textureSet.Normal0, $"Computed/{set.ComputedTextureName("normal")}")); + WithBaseColor(dataProvider.CacheTexture(textureSet.Color0, $"Computed/{set.ComputedTextureName("diffuse")}")); + //WithSpecularColor(dataProvider.CacheTexture(textureSet.Specular0, $"Computed/{set.ComputedTextureName("specular")}")); - // should not be applied uniformly. strength is probably dictated using one of the spec channels - // WithEmissive(CacheFunc(emmissive, $"{Path.GetFileNameWithoutExtension(set.MtrlPath)}_bg_emissive")); - //CacheFunc(detail.Diffuse, $"Debug/{Path.GetFileNameWithoutExtension(set.MtrlPath)}_{shpkSuffix}_detail{(int)detailId}_diffuse"); - //CacheFunc(detail.Normal, $"Debug/{Path.GetFileNameWithoutExtension(set.MtrlPath)}_{shpkSuffix}_detail{(int)detailId:D}_normal"); - SaveAllTextures(); - } - - private string ToHex(Vector4 color) - { - var r = (byte)(color.X * 255); - var g = (byte)(color.Y * 255); - var b = (byte)(color.Z * 255); - var a = (byte)(color.W * 255); - return $"{r:X2}{g:X2}{b:X2}{a:X2}"; + return this; } - - /* -g_AlphaThreshold = [0] -g_ShadowAlphaThreshold = [0.5] -g_ShaderID = [0] -g_DiffuseColor = [1, 1, 1] -g_MultiDiffuseColor = [1, 1, 1] -g_SpecularColor = [1, 1, 1] -g_MultiSpecularColor = [1, 1, 1] -g_EmissiveColor = [0, 0, 0] -g_MultiEmissiveColor = [0, 0, 0] -g_NormalScale = [1] -g_MultiNormalScale = [1] -g_HeightScale = [0.015] -g_MultiHeightScale = [0.015] -g_SSAOMask = [1] -g_MultiSSAOMask = [1] -0xBFE9D12D = [1] -0x093084AD = [1] -g_InclusionAperture = [1] -0x5106E045 = [0] -g_ColorUVScale = [1, 1, 1, 1] -g_SpecularUVScale = [1, 1, 1, 1] -g_NormalUVScale = [1, 1, 1, 1] -g_AlphaMultiParam = [0, 0, 0, 0] -g_DetailID = [0] -0xAC156136 = [0] -g_DetailColor = [0.5, 0.5, 0.5] -g_MultiDetailColor = [0.5, 0.5, 0.5] -0xF769298E = [0.3, 0.3, 0.3, 0.3] -g_DetailNormalScale = [1] -0xA83DBDF1 = [1] -g_DetailNormalUvScale = [4, 4, 4, 4] -g_DetailColorUvScale = [4, 4, 4, 4] -0xB8ACCE58 = [50, 100, 50, 100] -0xD67F62C8 = [1] -0x12F6AB51 = [3] -0x236EE793 = [0, 0] -0xF3F28C58 = [0, 0] -0x756DFE22 = [0, 0] -0xB10AF2DA = [0, 0] -0x9A696A17 = [10, 10, 10, 10] -g_EnvMapPower = [0.85] - */ } diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs new file mode 100644 index 0000000..55e19f0 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs @@ -0,0 +1,183 @@ +using System.Numerics; +using Meddle.Plugin.Models.Layout; +using Meddle.Plugin.Utils; +using Meddle.Utils; +using Meddle.Utils.Export; +using Meddle.Utils.Files; +using Meddle.Utils.Models; +using Microsoft.Extensions.Logging; +using SharpGLTF.Materials; +using SharpGLTF.Scenes; + +namespace Meddle.Plugin.Models.Composer; + +public class CharacterComposer +{ + private readonly ILogger log; + private readonly DataProvider dataProvider; + private static TexFile? CubeMapTex; + private static PbdFile? PbdFile; + private static readonly object StaticFileLock = new(); + + public CharacterComposer(ILogger log, DataProvider dataProvider) + { + this.log = log; + this.dataProvider = dataProvider; + + lock (StaticFileLock) + { + if (CubeMapTex == null) + { + var catchlight = this.dataProvider.LookupData("chara/common/texture/sphere_d_array.tex"); + if (catchlight == null) throw new Exception("Failed to load catchlight texture"); + CubeMapTex = new TexFile(catchlight); + } + + if (PbdFile == null) + { + var pbdData = this.dataProvider.LookupData("chara/xls/boneDeformer/human.pbd"); + if (pbdData == null) throw new Exception("Failed to load human.pbd"); + PbdFile = new PbdFile(pbdData); + } + } + } + + public void ComposeCharacterInstance(ParsedCharacterInstance characterInstance, SceneBuilder scene, NodeBuilder root) + { + var characterInfo = characterInstance.CharacterInfo; + if (characterInfo == null) return; + + var bones = SkeletonUtils.GetBoneMap(characterInfo.Skeleton, true, out var rootBone); + if (rootBone != null) + { + root.AddNode(rootBone); + } + + var textureMappings = characterInstance.TextureMap; + + for (var i = 0; i < characterInfo.Models.Count; i++) + { + var modelInfo = characterInfo.Models[i]; + if (modelInfo.PathFromCharacter.Contains("b0003_top")) continue; + var mdlData = dataProvider.LookupData(modelInfo.Path); + if (mdlData == null) + { + log.LogWarning("Failed to load model file: {modelPath}", modelInfo.Path); + continue; + } + + log.LogInformation("Loaded model {modelPath}", modelInfo.Path); + var mdlFile = new MdlFile(mdlData); + var materialBuilders = new List(); + foreach (var materialInfo in modelInfo.Materials) + { + var mtrlData = dataProvider.LookupData(materialInfo.Path); + if (mtrlData == null) + { + log.LogWarning("Failed to load material file: {mtrlPath}", materialInfo.Path); + throw new Exception($"Failed to load material file: {materialInfo.Path}"); + } + + log.LogInformation("Loaded material {mtrlPath}", materialInfo.Path); + var mtrlFile = new MtrlFile(mtrlData); + if (materialInfo.ColorTable != null) + { + mtrlFile.ColorTable = materialInfo.ColorTable.Value; + } + + var shpkName = mtrlFile.GetShaderPackageName(); + var shpkPath = $"shader/sm5/shpk/{shpkName}"; + var shpkData = dataProvider.LookupData(shpkPath); + if (shpkData == null) throw new Exception($"Failed to load shader package file: {shpkPath}"); + var shpkFile = new ShpkFile(shpkData); + var material = new MaterialSet(mtrlFile, materialInfo.Path, shpkFile, shpkName); + material.SetCustomizeParameters(characterInstance.CustomizeParameter); + material.SetCustomizeData(characterInstance.CustomizeData); + material.SetTexturePathMappings(characterInstance.TextureMap); + materialBuilders.Add(material.Compose(dataProvider)); + } + + var model = new Model(modelInfo.Path, mdlFile, modelInfo.ShapeAttributeGroup); + EnsureBonesExist(model, bones, rootBone); + (GenderRace from, GenderRace to, RaceDeformer deformer)? deform; + if (modelInfo.Deformer != null) + { + // custom pbd may exist + var pbdFileData = dataProvider.LookupData(modelInfo.Deformer.Value.PbdPath); + if (pbdFileData == null) throw new InvalidOperationException($"Failed to get deformer pbd {modelInfo.Deformer.Value.PbdPath}"); + deform = ((GenderRace)modelInfo.Deformer.Value.DeformerId, (GenderRace)modelInfo.Deformer.Value.RaceSexId, new RaceDeformer(new PbdFile(pbdFileData), bones)); + log.LogDebug("Using deformer pbd {Path}", modelInfo.Deformer.Value.PbdPath); + } + else + { + var parsed = RaceDeformer.ParseRaceCode(modelInfo.PathFromCharacter); + if (Enum.IsDefined(parsed)) + { + deform = (parsed, characterInfo.GenderRace, new RaceDeformer(PbdFile!, bones)); + } + else + { + deform = null; + } + } + + var meshes = ModelBuilder.BuildMeshes(model, materialBuilders, bones, deform); + foreach (var mesh in meshes) + { + InstanceBuilder instance; + if (bones.Count > 0) + { + instance = scene.AddSkinnedMesh(mesh.Mesh, Matrix4x4.Identity, bones.Cast().ToArray()); + } + else + { + instance = scene.AddRigidMesh(mesh.Mesh, Matrix4x4.Identity); + } + + if (model.Shapes.Count != 0 && mesh.Shapes != null) + { + // This will set the morphing value to 1 if the shape is enabled, 0 if not + var enabledShapes = Model.GetEnabledValues(model.EnabledShapeMask, model.ShapeMasks) + .ToArray(); + var shapes = model.Shapes + .Where(x => mesh.Shapes.Contains(x.Name)) + .Select(x => (x, enabledShapes.Contains(x.Name))); + instance.Content.UseMorphing().SetValue(shapes.Select(x => x.Item2 ? 1f : 0).ToArray()); + } + + if (mesh.Submesh != null) + { + // Remove subMeshes that are not enabled + var enabledAttributes = Model.GetEnabledValues(model.EnabledAttributeMask, model.AttributeMasks); + if (!mesh.Submesh.Attributes.All(enabledAttributes.Contains)) + { + instance.Remove(); + } + } + } + } + } + + private void EnsureBonesExist(Model model, List bones, BoneNodeBuilder? root) + { + foreach (var mesh in model.Meshes) + { + if (mesh.BoneTable == null) continue; + + foreach (var boneName in mesh.BoneTable) + { + if (bones.All(b => !b.BoneName.Equals(boneName, StringComparison.Ordinal))) + { + log.LogInformation("Adding bone {BoneName} from mesh {MeshPath}", boneName, + model.Path); + var bone = new BoneNodeBuilder(boneName); + if (root == null) throw new InvalidOperationException("Root bone not found"); + root.AddNode(bone); + log.LogInformation("Added bone {BoneName} to {ParentBone}", boneName, root.BoneName); + + bones.Add(bone); + } + } + } + } +} diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs new file mode 100644 index 0000000..0e35ddc --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs @@ -0,0 +1,97 @@ +using System.Numerics; +using Meddle.Utils; +using Meddle.Utils.Export; +using Meddle.Utils.Files; +using Meddle.Utils.Materials; +using Meddle.Utils.Models; +using SharpGLTF.Materials; + +namespace Meddle.Plugin.Models.Composer; + +public class CharacterMaterialBuilder : MeddleMaterialBuilder +{ + private readonly MaterialSet set; + private readonly DataProvider dataProvider; + + public CharacterMaterialBuilder(string name, MaterialSet set, DataProvider dataProvider) : base(name) + { + this.set = set; + this.dataProvider = dataProvider; + } + + public override MeddleMaterialBuilder Apply() + { + var textureMode = set.GetShaderKeyOrDefault(ShaderCategory.CategoryTextureType, TextureMode.Default); + var specularMode = set.GetShaderKeyOrDefault(ShaderCategory.CategorySpecularType, SpecularMode.Default); // TODO: is default actually default + var flowType = set.GetShaderKeyOrDefault(ShaderCategory.CategoryFlowMapType, FlowType.Standard); + + var normalTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerNormal).ToResource().ToTexture(); + var maskTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerMask).ToResource().ToTexture(normalTexture.Size); + var indexTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerIndex).ToResource().ToTexture(normalTexture.Size); + + var diffuseTexture = textureMode switch + { + TextureMode.Compatibility => set.GetTexture(dataProvider, TextureUsage.g_SamplerDiffuse).ToResource().ToTexture(normalTexture.Size), + _ => new SKTexture(normalTexture.Width, normalTexture.Height) + }; + + var flowTexture = flowType switch + { + FlowType.Flow => set.GetTexture(dataProvider, TextureUsage.g_SamplerFlow).ToResource().ToTexture(normalTexture.Size), + _ => null + }; + + var occlusionTexture = new SKTexture(normalTexture.Width, normalTexture.Height); + var metallicRoughness = new SKTexture(normalTexture.Width, normalTexture.Height); + for (int x = 0; x < normalTexture.Width; x++) + for (int y = 0; y < normalTexture.Height; y++) + { + var normal = normalTexture[x, y].ToVector4(); + var mask = maskTexture[x, y].ToVector4(); + var indexColor = indexTexture[x, y]; + + var blended = set.ColorTable!.Value.GetBlendedPair(indexColor.Red, indexColor.Green); + if (textureMode == TextureMode.Compatibility) + { + var diffuse = diffuseTexture![x, y].ToVector4(); + diffuse *= new Vector4(blended.Diffuse, normal.Z); + diffuseTexture[x, y] = (diffuse with {W = normal.Z }).ToSkColor(); + } + else if (textureMode == TextureMode.Default) + { + var diffuse = new Vector4(blended.Diffuse, normal.Z); + diffuseTexture[x, y] = diffuse.ToSkColor(); + } + else + { + throw new InvalidOperationException($"Unknown texture mode {textureMode}"); + } + + /*var spec = blended.Specular; + var specStrength = blended.SpecularStrength; + if (specularMode == SpecularMode.Mask) + { + var diffuseMask = mask.X; + var specMask = mask.Y; + var roughMask = mask.Z; + metallicRoughness[x, y] = new Vector4(specMask, roughMask, 0, 1).ToSkColor(); + }*/ + + normalTexture[x, y] = (normal with{ W = 1.0f}).ToSkColor(); + } + + WithBaseColor(dataProvider.CacheTexture(diffuseTexture, $"Computed/{set.ComputedTextureName("diffuse")}")); + WithNormal(dataProvider.CacheTexture(normalTexture, $"Computed/{set.ComputedTextureName("normal")}")); + + + var alphaThreshold = set.GetConstantOrDefault(MaterialConstant.g_AlphaThreshold, 0.0f); + if (alphaThreshold > 0) + WithAlpha(AlphaMode.MASK, alphaThreshold); + + WithDoubleSide(set.RenderBackfaces); + + + Extras = set.ComposeExtrasNode(); + return this; + } +} diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterOcclusionMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterOcclusionMaterialBuilder.cs new file mode 100644 index 0000000..2693788 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterOcclusionMaterialBuilder.cs @@ -0,0 +1,26 @@ +using System.Numerics; +using SharpGLTF.Materials; + +namespace Meddle.Plugin.Models.Composer; + +public class CharacterOcclusionMaterialBuilder : GenericMaterialBuilder +{ + private readonly MaterialSet set; + private readonly DataProvider dataProvider; + + public CharacterOcclusionMaterialBuilder(string name, MaterialSet set, DataProvider dataProvider) : base(name, set, dataProvider) + { + this.set = set; + this.dataProvider = dataProvider; + } + + public override MeddleMaterialBuilder Apply() + { + base.Apply(); + WithDoubleSide(set.RenderBackfaces); + WithBaseColor(new Vector4(1, 1, 1, 0f)); + WithAlpha(AlphaMode.BLEND, 0.5f); + Extras = set.ComposeExtrasNode(); + return this; + } +} diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs new file mode 100644 index 0000000..6f730e1 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs @@ -0,0 +1,69 @@ +using System.Numerics; +using Meddle.Utils; +using Meddle.Utils.Export; +using Meddle.Utils.Files; +using Meddle.Utils.Materials; +using Meddle.Utils.Models; +using SharpGLTF.Materials; + +namespace Meddle.Plugin.Models.Composer; + +public class CharacterTattooMaterialBuilder : MeddleMaterialBuilder +{ + private readonly MaterialSet set; + private readonly DataProvider dataProvider; + private readonly CustomizeParameter customizeParameter; + + public CharacterTattooMaterialBuilder(string name, MaterialSet set, DataProvider dataProvider, CustomizeParameter customizeParameter) : base(name) + { + this.set = set; + this.dataProvider = dataProvider; + this.customizeParameter = customizeParameter; + } + + public override MeddleMaterialBuilder Apply() + { + var hairType = set.GetShaderKeyOrDefault(ShaderCategory.CategoryHairType, (HairType)0); + + Vector3 color = hairType switch + { + HairType.Face => customizeParameter.OptionColor, + HairType.Hair => customizeParameter.MeshColor, + _ => Vector3.Zero + }; + + var normalTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerNormal).ToResource().ToTexture(); + var diffuseTexture = new SKTexture(normalTexture.Width, normalTexture.Height); + for (var x = 0; x < normalTexture.Width; x++) + for (var y = 0; y < normalTexture.Height; y++) + { + var normal = normalTexture[x, y].ToVector4(); + if (normal.Z != 0) + { + diffuseTexture[x, y] = new Vector4(color, normal.W).ToSkColor(); + } + else + { + diffuseTexture[x, y] = new Vector4(0, 0, 0, normal.W).ToSkColor(); + } + } + + WithBaseColor(dataProvider.CacheTexture(diffuseTexture, $"Computed/{set.ComputedTextureName("diffuse")}")); + WithNormal(dataProvider.CacheTexture(normalTexture, $"Computed/{set.ComputedTextureName("normal")}")); + + WithDoubleSide(set.RenderBackfaces); + + var alpha = set.GetConstantOrDefault(MaterialConstant.g_AlphaThreshold, 0.0f); + if (alpha > 0) + { + WithAlpha(AlphaMode.BLEND, alpha); + } + else + { + WithAlpha(AlphaMode.BLEND); + } + + Extras = set.ComposeExtrasNode(); + return this; + } +} diff --git a/Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs b/Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs new file mode 100644 index 0000000..0e75941 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs @@ -0,0 +1,152 @@ +using System.Collections.Concurrent; +using Meddle.Plugin.Utils; +using Meddle.Utils; +using Meddle.Utils.Files; +using Meddle.Utils.Files.SqPack; +using Meddle.Utils.Models; +using Microsoft.Extensions.Logging; +using SharpGLTF.Materials; +using SharpGLTF.Memory; +using SkiaSharp; + +namespace Meddle.Plugin.Models.Composer; + +public class DataProvider +{ + private readonly string cacheDir; + private readonly SqPack dataManager; + private readonly ILogger logger; + private readonly CancellationToken cancellationToken; + + private readonly ConcurrentDictionary> lookupCache = new(); + private readonly ConcurrentDictionary> mtrlCache = new(); + private readonly ConcurrentDictionary> mtrlFileCache = new(); + private readonly ConcurrentDictionary> shpkFileCache = new(); + + + public DataProvider(string cacheDir, SqPack dataManager, ILogger logger, CancellationToken cancellationToken) + { + this.cacheDir = cacheDir; + this.dataManager = dataManager; + this.logger = logger; + this.cancellationToken = cancellationToken; + } + + public MaterialBuilder GetMaterialBuilder(MaterialSet material, string path, string shpkName) + { + return mtrlCache.GetOrAdd(material.Uid(), key => + { + cancellationToken.ThrowIfCancellationRequested(); + return new Lazy(() => + { + logger.LogInformation("[{shpkName}] Composing material {path}", shpkName, path); + return material.Compose(this); + }, LazyThreadSafetyMode.ExecutionAndPublication); + }).Value; + } + + public MtrlFile GetMtrlFile(string path) + { + return mtrlFileCache.GetOrAdd(path, key => + { + var mtrlData = LookupData(key); + if (mtrlData == null) throw new Exception($"Failed to load material file: {key}"); + return new Lazy(() => new MtrlFile(mtrlData), LazyThreadSafetyMode.ExecutionAndPublication); + }).Value; + } + + public ShpkFile GetShpkFile(string fullPath) + { + return shpkFileCache.GetOrAdd(fullPath, key => + { + var shpkData = LookupData(key); + if (shpkData == null) throw new Exception($"Failed to load shader package file: {key}"); + return new Lazy(() => new ShpkFile(shpkData), LazyThreadSafetyMode.ExecutionAndPublication); + }).Value; + } + + public byte[]? LookupData(string fullPath) + { + fullPath = fullPath.TrimHandlePath(); + var shortPath = fullPath; + if (Path.IsPathRooted(fullPath)) + { + shortPath = Path.GetFileName(fullPath); + shortPath = Path.Combine("Rooted", shortPath); + } + var diskPath = lookupCache.GetOrAdd(fullPath, key => + { + cancellationToken.ThrowIfCancellationRequested(); + return new Lazy(() => LookupDataInner(key, shortPath), LazyThreadSafetyMode.ExecutionAndPublication); + }); + + return diskPath.Value == null ? null : File.ReadAllBytes(diskPath.Value); + } + + private string? LookupDataInner(string fullPath, string shortPath) + { + var outPath = Path.Combine(cacheDir, shortPath); + var outDir = Path.GetDirectoryName(outPath); + var data = dataManager.GetFileOrReadFromDisk(fullPath); + if (data == null) + { + logger.LogError("Failed to load file: {path}", fullPath); + return null; + } + + if (!string.IsNullOrEmpty(outPath)) + { + if (!string.IsNullOrEmpty(outDir) && !Directory.Exists(outDir)) + { + Directory.CreateDirectory(outDir); + } + + File.WriteAllBytes(outPath, data); + if (fullPath.EndsWith(".tex")) + { + try + { + var texFile = new TexFile(data).ToResource().ToTexture(); + CacheTexture(texFile, shortPath); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to cache tex file: {path}", fullPath); + } + } + } + + return outPath; + } + + public ImageBuilder CacheTexture(SKTexture texture, string texName) + { + texName = texName.TrimHandlePath(); + if (Path.IsPathRooted(texName)) + { + throw new ArgumentException("Texture name cannot be rooted", nameof(texName)); + } + + var outPath = Path.Combine(cacheDir, $"{texName}.png"); + byte[] textureBytes; + using (var memoryStream = new MemoryStream()) + { + texture.Bitmap.Encode(memoryStream, SKEncodedImageFormat.Png, 100); + textureBytes = memoryStream.ToArray(); + } + + var outDir = Path.GetDirectoryName(outPath); + if (!string.IsNullOrEmpty(outDir) && !Directory.Exists(outDir)) + { + Directory.CreateDirectory(outDir); + } + + File.WriteAllBytes(outPath, textureBytes); + var outImage = new MemoryImage(() => File.ReadAllBytes(outPath)); + + var name = Path.GetFileNameWithoutExtension(texName.Replace('.', '_')); + var builder = ImageBuilder.From(outImage, name); + builder.AlternateWriteFileName = $"{name}.*"; + return builder; + } +} diff --git a/Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs index f2d842f..5deb633 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs @@ -1,53 +1,70 @@ using Meddle.Utils; using Meddle.Utils.Export; using Meddle.Utils.Files; -using Meddle.Utils.Materials; -using Meddle.Utils.Models; using SharpGLTF.Materials; namespace Meddle.Plugin.Models.Composer; -public class GenericMaterialBuilder : InstanceMaterialBuilder +public class GenericMaterialBuilder : MeddleMaterialBuilder { private readonly MaterialSet set; - - public GenericMaterialBuilder(string name, MaterialSet set, Func lookupFunc, Func cacheFunc) : base(name, set.ShpkName, lookupFunc, cacheFunc) + private readonly DataProvider dataProvider; + + public GenericMaterialBuilder(string name, MaterialSet set, DataProvider dataProvider) : base(name) { this.set = set; + this.dataProvider = dataProvider; } - public GenericMaterialBuilder WithGeneric() + public override MeddleMaterialBuilder Apply() { var alphaThreshold = set.GetConstantOrDefault(MaterialConstant.g_AlphaThreshold, 0.0f); if (alphaThreshold > 0) WithAlpha(AlphaMode.MASK, alphaThreshold); - var texturePaths = set.File.GetTexturePaths(); var setTypes = new HashSet(); - foreach (var sampler in set.File.Samplers) + // TODO: + foreach (var textureUsage in set.TextureUsageDict) { - if (sampler.TextureIndex == byte.MaxValue) continue; - var textureInfo = set.File.TextureOffsets[sampler.TextureIndex]; - var texturePath = texturePaths[textureInfo.Offset]; - // bg textures can have additional textures, which may be dummy textures, ignore them - if (texturePath.Contains("dummy_")) continue; - if (!set.Package.TextureLookup.TryGetValue(sampler.SamplerId, out var usage)) - { - continue; - } - var texData = LookupFunc(texturePath); + var path = set.MapTexturePath(textureUsage.Value); + var texData = dataProvider.LookupData(path); if (texData == null) continue; + // caching the texture regardless of usage, but only applying it to the material if it's a known channel var texture = new TexFile(texData).ToResource().ToTexture(); - var tex = CacheFunc(texture, texturePath); - var channel = MaterialUtility.MapTextureUsageToChannel(usage); - if (channel != null && setTypes.Add(usage)) + var tex = dataProvider.CacheTexture(texture, textureUsage.Value); + + var channel = MapTextureUsageToChannel(textureUsage.Key); + if (channel != null && setTypes.Add(textureUsage.Key)) { WithChannelImage(channel.Value, tex); } } - + Extras = set.ComposeExtrasNode(); return this; } + + public static KnownChannel? MapTextureUsageToChannel(TextureUsage usage) + { + return usage switch + { + TextureUsage.g_SamplerDiffuse => KnownChannel.BaseColor, + TextureUsage.g_SamplerNormal => KnownChannel.Normal, + TextureUsage.g_SamplerMask => KnownChannel.SpecularFactor, + TextureUsage.g_SamplerSpecular => KnownChannel.SpecularColor, + TextureUsage.g_SamplerCatchlight => KnownChannel.Emissive, + TextureUsage.g_SamplerColorMap0 => KnownChannel.BaseColor, + TextureUsage.g_SamplerNormalMap0 => KnownChannel.Normal, + TextureUsage.g_SamplerSpecularMap0 => KnownChannel.SpecularColor, + TextureUsage.g_SamplerColorMap1 => KnownChannel.BaseColor, + TextureUsage.g_SamplerNormalMap1 => KnownChannel.Normal, + TextureUsage.g_SamplerSpecularMap1 => KnownChannel.SpecularColor, + TextureUsage.g_SamplerColorMap => KnownChannel.BaseColor, + TextureUsage.g_SamplerNormalMap => KnownChannel.Normal, + TextureUsage.g_SamplerSpecularMap => KnownChannel.SpecularColor, + TextureUsage.g_SamplerNormal2 => KnownChannel.Normal, + _ => null + }; + } } diff --git a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs index 5000907..1ca8fa7 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs @@ -7,54 +7,52 @@ using Meddle.Utils.Export; using Meddle.Utils.Files; using Meddle.Utils.Files.SqPack; -using Meddle.Utils.Materials; using Meddle.Utils.Models; using Microsoft.Extensions.Logging; using SharpGLTF.Materials; -using SharpGLTF.Memory; using SharpGLTF.Scenes; -using SkiaSharp; namespace Meddle.Plugin.Models.Composer; public class InstanceComposer : IDisposable { - - - public InstanceComposer(ILogger log, SqPack manager, - Configuration config, - ParsedInstance[] instances, - string? cacheDir = null, - Action? progress = null, - bool bakeMaterials = true, - CancellationToken cancellationToken = default) + private readonly CancellationToken cancellationToken; + private readonly Configuration config; + private readonly int count; + private readonly SqPack dataManager; + private readonly ParsedInstance[] instances; + private readonly ILogger log; + private readonly Action? progress; + private int countProgress; + private readonly DataProvider dataProvider; + + public InstanceComposer( + ILogger log, SqPack manager, + Configuration config, + ParsedInstance[] instances, + string? cacheDir = null, + Action? progress = null, + CancellationToken cancellationToken = default) { CacheDir = cacheDir ?? Path.GetTempPath(); Directory.CreateDirectory(CacheDir); this.instances = instances; this.log = log; - this.dataManager = manager; + dataManager = manager; this.config = config; this.progress = progress; - this.bakeMaterials = bakeMaterials; this.cancellationToken = cancellationToken; - this.count = instances.Length; + count = instances.Length; + dataProvider = new DataProvider(CacheDir, dataManager, log, cancellationToken); } - private readonly ILogger log; - private readonly SqPack dataManager; - private readonly Configuration config; - private readonly Action? progress; - private readonly bool bakeMaterials; - private readonly CancellationToken cancellationToken; - private readonly int count; - private int countProgress; public string CacheDir { get; } - private readonly ParsedInstance[] instances; - private readonly ConcurrentDictionary imageCache = new(); - private readonly ConcurrentDictionary shpkCache = new(); - private readonly ConcurrentDictionary mtrlCache = new(); - + + public void Dispose() + { + cancellationToken.ThrowIfCancellationRequested(); + } + private void Iterate(Action action, bool parallel) { if (parallel) @@ -73,7 +71,7 @@ private void Iterate(Action action, bool parallel) } } } - + public void Compose(SceneBuilder scene) { progress?.Invoke(new ProgressEvent("Export", 0, count)); @@ -96,6 +94,16 @@ public void Compose(SceneBuilder scene) Interlocked.Increment(ref countProgress); progress?.Invoke(new ProgressEvent("Export", countProgress, count)); }, false); + + try + { + var computeDir = Path.Combine(CacheDir, "Computed"); + Directory.Delete(computeDir, true); + } + catch (Exception ex) + { + log.LogError(ex, "Failed to delete computed directory"); + } } public NodeBuilder? ComposeInstance(SceneBuilder scene, ParsedInstance parsedInstance) @@ -110,8 +118,8 @@ public void Compose(SceneBuilder scene) { root.Name = $"{parsedInstance.Type}_{parsedInstance.Id}"; } - - bool wasAdded = false; + + var wasAdded = false; if (parsedInstance is ParsedBgPartsInstance {Path: not null} bgPartsInstance) { var meshes = ComposeBgPartsInstance(bgPartsInstance); @@ -119,23 +127,26 @@ public void Compose(SceneBuilder scene) { scene.AddRigidMesh(mesh.Mesh, root, Matrix4x4.Identity); } - + wasAdded = true; } - if (parsedInstance is ParsedCharacterInstance { CharacterInfo: not null } characterInstance) + if (parsedInstance is ParsedCharacterInstance {CharacterInfo: not null} characterInstance) { if (characterInstance.Kind == ObjectKind.Pc && !string.IsNullOrWhiteSpace(config.PlayerNameOverride)) { root.Name = $"{characterInstance.Type}_{characterInstance.Kind}_{config.PlayerNameOverride}"; } else - { + { root.Name = $"{characterInstance.Type}_{characterInstance.Kind}_{characterInstance.Name}"; } - ComposeCharacterInstance(characterInstance, scene, root); + + var characterComposer = new CharacterComposer(log, dataProvider); + characterComposer.ComposeCharacterInstance(characterInstance, scene, root); wasAdded = true; } + if (parsedInstance is ParsedLightInstance lightInstance) { // TODO: Probably can fill some parts here given more info @@ -162,8 +173,9 @@ public void Compose(SceneBuilder scene) root.AddNode(childNode); wasAdded = true; } - - progress?.Invoke(new ProgressEvent("Shared Instance", countProgress, count, new ProgressEvent(root.Name, i, sharedInstance.Children.Count))); + + progress?.Invoke(new ProgressEvent("Shared Instance", countProgress, count, + new ProgressEvent(root.Name, i, sharedInstance.Children.Count))); } } @@ -172,221 +184,35 @@ public void Compose(SceneBuilder scene) root.SetLocalTransform(parsedInstance.Transform.AffineTransform, true); return root; } - - return null; - } - - private TexFile? cubeMapTex; - private PbdFile? pbdFile; - - private void EnsureBonesExist(Model model, List bones, BoneNodeBuilder? root) - { - foreach (var mesh in model.Meshes) - { - if (mesh.BoneTable == null) continue; - - foreach (var boneName in mesh.BoneTable) - { - if (bones.All(b => !b.BoneName.Equals(boneName, StringComparison.Ordinal))) - { - log.LogInformation("Adding bone {BoneName} from mesh {MeshPath}", boneName, - model.Path); - var bone = new BoneNodeBuilder(boneName); - if (root == null) throw new InvalidOperationException("Root bone not found"); - root.AddNode(bone); - log.LogInformation("Added bone {BoneName} to {ParentBone}", boneName, root.BoneName); - - bones.Add(bone); - } - } - } - } - - private void ComposeCharacterInstance(ParsedCharacterInstance characterInstance, SceneBuilder scene, NodeBuilder root) - { - if (cubeMapTex == null) - { - var catchlight = dataManager.GetFileOrReadFromDisk("chara/common/texture/sphere_d_array.tex"); - if (catchlight == null) throw new Exception("Failed to load catchlight texture"); - cubeMapTex = new TexFile(catchlight); - } - - if (pbdFile == null) - { - var pbdData = dataManager.GetFileOrReadFromDisk("chara/xls/boneDeformer/human.pbd"); - if (pbdData == null) throw new Exception("Failed to load human.pbd"); - pbdFile = new PbdFile(pbdData); - } - - var characterInfo = characterInstance.CharacterInfo; - if (characterInfo == null) return; - - var bones = SkeletonUtils.GetBoneMap(characterInfo.Skeleton, true, out var rootBone); - if (rootBone != null) - { - root.AddNode(rootBone); - } - - for (var i = 0; i < characterInfo.Models.Count; i++) - { - var modelInfo = characterInfo.Models[i]; - if (modelInfo.PathFromCharacter.Contains("b0003_top")) continue; - var mdlData = dataManager.GetFileOrReadFromDisk(modelInfo.Path); - if (mdlData == null) - { - log.LogWarning("Failed to load model file: {modelPath}", modelInfo.Path); - continue; - } - - log.LogInformation("Loaded model {modelPath}", modelInfo.Path); - var mdlFile = new MdlFile(mdlData); - var materialBuilders = new List(); - foreach (var materialInfo in modelInfo.Materials) - { - var mtrlData = dataManager.GetFileOrReadFromDisk(materialInfo.Path); - if (mtrlData == null) - { - log.LogWarning("Failed to load material file: {mtrlPath}", materialInfo.Path); - throw new Exception($"Failed to load material file: {materialInfo.Path}"); - } - - log.LogInformation("Loaded material {mtrlPath}", materialInfo.Path); - var mtrlFile = new MtrlFile(mtrlData); - if (materialInfo.ColorTable != null) - { - mtrlFile.ColorTable = materialInfo.ColorTable.Value; - } - - var shpkName = mtrlFile.GetShaderPackageName(); - var shpkPath = $"shader/sm5/shpk/{shpkName}"; - if (!shpkCache.TryGetValue(shpkPath, out var shader)) - { - var shpkData = dataManager.GetFileOrReadFromDisk(shpkPath); - if (shpkData == null) throw new Exception($"Failed to load shader package file: {shpkPath}"); - var shpkFile = new ShpkFile(shpkData); - shader = (shpkFile, new ShaderPackage(shpkFile, null!)); - shpkCache.TryAdd(shpkPath, shader); - log.LogInformation("Loaded shader package {shpkPath}", shpkPath); - } - - var texDict = new Dictionary(); - - foreach (var textureInfo in materialInfo.Textures) - { - texDict[textureInfo.PathFromMaterial] = textureInfo.Resource; - } - - var material = new Material(materialInfo.Path, mtrlFile, texDict, shader.File); - var customizeParams = characterInfo.CustomizeParameter; - var customizeData = characterInfo.CustomizeData; - var name = $"{Path.GetFileNameWithoutExtension(materialInfo.PathFromModel)}_{Path.GetFileNameWithoutExtension(shpkName)}_{characterInstance.Id}"; - var builder = material.ShaderPackageName switch - { - "bg.shpk" => MaterialUtility.BuildBg(material, name), - "bgprop.shpk" => MaterialUtility.BuildBgProp(material, name), - "character.shpk" => MaterialUtility.BuildCharacter(material, name), - "characterocclusion.shpk" => MaterialUtility.BuildCharacterOcclusion(material, name), - "characterlegacy.shpk" => MaterialUtility.BuildCharacterLegacy(material, name), - "charactertattoo.shpk" => MaterialUtility.BuildCharacterTattoo( - material, name, customizeParams, customizeData), - "hair.shpk" => MaterialUtility.BuildHair(material, name, customizeParams, customizeData), - "skin.shpk" => MaterialUtility.BuildSkin(material, name, customizeParams, customizeData), - "iris.shpk" => - MaterialUtility.BuildIris(material, name, cubeMapTex, customizeParams, customizeData), - "water.shpk" => MaterialUtility.BuildWater(material, name), - "lightshaft.shpk" => MaterialUtility.BuildLightShaft(material, name), - _ => ComposeMaterial(materialInfo.Path, characterInstance) - }; - - materialBuilders.Add(builder); - } - - var model = new Model(modelInfo.Path, mdlFile, modelInfo.ShapeAttributeGroup); - EnsureBonesExist(model, bones, rootBone); - (GenderRace from, GenderRace to, RaceDeformer deformer)? deform; - if (modelInfo.Deformer != null) - { - // custom pbd may exist - var pbdFileData = dataManager.GetFileOrReadFromDisk(modelInfo.Deformer.Value.PbdPath); - if (pbdFileData == null) - throw new InvalidOperationException( - $"Failed to get deformer pbd {modelInfo.Deformer.Value.PbdPath}"); - deform = ((GenderRace)modelInfo.Deformer.Value.DeformerId, - (GenderRace)modelInfo.Deformer.Value.RaceSexId, - new RaceDeformer(new PbdFile(pbdFileData), bones)); - log.LogDebug("Using deformer pbd {Path}", modelInfo.Deformer.Value.PbdPath); - } - else - { - var parsed = RaceDeformer.ParseRaceCode(modelInfo.PathFromCharacter); - if (Enum.IsDefined(parsed)) - { - deform = (parsed, characterInfo.GenderRace, new RaceDeformer(pbdFile, bones)); - } - else - { - deform = null; - } - } - - var meshes = ModelBuilder.BuildMeshes(model, materialBuilders, bones, deform); - foreach (var mesh in meshes) - { - InstanceBuilder instance; - if (bones.Count > 0) - { - instance = scene.AddSkinnedMesh(mesh.Mesh, Matrix4x4.Identity, bones.Cast().ToArray()); - } - else - { - instance = scene.AddRigidMesh(mesh.Mesh, Matrix4x4.Identity); - } - if (model.Shapes.Count != 0 && mesh.Shapes != null) - { - // This will set the morphing value to 1 if the shape is enabled, 0 if not - var enabledShapes = Model.GetEnabledValues(model.EnabledShapeMask, model.ShapeMasks) - .ToArray(); - var shapes = model.Shapes - .Where(x => mesh.Shapes.Contains(x.Name)) - .Select(x => (x, enabledShapes.Contains(x.Name))); - instance.Content.UseMorphing().SetValue(shapes.Select(x => x.Item2 ? 1f : 0).ToArray()); - } - - if (mesh.Submesh != null) - { - // Remove subMeshes that are not enabled - var enabledAttributes = Model.GetEnabledValues(model.EnabledAttributeMask, model.AttributeMasks); - if (!mesh.Submesh.Attributes.All(enabledAttributes.Contains)) - { - instance.Remove(); - } - } - } - - progress?.Invoke(new ProgressEvent("Character Instance", countProgress, count, new ProgressEvent(root.Name, i, characterInfo.Models.Count))); - } + return null; } private void ComposeTerrainInstance(ParsedTerrainInstance terrainInstance, SceneBuilder scene, NodeBuilder root) { var teraPath = $"{terrainInstance.Path}/bgplate/terrain.tera"; - var teraData = dataManager.GetFileOrReadFromDisk(teraPath); + var teraData = dataProvider.LookupData(teraPath); if (teraData == null) throw new Exception($"Failed to load terrain file: {teraPath}"); var teraFile = new TeraFile(teraData); - - for (var i = 0; i < teraFile.Header.PlateCount; i++) + + //for (var i = 0; i < teraFile.Header.PlateCount; i++) + var processed = 0; + Parallel.For(0, teraFile.Header.PlateCount, new ParallelOptions + { + CancellationToken = cancellationToken, + MaxDegreeOfParallelism = Math.Max(Environment.ProcessorCount / 2, 1) + }, i => { if (cancellationToken.IsCancellationRequested) return; log.LogInformation("Parsing plate {i}", i); - var platePos = teraFile.GetPlatePosition(i); + var platePos = teraFile.GetPlatePosition((int)i); var plateTransform = new Transform(new Vector3(platePos.X, 0, platePos.Y), Quaternion.Identity, Vector3.One); var mdlPath = $"{terrainInstance.Path}/bgplate/{i:D4}.mdl"; - var mdlData = dataManager.GetFileOrReadFromDisk(mdlPath); + var mdlData = dataProvider.LookupData(mdlPath); if (mdlData == null) throw new Exception($"Failed to load model file: {mdlPath}"); log.LogInformation("Loaded model {mdlPath}", mdlPath); var mdlFile = new MdlFile(mdlData); - + var materials = mdlFile.GetMaterialNames().Select(x => x.Value).ToArray(); var materialBuilders = new List(); foreach (var mtrlPath in materials) @@ -397,7 +223,7 @@ private void ComposeTerrainInstance(ParsedTerrainInstance terrainInstance, Scene var model = new Model(mdlPath, mdlFile, null); var meshes = ModelBuilder.BuildMeshes(model, materialBuilders, [], null); - + var plateRoot = new NodeBuilder($"Plate{i:D4}"); foreach (var mesh in meshes) { @@ -405,13 +231,16 @@ private void ComposeTerrainInstance(ParsedTerrainInstance terrainInstance, Scene } root.AddNode(plateRoot); - progress?.Invoke(new ProgressEvent("Terrain Instance", countProgress, count, new ProgressEvent(root.Name, i, (int)teraFile.Header.PlateCount))); - } + Interlocked.Increment(ref processed); + progress?.Invoke(new ProgressEvent("Terrain Instance", countProgress, count, + new ProgressEvent(root.Name, processed, + (int)teraFile.Header.PlateCount))); + }); } private IReadOnlyList ComposeBgPartsInstance(ParsedBgPartsInstance bgPartsInstance) { - var mdlData = dataManager.GetFileOrReadFromDisk(bgPartsInstance.Path); + var mdlData = dataProvider.LookupData(bgPartsInstance.Path); if (mdlData == null) { log.LogWarning("Failed to load model file: {bgPartsInstance.Path}", bgPartsInstance.Path); @@ -433,99 +262,35 @@ private void ComposeTerrainInstance(ParsedTerrainInstance terrainInstance, Scene return meshes; } + + + private MaterialBuilder ComposeMaterial(string path, ParsedInstance instance) { - if (mtrlCache.TryGetValue(path, out var cached)) - { - return cached; - } - - var mtrlData = dataManager.GetFileOrReadFromDisk(path); - if (mtrlData == null) throw new Exception($"Failed to load material file: {path}"); - log.LogInformation("Loaded material {path}", path); - - var mtrlFile = new MtrlFile(mtrlData); + // TODO: Really not ideal but can't rely on just the path since material inputs can change + var mtrlFile = dataProvider.GetMtrlFile(path); var shpkName = mtrlFile.GetShaderPackageName(); var shpkPath = $"shader/sm5/shpk/{shpkName}"; - if (!shpkCache.TryGetValue(shpkPath, out var shader)) - { - var shpkData = dataManager.GetFileOrReadFromDisk(shpkPath); - if (shpkData == null) - throw new Exception($"Failed to load shader package file: {shpkPath}"); - var shpkFile = new ShpkFile(shpkData); - shader = (shpkFile, new ShaderPackage(shpkFile, null!)); - shpkCache.TryAdd(shpkPath, shader); - log.LogInformation("Loaded shader package {shpkPath}", shpkPath); - } - var material = new MaterialSet(mtrlFile, path, shader.File, shpkName); - - var materialName = $"{Path.GetFileNameWithoutExtension(path)}_{Path.GetFileNameWithoutExtension(shpkName)}"; + var shpkFile = dataProvider.GetShpkFile(shpkPath); - if (bakeMaterials) + Dictionary textureMap = new(); + if (instance is ITextureMappableInstance mappableInstance) { - if (shpkName == "lightshaft.shpk") - { - return new LightshaftMaterialBuilder(materialName, - material, - dataManager.GetFileOrReadFromDisk, - CacheTexture) - .WithLightShaft(); - } - - if (shpkName is "bg.shpk" or "bgprop.shpk") - { - return new BgMaterialBuilder(materialName, shpkName, material, dataManager.GetFileOrReadFromDisk, - CacheTexture) - .WithBg(); - } - - if (shpkName == "bgcolorchange.shpk") - { - Vector4? stainColor = instance switch - { - IStainableInstance stainable => stainable.StainColor, - _ => null - }; - - return new BgMaterialBuilder(materialName, shpkName, material, dataManager.GetFileOrReadFromDisk, - CacheTexture) - .WithBgColorChange(stainColor); - } + textureMap = mappableInstance.TextureMap; } - - return new GenericMaterialBuilder(materialName, material, dataManager.GetFileOrReadFromDisk, CacheTexture) - .WithGeneric(); - } - - private ImageBuilder CacheTexture(SKTexture texture, string texName) - { - byte[] textureBytes; - using (var memoryStream = new MemoryStream()) + + var material = new MaterialSet(mtrlFile, path, shpkFile, shpkName); + if (instance is IStainableInstance stainableInstance) { - texture.Bitmap.Encode(memoryStream, SKEncodedImageFormat.Png, 100); - textureBytes = memoryStream.ToArray(); + material.SetStainColor(stainableInstance.StainColor); } - var outPath = Path.Combine(CacheDir, $"{texName}.png"); - var outDir = Path.GetDirectoryName(outPath); - if (!string.IsNullOrEmpty(outDir) && !Directory.Exists(outDir)) + if (instance is ICharacterInstance characterInstance) { - Directory.CreateDirectory(outDir); + material.SetCustomizeParameters(characterInstance.CustomizeParameter); + material.SetCustomizeData(characterInstance.CustomizeData); } - File.WriteAllBytes(outPath, textureBytes); - - var outImage = new MemoryImage(() => File.ReadAllBytes(outPath)); - imageCache.TryAdd(texName, (outPath, outImage)); - - var name = Path.GetFileNameWithoutExtension(texName.Replace('.', '_')); - var builder = ImageBuilder.From(outImage, name); - builder.AlternateWriteFileName = $"{name}.*"; - return builder; - } - - public void Dispose() - { - cancellationToken.ThrowIfCancellationRequested(); + return dataProvider.GetMaterialBuilder(material, path, shpkName); } } diff --git a/Meddle/Meddle.Plugin/Models/Composer/InstanceMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/InstanceMaterialBuilder.cs deleted file mode 100644 index 1af07e2..0000000 --- a/Meddle/Meddle.Plugin/Models/Composer/InstanceMaterialBuilder.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Meddle.Utils.Materials; -using Meddle.Utils.Models; -using SharpGLTF.Materials; -using SharpGLTF.Memory; - -namespace Meddle.Plugin.Models.Composer; - -public abstract class InstanceMaterialBuilder : XivMaterialBuilder -{ - protected readonly Func LookupFunc; - protected readonly Func CacheFunc; - - public InstanceMaterialBuilder(string name, string shpk, Func lookupFunc, Func cacheFunc) : base(name, shpk) - { - this.LookupFunc = lookupFunc; - this.CacheFunc = cacheFunc; - } -} diff --git a/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs index 6d614a4..edb1ad8 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs @@ -1,71 +1,24 @@ using System.Numerics; -using System.Text.Json; -using System.Text.Json.Nodes; -using Meddle.Utils; -using Meddle.Utils.Export; -using Meddle.Utils.Files; -using Meddle.Utils.Materials; -using Meddle.Utils.Models; using SharpGLTF.Materials; -using SharpGLTF.Memory; namespace Meddle.Plugin.Models.Composer; -public class LightshaftMaterialBuilder : InstanceMaterialBuilder +public class LightshaftMaterialBuilder : GenericMaterialBuilder { private readonly MaterialSet set; + private readonly DataProvider dataProvider; - public LightshaftMaterialBuilder(string name, MaterialSet set, Func lookupFunc, Func cacheFunc) : base(name, "lightshaft.shpk", lookupFunc, cacheFunc) + public LightshaftMaterialBuilder(string name, MaterialSet set, DataProvider dataProvider) : base(name, set, dataProvider) { this.set = set; + this.dataProvider = dataProvider; } - public LightshaftMaterialBuilder WithLightShaft() + public override MeddleMaterialBuilder Apply() { - var sampler0 = set.TextureUsageDict[TextureUsage.g_Sampler0]; - var sampler1 = set.TextureUsageDict[TextureUsage.g_Sampler1]; - var texture0 = LookupFunc(sampler0); - var texture1 = LookupFunc(sampler1); - if (texture0 == null || texture1 == null) - { - return this; - } - - var tex0 = new TexFile(texture0).ToResource(); - var tex1 = new TexFile(texture1).ToResource(); - - var size = Vector2.Max(tex0.Size, tex1.Size); - var res0 = tex0.ToTexture(size); - var res1 = tex1.ToTexture(size); - - - var outTexture = new SKTexture((int)size.X, (int)size.Y); - set.TryGetConstant(MaterialConstant.g_Color, out Vector3 color); - for (var x = 0; x < outTexture.Width; x++) - for (var y = 0; y < outTexture.Height; y++) - { - var tex0Color = res0[x, y].ToVector4(); - var tex1Color = res1[x, y].ToVector4(); - var outColor = new Vector4(color, 1); - - outTexture[x, y] = (outColor * tex0Color * tex1Color).ToSkColor(); - } - - var fileName = $"Computed/{Path.GetFileNameWithoutExtension(set.MtrlPath)}_lightshaft_diffuse"; - var diffuseImage = CacheFunc(outTexture, fileName); - //this.WithBaseColor(diffuseImage); + base.Apply(); this.WithBaseColor(new Vector4(1, 1, 1, 0)); - this.WithEmissive(diffuseImage); - - - - if (!set.TryGetConstant(MaterialConstant.g_AlphaThreshold, out float alphaThreshold)) - { - alphaThreshold = 0.5f; - } - - this.WithAlpha(AlphaMode.MASK, alphaThreshold); - + WithAlpha(AlphaMode.BLEND, 0.5f); Extras = set.ComposeExtrasNode(); return this; diff --git a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs index a8a8a37..90802bc 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs @@ -1,55 +1,172 @@ using System.Numerics; using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using Meddle.Utils.Export; using Meddle.Utils.Files; +using Meddle.Utils.Files.Structs.Material; +using Meddle.Utils.Materials; using Meddle.Utils.Models; +using SharpGLTF.Materials; namespace Meddle.Plugin.Models.Composer; public class MaterialSet { + /// + /// Should be appended to all computed textures, so we can skip recomputing them + /// + public int Uid() + { + // hoping this is enough + var hash = new HashCode(); + hash.Add(MtrlPath); + hash.Add(ShpkName); + hash.Add(stainColor); + hash.Add(customizeParameters?.GetHashCode()); + hash.Add(customizeData?.GetHashCode()); + foreach (var (key, value) in Constants) + { + hash.Add(key); + foreach (var v in value) + { + hash.Add(v); + } + } + foreach (var (key, value) in TextureUsageDict) + { + hash.Add(key); + hash.Add(value); + } + foreach (var key in ShaderKeys) + { + hash.Add(key.Category); + hash.Add(key.Value); + } + return hash.ToHashCode(); + } + + private static string GetHashSha1(byte[] data) + { + var hash = SHA1.HashData(data); + var sb = new StringBuilder(hash.Length * 2); + foreach (var b in hash) + { + sb.Append(b.ToString("X2")); + } + + return sb.ToString(); + } + + public string ComputedTextureName(string name) + { + return $"{Path.GetFileNameWithoutExtension(MtrlPath)}_" + + $"{Path.GetFileNameWithoutExtension(ShpkName)}_" + + $"{name}_{HashStr()}"; + } + + public string HashStr() + { + var hash = Uid(); + var hashStr = hash < 0 ? $"n{hash:X8}" : $"{hash:X8}"; + return hashStr; + } + + //private readonly MtrlFile file; + //private readonly ShpkFile shpk; + //private readonly ShaderPackage package; public readonly string MtrlPath; - public readonly MtrlFile File; - public readonly ShpkFile Shpk; public readonly string ShpkName; - public readonly ShaderPackage Package; public readonly ShaderKey[] ShaderKeys; - public readonly Dictionary MaterialConstantDict; + public readonly Dictionary Constants; public readonly Dictionary TextureUsageDict; - - public Dictionary GetConstants() + private Dictionary? texturePathMappings; + public string MapTexturePath(string path) { - var dict = new Dictionary(); - foreach (var (key, value) in Package.MaterialConstants) + if (texturePathMappings != null) { - var values = new float[value.Length]; - value.CopyTo(values, 0); - dict[key] = values; + if (texturePathMappings.TryGetValue(path, out var mappedPath)) + { + return mappedPath; + } } - foreach (var (key, value) in MaterialConstantDict) + return path; + } + + public TexFile GetTexture(DataProvider provider, TextureUsage usage) + { + var path = GetTexturePath(usage); + var data = provider.LookupData(MapTexturePath(path)); + if (data == null) { - var values = new float[value.Length]; - value.CopyTo(values, 0); - dict[key] = values; + throw new Exception($"Failed to load texture for {usage}"); } - return dict; + return new TexFile(data); } - public bool TryGetConstant(MaterialConstant id, out float[] value) + public byte[] LookupData(DataProvider provider, string path) { - if (MaterialConstantDict.TryGetValue(id, out var values)) + var data = provider.LookupData(MapTexturePath(path)); + if (data == null) { - value = values; - return true; + throw new Exception($"Failed to load data for {path}"); } - if (Package.MaterialConstants.TryGetValue(id, out var constant)) + return data; + } + + public string GetTexturePath(TextureUsage usage) + { + var path = TextureUsageDict[usage]; + if (texturePathMappings != null && texturePathMappings.TryGetValue(usage.ToString(), out var mappedPath)) { - value = constant; + return mappedPath; + } + + return path; + } + + public void SetTexturePathMappings(Dictionary mappings) + { + texturePathMappings = mappings; + } + + private uint ShaderFlagData; + public bool RenderBackfaces => (ShaderFlagData & (uint)ShaderFlags.HideBackfaces) == 0; + public bool IsTransparent => (ShaderFlagData & (uint)ShaderFlags.EnableTranslucency) != 0; + + + public readonly ColorTable? ColorTable; + + // BgColorChange + private Vector4? stainColor; + public void SetStainColor(Vector4? color) + { + stainColor = color; + } + + // Character + private CustomizeParameter? customizeParameters; + public void SetCustomizeParameters(CustomizeParameter parameters) + { + customizeParameters = parameters; + } + + private CustomizeData? customizeData; + public void SetCustomizeData(CustomizeData data) + { + customizeData = data; + } + + public bool TryGetConstant(MaterialConstant id, out float[] value) + { + if (Constants.TryGetValue(id, out var values)) + { + value = values; return true; } @@ -59,108 +176,111 @@ public bool TryGetConstant(MaterialConstant id, out float[] value) public bool TryGetConstant(MaterialConstant id, out float value) { - if (MaterialConstantDict.TryGetValue(id, out var values)) + if (Constants.TryGetValue(id, out var values)) { value = values[0]; return true; } - if (Package.MaterialConstants.TryGetValue(id, out var constant)) - { - value = constant[0]; - return true; - } - value = 0; return false; } public bool TryGetConstant(MaterialConstant id, out Vector2 value) { - if (MaterialConstantDict.TryGetValue(id, out var values)) + if (Constants.TryGetValue(id, out var values)) { value = new Vector2(values[0], values[1]); return true; } - if (Package.MaterialConstants.TryGetValue(id, out var constant)) - { - value = new Vector2(constant[0], constant[1]); - return true; - } - value = Vector2.Zero; return false; } public bool TryGetConstant(MaterialConstant id, out Vector3 value) { - if (MaterialConstantDict.TryGetValue(id, out var values)) + if (Constants.TryGetValue(id, out var values)) { value = new Vector3(values[0], values[1], values[2]); return true; } - if (Package.MaterialConstants.TryGetValue(id, out var constant)) - { - value = new Vector3(constant[0], constant[1], constant[2]); - return true; - } - value = Vector3.Zero; return false; } public bool TryGetConstant(MaterialConstant id, out Vector4 value) { - if (MaterialConstantDict.TryGetValue(id, out var values)) + if (Constants.TryGetValue(id, out var values)) { value = new Vector4(values[0], values[1], values[2], values[3]); return true; } - if (Package.MaterialConstants.TryGetValue(id, out var constant)) - { - value = new Vector4(constant[0], constant[1], constant[2], constant[3]); - return true; - } - value = Vector4.Zero; return false; } public float GetConstantOrDefault(MaterialConstant id, float @default) { - return MaterialConstantDict.TryGetValue(id, out var values) ? values[0] : @default; + return Constants.TryGetValue(id, out var values) ? values[0] : @default; } public Vector2 GetConstantOrDefault(MaterialConstant id, Vector2 @default) { - return MaterialConstantDict.TryGetValue(id, out var values) ? new Vector2(values[0], values[1]) : @default; + return Constants.TryGetValue(id, out var values) ? new Vector2(values[0], values[1]) : @default; } public Vector3 GetConstantOrDefault(MaterialConstant id, Vector3 @default) { - return MaterialConstantDict.TryGetValue(id, out var values) + return Constants.TryGetValue(id, out var values) ? new Vector3(values[0], values[1], values[2]) : @default; } public Vector4 GetConstantOrDefault(MaterialConstant id, Vector4 @default) { - return MaterialConstantDict.TryGetValue(id, out var values) + return Constants.TryGetValue(id, out var values) ? new Vector4(values[0], values[1], values[2], values[3]) : @default; } + + public TValue GetShaderKeyOrDefault(TCategory category, TValue @default) where TCategory : Enum where TValue : Enum + { + var cat = Convert.ToUInt32(category); + var value = GetShaderKeyOrDefault(cat, Convert.ToUInt32(@default)); + return (TValue)Enum.ToObject(typeof(TValue), value); + } + + public TValue GetShaderKeyOrDefault(TCategory category, uint @default) where TCategory : Enum where TValue : Enum + { + var cat = Convert.ToUInt32(category); + var value = GetShaderKeyOrDefault(cat, @default); + return (TValue)Enum.ToObject(typeof(TValue), value); + } + + public uint GetShaderKeyOrDefault(uint category, uint @default) + { + foreach (var key in ShaderKeys) + { + if (key.Category == category) + { + return key.Value; + } + } + + return @default; + } public MaterialSet(MtrlFile file, string mtrlPath, ShpkFile shpk, string shpkName) { this.MtrlPath = mtrlPath; - this.File = file; - this.Shpk = shpk; this.ShpkName = shpkName; - this.Package = new ShaderPackage(shpk, shpkName); - + ShaderFlagData = file.ShaderHeader.Flags; + var package = new ShaderPackage(shpk, shpkName); + ColorTable = file.ColorTable; + ShaderKeys = new ShaderKey[file.ShaderKeys.Length]; for (var i = 0; i < file.ShaderKeys.Length; i++) { @@ -171,7 +291,14 @@ public MaterialSet(MtrlFile file, string mtrlPath, ShpkFile shpk, string shpkNam }; } - MaterialConstantDict = new Dictionary(); + Constants = new Dictionary(); + // pre-fill with shader constants + foreach (var constant in package.MaterialConstants) + { + Constants[constant.Key] = constant.Value; + } + + // override with material constants foreach (var constant in file.Constants) { var index = constant.ValueOffset / 4; @@ -193,7 +320,7 @@ public MaterialSet(MtrlFile file, string mtrlPath, ShpkFile shpk, string shpkNam // even if duplicate, last probably takes precedence var id = (MaterialConstant)constant.ConstantId; - MaterialConstantDict[id] = values; + Constants[id] = values; } TextureUsageDict = new Dictionary(); @@ -203,7 +330,7 @@ public MaterialSet(MtrlFile file, string mtrlPath, ShpkFile shpk, string shpkNam if (sampler.TextureIndex == byte.MaxValue) continue; var textureInfo = file.TextureOffsets[sampler.TextureIndex]; var texturePath = texturePaths[textureInfo.Offset]; - if (!Package.TextureLookup.TryGetValue(sampler.SamplerId, out var usage)) + if (!package.TextureLookup.TryGetValue(sampler.SamplerId, out var usage)) { continue; } @@ -212,17 +339,47 @@ public MaterialSet(MtrlFile file, string mtrlPath, ShpkFile shpk, string shpkNam } } - - public static JsonSerializerOptions JsonOptions => new() { IncludeFields = true }; + + private MeddleMaterialBuilder GetMaterialBuilder(DataProvider dataProvider) + { + var mtrlName = $"{Path.GetFileNameWithoutExtension(MtrlPath)}_{Path.GetFileNameWithoutExtension(ShpkName)}_{Uid()}"; + switch (ShpkName) + { + case "bg.shpk": + return new BgMaterialBuilder(mtrlName, new BgParams(), this, dataProvider); + case "bgcolorchange.shpk": + return new BgMaterialBuilder(mtrlName, new BgColorChangeParams(stainColor), this, dataProvider); + case "lightshaft.shpk": + return new LightshaftMaterialBuilder(mtrlName, this, dataProvider); + case "character.shpk": + return new CharacterMaterialBuilder(mtrlName, this, dataProvider); + case "charactertattoo.shpk": + return new CharacterTattooMaterialBuilder(mtrlName, this, dataProvider, customizeParameters ?? new CustomizeParameter()); + case "characterocclusion.shpk": + return new CharacterOcclusionMaterialBuilder(mtrlName, this, dataProvider); + case "skin.shpk": + return new SkinMaterialBuilder(mtrlName, this, dataProvider, customizeParameters ?? new CustomizeParameter(), customizeData ?? new CustomizeData()); + default: + return new GenericMaterialBuilder(mtrlName, this, dataProvider); + } + } + + public MaterialBuilder Compose(DataProvider dataProvider) + { + var builder = GetMaterialBuilder(dataProvider); + return builder.Apply(); + } + public JsonNode ComposeExtrasNode() { var extrasDict = ComposeExtras(); return JsonNode.Parse(JsonSerializer.Serialize(extrasDict, JsonOptions))!; } + public Dictionary ComposeExtras() { var extrasDict = new Dictionary @@ -233,9 +390,35 @@ public Dictionary ComposeExtras() AddConstants(); AddSamplers(); AddShaderKeys(); + AddCustomizeParameters(); + AddCustomizeData(); + AddColorTable(); + + if (stainColor.HasValue) + { + extrasDict["stainColor"] = stainColor.Value.AsFloatArray(); + } return extrasDict; + void AddCustomizeParameters() + { + if (customizeParameters == null) return; + extrasDict["CustomizeParameters"] = JsonNode.Parse(JsonSerializer.Serialize(customizeParameters, JsonOptions))!; + } + + void AddColorTable() + { + if (ColorTable == null) return; + extrasDict["ColorTable"] = JsonNode.Parse(JsonSerializer.Serialize(ColorTable, JsonOptions))!; + } + + void AddCustomizeData() + { + if (customizeData == null) return; + extrasDict["CustomizeData"] = JsonNode.Parse(JsonSerializer.Serialize(customizeData, JsonOptions))!; + } + void AddShaderKeys() { foreach (var key in ShaderKeys) @@ -256,7 +439,7 @@ void AddSamplers() void AddConstants() { - foreach (var (constant, value) in GetConstants()) + foreach (var (constant, value) in Constants) { if (Enum.IsDefined(typeof(MaterialConstant), constant)) { diff --git a/Meddle/Meddle.Plugin/Models/Composer/MeddleMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/MeddleMaterialBuilder.cs new file mode 100644 index 0000000..6a2ca15 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/MeddleMaterialBuilder.cs @@ -0,0 +1,25 @@ +using Meddle.Utils; +using Meddle.Utils.Files; +using SharpGLTF.Materials; + +namespace Meddle.Plugin.Models.Composer; + +public abstract class MeddleMaterialBuilder : MaterialBuilder +{ + public MeddleMaterialBuilder(string name) : base(name) + { + } + + public abstract MeddleMaterialBuilder Apply(); + + protected void SaveAllTextures(MaterialSet set, DataProvider provider) + { + foreach (var textureUsage in set.TextureUsageDict) + { + var texData = provider.LookupData(textureUsage.Value); + if (texData == null) continue; + var texture = new TexFile(texData).ToResource().ToTexture(); + provider.CacheTexture(texture, textureUsage.Value); + } + } +} diff --git a/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs new file mode 100644 index 0000000..335374d --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs @@ -0,0 +1,112 @@ +using System.Numerics; +using Meddle.Utils; +using Meddle.Utils.Export; +using Meddle.Utils.Files; +using Meddle.Utils.Materials; +using Meddle.Utils.Models; +using SharpGLTF.Materials; + +namespace Meddle.Plugin.Models.Composer; + +public class SkinMaterialBuilder : MeddleMaterialBuilder +{ + private readonly MaterialSet set; + private readonly DataProvider dataProvider; + private readonly CustomizeParameter parameters; + private readonly CustomizeData data; + + public SkinMaterialBuilder(string name, MaterialSet set, DataProvider dataProvider, CustomizeParameter parameters, CustomizeData data) : base(name) + { + this.set = set; + this.dataProvider = dataProvider; + this.parameters = parameters; + this.data = data; + } + + public override MeddleMaterialBuilder Apply() + { + var skinType = set.GetShaderKeyOrDefault(ShaderCategory.CategorySkinType, SkinType.Face); + + var normalTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerNormal).ToResource().ToTexture(); + var maskTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerMask).ToResource().ToTexture(normalTexture.Size); + var diffuseTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerDiffuse).ToResource().ToTexture(normalTexture.Size); + + // PART_BODY = no additional color + // PART_FACE/default = lip color + // PART_HRO = hairColor blend into hair highlight color + + var skinColor = parameters.SkinColor; + var lipColor = parameters.LipColor; + var hairColor = parameters.MainColor; + var highlightColor = parameters.MeshColor; + var diffuseColor = set.GetConstantOrDefault(MaterialConstant.g_DiffuseColor, Vector3.One); + var alphaThreshold = set.GetConstantOrDefault(MaterialConstant.g_AlphaThreshold, 0.0f); + var lipRoughnessScale = set.GetConstantOrDefault(MaterialConstant.g_LipRoughnessScale, 0.7f); + var alphaMultiplier = alphaThreshold != 0 ? (float)(1.0f / alphaThreshold) : 1.0f; + + var metallicRoughness = new SKTexture(normalTexture.Width, normalTexture.Height); + for (var x = 0; x < normalTexture.Width; x++) + for (var y = 0; y < diffuseTexture.Width; y++) + { + var normal = normalTexture[x, y].ToVector4(); + var mask = maskTexture[x, y].ToVector4(); + var diffuse = diffuseTexture[x, y].ToVector4(); + + + var diffuseAlpha = diffuse.W; + var skinInfluence = normal.Z; + var sColor = Vector3.Lerp(diffuseColor, skinColor, skinInfluence); + diffuse *= new Vector4(sColor, 1.0f); + + if (skinType == SkinType.Hrothgar) + { + var hair = hairColor; + if (data.Highlights) + { + hair = Vector3.Lerp(hairColor, highlightColor, mask.W); + } + + // tt arbitrary darkening instead of using flow map + hair *= 0.4f; + + var delta = Math.Min(Math.Max(normal.W - skinInfluence, 0), 1.0f); + diffuse = Vector4.Lerp(diffuse, new Vector4(hair, 1.0f), delta); + diffuseAlpha = 1.0f; + } + + var specular = mask.X; + var roughness = mask.Y; + var subsurface = mask.Z; + var metallic = 0.0f; + var roughnessPixel = new Vector4(subsurface, roughness, metallic, specular); + //diffuseAlpha = material.ComputeAlpha(diffuseAlpha * alphaMultiplier); + + if (skinType == SkinType.Face) + { + if (data.LipStick) + { + diffuse = Vector4.Lerp(diffuse, lipColor, normal.W * lipColor.W); + roughnessPixel *= lipRoughnessScale; + } + } + + diffuseTexture[x, y] = (diffuse with {W = diffuseAlpha}).ToSkColor(); + normalTexture[x, y] = (normal with {W = 1.0f}).ToSkColor(); + metallicRoughness[x, y] = roughnessPixel.ToSkColor(); + } + + WithBaseColor(dataProvider.CacheTexture(diffuseTexture, $"Computed/{set.ComputedTextureName("diffuse")}")); + WithNormal(dataProvider.CacheTexture(normalTexture, $"Computed/{set.ComputedTextureName("normal")}")); + WithMetallicRoughness(dataProvider.CacheTexture(metallicRoughness, $"Computed/{set.ComputedTextureName("metallicRoughness")}")); + + if (alphaThreshold > 0) + { + WithAlpha(AlphaMode.MASK, alphaThreshold); + } + + WithDoubleSide(set.RenderBackfaces); + + Extras = set.ComposeExtrasNode(); + return this; + } +} diff --git a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs index 227422e..abafee9 100644 --- a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs +++ b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs @@ -177,6 +177,17 @@ public class ParsedModelInfo(string path, string pathFromCharacter, DeformerCach public IList Materials { get; } = materials; } +public interface ICharacterInstance +{ + public CustomizeData CustomizeData { get; } + public CustomizeParameter CustomizeParameter { get; } +} + +public interface ITextureMappableInstance +{ + public Dictionary TextureMap { get; } +} + public class ParsedCharacterInfo { public readonly IList Models; @@ -195,13 +206,14 @@ public ParsedCharacterInfo(IList models, ParsedSkeleton skeleto } } -public class ParsedCharacterInstance : ParsedInstance, IResolvableInstance +public class ParsedCharacterInstance : ParsedInstance, IResolvableInstance, ICharacterInstance, ITextureMappableInstance { public ParsedCharacterInfo? CharacterInfo; public string Name; public ObjectKind Kind; public bool Visible; - + + public ParsedCharacterInstance(nint id, string name, ObjectKind kind, Transform transform, bool visible) : base(id, ParsedInstanceType.Character, transform) { Name = name; @@ -217,4 +229,26 @@ public void Resolve(LayoutService layoutService) layoutService.ResolveInstance(this); IsResolved = true; } + + public CustomizeData CustomizeData => CharacterInfo?.CustomizeData ?? new CustomizeData(); + public CustomizeParameter CustomizeParameter => CharacterInfo?.CustomizeParameter ?? new CustomizeParameter(); + public Dictionary TextureMap => ResolveTextureMappings(); + + private Dictionary ResolveTextureMappings() + { + var textureMap = new Dictionary(); + if (CharacterInfo == null) return textureMap; + foreach (var model in CharacterInfo.Models) + { + foreach (var material in model.Materials) + { + foreach (var texture in material.Textures) + { + textureMap[texture.PathFromMaterial] = texture.Path; + } + } + } + + return textureMap; + } } diff --git a/Meddle/Meddle.Plugin/UI/Layout/Config.cs b/Meddle/Meddle.Plugin/UI/Layout/Config.cs index aff5893..bcab66f 100644 --- a/Meddle/Meddle.Plugin/UI/Layout/Config.cs +++ b/Meddle/Meddle.Plugin/UI/Layout/Config.cs @@ -25,7 +25,6 @@ public enum ExportType private bool traceToHovered = true; private bool hideOffscreenCharacters = true; private int maxItemCount = 100; - private bool bakeTextures = true; private void DrawOptions() { @@ -48,13 +47,6 @@ private void DrawOptions() ImGui.Checkbox("Order by Distance", ref orderByDistance); ImGui.Checkbox("Trace to Hovered", ref traceToHovered); ImGui.Checkbox("Hide Offscreen Characters", ref hideOffscreenCharacters); - ImGui.Checkbox("Bake Textures", ref bakeTextures); - ImGui.SameLine(); - ImGui.TextDisabled("(?)"); - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip("Computes some properties of textures before exporting them, will increase export time significantly"); - } ImGui.DragInt("Max Item Count", ref maxItemCount, 1, 1, 50000); ImGui.SameLine(); diff --git a/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs b/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs index d677429..1aec1bb 100644 --- a/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs +++ b/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs @@ -281,7 +281,7 @@ private void InstanceExport(ParsedInstance[] instances) Directory.CreateDirectory(cacheDir); var instanceSet = new InstanceComposer(log, dataManager, config, instances, cacheDir, - x => progress = x, bakeTextures, cancelToken.Token); + x => progress = x, cancelToken.Token); var scene = new SceneBuilder(); instanceSet.Compose(scene); var gltf = scene.ToGltf2(); diff --git a/Meddle/Meddle.Plugin/Utils/PathUtil.cs b/Meddle/Meddle.Plugin/Utils/PathUtil.cs index c62fdb5..0d7bef1 100644 --- a/Meddle/Meddle.Plugin/Utils/PathUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/PathUtil.cs @@ -7,13 +7,10 @@ namespace Meddle.Plugin.Utils; public static class PathUtil { + // lumina still fails on certain textures ie. array public static byte[]? GetFileOrReadFromDisk(this IDataManager pack, string path) { - // if path is in format |...|path/to/file, trim the |...| part - if (path[0] == '|') - { - path = path.Substring(path.IndexOf('|', 1) + 1); - } + path = path.TrimHandlePath(); // if path is rooted, get from disk if (Path.IsPathRooted(path)) @@ -28,12 +25,8 @@ public static class PathUtil public static byte[]? GetFileOrReadFromDisk(this SqPack pack, string path) { - // if path is in format |...|path/to/file, trim the |...| part - if (path[0] == '|') - { - path = path.Substring(path.IndexOf('|', 1) + 1); - } - + path = path.TrimHandlePath(); + // if path is rooted, get from disk if (Path.IsPathRooted(path)) { @@ -45,6 +38,17 @@ public static class PathUtil return file?.file.RawData.ToArray(); } + public static string TrimHandlePath(this string path) + { + // if path is in format |...|path/to/file, trim the |...| part + if (path[0] == '|') + { + path = path.Substring(path.IndexOf('|', 1) + 1); + } + + return path; + } + public static string ParseString(this StdString stdString) { var data = stdString.ToArray(); diff --git a/Meddle/Meddle.Utils/Export/CustomizeParameter.cs b/Meddle/Meddle.Utils/Export/CustomizeParameter.cs index 157b287..626cdbc 100644 --- a/Meddle/Meddle.Utils/Export/CustomizeParameter.cs +++ b/Meddle/Meddle.Utils/Export/CustomizeParameter.cs @@ -6,6 +6,14 @@ public class CustomizeData { public bool LipStick; public bool Highlights; + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(LipStick); + hash.Add(Highlights); + return hash.ToHashCode(); + } } public class CustomizeParameter { @@ -53,4 +61,22 @@ public class CustomizeParameter { /// XYZ : Race feature color, as squared RGB. /// public Vector3 OptionColor; + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(SkinColor); + hash.Add(MuscleTone); + hash.Add(SkinFresnelValue0); + hash.Add(LipColor); + hash.Add(MainColor); + hash.Add(FacePaintUVMultiplier); + hash.Add(HairFresnelValue0); + hash.Add(MeshColor); + hash.Add(FacePaintUVOffset); + hash.Add(LeftColor); + hash.Add(RightColor); + hash.Add(OptionColor); + return hash.ToHashCode(); + } } diff --git a/Meddle/Meddle.Utils/ImageUtils.cs b/Meddle/Meddle.Utils/ImageUtils.cs index 8081e13..ddc9d10 100644 --- a/Meddle/Meddle.Utils/ImageUtils.cs +++ b/Meddle/Meddle.Utils/ImageUtils.cs @@ -187,6 +187,11 @@ public static unsafe SKTexture ToTexture(this Image img, Vector2? resize = null) public static SKTexture ToTexture(this TextureResource resource, Vector2 size) { + if (resource.Width == (int)size.X && resource.Height == (int)size.Y) + { + return resource.ToTexture(); + } + var bitmap = resource.ToBitmap(); bitmap = bitmap.Resize(new SKImageInfo((int)size.X, (int)size.Y, SKColorType.Rgba8888, SKAlphaType.Unpremul), SKFilterQuality.High); return new SKTexture(bitmap); diff --git a/Meddle/Meddle.Utils/Materials/MaterialUtility.cs b/Meddle/Meddle.Utils/Materials/MaterialUtility.cs index 7a22a6f..a168ff2 100644 --- a/Meddle/Meddle.Utils/Materials/MaterialUtility.cs +++ b/Meddle/Meddle.Utils/Materials/MaterialUtility.cs @@ -99,29 +99,6 @@ public static MaterialBuilder BuildFallback(Material material, string name) return output; } - public static KnownChannel? MapTextureUsageToChannel(TextureUsage usage) - { - return usage switch - { - TextureUsage.g_SamplerDiffuse => KnownChannel.BaseColor, - TextureUsage.g_SamplerNormal => KnownChannel.Normal, - TextureUsage.g_SamplerMask => KnownChannel.SpecularFactor, - TextureUsage.g_SamplerSpecular => KnownChannel.SpecularColor, - TextureUsage.g_SamplerCatchlight => KnownChannel.Emissive, - TextureUsage.g_SamplerColorMap0 => KnownChannel.BaseColor, - TextureUsage.g_SamplerNormalMap0 => KnownChannel.Normal, - TextureUsage.g_SamplerSpecularMap0 => KnownChannel.SpecularColor, - TextureUsage.g_SamplerColorMap1 => KnownChannel.BaseColor, - TextureUsage.g_SamplerNormalMap1 => KnownChannel.Normal, - TextureUsage.g_SamplerSpecularMap1 => KnownChannel.SpecularColor, - TextureUsage.g_SamplerColorMap => KnownChannel.BaseColor, - TextureUsage.g_SamplerNormalMap => KnownChannel.Normal, - TextureUsage.g_SamplerSpecularMap => KnownChannel.SpecularColor, - TextureUsage.g_SamplerNormal2 => KnownChannel.Normal, - _ => null - }; - } - public static MaterialBuilder BuildSharedBase(Material material, string name) { const uint backfaceMask = 0x1; @@ -139,6 +116,11 @@ public static SKColor ToSkColor(this Vector4 color) var c = color.Clamp(0, 1); return new SKColor((byte)(c.X * 255), (byte)(c.Y * 255), (byte)(c.Z * 255), (byte)(c.W * 255)); } + + public static float[] AsFloatArray(this Vector4 v) => new[] { v.X, v.Y, v.Z, v.W }; + public static float[] AsFloatArray(this Vector3 v) => new[] { v.X, v.Y, v.Z }; + public static float[] AsFloatArray(this Vector2 v) => new[] { v.X, v.Y }; + public static Vector4 Clamp(this Vector4 v, float min, float max) { return new Vector4( diff --git a/Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs b/Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs index a59bc07..20dd62d 100644 --- a/Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs +++ b/Meddle/Meddle.Utils/Materials/XIVMaterialBuilder.cs @@ -1,24 +1,6 @@ -using System.Numerics; -using SharpGLTF.Materials; - -namespace Meddle.Utils.Materials; - -public class XivMaterialBuilder : MaterialBuilder -{ - public string Shpk; - - public XivMaterialBuilder(string name, string shpk) : base(name) - { - this.Shpk = shpk; - } -} +namespace Meddle.Utils.Materials; public interface IVertexPaintMaterialBuilder { public bool VertexPaint { get; } } - -public interface ITangentMuliplierMaterialBuilder -{ - public Vector4 TangentMultiplier { get; } -} diff --git a/Meddle/Meddle.Utils/MeshBuilder.cs b/Meddle/Meddle.Utils/MeshBuilder.cs index c8daf5b..88743f1 100644 --- a/Meddle/Meddle.Utils/MeshBuilder.cs +++ b/Meddle/Meddle.Utils/MeshBuilder.cs @@ -229,6 +229,7 @@ private IVertexBuilder BuildVertex(Vertex vertex) // ReSharper restore CompareOfFloatsByEqualityOperator // AKA: Has "Color1" component + // Some models have a color component, but it's packed data, so we don't use it as color if (MaterialT != typeof(VertexTexture2)) { Vector4 vertexColor = new Vector4(1, 1, 1, 1); @@ -244,8 +245,8 @@ private IVertexBuilder BuildVertex(Vertex vertex) materialParamCache.Insert(0, vertexColor); } - //if( MaterialT != typeof( VertexTexture2 ) ) materialParamCache.Insert( 0, vertex.Color!.Value ); - //if (MaterialT != typeof(VertexTexture2)) materialParamCache.Insert(0, new Vector4(1, 1, 1, 1)); + //if(MaterialT != typeof(VertexTexture2)) materialParamCache.Insert(0, vertex.Color!.Value); + //if(MaterialT != typeof(VertexTexture2)) materialParamCache.Insert(0, new Vector4(1, 1, 1, 1)); // AKA: Has "TextureN" component if (MaterialT != typeof(VertexColor1)) From ebbe6d1b274164cfd256d5fef1da4d2cdc3c676f Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:32:44 +1000 Subject: [PATCH 05/44] More wip - Update instance path structure to use HandleString, which defines Full and game paths - Some fixes for character exports under layout ui - Hair and iris updates - Add IOR exports (currently blocked by bug in SharpGLTF however) - MaterialSet method cleanup - bugfix in skin comp - remove blue channel from some normals, causing incorrect outputs --- .../Models/Composer/BgMaterialBuilder.cs | 91 ++++----- .../Models/Composer/CharacterComposer.cs | 36 ++-- .../Composer/CharacterMaterialBuilder.cs | 34 ++-- .../CharacterOcclusionMaterialBuilder.cs | 2 + .../CharacterTattooMaterialBuilder.cs | 17 +- .../Models/Composer/DataProvider.cs | 27 ++- .../Models/Composer/GenericMaterialBuilder.cs | 6 +- .../Models/Composer/HairMaterialBuilder.cs | 80 ++++++++ .../Models/Composer/InstanceComposer.cs | 49 ++--- .../Models/Composer/IrisMaterialBuilder.cs | 95 ++++++++++ .../Composer/LightshaftMaterialBuilder.cs | 2 +- .../Models/Composer/MaterialSet.cs | 174 +++++++++++++----- .../Models/Composer/MeddleMaterialBuilder.cs | 15 +- .../Models/Composer/SkinMaterialBuilder.cs | 50 +++-- .../Models/Layout/ParsedInstance.cs | 45 ++--- Meddle/Meddle.Plugin/Services/ParseService.cs | 18 +- Meddle/Meddle.Plugin/UI/Layout/Instance.cs | 25 +-- .../Meddle.Plugin/UI/Layout/LayoutWindow.cs | 2 +- Meddle/Meddle.Utils/MeshBuilder.cs | 31 ++-- 19 files changed, 493 insertions(+), 306 deletions(-) create mode 100644 Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs create mode 100644 Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs diff --git a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs index ef85d6a..104876b 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs @@ -112,71 +112,41 @@ private DetailSet GetDetail(int detailId, Vector2 size) } } - private TextureResource? GetTextureResourceOrNull(TextureUsage usage, ref List sizes) + private TextureSet GetTextureSet() { - if (set.TextureUsageDict.TryGetValue(usage, out var texture)) + if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerColorMap0, out var colorMap0Texture)) { - if (texture.Contains("dummy_")) - { - return null; - } - var mappedPath = set.MapTexturePath(texture); - var textureResource = dataProvider.LookupData(mappedPath); - if (textureResource != null) - { - var resource = new TexFile(textureResource).ToResource(); - sizes.Add(resource.Size); - return resource; - } + throw new Exception("ColorMap0 texture not found"); } - return null; - } - - private TextureSet GetTextureSet() - { - var colorMap0Texture = set.GetTexture(dataProvider, TextureUsage.g_SamplerColorMap0) ?? throw new Exception("ColorMap0 texture not found"); - var specularMap0Texture = set.GetTexture(dataProvider, TextureUsage.g_SamplerSpecularMap0) ?? throw new Exception("SpecularMap0 texture not found"); - var normalMap0Texture = set.GetTexture(dataProvider, TextureUsage.g_SamplerNormalMap0) ?? throw new Exception("NormalMap0 texture not found"); - - var colorRes0 = colorMap0Texture.ToResource(); - var specularRes0 = specularMap0Texture.ToResource(); - var normalRes0 = normalMap0Texture.ToResource(); - var sizes = new List + if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerSpecularMap0, out var specularMap0Texture)) + { + throw new Exception("SpecularMap0 texture not found"); + } + + if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerNormalMap0, out var normalMap0Texture)) { - colorRes0.Size, - specularRes0.Size, - normalRes0.Size - }; + throw new Exception("NormalMap0 texture not found"); + } - var colorRes1 = GetTextureResourceOrNull(TextureUsage.g_SamplerColorMap1, ref sizes); - var specularRes1 = GetTextureResourceOrNull(TextureUsage.g_SamplerSpecularMap1, ref sizes); - var normalRes1 = GetTextureResourceOrNull(TextureUsage.g_SamplerNormalMap1, ref sizes); + var sizes = new List {colorMap0Texture.Size, specularMap0Texture.Size, normalMap0Texture.Size}; + var colorMap1 = set.TryGetTexture(dataProvider, TextureUsage.g_SamplerColorMap1, out var colorMap1Texture); + var specularMap1 = set.TryGetTexture(dataProvider, TextureUsage.g_SamplerSpecularMap1, out var specularMap1Texture); + var normalMap1 = set.TryGetTexture(dataProvider, TextureUsage.g_SamplerNormalMap1, out var normalMap1Texture); var size = sizes.MaxBy(x => x.X * x.Y); - var colorTex0 = colorRes0.ToTexture(size); - var specularTex0 = specularRes0.ToTexture(size); - var normalTex0 = normalRes0.ToTexture(size); - var colorTex1 = colorRes1?.ToTexture(size); - var specularTex1 = specularRes1?.ToTexture(size); - var normalTex1 = normalRes1?.ToTexture(size); + var colorTex0 = colorMap0Texture.ToTexture(size); + var specularTex0 = specularMap0Texture.ToTexture(size); + var normalTex0 = normalMap0Texture.ToTexture(size); + var colorTex1 = colorMap1 ? colorMap1Texture.ToTexture(size) : null; + var specularTex1 = specularMap1 ? specularMap1Texture.ToTexture(size) : null; + var normalTex1 = normalMap1 ? normalMap1Texture.ToTexture(size) : null; return new TextureSet(colorTex0, specularTex0, normalTex0, colorTex1, specularTex1, normalTex1); } public bool VertexPaint { get; private set; } - - private bool UseAlpha(out float alphaThreshold) - { - var useAlpha = set.ShaderKeys.Any(x => x is {Category: DiffuseAlphaKey, Value: DiffuseAlphaValue}); - alphaThreshold = 0; - if (useAlpha) - { - set.TryGetConstant(MaterialConstant.g_AlphaThreshold, out alphaThreshold); - } - return useAlpha; - } private bool GetDiffuseColor(out Vector4 diffuseColor) { @@ -192,29 +162,32 @@ private bool GetDiffuseColor(out Vector4 diffuseColor) public override MeddleMaterialBuilder Apply() { - var extrasDict = set.ComposeExtras(); + var extras = new List<(string, object)>(); var textureSet = GetTextureSet(); - if (UseAlpha(out var alphaThreshold)) + var alphaType = set.GetShaderKeyOrDefault(DiffuseAlphaKey, 0); + if (alphaType == DiffuseAlphaValue) { - WithAlpha(AlphaMode.MASK, alphaThreshold); + var alphaThreshold = set.GetConstantOrThrow(MaterialConstant.g_AlphaThreshold); + WithAlpha(AlphaMode.MASK, alphaThreshold); // TODO: which mode? + extras.Add(("AlphaThreshold", alphaThreshold)); } - + if (bgParams is BgColorChangeParams bgColorChangeParams && GetDiffuseColor(out var bgColorChangeDiffuseColor)) { var diffuseColor = bgColorChangeParams.StainColor ?? bgColorChangeDiffuseColor; - extrasDict["stainColor"] = diffuseColor.AsFloatArray(); + extras.Add(("DiffuseColor", diffuseColor.AsFloatArray())); } VertexPaint = set.ShaderKeys.Any(x => x is {Category: BgVertexPaintKey, Value: BgVertexPaintValue}); - Extras = set.ComposeExtrasNode(); - - Extras = JsonNode.Parse(JsonSerializer.Serialize(extrasDict, MaterialSet.JsonOptions))!; + extras.Add(("VertexPaint", VertexPaint)); + // Stub WithNormal(dataProvider.CacheTexture(textureSet.Normal0, $"Computed/{set.ComputedTextureName("normal")}")); WithBaseColor(dataProvider.CacheTexture(textureSet.Color0, $"Computed/{set.ComputedTextureName("diffuse")}")); //WithSpecularColor(dataProvider.CacheTexture(textureSet.Specular0, $"Computed/{set.ComputedTextureName("specular")}")); + Extras = set.ComposeExtrasNode(extras.ToArray()); return this; } } diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs index 55e19f0..7ba857a 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs @@ -53,32 +53,30 @@ public void ComposeCharacterInstance(ParsedCharacterInstance characterInstance, root.AddNode(rootBone); } - var textureMappings = characterInstance.TextureMap; - for (var i = 0; i < characterInfo.Models.Count; i++) { var modelInfo = characterInfo.Models[i]; - if (modelInfo.PathFromCharacter.Contains("b0003_top")) continue; - var mdlData = dataProvider.LookupData(modelInfo.Path); + if (modelInfo.Path.GamePath.Contains("b0003_top")) continue; + var mdlData = dataProvider.LookupData(modelInfo.Path.FullPath); if (mdlData == null) { log.LogWarning("Failed to load model file: {modelPath}", modelInfo.Path); continue; } - log.LogInformation("Loaded model {modelPath}", modelInfo.Path); + log.LogInformation("Loaded model {modelPath}", modelInfo.Path.FullPath); var mdlFile = new MdlFile(mdlData); var materialBuilders = new List(); foreach (var materialInfo in modelInfo.Materials) { - var mtrlData = dataProvider.LookupData(materialInfo.Path); + var mtrlData = dataProvider.LookupData(materialInfo.Path.FullPath); if (mtrlData == null) { - log.LogWarning("Failed to load material file: {mtrlPath}", materialInfo.Path); - throw new Exception($"Failed to load material file: {materialInfo.Path}"); + log.LogWarning("Failed to load material file: {mtrlPath}", materialInfo.Path.FullPath); + throw new Exception($"Failed to load material file: {materialInfo.Path.FullPath}"); } - log.LogInformation("Loaded material {mtrlPath}", materialInfo.Path); + log.LogInformation("Loaded material {mtrlPath}", materialInfo.Path.FullPath); var mtrlFile = new MtrlFile(mtrlData); if (materialInfo.ColorTable != null) { @@ -90,14 +88,26 @@ public void ComposeCharacterInstance(ParsedCharacterInstance characterInstance, var shpkData = dataProvider.LookupData(shpkPath); if (shpkData == null) throw new Exception($"Failed to load shader package file: {shpkPath}"); var shpkFile = new ShpkFile(shpkData); - var material = new MaterialSet(mtrlFile, materialInfo.Path, shpkFile, shpkName); + var material = new MaterialSet(mtrlFile, materialInfo.Path.GamePath, + shpkFile, + shpkName, + materialInfo.Textures + .Select(x => x.Path) + .ToArray(), + handleString => + { + var match = materialInfo.Textures.FirstOrDefault(x => + x.Path.GamePath == handleString.GamePath && + x.Path.FullPath == handleString.FullPath); + return match?.Resource; + }); material.SetCustomizeParameters(characterInstance.CustomizeParameter); material.SetCustomizeData(characterInstance.CustomizeData); - material.SetTexturePathMappings(characterInstance.TextureMap); + materialBuilders.Add(material.Compose(dataProvider)); } - var model = new Model(modelInfo.Path, mdlFile, modelInfo.ShapeAttributeGroup); + var model = new Model(modelInfo.Path.GamePath, mdlFile, modelInfo.ShapeAttributeGroup); EnsureBonesExist(model, bones, rootBone); (GenderRace from, GenderRace to, RaceDeformer deformer)? deform; if (modelInfo.Deformer != null) @@ -110,7 +120,7 @@ public void ComposeCharacterInstance(ParsedCharacterInstance characterInstance, } else { - var parsed = RaceDeformer.ParseRaceCode(modelInfo.PathFromCharacter); + var parsed = RaceDeformer.ParseRaceCode(modelInfo.Path.GamePath); if (Enum.IsDefined(parsed)) { deform = (parsed, characterInfo.GenderRace, new RaceDeformer(PbdFile!, bones)); diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs index 0e35ddc..3c93674 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs @@ -25,26 +25,32 @@ public override MeddleMaterialBuilder Apply() var specularMode = set.GetShaderKeyOrDefault(ShaderCategory.CategorySpecularType, SpecularMode.Default); // TODO: is default actually default var flowType = set.GetShaderKeyOrDefault(ShaderCategory.CategoryFlowMapType, FlowType.Standard); - var normalTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerNormal).ToResource().ToTexture(); - var maskTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerMask).ToResource().ToTexture(normalTexture.Size); - var indexTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerIndex).ToResource().ToTexture(normalTexture.Size); - + if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerNormal, out var normalRes)) + throw new InvalidOperationException("Missing normal texture"); + if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerMask, out var maskRes)) + throw new InvalidOperationException("Missing mask texture"); + if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerIndex, out var indexRes)) + throw new InvalidOperationException("Missing index texture"); + var diffuseTexture = textureMode switch { - TextureMode.Compatibility => set.GetTexture(dataProvider, TextureUsage.g_SamplerDiffuse).ToResource().ToTexture(normalTexture.Size), - _ => new SKTexture(normalTexture.Width, normalTexture.Height) + TextureMode.Compatibility => set.TryGetTexture(dataProvider, TextureUsage.g_SamplerDiffuse, out var tex) ? tex.ToTexture(normalRes.Size) : throw new InvalidOperationException("Missing diffuse texture"), + _ => new SKTexture(normalRes.Width, normalRes.Height) }; var flowTexture = flowType switch { - FlowType.Flow => set.GetTexture(dataProvider, TextureUsage.g_SamplerFlow).ToResource().ToTexture(normalTexture.Size), + FlowType.Flow => set.TryGetTexture(dataProvider, TextureUsage.g_SamplerFlow, out var tex) ? tex.ToTexture(normalRes.Size) : throw new InvalidOperationException("Missing flow texture"), _ => null }; - - var occlusionTexture = new SKTexture(normalTexture.Width, normalTexture.Height); - var metallicRoughness = new SKTexture(normalTexture.Width, normalTexture.Height); - for (int x = 0; x < normalTexture.Width; x++) - for (int y = 0; y < normalTexture.Height; y++) + + var normalTexture = normalRes.ToTexture(); + var maskTexture = maskRes.ToTexture(normalRes.Size); + var indexTexture = indexRes.ToTexture(normalRes.Size); + var occlusionTexture = new SKTexture(normalRes.Width, normalRes.Height); + var metallicRoughness = new SKTexture(normalRes.Width, normalRes.Height); + for (int x = 0; x < normalRes.Width; x++) + for (int y = 0; y < normalRes.Height; y++) { var normal = normalTexture[x, y].ToVector4(); var mask = maskTexture[x, y].ToVector4(); @@ -77,12 +83,12 @@ public override MeddleMaterialBuilder Apply() metallicRoughness[x, y] = new Vector4(specMask, roughMask, 0, 1).ToSkColor(); }*/ - normalTexture[x, y] = (normal with{ W = 1.0f}).ToSkColor(); + normalTexture[x, y] = (normal with{ Z = 1.0f, W = 1.0f}).ToSkColor(); } WithBaseColor(dataProvider.CacheTexture(diffuseTexture, $"Computed/{set.ComputedTextureName("diffuse")}")); WithNormal(dataProvider.CacheTexture(normalTexture, $"Computed/{set.ComputedTextureName("normal")}")); - + IndexOfRefraction = set.GetConstantOrThrow(MaterialConstant.g_GlassIOR); var alphaThreshold = set.GetConstantOrDefault(MaterialConstant.g_AlphaThreshold, 0.0f); if (alphaThreshold > 0) diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterOcclusionMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterOcclusionMaterialBuilder.cs index 2693788..3a49fd6 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/CharacterOcclusionMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterOcclusionMaterialBuilder.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Meddle.Utils.Export; using SharpGLTF.Materials; namespace Meddle.Plugin.Models.Composer; @@ -20,6 +21,7 @@ public override MeddleMaterialBuilder Apply() WithDoubleSide(set.RenderBackfaces); WithBaseColor(new Vector4(1, 1, 1, 0f)); WithAlpha(AlphaMode.BLEND, 0.5f); + IndexOfRefraction = set.GetConstantOrThrow(MaterialConstant.g_GlassIOR); Extras = set.ComposeExtrasNode(); return this; } diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs index 6f730e1..27cbb89 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs @@ -32,7 +32,10 @@ public override MeddleMaterialBuilder Apply() _ => Vector3.Zero }; - var normalTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerNormal).ToResource().ToTexture(); + if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerNormal, out var normalRes)) + throw new InvalidOperationException("Missing normal texture"); + + var normalTexture = normalRes.ToTexture(); var diffuseTexture = new SKTexture(normalTexture.Width, normalTexture.Height); for (var x = 0; x < normalTexture.Width; x++) for (var y = 0; y < normalTexture.Height; y++) @@ -53,15 +56,9 @@ public override MeddleMaterialBuilder Apply() WithDoubleSide(set.RenderBackfaces); - var alpha = set.GetConstantOrDefault(MaterialConstant.g_AlphaThreshold, 0.0f); - if (alpha > 0) - { - WithAlpha(AlphaMode.BLEND, alpha); - } - else - { - WithAlpha(AlphaMode.BLEND); - } + IndexOfRefraction = set.GetConstantOrThrow(MaterialConstant.g_GlassIOR); + var alphaThreshold = set.GetConstantOrThrow(MaterialConstant.g_AlphaThreshold); + WithAlpha(AlphaMode.MASK, alphaThreshold); Extras = set.ComposeExtrasNode(); return this; diff --git a/Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs b/Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs index 0e75941..a357f2c 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using Meddle.Plugin.Models.Layout; using Meddle.Plugin.Utils; using Meddle.Utils; using Meddle.Utils.Files; @@ -17,13 +18,11 @@ public class DataProvider private readonly SqPack dataManager; private readonly ILogger logger; private readonly CancellationToken cancellationToken; - private readonly ConcurrentDictionary> lookupCache = new(); private readonly ConcurrentDictionary> mtrlCache = new(); private readonly ConcurrentDictionary> mtrlFileCache = new(); private readonly ConcurrentDictionary> shpkFileCache = new(); - public DataProvider(string cacheDir, SqPack dataManager, ILogger logger, CancellationToken cancellationToken) { this.cacheDir = cacheDir; @@ -65,32 +64,30 @@ public ShpkFile GetShpkFile(string fullPath) }).Value; } - public byte[]? LookupData(string fullPath) + public byte[]? LookupData(string fullPath, bool cacheIfTexture = true) { fullPath = fullPath.TrimHandlePath(); - var shortPath = fullPath; if (Path.IsPathRooted(fullPath)) { - shortPath = Path.GetFileName(fullPath); - shortPath = Path.Combine("Rooted", shortPath); + return File.ReadAllBytes(fullPath); } var diskPath = lookupCache.GetOrAdd(fullPath, key => { cancellationToken.ThrowIfCancellationRequested(); - return new Lazy(() => LookupDataInner(key, shortPath), LazyThreadSafetyMode.ExecutionAndPublication); + return new Lazy(() => LookupDataInner(key, cacheIfTexture), LazyThreadSafetyMode.ExecutionAndPublication); }); return diskPath.Value == null ? null : File.ReadAllBytes(diskPath.Value); } - private string? LookupDataInner(string fullPath, string shortPath) + private string? LookupDataInner(string gamePath, bool cacheIfTexture) { - var outPath = Path.Combine(cacheDir, shortPath); + var outPath = Path.Combine(cacheDir, gamePath); var outDir = Path.GetDirectoryName(outPath); - var data = dataManager.GetFileOrReadFromDisk(fullPath); + var data = dataManager.GetFileOrReadFromDisk(gamePath); if (data == null) { - logger.LogError("Failed to load file: {path}", fullPath); + logger.LogError("Failed to load file: {path}", gamePath); return null; } @@ -102,16 +99,16 @@ public ShpkFile GetShpkFile(string fullPath) } File.WriteAllBytes(outPath, data); - if (fullPath.EndsWith(".tex")) + if (gamePath.EndsWith(".tex") && cacheIfTexture) { try { var texFile = new TexFile(data).ToResource().ToTexture(); - CacheTexture(texFile, shortPath); + CacheTexture(texFile, gamePath); } catch (Exception ex) { - logger.LogError(ex, "Failed to cache tex file: {path}", fullPath); + logger.LogError(ex, "Failed to cache tex file: {path}", gamePath); } } } @@ -124,7 +121,7 @@ public ImageBuilder CacheTexture(SKTexture texture, string texName) texName = texName.TrimHandlePath(); if (Path.IsPathRooted(texName)) { - throw new ArgumentException("Texture name cannot be rooted", nameof(texName)); + texName = Path.GetFileName(texName); } var outPath = Path.Combine(cacheDir, $"{texName}.png"); diff --git a/Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs index 5deb633..0851e8b 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs @@ -26,12 +26,11 @@ public override MeddleMaterialBuilder Apply() // TODO: foreach (var textureUsage in set.TextureUsageDict) { - var path = set.MapTexturePath(textureUsage.Value); - var texData = dataProvider.LookupData(path); + var texData = dataProvider.LookupData(textureUsage.Value.FullPath); if (texData == null) continue; // caching the texture regardless of usage, but only applying it to the material if it's a known channel var texture = new TexFile(texData).ToResource().ToTexture(); - var tex = dataProvider.CacheTexture(texture, textureUsage.Value); + var tex = dataProvider.CacheTexture(texture, textureUsage.Value.FullPath); var channel = MapTextureUsageToChannel(textureUsage.Key); if (channel != null && setTypes.Add(textureUsage.Key)) @@ -41,7 +40,6 @@ public override MeddleMaterialBuilder Apply() } Extras = set.ComposeExtrasNode(); - return this; } diff --git a/Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs new file mode 100644 index 0000000..b751e32 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs @@ -0,0 +1,80 @@ +using System.Numerics; +using Meddle.Utils; +using Meddle.Utils.Export; +using Meddle.Utils.Materials; +using Meddle.Utils.Models; +using SharpGLTF.Materials; + +namespace Meddle.Plugin.Models.Composer; + +public class HairMaterialBuilder : MeddleMaterialBuilder +{ + private readonly MaterialSet set; + private readonly DataProvider dataProvider; + private readonly CustomizeParameter parameters; + + public HairMaterialBuilder(string name, MaterialSet set, DataProvider dataProvider, CustomizeParameter parameters) : base(name) + { + this.set = set; + this.dataProvider = dataProvider; + this.parameters = parameters; + } + + public override MeddleMaterialBuilder Apply() + { + var hairType = set.GetShaderKeyOrDefault(ShaderCategory.CategoryHairType, HairType.Hair); + + if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerNormal, out var normalRes)) + throw new InvalidOperationException("Missing normal texture"); + if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerMask, out var maskRes)) + throw new InvalidOperationException("Missing mask texture"); + + var normalTexture = normalRes.ToTexture(); + var maskTexture = maskRes.ToTexture(normalTexture.Size); + + var hairColor = parameters.MainColor; + var tattooColor = parameters.OptionColor; + var highlightColor = parameters.MeshColor; + + var diffuseTexture = new SKTexture(normalTexture.Width, normalTexture.Height); + var occ_x_x_x_Texture = new SKTexture(normalTexture.Width, normalTexture.Height); + var vol_thick_x_x_Texture = new SKTexture(normalTexture.Width, normalTexture.Height); + for (int x = 0; x < normalTexture.Width; x++) + for (int y = 0; y < normalTexture.Height; y++) + { + var normal = normalTexture[x, y].ToVector4(); + var mask = maskTexture[x, y].ToVector4(); + + var bonusColor = hairType switch + { + HairType.Face => tattooColor, + HairType.Hair => highlightColor, + _ => hairColor + }; + + var bonusIntensity = normal.Z; + var diffusePixel = Vector3.Lerp(hairColor, bonusColor, bonusIntensity); + var occlusion = mask.W * mask.W; + + diffuseTexture[x, y] = new Vector4(diffusePixel, normal.W).ToSkColor(); + occ_x_x_x_Texture[x, y] = new Vector4(occlusion, 0f, 0f, 1.0f).ToSkColor(); + normalTexture[x, y] = (normal with { Z = 1.0f, W = 1.0f }).ToSkColor(); + vol_thick_x_x_Texture[x, y] = new Vector4(mask.Z, mask.Z, mask.Z, 1.0f).ToSkColor(); + } + + WithDoubleSide(set.RenderBackfaces); + WithBaseColor(dataProvider.CacheTexture(diffuseTexture, $"Computed/{set.ComputedTextureName("diffuse")}")); + WithNormal(dataProvider.CacheTexture(normalTexture, $"Computed/{set.ComputedTextureName("normal")}")); + WithOcclusion(dataProvider.CacheTexture(occ_x_x_x_Texture, $"Computed/{set.ComputedTextureName("occlusion")}")); + WithMetallicRoughness(0, 1); + WithAlpha(AlphaMode.BLEND, set.GetConstantOrThrow(MaterialConstant.g_AlphaThreshold)); + IndexOfRefraction = set.GetConstantOrThrow(MaterialConstant.g_GlassIOR); + + Extras = set.ComposeExtrasNode( + ("hairColor", hairColor.AsFloatArray()), + ("tattooColor", tattooColor.AsFloatArray()), + ("highlightColor", highlightColor.AsFloatArray()) + ); + return this; + } +} diff --git a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs index 1ca8fa7..d5f9cd1 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs @@ -94,16 +94,6 @@ public void Compose(SceneBuilder scene) Interlocked.Increment(ref countProgress); progress?.Invoke(new ProgressEvent("Export", countProgress, count)); }, false); - - try - { - var computeDir = Path.Combine(CacheDir, "Computed"); - Directory.Delete(computeDir, true); - } - catch (Exception ex) - { - log.LogError(ex, "Failed to delete computed directory"); - } } public NodeBuilder? ComposeInstance(SceneBuilder scene, ParsedInstance parsedInstance) @@ -112,7 +102,7 @@ public void Compose(SceneBuilder scene) var root = new NodeBuilder(); if (parsedInstance is IPathInstance pathInstance) { - root.Name = $"{parsedInstance.Type}_{Path.GetFileNameWithoutExtension(pathInstance.Path)}"; + root.Name = $"{parsedInstance.Type}_{Path.GetFileNameWithoutExtension(pathInstance.Path.GamePath)}"; } else { @@ -120,7 +110,7 @@ public void Compose(SceneBuilder scene) } var wasAdded = false; - if (parsedInstance is ParsedBgPartsInstance {Path: not null} bgPartsInstance) + if (parsedInstance is ParsedBgPartsInstance {Path.FullPath: not null} bgPartsInstance) { var meshes = ComposeBgPartsInstance(bgPartsInstance); foreach (var mesh in meshes) @@ -190,24 +180,19 @@ public void Compose(SceneBuilder scene) private void ComposeTerrainInstance(ParsedTerrainInstance terrainInstance, SceneBuilder scene, NodeBuilder root) { - var teraPath = $"{terrainInstance.Path}/bgplate/terrain.tera"; + var teraPath = $"{terrainInstance.Path.GamePath}/bgplate/terrain.tera"; var teraData = dataProvider.LookupData(teraPath); if (teraData == null) throw new Exception($"Failed to load terrain file: {teraPath}"); var teraFile = new TeraFile(teraData); - //for (var i = 0; i < teraFile.Header.PlateCount; i++) var processed = 0; - Parallel.For(0, teraFile.Header.PlateCount, new ParallelOptions - { - CancellationToken = cancellationToken, - MaxDegreeOfParallelism = Math.Max(Environment.ProcessorCount / 2, 1) - }, i => + for (var i = 0; i < teraFile.Header.PlateCount; i++) { if (cancellationToken.IsCancellationRequested) return; log.LogInformation("Parsing plate {i}", i); var platePos = teraFile.GetPlatePosition((int)i); var plateTransform = new Transform(new Vector3(platePos.X, 0, platePos.Y), Quaternion.Identity, Vector3.One); - var mdlPath = $"{terrainInstance.Path}/bgplate/{i:D4}.mdl"; + var mdlPath = $"{terrainInstance.Path.GamePath}/bgplate/{i:D4}.mdl"; var mdlData = dataProvider.LookupData(mdlPath); if (mdlData == null) throw new Exception($"Failed to load model file: {mdlPath}"); log.LogInformation("Loaded model {mdlPath}", mdlPath); @@ -232,18 +217,16 @@ private void ComposeTerrainInstance(ParsedTerrainInstance terrainInstance, Scene root.AddNode(plateRoot); Interlocked.Increment(ref processed); - progress?.Invoke(new ProgressEvent("Terrain Instance", countProgress, count, - new ProgressEvent(root.Name, processed, - (int)teraFile.Header.PlateCount))); - }); + progress?.Invoke(new ProgressEvent("Terrain Instance", countProgress, count, new ProgressEvent(root.Name, processed, (int)teraFile.Header.PlateCount))); + } } private IReadOnlyList ComposeBgPartsInstance(ParsedBgPartsInstance bgPartsInstance) { - var mdlData = dataProvider.LookupData(bgPartsInstance.Path); + var mdlData = dataProvider.LookupData(bgPartsInstance.Path.FullPath); if (mdlData == null) { - log.LogWarning("Failed to load model file: {bgPartsInstance.Path}", bgPartsInstance.Path); + log.LogWarning("Failed to load model file: {Path}", bgPartsInstance.Path.FullPath); return []; } @@ -257,14 +240,11 @@ private void ComposeTerrainInstance(ParsedTerrainInstance terrainInstance, Scene materialBuilders.Add(output); } - var model = new Model(bgPartsInstance.Path, mdlFile, null); + var model = new Model(bgPartsInstance.Path.GamePath, mdlFile, null); var meshes = ModelBuilder.BuildMeshes(model, materialBuilders, [], null); return meshes; } - - - private MaterialBuilder ComposeMaterial(string path, ParsedInstance instance) { // TODO: Really not ideal but can't rely on just the path since material inputs can change @@ -272,14 +252,7 @@ private MaterialBuilder ComposeMaterial(string path, ParsedInstance instance) var shpkName = mtrlFile.GetShaderPackageName(); var shpkPath = $"shader/sm5/shpk/{shpkName}"; var shpkFile = dataProvider.GetShpkFile(shpkPath); - - Dictionary textureMap = new(); - if (instance is ITextureMappableInstance mappableInstance) - { - textureMap = mappableInstance.TextureMap; - } - - var material = new MaterialSet(mtrlFile, path, shpkFile, shpkName); + var material = new MaterialSet(mtrlFile, path, shpkFile, shpkName, null, null); if (instance is IStainableInstance stainableInstance) { material.SetStainColor(stainableInstance.StainColor); diff --git a/Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs new file mode 100644 index 0000000..b6fd0a1 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs @@ -0,0 +1,95 @@ +using System.Numerics; +using Meddle.Utils; +using Meddle.Utils.Export; +using Meddle.Utils.Files; +using Meddle.Utils.Materials; +using Meddle.Utils.Models; +using SharpGLTF.Materials; + +namespace Meddle.Plugin.Models.Composer; + +public class IrisMaterialBuilder : MeddleMaterialBuilder +{ + private readonly MaterialSet set; + private readonly DataProvider dataProvider; + private readonly CustomizeParameter parameters; + + public IrisMaterialBuilder(string name, MaterialSet set, DataProvider dataProvider, CustomizeParameter parameters) : base(name) + { + this.set = set; + this.dataProvider = dataProvider; + this.parameters = parameters; + } + + private SKTexture GetCubeMap(int index) + { + var cubeMapData = dataProvider.LookupData("chara/common/texture/sphere_d_array.tex"); + if (cubeMapData == null) + throw new InvalidOperationException("Missing cube map"); + var cubeMap = new TexFile(cubeMapData); + return ImageUtils.GetTexData(cubeMap, index, 0, 0).ToTexture(); + } + + public override MeddleMaterialBuilder Apply() + { + if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerNormal, out var normalRes)) + throw new InvalidOperationException("Missing normal texture"); + if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerMask, out var maskRes)) + throw new InvalidOperationException("Missing mask texture"); + if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerDiffuse, out var diffuseRes)) + throw new InvalidOperationException("Missing diffuse texture"); + + var normalTexture = normalRes.ToTexture(); + var maskTexture = maskRes.ToTexture(normalTexture.Size); + var diffuseTexture = diffuseRes.ToTexture(normalTexture.Size); + + var sphereMapIndex = set.GetConstantOrThrow(MaterialConstant.g_SphereMapIndex); + var cubeMapTexture = GetCubeMap((int)sphereMapIndex).Resize(normalTexture.Width, normalTexture.Height); + var whiteEyeColor = set.GetConstantOrThrow(MaterialConstant.g_WhiteEyeColor); + var leftIrisColor = parameters.LeftColor; + var emissiveTexture = new SKTexture(normalTexture.Width, normalTexture.Height); + var specularTexture = new SKTexture(normalTexture.Width, normalTexture.Height); + + for (var x = 0; x < normalTexture.Width; x++) + for (var y = 0; y < normalTexture.Height; y++) + { + var normal = normalTexture[x, y].ToVector4(); + var mask = maskTexture[x, y].ToVector4(); + var diffuse = diffuseTexture[x, y].ToVector4(); + var cubeMap = cubeMapTexture[x, y].ToVector4(); + + var irisMask = mask.Z; + var whites = diffuse * new Vector4(whiteEyeColor, 1.0f); + var iris = diffuse * (leftIrisColor with {W = 1.0f }); + diffuse = Vector4.Lerp(whites, iris, irisMask); + + // most textures this channel is just 0 + // use mask red as emissive mask + emissiveTexture[x, y] = new Vector4(mask.X, mask.X, mask.X, 1.0f).ToSkColor(); + + // use mask green as reflection mask/cubemap intensity + var specular = new Vector4(cubeMap.X * mask.Y); + specularTexture[x, y] = (specular with {W = 1.0f}).ToSkColor(); + + diffuseTexture[x, y] = diffuse.ToSkColor(); + normalTexture[x, y] = (normal with {Z = 1.0f, W = 1.0f}).ToSkColor(); + } + + WithDoubleSide(set.RenderBackfaces); + WithBaseColor(dataProvider.CacheTexture(diffuseTexture, $"Computed/{set.ComputedTextureName("diffuse")}")); + WithNormal(dataProvider.CacheTexture(normalTexture, $"Computed/{set.ComputedTextureName("normal")}")); + WithSpecularFactor(dataProvider.CacheTexture(specularTexture, $"Computed/{set.ComputedTextureName("specular")}"), 0.2f); + WithSpecularColor(dataProvider.CacheTexture(specularTexture, $"Computed/{set.ComputedTextureName("specular")}")); + WithEmissive(dataProvider.CacheTexture(emissiveTexture, $"Computed/{set.ComputedTextureName("emissive")}")); + + var alphaThreshold = set.GetConstantOrThrow(MaterialConstant.g_AlphaThreshold); + if (alphaThreshold > 0) + WithAlpha(AlphaMode.MASK, alphaThreshold); + + Extras = set.ComposeExtrasNode( + ("leftIrisColor", leftIrisColor.AsFloatArray()), + ("rightIrisColor", parameters.RightColor.AsFloatArray()) + ); + return this; + } +} diff --git a/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs index edb1ad8..454b406 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs @@ -19,8 +19,8 @@ public override MeddleMaterialBuilder Apply() base.Apply(); this.WithBaseColor(new Vector4(1, 1, 1, 0)); WithAlpha(AlphaMode.BLEND, 0.5f); - Extras = set.ComposeExtrasNode(); + Extras = set.ComposeExtrasNode(); return this; } } diff --git a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs index 90802bc..8473628 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs @@ -4,12 +4,15 @@ using System.Text; using System.Text.Json; using System.Text.Json.Nodes; +using Meddle.Plugin.Models.Layout; +using Meddle.Utils; using Meddle.Utils.Export; using Meddle.Utils.Files; using Meddle.Utils.Files.Structs.Material; using Meddle.Utils.Materials; using Meddle.Utils.Models; using SharpGLTF.Materials; +using ShaderPackage = Meddle.Utils.Export.ShaderPackage; namespace Meddle.Plugin.Models.Composer; @@ -81,63 +84,73 @@ public string HashStr() public readonly string ShpkName; public readonly ShaderKey[] ShaderKeys; public readonly Dictionary Constants; - public readonly Dictionary TextureUsageDict; - private Dictionary? texturePathMappings; - public string MapTexturePath(string path) + public readonly Dictionary TextureUsageDict; + private readonly Func? textureLoader; + + public bool TryGetTextureStrict(DataProvider provider, TextureUsage usage, out TextureResource texture) { - if (texturePathMappings != null) + if (!TextureUsageDict.TryGetValue(usage, out var path)) + { + throw new Exception($"Texture usage {usage} not found in material set"); + } + + if (textureLoader != null) { - if (texturePathMappings.TryGetValue(path, out var mappedPath)) + if (textureLoader(path) is { } tex) { - return mappedPath; + texture = tex; + return true; } + + texture = default; + return false; } - return path; - } - - public TexFile GetTexture(DataProvider provider, TextureUsage usage) - { - var path = GetTexturePath(usage); - var data = provider.LookupData(MapTexturePath(path)); + var data = provider.LookupData(path.FullPath); if (data == null) { - throw new Exception($"Failed to load texture for {usage}"); + texture = default; + return false; } - return new TexFile(data); + texture = new TexFile(data).ToResource(); + return true; } - public byte[] LookupData(DataProvider provider, string path) + public bool TryGetTexture(DataProvider provider, TextureUsage usage, out TextureResource texture) { - var data = provider.LookupData(MapTexturePath(path)); - if (data == null) + if (!TextureUsageDict.TryGetValue(usage, out var path)) { - throw new Exception($"Failed to load data for {path}"); + texture = default; + return false; } - - return data; - } - - public string GetTexturePath(TextureUsage usage) - { - var path = TextureUsageDict[usage]; - if (texturePathMappings != null && texturePathMappings.TryGetValue(usage.ToString(), out var mappedPath)) + + if (textureLoader != null) { - return mappedPath; + if (textureLoader(path) is { } tex) + { + texture = tex; + return true; + } + + texture = default; + return false; } - return path; - } - - public void SetTexturePathMappings(Dictionary mappings) - { - texturePathMappings = mappings; + var data = provider.LookupData(path.FullPath); + if (data == null) + { + texture = default; + return false; + } + + texture = new TexFile(data).ToResource(); + return true; } - private uint ShaderFlagData; - public bool RenderBackfaces => (ShaderFlagData & (uint)ShaderFlags.HideBackfaces) == 0; - public bool IsTransparent => (ShaderFlagData & (uint)ShaderFlags.EnableTranslucency) != 0; + private readonly uint shaderFlagData; + public bool RenderBackfaces => (shaderFlagData & (uint)ShaderFlags.HideBackfaces) == 0; + public bool IsTransparent => (shaderFlagData & (uint)ShaderFlags.EnableTranslucency) != 0; public readonly ColorTable? ColorTable; @@ -226,6 +239,28 @@ public float GetConstantOrDefault(MaterialConstant id, float @default) { return Constants.TryGetValue(id, out var values) ? values[0] : @default; } + + public T GetConstantOrThrow(MaterialConstant id) where T : struct + { + if (!TryGetConstant(id, out float[] value)) + { + throw new InvalidOperationException($"Missing constant {id}"); + } + + switch (typeof(T).Name) + { + case "Single": + return (T)(object)value[0]; + case "Vector2": + return (T)(object)new Vector2(value[0], value[1]); + case "Vector3": + return (T)(object)new Vector3(value[0], value[1], value[2]); + case "Vector4": + return (T)(object)new Vector4(value[0], value[1], value[2], value[3]); + default: + throw new InvalidOperationException($"Unsupported type {typeof(T).Name}"); + } + } public Vector2 GetConstantOrDefault(MaterialConstant id, Vector2 @default) { @@ -273,11 +308,12 @@ public uint GetShaderKeyOrDefault(uint category, uint @default) return @default; } - public MaterialSet(MtrlFile file, string mtrlPath, ShpkFile shpk, string shpkName) + public MaterialSet(MtrlFile file, string mtrlPath, ShpkFile shpk, string shpkName, HandleString[]? texturePathOverride, Func? textureLoader) { this.MtrlPath = mtrlPath; this.ShpkName = shpkName; - ShaderFlagData = file.ShaderHeader.Flags; + this.textureLoader = textureLoader; + shaderFlagData = file.ShaderHeader.Flags; var package = new ShaderPackage(shpk, shpkName); ColorTable = file.ColorTable; @@ -323,19 +359,20 @@ public MaterialSet(MtrlFile file, string mtrlPath, ShpkFile shpk, string shpkNam Constants[id] = values; } - TextureUsageDict = new Dictionary(); + TextureUsageDict = new Dictionary(); var texturePaths = file.GetTexturePaths(); foreach (var sampler in file.Samplers) { if (sampler.TextureIndex == byte.MaxValue) continue; var textureInfo = file.TextureOffsets[sampler.TextureIndex]; - var texturePath = texturePaths[textureInfo.Offset]; + var gamePath = texturePaths[textureInfo.Offset]; if (!package.TextureLookup.TryGetValue(sampler.SamplerId, out var usage)) { continue; } - TextureUsageDict[usage] = texturePath; + var path = texturePathOverride?[sampler.TextureIndex] ?? gamePath; + TextureUsageDict[usage] = path; } } @@ -363,6 +400,10 @@ private MeddleMaterialBuilder GetMaterialBuilder(DataProvider dataProvider) return new CharacterOcclusionMaterialBuilder(mtrlName, this, dataProvider); case "skin.shpk": return new SkinMaterialBuilder(mtrlName, this, dataProvider, customizeParameters ?? new CustomizeParameter(), customizeData ?? new CustomizeData()); + case "hair.shpk": + return new HairMaterialBuilder(mtrlName, this, dataProvider, customizeParameters ?? new CustomizeParameter()); + case "iris.shpk": + return new IrisMaterialBuilder(mtrlName, this, dataProvider, customizeParameters ?? new CustomizeParameter()); default: return new GenericMaterialBuilder(mtrlName, this, dataProvider); } @@ -374,9 +415,17 @@ public MaterialBuilder Compose(DataProvider dataProvider) return builder.Apply(); } - public JsonNode ComposeExtrasNode() + public JsonNode ComposeExtrasNode(params (string key, object value)[]? additionalExtras) { var extrasDict = ComposeExtras(); + if (additionalExtras != null) + { + foreach (var (key, value) in additionalExtras) + { + extrasDict[key] = value; + } + } + return JsonNode.Parse(JsonSerializer.Serialize(extrasDict, JsonOptions))!; } @@ -393,11 +442,6 @@ public Dictionary ComposeExtras() AddCustomizeParameters(); AddCustomizeData(); AddColorTable(); - - if (stainColor.HasValue) - { - extrasDict["stainColor"] = stainColor.Value.AsFloatArray(); - } return extrasDict; @@ -419,13 +463,38 @@ void AddCustomizeData() extrasDict["CustomizeData"] = JsonNode.Parse(JsonSerializer.Serialize(customizeData, JsonOptions))!; } + string IsDefinedOrHex(TEnum value) where TEnum : Enum + { + return Enum.IsDefined(typeof(TEnum), value) ? value.ToString() : $"0x{Convert.ToUInt32(value):X8}"; + } + void AddShaderKeys() { foreach (var key in ShaderKeys) { var category = key.Category; var value = key.Value; - extrasDict[$"0x{category:X8}"] = $"0x{value:X8}"; + if (Enum.IsDefined(typeof(ShaderCategory), category)) + { + var keyCat = (ShaderCategory)category; + var valStr = keyCat switch + { + ShaderCategory.CategoryHairType => IsDefinedOrHex((HairType)value), + ShaderCategory.CategorySkinType => IsDefinedOrHex((SkinType)value), + ShaderCategory.CategoryDiffuseAlpha => IsDefinedOrHex((DiffuseAlpha)value), + ShaderCategory.CategorySpecularType => IsDefinedOrHex((SpecularMode)value), + ShaderCategory.CategoryTextureType => IsDefinedOrHex((TextureMode)value), + ShaderCategory.CategoryFlowMapType => IsDefinedOrHex((FlowType)value), + _ => $"0x{value:X8}" + }; + + extrasDict[keyCat.ToString()] = valStr; + } + else + { + var keyStr = $"0x{category:X8}"; + extrasDict[keyStr] = $"0x{value:X8}"; + } } } @@ -433,7 +502,12 @@ void AddSamplers() { foreach (var (usage, path) in TextureUsageDict) { - extrasDict[usage.ToString()] = path; + var usageStr = usage.ToString(); + extrasDict[usageStr] = path.GamePath; + if (path.FullPath != path.GamePath) + { + extrasDict[$"{usageStr}_FullPath"] = path.FullPath; + } } } diff --git a/Meddle/Meddle.Plugin/Models/Composer/MeddleMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/MeddleMaterialBuilder.cs index 6a2ca15..3b5c4f2 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/MeddleMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/MeddleMaterialBuilder.cs @@ -1,6 +1,4 @@ -using Meddle.Utils; -using Meddle.Utils.Files; -using SharpGLTF.Materials; +using SharpGLTF.Materials; namespace Meddle.Plugin.Models.Composer; @@ -11,15 +9,4 @@ public MeddleMaterialBuilder(string name) : base(name) } public abstract MeddleMaterialBuilder Apply(); - - protected void SaveAllTextures(MaterialSet set, DataProvider provider) - { - foreach (var textureUsage in set.TextureUsageDict) - { - var texData = provider.LookupData(textureUsage.Value); - if (texData == null) continue; - var texture = new TexFile(texData).ToResource().ToTexture(); - provider.CacheTexture(texture, textureUsage.Value); - } - } } diff --git a/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs index 335374d..aedea33 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs @@ -1,10 +1,9 @@ using System.Numerics; using Meddle.Utils; using Meddle.Utils.Export; -using Meddle.Utils.Files; using Meddle.Utils.Materials; using Meddle.Utils.Models; -using SharpGLTF.Materials; +using AlphaMode = SharpGLTF.Materials.AlphaMode; namespace Meddle.Plugin.Models.Composer; @@ -27,9 +26,20 @@ public override MeddleMaterialBuilder Apply() { var skinType = set.GetShaderKeyOrDefault(ShaderCategory.CategorySkinType, SkinType.Face); - var normalTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerNormal).ToResource().ToTexture(); - var maskTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerMask).ToResource().ToTexture(normalTexture.Size); - var diffuseTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerDiffuse).ToResource().ToTexture(normalTexture.Size); + // var normalTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerNormal).ToResource().ToTexture(); + // var maskTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerMask).ToResource().ToTexture(normalTexture.Size); + // var diffuseTexture = set.GetTexture(dataProvider, TextureUsage.g_SamplerDiffuse).ToResource().ToTexture(normalTexture.Size); + if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerNormal, out var normalRes)) + throw new InvalidOperationException("Missing normal texture"); + if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerMask, out var maskRes)) + throw new InvalidOperationException("Missing mask texture"); + if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerDiffuse, out var diffuseRes)) + throw new InvalidOperationException("Missing diffuse texture"); + + var normalTexture = normalRes.ToTexture(); + var maskTexture = maskRes.ToTexture(normalTexture.Size); + var diffuseTexture = diffuseRes.ToTexture(normalTexture.Size); + // PART_BODY = no additional color // PART_FACE/default = lip color @@ -42,11 +52,12 @@ public override MeddleMaterialBuilder Apply() var diffuseColor = set.GetConstantOrDefault(MaterialConstant.g_DiffuseColor, Vector3.One); var alphaThreshold = set.GetConstantOrDefault(MaterialConstant.g_AlphaThreshold, 0.0f); var lipRoughnessScale = set.GetConstantOrDefault(MaterialConstant.g_LipRoughnessScale, 0.7f); - var alphaMultiplier = alphaThreshold != 0 ? (float)(1.0f / alphaThreshold) : 1.0f; + var alphaMultiplier = alphaThreshold != 0 ? 1.0f / alphaThreshold : 1.0f; - var metallicRoughness = new SKTexture(normalTexture.Width, normalTexture.Height); + var metallicRoughnessTexture = new SKTexture(normalTexture.Width, normalTexture.Height); + var sssTexture = new SKTexture(normalTexture.Width, normalTexture.Height); for (var x = 0; x < normalTexture.Width; x++) - for (var y = 0; y < diffuseTexture.Width; y++) + for (var y = 0; y < normalTexture.Height; y++) { var normal = normalTexture[x, y].ToVector4(); var mask = maskTexture[x, y].ToVector4(); @@ -74,12 +85,13 @@ public override MeddleMaterialBuilder Apply() diffuseAlpha = 1.0f; } + diffuseAlpha = set.IsTransparent ? diffuseAlpha * alphaMultiplier : (diffuseAlpha * alphaMultiplier < 1.0f ? 0.0f : 1.0f); + var specular = mask.X; var roughness = mask.Y; var subsurface = mask.Z; var metallic = 0.0f; var roughnessPixel = new Vector4(subsurface, roughness, metallic, specular); - //diffuseAlpha = material.ComputeAlpha(diffuseAlpha * alphaMultiplier); if (skinType == SkinType.Face) { @@ -89,21 +101,21 @@ public override MeddleMaterialBuilder Apply() roughnessPixel *= lipRoughnessScale; } } - + diffuseTexture[x, y] = (diffuse with {W = diffuseAlpha}).ToSkColor(); - normalTexture[x, y] = (normal with {W = 1.0f}).ToSkColor(); - metallicRoughness[x, y] = roughnessPixel.ToSkColor(); + normalTexture[x, y] = (normal with { Z = 1.0f, W = 1.0f}).ToSkColor(); + metallicRoughnessTexture[x, y] = roughnessPixel.ToSkColor(); + sssTexture[x, y] = new Vector4(subsurface, subsurface, subsurface, 1).ToSkColor(); } WithBaseColor(dataProvider.CacheTexture(diffuseTexture, $"Computed/{set.ComputedTextureName("diffuse")}")); WithNormal(dataProvider.CacheTexture(normalTexture, $"Computed/{set.ComputedTextureName("normal")}")); - WithMetallicRoughness(dataProvider.CacheTexture(metallicRoughness, $"Computed/{set.ComputedTextureName("metallicRoughness")}")); - - if (alphaThreshold > 0) - { - WithAlpha(AlphaMode.MASK, alphaThreshold); - } - + WithMetallicRoughness(dataProvider.CacheTexture(metallicRoughnessTexture, $"Computed/{set.ComputedTextureName("metallicRoughness")}")); + WithVolumeThickness(dataProvider.CacheTexture(sssTexture, $"Computed/{set.ComputedTextureName("sss")}"), 1.0f); + + IndexOfRefraction = set.GetConstantOrThrow(MaterialConstant.g_GlassIOR); + WithMetallicRoughnessShader(); + WithAlpha(AlphaMode.MASK, alphaThreshold); WithDoubleSide(set.RenderBackfaces); Extras = set.ComposeExtrasNode(); diff --git a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs index abafee9..a59786e 100644 --- a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs +++ b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs @@ -31,7 +31,7 @@ public interface IResolvableInstance public interface IPathInstance { - public string Path { get; } + public HandleString Path { get; } } public abstract class ParsedInstance @@ -63,7 +63,7 @@ public ParsedUnsupportedInstance(nint id, InstanceType instanceType, Transform t public class ParsedSharedInstance : ParsedInstance, IPathInstance { - public string Path { get; } + public HandleString Path { get; } public IReadOnlyList Children { get; } public ParsedSharedInstance(nint id, Transform transform, string path, IReadOnlyList children) : base(id, ParsedInstanceType.SharedGroup, transform) @@ -115,7 +115,7 @@ public ParsedHousingInstance(nint id, Transform transform, string path, string n public class ParsedBgPartsInstance : ParsedInstance, IPathInstance, IStainableInstance { - public string Path { get; } + public HandleString Path { get; } public ParsedBgPartsInstance(nint id, Transform transform, string path) : base(id, ParsedInstanceType.BgPart, transform) { @@ -139,7 +139,7 @@ public ParsedLightInstance(nint id, Transform transform) : base(id, ParsedInstan public class ParsedTerrainInstance : ParsedInstance, IPathInstance { - public string Path { get; } + public HandleString Path { get; } public ParsedTerrainInstance(nint id, Transform transform, string path) : base(id, ParsedInstanceType.Terrain, transform) { @@ -154,15 +154,13 @@ public class ParsedInstanceSet public class ParsedTextureInfo(string path, string pathFromMaterial, TextureResource resource) { - public string Path { get; } = path; - public string PathFromMaterial { get; } = pathFromMaterial; + public HandleString Path { get; } = new() { FullPath = path, GamePath = pathFromMaterial }; public TextureResource Resource { get; } = resource; } public class ParsedMaterialInfo(string path, string pathFromModel, string shpk, ColorTable? colorTable, IList textures) { - public string Path { get; } = path; - public string PathFromModel { get; } = pathFromModel; + public HandleString Path { get; } = new() { FullPath = path, GamePath = pathFromModel }; public string Shpk { get; } = shpk; public ColorTable? ColorTable { get; } = colorTable; public IList Textures { get; } = textures; @@ -170,8 +168,7 @@ public class ParsedMaterialInfo(string path, string pathFromModel, string shpk, public class ParsedModelInfo(string path, string pathFromCharacter, DeformerCachedStruct? deformer, Model.ShapeAttributeGroup shapeAttributeGroup, IList materials) { - public string Path { get; } = path; - public string PathFromCharacter { get; } = pathFromCharacter; + public HandleString Path { get; } = new() { FullPath = path, GamePath = pathFromCharacter }; public DeformerCachedStruct? Deformer { get; } = deformer; public Model.ShapeAttributeGroup ShapeAttributeGroup { get; } = shapeAttributeGroup; public IList Materials { get; } = materials; @@ -183,9 +180,12 @@ public interface ICharacterInstance public CustomizeParameter CustomizeParameter { get; } } -public interface ITextureMappableInstance +public struct HandleString { - public Dictionary TextureMap { get; } + public string FullPath; + public string GamePath; + + public static implicit operator HandleString(string path) => new() { FullPath = path, GamePath = path }; } public class ParsedCharacterInfo @@ -206,7 +206,7 @@ public ParsedCharacterInfo(IList models, ParsedSkeleton skeleto } } -public class ParsedCharacterInstance : ParsedInstance, IResolvableInstance, ICharacterInstance, ITextureMappableInstance +public class ParsedCharacterInstance : ParsedInstance, IResolvableInstance, ICharacterInstance { public ParsedCharacterInfo? CharacterInfo; public string Name; @@ -232,23 +232,4 @@ public void Resolve(LayoutService layoutService) public CustomizeData CustomizeData => CharacterInfo?.CustomizeData ?? new CustomizeData(); public CustomizeParameter CustomizeParameter => CharacterInfo?.CustomizeParameter ?? new CustomizeParameter(); - public Dictionary TextureMap => ResolveTextureMappings(); - - private Dictionary ResolveTextureMappings() - { - var textureMap = new Dictionary(); - if (CharacterInfo == null) return textureMap; - foreach (var model in CharacterInfo.Models) - { - foreach (var material in model.Materials) - { - foreach (var texture in material.Textures) - { - textureMap[texture.PathFromMaterial] = texture.Path; - } - } - } - - return textureMap; - } } diff --git a/Meddle/Meddle.Plugin/Services/ParseService.cs b/Meddle/Meddle.Plugin/Services/ParseService.cs index 1f9612e..940a83e 100644 --- a/Meddle/Meddle.Plugin/Services/ParseService.cs +++ b/Meddle/Meddle.Plugin/Services/ParseService.cs @@ -152,10 +152,10 @@ public unsafe CharacterGroup ParseCharacterBase(CharacterBase* characterBase) public Task ParseFromModelInfo(ParsedModelInfo info) { - var mdlFileResource = pack.GetFileOrReadFromDisk(info.Path); + var mdlFileResource = pack.GetFileOrReadFromDisk(info.Path.FullPath); if (mdlFileResource == null) { - throw new Exception($"Failed to load model file {info.Path}"); + throw new Exception($"Failed to load model file {info.Path.FullPath}"); } var mdlFile = new MdlFile(mdlFileResource); @@ -163,11 +163,11 @@ public Task ParseFromModelInfo(ParsedModelInfo info) foreach (var materialInfo in info.Materials) { - var mtrlFileResource = pack.GetFileOrReadFromDisk(materialInfo.Path); + var mtrlFileResource = pack.GetFileOrReadFromDisk(materialInfo.Path.FullPath); if (mtrlFileResource == null) { - logger.LogWarning("Material file {MtrlFileName} not found", materialInfo.Path); - mtrlGroups.Add(new MtrlFileStubGroup(materialInfo.Path)); + logger.LogWarning("Material file {MtrlFileName} not found", materialInfo.Path.FullPath); + mtrlGroups.Add(new MtrlFileStubGroup(materialInfo.Path.GamePath)); continue; } @@ -183,18 +183,18 @@ public Task ParseFromModelInfo(ParsedModelInfo info) var texGroups = new List(); foreach (var textureInfo in materialInfo.Textures) { - var textureResource = pack.GetFileOrReadFromDisk(textureInfo.Path); + var textureResource = pack.GetFileOrReadFromDisk(textureInfo.Path.FullPath); if (textureResource == null) { logger.LogWarning("Texture file {TexturePath} not found", textureInfo.Path); continue; } - var texGroup = new TexResourceGroup(textureInfo.PathFromMaterial, textureInfo.Path, textureInfo.Resource); + var texGroup = new TexResourceGroup(textureInfo.Path.GamePath, textureInfo.Path.FullPath, textureInfo.Resource); texGroups.Add(texGroup); } - mtrlGroups.Add(new MtrlFileGroup(materialInfo.PathFromModel, materialInfo.Path, mtrlFile, shpkName, shpkFile, + mtrlGroups.Add(new MtrlFileGroup(materialInfo.Path.GamePath, materialInfo.Path.FullPath, mtrlFile, shpkName, shpkFile, texGroups.ToArray())); } @@ -206,7 +206,7 @@ public Task ParseFromModelInfo(ParsedModelInfo info) info.Deformer.Value.DeformerId); } - return Task.FromResult(new MdlFileGroup(info.PathFromCharacter, info.Path, deformerGroup, mdlFile, mtrlGroups.ToArray(), info.ShapeAttributeGroup)); + return Task.FromResult(new MdlFileGroup(info.Path.GamePath, info.Path.FullPath, deformerGroup, mdlFile, mtrlGroups.ToArray(), info.ShapeAttributeGroup)); } public Task ParseFromPath(string mdlPath) diff --git a/Meddle/Meddle.Plugin/UI/Layout/Instance.cs b/Meddle/Meddle.Plugin/UI/Layout/Instance.cs index eef6a12..f162a8e 100644 --- a/Meddle/Meddle.Plugin/UI/Layout/Instance.cs +++ b/Meddle/Meddle.Plugin/UI/Layout/Instance.cs @@ -40,7 +40,7 @@ private void DrawInstance(ParsedInstance instance, Stack stack, var infoHeader = instance switch { ParsedHousingInstance housingObject => $"{housingObject.Type} - {housingObject.Name}", - ParsedBgPartsInstance bgObject => $"{bgObject.Type} - {bgObject.Path}", + ParsedBgPartsInstance bgObject => $"{bgObject.Type} - {bgObject.Path.GamePath}", ParsedUnsupportedInstance unsupported => $"{unsupported.Type} - {unsupported.InstanceType}", ParsedCharacterInstance character => $"{character.Type} - {character.Kind}", _ => $"{instance.Type}" @@ -77,7 +77,8 @@ private void DrawInstance(ParsedInstance instance, Stack stack, ImGui.Text($"Scale: {instance.Transform.Scale}"); if (instance is IPathInstance pathedInstance) { - UiUtil.Text($"Path: {pathedInstance.Path}", pathedInstance.Path); + UiUtil.Text($"Full Path: {pathedInstance.Path.FullPath}", pathedInstance.Path.FullPath); + UiUtil.Text($"Game Path: {pathedInstance.Path.GamePath}", pathedInstance.Path.GamePath); } if (instance is ParsedHousingInstance ho) @@ -122,11 +123,11 @@ private void DrawCharacter(ParsedCharacterInstance character) ImGui.Text("Models"); foreach (var modelInfo in character.CharacterInfo.Models) { - using var treeNode = ImRaii.TreeNode($"Model: {modelInfo.Path}"); + using var treeNode = ImRaii.TreeNode($"Model: {modelInfo.Path.GamePath}"); if (treeNode.Success) { - UiUtil.Text($"Model Path: {modelInfo.Path}", modelInfo.Path); - UiUtil.Text($"Game Path: {modelInfo.PathFromCharacter}", modelInfo.PathFromCharacter); + UiUtil.Text($"Model Path: {modelInfo.Path.FullPath}", modelInfo.Path.FullPath); + UiUtil.Text($"Game Path: {modelInfo.Path.GamePath}", modelInfo.Path.GamePath); if (modelInfo.Deformer != null) { UiUtil.Text($"Deformer Path: {modelInfo.Deformer.Value.PbdPath}", modelInfo.Deformer.Value.PbdPath); @@ -136,21 +137,21 @@ private void DrawCharacter(ParsedCharacterInstance character) foreach (var materialInfo in modelInfo.Materials) { - using var materialNode = ImRaii.TreeNode($"Material: {materialInfo.Path}"); + using var materialNode = ImRaii.TreeNode($"Material: {materialInfo.Path.GamePath}"); if (materialNode.Success) { - UiUtil.Text($"Material Path: {materialInfo.Path}", materialInfo.Path); - UiUtil.Text($"Game Path: {materialInfo.PathFromModel}", materialInfo.PathFromModel); + UiUtil.Text($"Material Path: {materialInfo.Path.FullPath}", materialInfo.Path.FullPath); + UiUtil.Text($"Game Path: {materialInfo.Path.GamePath}", materialInfo.Path.GamePath); UiUtil.Text($"Shader Path: {materialInfo.Shpk}", materialInfo.Shpk); ImGui.Text($"Texture Count: {materialInfo.Textures.Count}"); foreach (var textureInfo in materialInfo.Textures) { - using var textureNode = ImRaii.TreeNode($"Texture: {textureInfo.Path}"); + using var textureNode = ImRaii.TreeNode($"Texture: {textureInfo.Path.GamePath}"); if (textureNode.Success) { - UiUtil.Text($"Texture Path: {textureInfo.Path}", textureInfo.Path); - UiUtil.Text($"Game Path: {textureInfo.PathFromMaterial}", textureInfo.PathFromMaterial); - DrawTexture(textureInfo.Path, textureInfo.Resource); + UiUtil.Text($"Texture Path: {textureInfo.Path.FullPath}", textureInfo.Path.FullPath); + UiUtil.Text($"Game Path: {textureInfo.Path.GamePath}", textureInfo.Path.GamePath); + DrawTexture(textureInfo.Path.FullPath, textureInfo.Resource); } } } diff --git a/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs b/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs index 1aec1bb..5bdd860 100644 --- a/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs +++ b/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs @@ -299,7 +299,7 @@ private void InstanceExport(ParsedInstance[] instances) { gltf.SaveAsWavefront(Path.Combine(path, $"{defaultName}.obj")); } - + Process.Start("explorer.exe", path); }, cancelToken.Token); }, Plugin.TempDirectory); diff --git a/Meddle/Meddle.Utils/MeshBuilder.cs b/Meddle/Meddle.Utils/MeshBuilder.cs index 88743f1..b5cf8d9 100644 --- a/Meddle/Meddle.Utils/MeshBuilder.cs +++ b/Meddle/Meddle.Utils/MeshBuilder.cs @@ -221,11 +221,11 @@ private IVertexBuilder BuildVertex(Vertex vertex) { geometryParamCache.Add(vertex.Tangent1!.Value with { W = vertex.Tangent1.Value.W == 1 ? 1 : -1 }); } - if (GeometryT == typeof(VertexPositionNormalTangent2)) - { - geometryParamCache.Add(vertex.Tangent1!.Value with { W = vertex.Tangent1.Value.W == 1 ? 1 : -1 }); - geometryParamCache.Add(vertex.Tangent2!.Value with { W = vertex.Tangent2.Value.W == 1 ? 1 : -1 }); - } + // if (GeometryT == typeof(VertexPositionNormalTangent2)) + // { + // geometryParamCache.Add(vertex.Tangent1!.Value with { W = vertex.Tangent1.Value.W == 1 ? 1 : -1 }); + // geometryParamCache.Add(vertex.Tangent2!.Value with { W = vertex.Tangent2.Value.W == 1 ? 1 : -1 }); + // } // ReSharper restore CompareOfFloatsByEqualityOperator // AKA: Has "Color1" component @@ -275,10 +275,10 @@ private static Type GetVertexGeometryType(IReadOnlyList vertex) return typeof(VertexPosition); } - if (vertex[0].Tangent2 != null && vertex[0].Tangent1 != null) - { - return typeof(VertexPositionNormalTangent2); - } + // if (vertex[0].Tangent2 != null && vertex[0].Tangent1 != null) + // { + // return typeof(VertexPositionNormalTangent2); + // } if (vertex[0].Tangent1 != null) { @@ -307,11 +307,11 @@ private static IVertexGeometry CreateGeometryParamCache(Vertex vertex, Type type return new VertexPositionNormalTangent(vertex.Position!.Value, vertex.Normal!.Value, vertex.Tangent1!.Value with { W = vertex.Tangent1.Value.W == 1 ? 1 : -1 }); - case not null when type == typeof(VertexPositionNormalTangent2): - return new VertexPositionNormalTangent2(vertex.Position!.Value, - vertex.Normal!.Value, - vertex.Tangent1!.Value with { W = vertex.Tangent1.Value.W == 1 ? 1 : -1 }, - vertex.Tangent2!.Value with { W = vertex.Tangent2.Value.W == 1 ? 1 : -1 }); + // case not null when type == typeof(VertexPositionNormalTangent2): + // return new VertexPositionNormalTangent2(vertex.Position!.Value, + // vertex.Normal!.Value, + // vertex.Tangent1!.Value with { W = vertex.Tangent1.Value.W == 1 ? 1 : -1 }, + // vertex.Tangent2!.Value with { W = vertex.Tangent2.Value.W == 1 ? 1 : -1 }); default: return new VertexPosition(vertex.Position!.Value); } @@ -390,7 +390,7 @@ private static Type GetVertexSkinningType(IReadOnlyList vertex, bool isS private static (Vector2 XY, Vector2 ZW) ToVec2(Vector4 v) => (new(v.X, v.Y), new(v.Z, v.W)); } -public struct VertexPositionNormalTangent2 : IVertexGeometry, IEquatable +/*public struct VertexPositionNormalTangent2 : IVertexGeometry, IEquatable { public VertexPositionNormalTangent2(in Vector3 p, in Vector3 n, in Vector4 t, in Vector4 t2) { @@ -485,3 +485,4 @@ public void ApplyTransform(in Matrix4x4 xform) #endregion } +*/ From 9c105c1bb1b0f3a61d0e4e3f7b426decf6b4e6d4 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 5 Sep 2024 19:55:00 +1000 Subject: [PATCH 06/44] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index bc3391d..0bf0923 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ riderModule.iml .idea/* /.vs/ +.venv/ + # Created by https://www.toptal.com/developers/gitignore/api/intellij # Edit at https://www.toptal.com/developers/gitignore?templates=intellij From 3eec38da86c75717978c12331e4bb92ebd09a29e Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 5 Sep 2024 20:07:39 +1000 Subject: [PATCH 07/44] Blend seems better here --- .../Models/Composer/CharacterTattooMaterialBuilder.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs index 27cbb89..247cce4 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs @@ -41,7 +41,9 @@ public override MeddleMaterialBuilder Apply() for (var y = 0; y < normalTexture.Height; y++) { var normal = normalTexture[x, y].ToVector4(); - if (normal.Z != 0) + var influence = normal.Z; + + if (influence > 0) { diffuseTexture[x, y] = new Vector4(color, normal.W).ToSkColor(); } @@ -49,6 +51,8 @@ public override MeddleMaterialBuilder Apply() { diffuseTexture[x, y] = new Vector4(0, 0, 0, normal.W).ToSkColor(); } + + normalTexture[x, y] = (normal with { Z = 1.0f, W = 1.0f }).ToSkColor(); } WithBaseColor(dataProvider.CacheTexture(diffuseTexture, $"Computed/{set.ComputedTextureName("diffuse")}")); @@ -58,7 +62,7 @@ public override MeddleMaterialBuilder Apply() IndexOfRefraction = set.GetConstantOrThrow(MaterialConstant.g_GlassIOR); var alphaThreshold = set.GetConstantOrThrow(MaterialConstant.g_AlphaThreshold); - WithAlpha(AlphaMode.MASK, alphaThreshold); + WithAlpha(AlphaMode.BLEND, alphaThreshold); Extras = set.ComposeExtrasNode(); return this; From 5527e8028187ce82528af8fd1ed0fd1809c7e589 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 5 Sep 2024 20:08:32 +1000 Subject: [PATCH 08/44] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0bf0923..9190e41 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ riderModule.iml /.vs/ .venv/ +__pycache__/ # Created by https://www.toptal.com/developers/gitignore/api/intellij # Edit at https://www.toptal.com/developers/gitignore?templates=intellij From ee1d082dbf6728ef5bdca846f26d2618d6717029 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 5 Sep 2024 21:26:33 +1000 Subject: [PATCH 09/44] Update BgMaterialBuilder.cs --- .../Models/Composer/BgMaterialBuilder.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs index 104876b..899786b 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs @@ -181,11 +181,24 @@ public override MeddleMaterialBuilder Apply() VertexPaint = set.ShaderKeys.Any(x => x is {Category: BgVertexPaintKey, Value: BgVertexPaintValue}); extras.Add(("VertexPaint", VertexPaint)); + var metallicRoughnessTexture = new SKTexture(textureSet.Color0.Width, textureSet.Color0.Height); + for (var x = 0; x < textureSet.Color0.Width; x++) + for (var y = 0; y < textureSet.Color0.Height; y++) + { + var mask = textureSet.Specular0[x, y].ToVector4(); + var specMaskA = mask.X; // ? + var roughness = mask.Y; + var specMaskB = mask.Z; // ? + + var metallic = 0.0f; + metallicRoughnessTexture[x, y] = new Vector4(1.0f, roughness, metallic, 1.0f).ToSkColor(); + } // Stub WithNormal(dataProvider.CacheTexture(textureSet.Normal0, $"Computed/{set.ComputedTextureName("normal")}")); WithBaseColor(dataProvider.CacheTexture(textureSet.Color0, $"Computed/{set.ComputedTextureName("diffuse")}")); //WithSpecularColor(dataProvider.CacheTexture(textureSet.Specular0, $"Computed/{set.ComputedTextureName("specular")}")); + WithMetallicRoughness(dataProvider.CacheTexture(metallicRoughnessTexture, $"Computed/{set.ComputedTextureName("metallicRoughness")}")); Extras = set.ComposeExtrasNode(extras.ToArray()); return this; From e01e0d7cd6bef4dadeb83aff3024ae5726dd086a Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 5 Sep 2024 21:26:51 +1000 Subject: [PATCH 10/44] Performance improvements --- .../Models/Composer/CharacterComposer.cs | 213 +++++++++--------- 1 file changed, 112 insertions(+), 101 deletions(-) diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs index 7ba857a..76721cc 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs @@ -41,133 +41,144 @@ public CharacterComposer(ILogger log, DataProvider dataProvider) } } } - - public void ComposeCharacterInstance(ParsedCharacterInstance characterInstance, SceneBuilder scene, NodeBuilder root) - { - var characterInfo = characterInstance.CharacterInfo; - if (characterInfo == null) return; - var bones = SkeletonUtils.GetBoneMap(characterInfo.Skeleton, true, out var rootBone); - if (rootBone != null) + private void HandleModel(ParsedCharacterInfo characterInfo, ParsedModelInfo modelInfo, SceneBuilder scene, List bones, BoneNodeBuilder? rootBone) + { + if (modelInfo.Path.GamePath.Contains("b0003_top")) { - root.AddNode(rootBone); + log.LogDebug("Skipping model {ModelPath}", modelInfo.Path.GamePath); + return; + } + var mdlData = dataProvider.LookupData(modelInfo.Path.FullPath); + if (mdlData == null) + { + log.LogWarning("Failed to load model file: {modelPath}", modelInfo.Path); + return; } - for (var i = 0; i < characterInfo.Models.Count; i++) + log.LogInformation("Loaded model {modelPath}", modelInfo.Path.FullPath); + var mdlFile = new MdlFile(mdlData); + //var materialBuilders = new List(); + var materialBuilders = new MaterialBuilder[modelInfo.Materials.Count]; + //for (int i = 0; i < modelInfo.Materials.Count; i++) + Parallel.For(0, modelInfo.Materials.Count, i => { - var modelInfo = characterInfo.Models[i]; - if (modelInfo.Path.GamePath.Contains("b0003_top")) continue; - var mdlData = dataProvider.LookupData(modelInfo.Path.FullPath); - if (mdlData == null) + var materialInfo = modelInfo.Materials[i]; + var mtrlData = dataProvider.LookupData(materialInfo.Path.FullPath); + if (mtrlData == null) { - log.LogWarning("Failed to load model file: {modelPath}", modelInfo.Path); - continue; + log.LogWarning("Failed to load material file: {mtrlPath}", materialInfo.Path.FullPath); + throw new Exception($"Failed to load material file: {materialInfo.Path.FullPath}"); } - log.LogInformation("Loaded model {modelPath}", modelInfo.Path.FullPath); - var mdlFile = new MdlFile(mdlData); - var materialBuilders = new List(); - foreach (var materialInfo in modelInfo.Materials) + log.LogInformation("Loaded material {mtrlPath}", materialInfo.Path.FullPath); + var mtrlFile = new MtrlFile(mtrlData); + if (materialInfo.ColorTable != null) { - var mtrlData = dataProvider.LookupData(materialInfo.Path.FullPath); - if (mtrlData == null) - { - log.LogWarning("Failed to load material file: {mtrlPath}", materialInfo.Path.FullPath); - throw new Exception($"Failed to load material file: {materialInfo.Path.FullPath}"); - } + mtrlFile.ColorTable = materialInfo.ColorTable.Value; + } - log.LogInformation("Loaded material {mtrlPath}", materialInfo.Path.FullPath); - var mtrlFile = new MtrlFile(mtrlData); - if (materialInfo.ColorTable != null) - { - mtrlFile.ColorTable = materialInfo.ColorTable.Value; - } - - var shpkName = mtrlFile.GetShaderPackageName(); - var shpkPath = $"shader/sm5/shpk/{shpkName}"; - var shpkData = dataProvider.LookupData(shpkPath); - if (shpkData == null) throw new Exception($"Failed to load shader package file: {shpkPath}"); - var shpkFile = new ShpkFile(shpkData); - var material = new MaterialSet(mtrlFile, materialInfo.Path.GamePath, - shpkFile, - shpkName, - materialInfo.Textures - .Select(x => x.Path) - .ToArray(), - handleString => - { - var match = materialInfo.Textures.FirstOrDefault(x => - x.Path.GamePath == handleString.GamePath && + var shpkName = mtrlFile.GetShaderPackageName(); + var shpkPath = $"shader/sm5/shpk/{shpkName}"; + var shpkData = dataProvider.LookupData(shpkPath); + if (shpkData == null) throw new Exception($"Failed to load shader package file: {shpkPath}"); + var shpkFile = new ShpkFile(shpkData); + var material = new MaterialSet(mtrlFile, materialInfo.Path.GamePath, + shpkFile, + shpkName, + materialInfo.Textures + .Select(x => x.Path) + .ToArray(), + handleString => + { + var match = materialInfo.Textures.FirstOrDefault(x => + x.Path.GamePath == handleString.GamePath && x.Path.FullPath == handleString.FullPath); - return match?.Resource; - }); - material.SetCustomizeParameters(characterInstance.CustomizeParameter); - material.SetCustomizeData(characterInstance.CustomizeData); - - materialBuilders.Add(material.Compose(dataProvider)); - } + return match?.Resource; + }); + material.SetCustomizeParameters(characterInfo.CustomizeParameter); + material.SetCustomizeData(characterInfo.CustomizeData); + + materialBuilders[i] = material.Compose(dataProvider); + }); - var model = new Model(modelInfo.Path.GamePath, mdlFile, modelInfo.ShapeAttributeGroup); - EnsureBonesExist(model, bones, rootBone); - (GenderRace from, GenderRace to, RaceDeformer deformer)? deform; - if (modelInfo.Deformer != null) + var model = new Model(modelInfo.Path.GamePath, mdlFile, modelInfo.ShapeAttributeGroup); + EnsureBonesExist(model, bones, rootBone); + (GenderRace from, GenderRace to, RaceDeformer deformer)? deform; + if (modelInfo.Deformer != null) + { + // custom pbd may exist + var pbdFileData = dataProvider.LookupData(modelInfo.Deformer.Value.PbdPath); + if (pbdFileData == null) throw new InvalidOperationException($"Failed to get deformer pbd {modelInfo.Deformer.Value.PbdPath}"); + deform = ((GenderRace)modelInfo.Deformer.Value.DeformerId, (GenderRace)modelInfo.Deformer.Value.RaceSexId, new RaceDeformer(new PbdFile(pbdFileData), bones)); + log.LogDebug("Using deformer pbd {Path}", modelInfo.Deformer.Value.PbdPath); + } + else + { + var parsed = RaceDeformer.ParseRaceCode(modelInfo.Path.GamePath); + if (Enum.IsDefined(parsed)) { - // custom pbd may exist - var pbdFileData = dataProvider.LookupData(modelInfo.Deformer.Value.PbdPath); - if (pbdFileData == null) throw new InvalidOperationException($"Failed to get deformer pbd {modelInfo.Deformer.Value.PbdPath}"); - deform = ((GenderRace)modelInfo.Deformer.Value.DeformerId, (GenderRace)modelInfo.Deformer.Value.RaceSexId, new RaceDeformer(new PbdFile(pbdFileData), bones)); - log.LogDebug("Using deformer pbd {Path}", modelInfo.Deformer.Value.PbdPath); + deform = (parsed, characterInfo.GenderRace, new RaceDeformer(PbdFile!, bones)); } else { - var parsed = RaceDeformer.ParseRaceCode(modelInfo.Path.GamePath); - if (Enum.IsDefined(parsed)) - { - deform = (parsed, characterInfo.GenderRace, new RaceDeformer(PbdFile!, bones)); - } - else - { - deform = null; - } + deform = null; } + } - var meshes = ModelBuilder.BuildMeshes(model, materialBuilders, bones, deform); - foreach (var mesh in meshes) + var meshes = ModelBuilder.BuildMeshes(model, materialBuilders, bones, deform); + foreach (var mesh in meshes) + { + InstanceBuilder instance; + if (bones.Count > 0) { - InstanceBuilder instance; - if (bones.Count > 0) - { - instance = scene.AddSkinnedMesh(mesh.Mesh, Matrix4x4.Identity, bones.Cast().ToArray()); - } - else - { - instance = scene.AddRigidMesh(mesh.Mesh, Matrix4x4.Identity); - } + instance = scene.AddSkinnedMesh(mesh.Mesh, Matrix4x4.Identity, bones.Cast().ToArray()); + } + else + { + instance = scene.AddRigidMesh(mesh.Mesh, Matrix4x4.Identity); + } - if (model.Shapes.Count != 0 && mesh.Shapes != null) - { - // This will set the morphing value to 1 if the shape is enabled, 0 if not - var enabledShapes = Model.GetEnabledValues(model.EnabledShapeMask, model.ShapeMasks) - .ToArray(); - var shapes = model.Shapes - .Where(x => mesh.Shapes.Contains(x.Name)) - .Select(x => (x, enabledShapes.Contains(x.Name))); - instance.Content.UseMorphing().SetValue(shapes.Select(x => x.Item2 ? 1f : 0).ToArray()); - } - - if (mesh.Submesh != null) + if (model.Shapes.Count != 0 && mesh.Shapes != null) + { + // This will set the morphing value to 1 if the shape is enabled, 0 if not + var enabledShapes = Model.GetEnabledValues(model.EnabledShapeMask, model.ShapeMasks) + .ToArray(); + var shapes = model.Shapes + .Where(x => mesh.Shapes.Contains(x.Name)) + .Select(x => (x, enabledShapes.Contains(x.Name))); + instance.Content.UseMorphing().SetValue(shapes.Select(x => x.Item2 ? 1f : 0).ToArray()); + } + + if (mesh.Submesh != null) + { + // Remove subMeshes that are not enabled + var enabledAttributes = Model.GetEnabledValues(model.EnabledAttributeMask, model.AttributeMasks); + if (!mesh.Submesh.Attributes.All(enabledAttributes.Contains)) { - // Remove subMeshes that are not enabled - var enabledAttributes = Model.GetEnabledValues(model.EnabledAttributeMask, model.AttributeMasks); - if (!mesh.Submesh.Attributes.All(enabledAttributes.Contains)) - { - instance.Remove(); - } + instance.Remove(); } } } } + public void ComposeCharacterInstance(ParsedCharacterInstance characterInstance, SceneBuilder scene, NodeBuilder root) + { + var characterInfo = characterInstance.CharacterInfo; + if (characterInfo == null) return; + + var bones = SkeletonUtils.GetBoneMap(characterInfo.Skeleton, true, out var rootBone); + if (rootBone != null) + { + root.AddNode(rootBone); + } + + foreach (var t in characterInfo.Models) + { + HandleModel(characterInfo, t, scene, bones, rootBone); + } + } + private void EnsureBonesExist(Model model, List bones, BoneNodeBuilder? root) { foreach (var mesh in model.Meshes) From adb2a09ab7bb728460fff0c8cce2a475e4eafc78 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 5 Sep 2024 21:27:09 +1000 Subject: [PATCH 11/44] Naming changes --- .../Models/Composer/HairMaterialBuilder.cs | 37 +++++++++---------- .../Models/Composer/SkinMaterialBuilder.cs | 22 +++++------ 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs index b751e32..86ac0d7 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs @@ -33,47 +33,46 @@ public override MeddleMaterialBuilder Apply() var maskTexture = maskRes.ToTexture(normalTexture.Size); var hairColor = parameters.MainColor; - var tattooColor = parameters.OptionColor; - var highlightColor = parameters.MeshColor; + var bonusColor = hairType switch + { + HairType.Face => parameters.OptionColor, // tattoo + HairType.Hair => parameters.MeshColor, // hair highlight + _ => hairColor + }; + // TODO: Eyelashes should be black, possibly to do with vertex colors var diffuseTexture = new SKTexture(normalTexture.Width, normalTexture.Height); - var occ_x_x_x_Texture = new SKTexture(normalTexture.Width, normalTexture.Height); - var vol_thick_x_x_Texture = new SKTexture(normalTexture.Width, normalTexture.Height); + var metallicRoughnessTexture = new SKTexture(normalTexture.Width, normalTexture.Height); for (int x = 0; x < normalTexture.Width; x++) for (int y = 0; y < normalTexture.Height; y++) { var normal = normalTexture[x, y].ToVector4(); var mask = maskTexture[x, y].ToVector4(); - - var bonusColor = hairType switch - { - HairType.Face => tattooColor, - HairType.Hair => highlightColor, - _ => hairColor - }; var bonusIntensity = normal.Z; + var specular = mask.X; + var roughness = mask.Y; + var sssThickness = mask.Z; + var metallic = 0.0f; + var diffuseMaskOrAmbientOcclusion = mask.W; + var diffusePixel = Vector3.Lerp(hairColor, bonusColor, bonusIntensity); - var occlusion = mask.W * mask.W; + metallicRoughnessTexture[x, y] = new Vector4(1.0f, roughness, metallic, 1.0f).ToSkColor(); diffuseTexture[x, y] = new Vector4(diffusePixel, normal.W).ToSkColor(); - occ_x_x_x_Texture[x, y] = new Vector4(occlusion, 0f, 0f, 1.0f).ToSkColor(); normalTexture[x, y] = (normal with { Z = 1.0f, W = 1.0f }).ToSkColor(); - vol_thick_x_x_Texture[x, y] = new Vector4(mask.Z, mask.Z, mask.Z, 1.0f).ToSkColor(); } WithDoubleSide(set.RenderBackfaces); WithBaseColor(dataProvider.CacheTexture(diffuseTexture, $"Computed/{set.ComputedTextureName("diffuse")}")); WithNormal(dataProvider.CacheTexture(normalTexture, $"Computed/{set.ComputedTextureName("normal")}")); - WithOcclusion(dataProvider.CacheTexture(occ_x_x_x_Texture, $"Computed/{set.ComputedTextureName("occlusion")}")); - WithMetallicRoughness(0, 1); - WithAlpha(AlphaMode.BLEND, set.GetConstantOrThrow(MaterialConstant.g_AlphaThreshold)); + WithMetallicRoughness(dataProvider.CacheTexture(metallicRoughnessTexture, $"Computed/{set.ComputedTextureName("metallicRoughness")}")); + WithAlpha(AlphaMode.MASK, set.GetConstantOrThrow(MaterialConstant.g_AlphaThreshold)); IndexOfRefraction = set.GetConstantOrThrow(MaterialConstant.g_GlassIOR); Extras = set.ComposeExtrasNode( ("hairColor", hairColor.AsFloatArray()), - ("tattooColor", tattooColor.AsFloatArray()), - ("highlightColor", highlightColor.AsFloatArray()) + ("bonusColor", bonusColor.AsFloatArray()) ); return this; } diff --git a/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs index aedea33..a623fbd 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs @@ -63,9 +63,14 @@ public override MeddleMaterialBuilder Apply() var mask = maskTexture[x, y].ToVector4(); var diffuse = diffuseTexture[x, y].ToVector4(); - var diffuseAlpha = diffuse.W; var skinInfluence = normal.Z; + var specularPower = mask.X; + var roughness = mask.Y; + var sssThickness = mask.Z; + var metallic = 0.0f; + var hairHighlightInfluence = mask.W; + var sColor = Vector3.Lerp(diffuseColor, skinColor, skinInfluence); diffuse *= new Vector4(sColor, 1.0f); @@ -74,7 +79,7 @@ public override MeddleMaterialBuilder Apply() var hair = hairColor; if (data.Highlights) { - hair = Vector3.Lerp(hairColor, highlightColor, mask.W); + hair = Vector3.Lerp(hairColor, highlightColor, hairHighlightInfluence); } // tt arbitrary darkening instead of using flow map @@ -86,26 +91,21 @@ public override MeddleMaterialBuilder Apply() } diffuseAlpha = set.IsTransparent ? diffuseAlpha * alphaMultiplier : (diffuseAlpha * alphaMultiplier < 1.0f ? 0.0f : 1.0f); - - var specular = mask.X; - var roughness = mask.Y; - var subsurface = mask.Z; - var metallic = 0.0f; - var roughnessPixel = new Vector4(subsurface, roughness, metallic, specular); + if (skinType == SkinType.Face) { if (data.LipStick) { diffuse = Vector4.Lerp(diffuse, lipColor, normal.W * lipColor.W); - roughnessPixel *= lipRoughnessScale; } + roughness *= lipRoughnessScale; } diffuseTexture[x, y] = (diffuse with {W = diffuseAlpha}).ToSkColor(); normalTexture[x, y] = (normal with { Z = 1.0f, W = 1.0f}).ToSkColor(); - metallicRoughnessTexture[x, y] = roughnessPixel.ToSkColor(); - sssTexture[x, y] = new Vector4(subsurface, subsurface, subsurface, 1).ToSkColor(); + metallicRoughnessTexture[x, y] = new Vector4(1.0f, roughness, metallic, 1.0f).ToSkColor(); + sssTexture[x, y] = new Vector4(sssThickness, sssThickness, sssThickness, 1).ToSkColor(); } WithBaseColor(dataProvider.CacheTexture(diffuseTexture, $"Computed/{set.ComputedTextureName("diffuse")}")); From 5c279b54782554a6670e0552e0a7dc44e2002762 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 5 Sep 2024 21:27:25 +1000 Subject: [PATCH 12/44] Init blender plugin --- Blender/__init__.py | 211 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 Blender/__init__.py diff --git a/Blender/__init__.py b/Blender/__init__.py new file mode 100644 index 0000000..41af6d4 --- /dev/null +++ b/Blender/__init__.py @@ -0,0 +1,211 @@ +bl_info = { + "name": "Meddle Utils", + "blender": (4, 0, 0), + "category": "3D View", +} + +import bpy +from bpy.types import Operator, Panel, ShaderNodeBsdfPrincipled +from bpy.props import StringProperty +import os + +class VIEW3D_PT_update_meddle_shaders(Panel): + bl_label = "Meddle Utils" + bl_idname = "VIEW3D_PT_update_meddle_shaders" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'Meddle' + + def draw(self, context): + layout = self.layout + + row = layout.row() + row.operator("meddle.fix_ior", text="Fix IOR") + + row = layout.row() + row.operator("meddle.fix_terrain", text="Fix Terrain") + + + +class MEDDLE_OT_fix_ior(Operator): + """Sets the IOR value in the Principled BSDF node of all materials""" + bl_idname = "meddle.fix_ior" + bl_label = "Update Shaders" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + # Iterate all materials in the scene + for mat in bpy.data.materials: + # Check if the material uses nodes + if not mat.use_nodes: + continue + + # Look for the Principled BSDF node + principled_bsdf = None + for node in mat.node_tree.nodes: + if node.type == 'BSDF_PRINCIPLED': + principled_bsdf = node + break + + if not principled_bsdf: + continue + + # Check if the material has the custom property g_GlassIOR + if "g_GlassIOR" in mat: + ior_value = mat["g_GlassIOR"] + # get first value of the IOR + print(f"Found custom property 'g_GlassIOR' in material '{mat.name}' with value {ior_value[0]}.") + # Set the IOR value in the Principled BSDF node + principled_bsdf.inputs['IOR'].default_value = ior_value[0] + else: + print(f"Material '{mat.name}' does not have the custom property 'g_GlassIOR'.") + + return {'FINISHED'} + +class MEDDLE_OT_fix_terrain(Operator): + """Looks up the g_SamplerXXXMap1 values on bg materials and creates the relevant texture nodes""" + bl_idname = "meddle.fix_terrain" + bl_label = "Fix Terrain" + bl_options = {'REGISTER', 'UNDO'} + + directory: StringProperty(subtype='DIR_PATH') + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + + def execute(self, context): + context.scene.selected_folder = self.directory + print(f"Folder selected: {self.directory}") + + # Iterate all materials in the scene + for mat in bpy.data.materials: + # Check if the material uses nodes + if not mat.use_nodes: + continue + + if "ShaderPackage" not in mat: + continue + + if mat["ShaderPackage"] != "bg.shpk": + continue + + # Look for the Principled BSDF node + principled_bsdf = None + for node in mat.node_tree.nodes: + if node.type == 'BSDF_PRINCIPLED': + principled_bsdf = node + break + + if not principled_bsdf: + continue + + # look for g_SamplerColorMap1, g_SamplerNormalMap1, g_SamplerSpecularMap1 + color_map = None + if "g_SamplerColorMap1" in mat: + color_map = mat["g_SamplerColorMap1"] + print(f"Found custom property 'g_SamplerColorMap1' in material '{mat.name}' with value {color_map}.") + else: + print(f"Material '{mat.name}' does not have the custom property 'g_SamplerColorMap1'.") + + base_color = None + for node in mat.node_tree.nodes: + if node.label == "BASE COLOR": + base_color = node + break + + normal_map = None + if "g_SamplerNormalMap1" in mat: + normal_map = mat["g_SamplerNormalMap1"] + print(f"Found custom property 'g_SamplerNormalMap1' in material '{mat.name}' with value {normal_map}.") + else: + print(f"Material '{mat.name}' does not have the custom property 'g_SamplerNormalMap1'.") + + base_normal = None + for node in mat.node_tree.nodes: + if node.label == "NORMAL MAP": + base_normal = node + break + + normal_tangent = None + for node in mat.node_tree.nodes: + if node.name == "Normal Map": + normal_tangent = node + break + + # specular_map = None + #if "g_SamplerSpecularMap1" in mat: + # specular_map = mat["g_SamplerSpecularMap1"] + # print(f"Found custom property 'g_SamplerSpecularMap1' in material '{mat.name}' with value {specular_map}.") + + # get vertex color node + vertex_color_node = None + for node in mat.node_tree.nodes: + if node.type == 'VERTEX_COLOR': + vertex_color_node = node + break + + if vertex_color_node is None: + break + + if color_map is not None and base_color is not None: + mix_color = mat.node_tree.nodes.new('ShaderNodeMixRGB') + if "dummy_" in color_map: + mix_color.blend_type = 'MULTIPLY' + else: + mix_color.blend_type = 'MIX' + mix_color.inputs['Fac'].default_value = 1.0 + mat.node_tree.links.new(mix_color.outputs['Color'], principled_bsdf.inputs['Base Color']) + mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_color.inputs['Fac']) + + # load color texture using the selected folder + color_map + ".png" + color_texture = mat.node_tree.nodes.new('ShaderNodeTexImage') + color_texture.image = bpy.data.images.load(self.directory + color_map + ".png") + mat.node_tree.links.new(color_texture.outputs['Color'], mix_color.inputs['Color2']) + + # use base_color + mat.node_tree.links.new(base_color.outputs['Color'], mix_color.inputs['Color1']) + + # organize nodes + color_texture.location = (base_color.location.x, base_color.location.y - 150) + mix_color.location = (base_color.location.x + 300, base_color.location.y) + + if normal_map is not None and base_normal is not None and normal_tangent is not None: + mix_normal = mat.node_tree.nodes.new('ShaderNodeMixRGB') + if "dummy_" in normal_map: + mix_normal.blend_type = 'MULTIPLY' + else: + mix_normal.blend_type = 'MIX' + mix_normal.inputs['Fac'].default_value = 1.0 + mat.node_tree.links.new(mix_normal.outputs['Color'], normal_tangent.inputs['Color']) + mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_normal.inputs['Fac']) + + # load normal texture using the selected folder + normal_map + ".png" + normal_texture = mat.node_tree.nodes.new('ShaderNodeTexImage') + normal_texture.image = bpy.data.images.load(self.directory + normal_map + ".png") + mat.node_tree.links.new(normal_texture.outputs['Color'], mix_normal.inputs['Color2']) + + # use base_normal + mat.node_tree.links.new(base_normal.outputs['Color'], mix_normal.inputs['Color1']) + + # organize nodes + normal_texture.location = (base_normal.location.x, base_normal.location.y - 150) + mix_normal.location = (base_normal.location.x + 300, base_normal.location.y) + + + + return {'FINISHED'} + +def register(): + bpy.utils.register_class(VIEW3D_PT_update_meddle_shaders) + bpy.utils.register_class(MEDDLE_OT_fix_ior) + bpy.utils.register_class(MEDDLE_OT_fix_terrain) + bpy.types.Scene.selected_folder = StringProperty(name="Selected Folder", description="Path to the selected folder") + +def unregister(): + bpy.utils.unregister_class(VIEW3D_PT_update_meddle_shaders) + bpy.utils.unregister_class(MEDDLE_OT_fix_ior) + bpy.utils.unregister_class(MEDDLE_OT_fix_terrain) + +if __name__ == "__main__": + register() \ No newline at end of file From 80b96781b3658f3184e236ed394e34e3a807dccc Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 5 Sep 2024 21:34:34 +1000 Subject: [PATCH 13/44] Set bg.shpk IOR to 1.0 --- Blender/__init__.py | 3 +++ Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/Blender/__init__.py b/Blender/__init__.py index 41af6d4..8b2a3e3 100644 --- a/Blender/__init__.py +++ b/Blender/__init__.py @@ -99,6 +99,9 @@ def execute(self, context): if not principled_bsdf: continue + + # set IOR to 1.0 + principled_bsdf.inputs['IOR'].default_value = 1.0 # look for g_SamplerColorMap1, g_SamplerNormalMap1, g_SamplerSpecularMap1 color_map = None diff --git a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs index 899786b..90148b4 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs @@ -200,6 +200,8 @@ public override MeddleMaterialBuilder Apply() //WithSpecularColor(dataProvider.CacheTexture(textureSet.Specular0, $"Computed/{set.ComputedTextureName("specular")}")); WithMetallicRoughness(dataProvider.CacheTexture(metallicRoughnessTexture, $"Computed/{set.ComputedTextureName("metallicRoughness")}")); + IndexOfRefraction = 1.0f; + Extras = set.ComposeExtrasNode(extras.ToArray()); return this; } From d9703d0981ae15b78ab8e9748a014d130fef0f22 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 5 Sep 2024 22:14:57 +1000 Subject: [PATCH 14/44] Handle errors --- Blender/__init__.py | 95 +++++++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/Blender/__init__.py b/Blender/__init__.py index 8b2a3e3..6563292 100644 --- a/Blender/__init__.py +++ b/Blender/__init__.py @@ -149,52 +149,55 @@ def execute(self, context): break if vertex_color_node is None: - break - - if color_map is not None and base_color is not None: - mix_color = mat.node_tree.nodes.new('ShaderNodeMixRGB') - if "dummy_" in color_map: - mix_color.blend_type = 'MULTIPLY' - else: - mix_color.blend_type = 'MIX' - mix_color.inputs['Fac'].default_value = 1.0 - mat.node_tree.links.new(mix_color.outputs['Color'], principled_bsdf.inputs['Base Color']) - mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_color.inputs['Fac']) - - # load color texture using the selected folder + color_map + ".png" - color_texture = mat.node_tree.nodes.new('ShaderNodeTexImage') - color_texture.image = bpy.data.images.load(self.directory + color_map + ".png") - mat.node_tree.links.new(color_texture.outputs['Color'], mix_color.inputs['Color2']) - - # use base_color - mat.node_tree.links.new(base_color.outputs['Color'], mix_color.inputs['Color1']) - - # organize nodes - color_texture.location = (base_color.location.x, base_color.location.y - 150) - mix_color.location = (base_color.location.x + 300, base_color.location.y) - - if normal_map is not None and base_normal is not None and normal_tangent is not None: - mix_normal = mat.node_tree.nodes.new('ShaderNodeMixRGB') - if "dummy_" in normal_map: - mix_normal.blend_type = 'MULTIPLY' - else: - mix_normal.blend_type = 'MIX' - mix_normal.inputs['Fac'].default_value = 1.0 - mat.node_tree.links.new(mix_normal.outputs['Color'], normal_tangent.inputs['Color']) - mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_normal.inputs['Fac']) - - # load normal texture using the selected folder + normal_map + ".png" - normal_texture = mat.node_tree.nodes.new('ShaderNodeTexImage') - normal_texture.image = bpy.data.images.load(self.directory + normal_map + ".png") - mat.node_tree.links.new(normal_texture.outputs['Color'], mix_normal.inputs['Color2']) - - # use base_normal - mat.node_tree.links.new(base_normal.outputs['Color'], mix_normal.inputs['Color1']) - - # organize nodes - normal_texture.location = (base_normal.location.x, base_normal.location.y - 150) - mix_normal.location = (base_normal.location.x + 300, base_normal.location.y) - + continue + + try: + if color_map is not None and base_color is not None: + mix_color = mat.node_tree.nodes.new('ShaderNodeMixRGB') + if "dummy_" in color_map: + mix_color.blend_type = 'MULTIPLY' + else: + mix_color.blend_type = 'MIX' + mix_color.inputs['Fac'].default_value = 1.0 + mat.node_tree.links.new(mix_color.outputs['Color'], principled_bsdf.inputs['Base Color']) + mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_color.inputs['Fac']) + + # load color texture using the selected folder + color_map + ".png" + color_texture = mat.node_tree.nodes.new('ShaderNodeTexImage') + color_texture.image = bpy.data.images.load(self.directory + color_map + ".png") + mat.node_tree.links.new(color_texture.outputs['Color'], mix_color.inputs['Color2']) + + # use base_color + mat.node_tree.links.new(base_color.outputs['Color'], mix_color.inputs['Color1']) + + # organize nodes + color_texture.location = (base_color.location.x, base_color.location.y - 150) + mix_color.location = (base_color.location.x + 300, base_color.location.y) + + if normal_map is not None and base_normal is not None and normal_tangent is not None: + mix_normal = mat.node_tree.nodes.new('ShaderNodeMixRGB') + if "dummy_" in normal_map: + mix_normal.blend_type = 'MULTIPLY' + else: + mix_normal.blend_type = 'MIX' + mix_normal.inputs['Fac'].default_value = 1.0 + mat.node_tree.links.new(mix_normal.outputs['Color'], normal_tangent.inputs['Color']) + mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_normal.inputs['Fac']) + + # load normal texture using the selected folder + normal_map + ".png" + normal_texture = mat.node_tree.nodes.new('ShaderNodeTexImage') + normal_texture.image = bpy.data.images.load(self.directory + normal_map + ".png") + mat.node_tree.links.new(normal_texture.outputs['Color'], mix_normal.inputs['Color2']) + + # use base_normal + mat.node_tree.links.new(base_normal.outputs['Color'], mix_normal.inputs['Color1']) + + # organize nodes + normal_texture.location = (base_normal.location.x, base_normal.location.y - 150) + mix_normal.location = (base_normal.location.x + 300, base_normal.location.y) + except Exception as e: + print(f"Error: {e}") + continue return {'FINISHED'} From 10bfb0245b8b284cc75408b13f3e79bd273a50da Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 5 Sep 2024 23:06:34 +1000 Subject: [PATCH 15/44] yawn --- Blender/__init__.py | 92 +++++++++++++------ .../Models/Composer/BgMaterialBuilder.cs | 20 +--- .../Composer/CharacterMaterialBuilder.cs | 11 +-- .../CharacterTattooMaterialBuilder.cs | 11 +-- .../Models/Composer/HairMaterialBuilder.cs | 13 ++- .../Models/Composer/IrisMaterialBuilder.cs | 17 ++-- .../Models/Composer/Partitioner.cs | 32 +++++++ .../Models/Composer/SkinMaterialBuilder.cs | 24 ++--- .../Meddle.Plugin/UI/Layout/LayoutWindow.cs | 15 +++ 9 files changed, 150 insertions(+), 85 deletions(-) create mode 100644 Meddle/Meddle.Plugin/Models/Composer/Partitioner.cs diff --git a/Blender/__init__.py b/Blender/__init__.py index 6563292..96c2968 100644 --- a/Blender/__init__.py +++ b/Blender/__init__.py @@ -104,30 +104,30 @@ def execute(self, context): principled_bsdf.inputs['IOR'].default_value = 1.0 # look for g_SamplerColorMap1, g_SamplerNormalMap1, g_SamplerSpecularMap1 - color_map = None + g_SamplerColorMap1 = None if "g_SamplerColorMap1" in mat: - color_map = mat["g_SamplerColorMap1"] - print(f"Found custom property 'g_SamplerColorMap1' in material '{mat.name}' with value {color_map}.") + g_SamplerColorMap1 = mat["g_SamplerColorMap1"] + print(f"Found custom property 'g_SamplerColorMap1' in material '{mat.name}' with value {g_SamplerColorMap1}.") else: print(f"Material '{mat.name}' does not have the custom property 'g_SamplerColorMap1'.") - base_color = None + g_SamplerColorMap0Node = None for node in mat.node_tree.nodes: if node.label == "BASE COLOR": - base_color = node + g_SamplerColorMap0Node = node break - normal_map = None + g_SamplerNormalMap1 = None if "g_SamplerNormalMap1" in mat: - normal_map = mat["g_SamplerNormalMap1"] - print(f"Found custom property 'g_SamplerNormalMap1' in material '{mat.name}' with value {normal_map}.") + g_SamplerNormalMap1 = mat["g_SamplerNormalMap1"] + print(f"Found custom property 'g_SamplerNormalMap1' in material '{mat.name}' with value {g_SamplerNormalMap1}.") else: print(f"Material '{mat.name}' does not have the custom property 'g_SamplerNormalMap1'.") - base_normal = None + g_SamplerNormalMap0Node = None for node in mat.node_tree.nodes: if node.label == "NORMAL MAP": - base_normal = node + g_SamplerNormalMap0Node = node break normal_tangent = None @@ -152,9 +152,17 @@ def execute(self, context): continue try: - if color_map is not None and base_color is not None: - mix_color = mat.node_tree.nodes.new('ShaderNodeMixRGB') - if "dummy_" in color_map: + if g_SamplerColorMap1 is not None and g_SamplerColorMap0Node is not None: + mix_color = None + for node in mat.node_tree.nodes: + if node.label == "MIX COLOR": + mix_color = node + break + if mix_color is None: + mix_color = mat.node_tree.nodes.new('ShaderNodeMixRGB') + mix_color.label = "MIX COLOR" + + if "dummy_" in g_SamplerColorMap1: mix_color.blend_type = 'MULTIPLY' else: mix_color.blend_type = 'MIX' @@ -163,20 +171,37 @@ def execute(self, context): mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_color.inputs['Fac']) # load color texture using the selected folder + color_map + ".png" - color_texture = mat.node_tree.nodes.new('ShaderNodeTexImage') - color_texture.image = bpy.data.images.load(self.directory + color_map + ".png") - mat.node_tree.links.new(color_texture.outputs['Color'], mix_color.inputs['Color2']) + g_SamplerColorMap1Node = None + for node in mat.node_tree.nodes: + if node.label == "BASE COLOR 1": + g_SamplerColorMap1Node = node + break + + if g_SamplerColorMap1Node is None: + g_SamplerColorMap1Node = mat.node_tree.nodes.new('ShaderNodeTexImage') + g_SamplerColorMap1Node.label = "BASE COLOR 1" + + g_SamplerColorMap1Node.image = bpy.data.images.load(self.directory + g_SamplerColorMap1 + ".png") + mat.node_tree.links.new(g_SamplerColorMap1Node.outputs['Color'], mix_color.inputs['Color1']) # use base_color - mat.node_tree.links.new(base_color.outputs['Color'], mix_color.inputs['Color1']) + mat.node_tree.links.new(g_SamplerColorMap0Node.outputs['Color'], mix_color.inputs['Color2']) # organize nodes - color_texture.location = (base_color.location.x, base_color.location.y - 150) - mix_color.location = (base_color.location.x + 300, base_color.location.y) - - if normal_map is not None and base_normal is not None and normal_tangent is not None: - mix_normal = mat.node_tree.nodes.new('ShaderNodeMixRGB') - if "dummy_" in normal_map: + g_SamplerColorMap1Node.location = (g_SamplerColorMap0Node.location.x, g_SamplerColorMap0Node.location.y - 150) + mix_color.location = (g_SamplerColorMap0Node.location.x + 300, g_SamplerColorMap0Node.location.y) + + if g_SamplerNormalMap1 is not None and g_SamplerNormalMap0Node is not None and normal_tangent is not None: + mix_normal = None + for node in mat.node_tree.nodes: + if node.label == "MIX NORMAL": + mix_normal = node + break + if mix_normal is None: + mix_normal = mat.node_tree.nodes.new('ShaderNodeMixRGB') + mix_normal.label = "MIX NORMAL" + + if "dummy_" in g_SamplerNormalMap1: mix_normal.blend_type = 'MULTIPLY' else: mix_normal.blend_type = 'MIX' @@ -185,16 +210,25 @@ def execute(self, context): mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_normal.inputs['Fac']) # load normal texture using the selected folder + normal_map + ".png" - normal_texture = mat.node_tree.nodes.new('ShaderNodeTexImage') - normal_texture.image = bpy.data.images.load(self.directory + normal_map + ".png") - mat.node_tree.links.new(normal_texture.outputs['Color'], mix_normal.inputs['Color2']) + gSamplerNormalMap1Node = None + for node in mat.node_tree.nodes: + if node.label == "NORMAL MAP 1": + gSamplerNormalMap1Node = node + break + + if gSamplerNormalMap1Node is None: + gSamplerNormalMap1Node = mat.node_tree.nodes.new('ShaderNodeTexImage') + gSamplerNormalMap1Node.label = "NORMAL MAP 1" + gSamplerNormalMap1Node.image = bpy.data.images.load(self.directory + g_SamplerNormalMap1 + ".png") + mat.node_tree.links.new(gSamplerNormalMap1Node.outputs['Color'], mix_normal.inputs['Color1']) # use base_normal - mat.node_tree.links.new(base_normal.outputs['Color'], mix_normal.inputs['Color1']) + mat.node_tree.links.new(g_SamplerNormalMap0Node.outputs['Color'], mix_normal.inputs['Color2']) # organize nodes - normal_texture.location = (base_normal.location.x, base_normal.location.y - 150) - mix_normal.location = (base_normal.location.x + 300, base_normal.location.y) + gSamplerNormalMap1Node.location = (g_SamplerNormalMap0Node.location.x, g_SamplerNormalMap0Node.location.y - 150) + mix_normal.location = (g_SamplerNormalMap0Node.location.x + 300, g_SamplerNormalMap0Node.location.y) + normal_tangent.location = (g_SamplerNormalMap0Node.location.x + 600, g_SamplerNormalMap0Node.location.y) except Exception as e: print(f"Error: {e}") continue diff --git a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs index 90148b4..90282d5 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs @@ -180,25 +180,11 @@ public override MeddleMaterialBuilder Apply() VertexPaint = set.ShaderKeys.Any(x => x is {Category: BgVertexPaintKey, Value: BgVertexPaintValue}); extras.Add(("VertexPaint", VertexPaint)); - - var metallicRoughnessTexture = new SKTexture(textureSet.Color0.Width, textureSet.Color0.Height); - for (var x = 0; x < textureSet.Color0.Width; x++) - for (var y = 0; y < textureSet.Color0.Height; y++) - { - var mask = textureSet.Specular0[x, y].ToVector4(); - var specMaskA = mask.X; // ? - var roughness = mask.Y; - var specMaskB = mask.Z; // ? - - var metallic = 0.0f; - metallicRoughnessTexture[x, y] = new Vector4(1.0f, roughness, metallic, 1.0f).ToSkColor(); - } - - // Stub + WithNormal(dataProvider.CacheTexture(textureSet.Normal0, $"Computed/{set.ComputedTextureName("normal")}")); WithBaseColor(dataProvider.CacheTexture(textureSet.Color0, $"Computed/{set.ComputedTextureName("diffuse")}")); - //WithSpecularColor(dataProvider.CacheTexture(textureSet.Specular0, $"Computed/{set.ComputedTextureName("specular")}")); - WithMetallicRoughness(dataProvider.CacheTexture(metallicRoughnessTexture, $"Computed/{set.ComputedTextureName("metallicRoughness")}")); + // only the green/y channel is used here for roughness + WithMetallicRoughness(dataProvider.CacheTexture(textureSet.Specular0, $"Computed/{set.ComputedTextureName("specular")}"), 0.0f, 1.0f); IndexOfRefraction = 1.0f; diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs index 3c93674..0965137 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs @@ -49,8 +49,7 @@ public override MeddleMaterialBuilder Apply() var indexTexture = indexRes.ToTexture(normalRes.Size); var occlusionTexture = new SKTexture(normalRes.Width, normalRes.Height); var metallicRoughness = new SKTexture(normalRes.Width, normalRes.Height); - for (int x = 0; x < normalRes.Width; x++) - for (int y = 0; y < normalRes.Height; y++) + Partitioner.Iterate(normalTexture.Size, (x, y) => { var normal = normalTexture[x, y].ToVector4(); var mask = maskTexture[x, y].ToVector4(); @@ -61,7 +60,7 @@ public override MeddleMaterialBuilder Apply() { var diffuse = diffuseTexture![x, y].ToVector4(); diffuse *= new Vector4(blended.Diffuse, normal.Z); - diffuseTexture[x, y] = (diffuse with {W = normal.Z }).ToSkColor(); + diffuseTexture[x, y] = (diffuse with {W = normal.Z}).ToSkColor(); } else if (textureMode == TextureMode.Default) { @@ -82,9 +81,9 @@ public override MeddleMaterialBuilder Apply() var roughMask = mask.Z; metallicRoughness[x, y] = new Vector4(specMask, roughMask, 0, 1).ToSkColor(); }*/ - - normalTexture[x, y] = (normal with{ Z = 1.0f, W = 1.0f}).ToSkColor(); - } + + normalTexture[x, y] = (normal with {Z = 1.0f, W = 1.0f}).ToSkColor(); + }); WithBaseColor(dataProvider.CacheTexture(diffuseTexture, $"Computed/{set.ComputedTextureName("diffuse")}")); WithNormal(dataProvider.CacheTexture(normalTexture, $"Computed/{set.ComputedTextureName("normal")}")); diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs index 247cce4..cef449c 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs @@ -37,12 +37,11 @@ public override MeddleMaterialBuilder Apply() var normalTexture = normalRes.ToTexture(); var diffuseTexture = new SKTexture(normalTexture.Width, normalTexture.Height); - for (var x = 0; x < normalTexture.Width; x++) - for (var y = 0; y < normalTexture.Height; y++) + Partitioner.Iterate(normalTexture.Size, (x, y) => { var normal = normalTexture[x, y].ToVector4(); var influence = normal.Z; - + if (influence > 0) { diffuseTexture[x, y] = new Vector4(color, normal.W).ToSkColor(); @@ -51,9 +50,9 @@ public override MeddleMaterialBuilder Apply() { diffuseTexture[x, y] = new Vector4(0, 0, 0, normal.W).ToSkColor(); } - - normalTexture[x, y] = (normal with { Z = 1.0f, W = 1.0f }).ToSkColor(); - } + + normalTexture[x, y] = (normal with {Z = 1.0f, W = 1.0f}).ToSkColor(); + }); WithBaseColor(dataProvider.CacheTexture(diffuseTexture, $"Computed/{set.ComputedTextureName("diffuse")}")); WithNormal(dataProvider.CacheTexture(normalTexture, $"Computed/{set.ComputedTextureName("normal")}")); diff --git a/Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs index 86ac0d7..9c548c8 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs @@ -43,25 +43,24 @@ public override MeddleMaterialBuilder Apply() // TODO: Eyelashes should be black, possibly to do with vertex colors var diffuseTexture = new SKTexture(normalTexture.Width, normalTexture.Height); var metallicRoughnessTexture = new SKTexture(normalTexture.Width, normalTexture.Height); - for (int x = 0; x < normalTexture.Width; x++) - for (int y = 0; y < normalTexture.Height; y++) + Partitioner.Iterate(normalTexture.Size, (x, y) => { var normal = normalTexture[x, y].ToVector4(); var mask = maskTexture[x, y].ToVector4(); - + var bonusIntensity = normal.Z; var specular = mask.X; var roughness = mask.Y; var sssThickness = mask.Z; var metallic = 0.0f; var diffuseMaskOrAmbientOcclusion = mask.W; - + var diffusePixel = Vector3.Lerp(hairColor, bonusColor, bonusIntensity); - + metallicRoughnessTexture[x, y] = new Vector4(1.0f, roughness, metallic, 1.0f).ToSkColor(); diffuseTexture[x, y] = new Vector4(diffusePixel, normal.W).ToSkColor(); - normalTexture[x, y] = (normal with { Z = 1.0f, W = 1.0f }).ToSkColor(); - } + normalTexture[x, y] = (normal with {Z = 1.0f, W = 1.0f}).ToSkColor(); + }); WithDoubleSide(set.RenderBackfaces); WithBaseColor(dataProvider.CacheTexture(diffuseTexture, $"Computed/{set.ComputedTextureName("diffuse")}")); diff --git a/Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs index b6fd0a1..2fa0228 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs @@ -49,31 +49,30 @@ public override MeddleMaterialBuilder Apply() var leftIrisColor = parameters.LeftColor; var emissiveTexture = new SKTexture(normalTexture.Width, normalTexture.Height); var specularTexture = new SKTexture(normalTexture.Width, normalTexture.Height); - - for (var x = 0; x < normalTexture.Width; x++) - for (var y = 0; y < normalTexture.Height; y++) + + Partitioner.Iterate(normalTexture.Size, (x, y) => { var normal = normalTexture[x, y].ToVector4(); var mask = maskTexture[x, y].ToVector4(); var diffuse = diffuseTexture[x, y].ToVector4(); var cubeMap = cubeMapTexture[x, y].ToVector4(); - + var irisMask = mask.Z; var whites = diffuse * new Vector4(whiteEyeColor, 1.0f); - var iris = diffuse * (leftIrisColor with {W = 1.0f }); + var iris = diffuse * (leftIrisColor with {W = 1.0f}); diffuse = Vector4.Lerp(whites, iris, irisMask); - + // most textures this channel is just 0 // use mask red as emissive mask emissiveTexture[x, y] = new Vector4(mask.X, mask.X, mask.X, 1.0f).ToSkColor(); - + // use mask green as reflection mask/cubemap intensity var specular = new Vector4(cubeMap.X * mask.Y); specularTexture[x, y] = (specular with {W = 1.0f}).ToSkColor(); - + diffuseTexture[x, y] = diffuse.ToSkColor(); normalTexture[x, y] = (normal with {Z = 1.0f, W = 1.0f}).ToSkColor(); - } + }); WithDoubleSide(set.RenderBackfaces); WithBaseColor(dataProvider.CacheTexture(diffuseTexture, $"Computed/{set.ComputedTextureName("diffuse")}")); diff --git a/Meddle/Meddle.Plugin/Models/Composer/Partitioner.cs b/Meddle/Meddle.Plugin/Models/Composer/Partitioner.cs new file mode 100644 index 0000000..e5f897a --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/Partitioner.cs @@ -0,0 +1,32 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using Meddle.Utils.Models; + +namespace Meddle.Plugin.Models.Composer; + +public static class Partitioner +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Iterate(this SKTexture texture, Action partitionAction) + { + Iterate(texture.Width, texture.Height, partitionAction); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Iterate(this Vector2 size, Action partitionAction) + { + Iterate((int)size.X, (int)size.Y, partitionAction); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Iterate(int width, int height, Action partitionAction) + { + Parallel.For(0, height, y => + { + for (var x = 0; x < width; x++) + { + partitionAction(x, y); + } + }); + } +} diff --git a/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs index a623fbd..539d312 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs @@ -56,13 +56,12 @@ public override MeddleMaterialBuilder Apply() var metallicRoughnessTexture = new SKTexture(normalTexture.Width, normalTexture.Height); var sssTexture = new SKTexture(normalTexture.Width, normalTexture.Height); - for (var x = 0; x < normalTexture.Width; x++) - for (var y = 0; y < normalTexture.Height; y++) + Partitioner.Iterate(normalTexture.Size, (x, y) => { var normal = normalTexture[x, y].ToVector4(); var mask = maskTexture[x, y].ToVector4(); var diffuse = diffuseTexture[x, y].ToVector4(); - + var diffuseAlpha = diffuse.W; var skinInfluence = normal.Z; var specularPower = mask.X; @@ -70,10 +69,10 @@ public override MeddleMaterialBuilder Apply() var sssThickness = mask.Z; var metallic = 0.0f; var hairHighlightInfluence = mask.W; - + var sColor = Vector3.Lerp(diffuseColor, skinColor, skinInfluence); diffuse *= new Vector4(sColor, 1.0f); - + if (skinType == SkinType.Hrothgar) { var hair = hairColor; @@ -89,24 +88,27 @@ public override MeddleMaterialBuilder Apply() diffuse = Vector4.Lerp(diffuse, new Vector4(hair, 1.0f), delta); diffuseAlpha = 1.0f; } - - diffuseAlpha = set.IsTransparent ? diffuseAlpha * alphaMultiplier : (diffuseAlpha * alphaMultiplier < 1.0f ? 0.0f : 1.0f); - + diffuseAlpha = set.IsTransparent + ? diffuseAlpha * alphaMultiplier + : (diffuseAlpha * alphaMultiplier < 1.0f ? 0.0f : 1.0f); + + if (skinType == SkinType.Face) { if (data.LipStick) { diffuse = Vector4.Lerp(diffuse, lipColor, normal.W * lipColor.W); } + roughness *= lipRoughnessScale; } - + diffuseTexture[x, y] = (diffuse with {W = diffuseAlpha}).ToSkColor(); - normalTexture[x, y] = (normal with { Z = 1.0f, W = 1.0f}).ToSkColor(); + normalTexture[x, y] = (normal with {Z = 1.0f, W = 1.0f}).ToSkColor(); metallicRoughnessTexture[x, y] = new Vector4(1.0f, roughness, metallic, 1.0f).ToSkColor(); sssTexture[x, y] = new Vector4(sssThickness, sssThickness, sssThickness, 1).ToSkColor(); - } + }); WithBaseColor(dataProvider.CacheTexture(diffuseTexture, $"Computed/{set.ComputedTextureName("diffuse")}")); WithNormal(dataProvider.CacheTexture(normalTexture, $"Computed/{set.ComputedTextureName("normal")}")); diff --git a/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs b/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs index 5bdd860..6736cdf 100644 --- a/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs +++ b/Meddle/Meddle.Plugin/UI/Layout/LayoutWindow.cs @@ -157,6 +157,19 @@ public override void Draw() var items = set.ToArray(); + var count = 0; + foreach (var item in items) + { + if (item is ParsedSharedInstance shared) + { + count += shared.Flatten().Length; + } + else + { + count++; + } + } + ExportButton($"Export {items.Length} instance(s)", items); ImGui.SameLine(); if (ImGui.Button($"Add {items.Length} instance(s) to selection")) @@ -166,6 +179,8 @@ public override void Draw() selectedInstances[item.Id] = item; } } + ImGui.SameLine(); + ImGui.Text($"Total: {count}"); DrawInstanceTable(items, DrawLayoutButtons); } From 4bd65ea317efc418f0fe3018e674623d93fe1da8 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Fri, 6 Sep 2024 00:19:59 +1000 Subject: [PATCH 16/44] Avoid computing any bg materials --- Blender/__init__.py | 72 ++++++++++++++----- .../Models/Composer/BgMaterialBuilder.cs | 61 +++++++--------- .../Models/Composer/DataProvider.cs | 23 ++++++ .../Models/Composer/MaterialSet.cs | 24 +++++++ 4 files changed, 127 insertions(+), 53 deletions(-) diff --git a/Blender/__init__.py b/Blender/__init__.py index 96c2968..0704ea5 100644 --- a/Blender/__init__.py +++ b/Blender/__init__.py @@ -20,15 +20,54 @@ def draw(self, context): layout = self.layout row = layout.row() - row.operator("meddle.fix_ior", text="Fix IOR") + row.operator("meddle.fix_ior", text="Fix ior") row = layout.row() - row.operator("meddle.fix_terrain", text="Fix Terrain") + row.operator("meddle.fix_bg", text="Fix bg.shpk") + row = layout.row() + row.operator("meddle.connect_volume", text="Connect Volume") + +class MEDDLE_OT_connect_volume(Operator): + """Connects the volume output to the material output""" + bl_idname = "meddle.connect_volume" + bl_label = "Connect Volume" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + # Iterate all materials in the scene + for mat in bpy.data.materials: + # Check if the material uses nodes + if not mat.use_nodes: + continue + + # Look for the Principled BSDF node + principled_bsdf = None + for node in mat.node_tree.nodes: + if node.type == 'BSDF_PRINCIPLED': + principled_bsdf = node + break + + if not principled_bsdf: + print(f"Material '{mat.name}' does not have a Principled BSDF node.") + continue + + material_output = None + for node in mat.node_tree.nodes: + if node.type == 'OUTPUT_MATERIAL': + material_output = node + break + + if not material_output: + print(f"Material '{mat.name}' does not have a Material Output node.") + continue + mat.node_tree.links.new(principled_bsdf.outputs['BSDF'], material_output.inputs['Volume']) + + return {'FINISHED'} class MEDDLE_OT_fix_ior(Operator): - """Sets the IOR value in the Principled BSDF node of all materials""" + """Sets the IOR value in the Principled BSDF node of all materials to the value of the custom property g_GlassIOR or 1.0 if not found""" bl_idname = "meddle.fix_ior" bl_label = "Update Shaders" bl_options = {'REGISTER', 'UNDO'} @@ -58,14 +97,16 @@ def execute(self, context): # Set the IOR value in the Principled BSDF node principled_bsdf.inputs['IOR'].default_value = ior_value[0] else: - print(f"Material '{mat.name}' does not have the custom property 'g_GlassIOR'.") + print(f"Material '{mat.name}' does not have the custom property 'g_GlassIOR'. Setting IOR to 1.0.") + # Set the IOR value in the Principled BSDF node to 1.0 + principled_bsdf.inputs['IOR'].default_value = 1.0 return {'FINISHED'} -class MEDDLE_OT_fix_terrain(Operator): +class MEDDLE_OT_fix_bg(Operator): """Looks up the g_SamplerXXXMap1 values on bg materials and creates the relevant texture nodes""" - bl_idname = "meddle.fix_terrain" - bl_label = "Fix Terrain" + bl_idname = "meddle.fix_bg" + bl_label = "Fix bg.shpk" bl_options = {'REGISTER', 'UNDO'} directory: StringProperty(subtype='DIR_PATH') @@ -100,9 +141,6 @@ def execute(self, context): if not principled_bsdf: continue - # set IOR to 1.0 - principled_bsdf.inputs['IOR'].default_value = 1.0 - # look for g_SamplerColorMap1, g_SamplerNormalMap1, g_SamplerSpecularMap1 g_SamplerColorMap1 = None if "g_SamplerColorMap1" in mat: @@ -182,10 +220,10 @@ def execute(self, context): g_SamplerColorMap1Node.label = "BASE COLOR 1" g_SamplerColorMap1Node.image = bpy.data.images.load(self.directory + g_SamplerColorMap1 + ".png") - mat.node_tree.links.new(g_SamplerColorMap1Node.outputs['Color'], mix_color.inputs['Color1']) + mat.node_tree.links.new(g_SamplerColorMap1Node.outputs['Color'], mix_color.inputs['Color2']) # use base_color - mat.node_tree.links.new(g_SamplerColorMap0Node.outputs['Color'], mix_color.inputs['Color2']) + mat.node_tree.links.new(g_SamplerColorMap0Node.outputs['Color'], mix_color.inputs['Color1']) # organize nodes g_SamplerColorMap1Node.location = (g_SamplerColorMap0Node.location.x, g_SamplerColorMap0Node.location.y - 150) @@ -220,10 +258,10 @@ def execute(self, context): gSamplerNormalMap1Node = mat.node_tree.nodes.new('ShaderNodeTexImage') gSamplerNormalMap1Node.label = "NORMAL MAP 1" gSamplerNormalMap1Node.image = bpy.data.images.load(self.directory + g_SamplerNormalMap1 + ".png") - mat.node_tree.links.new(gSamplerNormalMap1Node.outputs['Color'], mix_normal.inputs['Color1']) + mat.node_tree.links.new(gSamplerNormalMap1Node.outputs['Color'], mix_normal.inputs['Color2']) # use base_normal - mat.node_tree.links.new(g_SamplerNormalMap0Node.outputs['Color'], mix_normal.inputs['Color2']) + mat.node_tree.links.new(g_SamplerNormalMap0Node.outputs['Color'], mix_normal.inputs['Color1']) # organize nodes gSamplerNormalMap1Node.location = (g_SamplerNormalMap0Node.location.x, g_SamplerNormalMap0Node.location.y - 150) @@ -239,13 +277,15 @@ def execute(self, context): def register(): bpy.utils.register_class(VIEW3D_PT_update_meddle_shaders) bpy.utils.register_class(MEDDLE_OT_fix_ior) - bpy.utils.register_class(MEDDLE_OT_fix_terrain) + bpy.utils.register_class(MEDDLE_OT_fix_bg) + bpy.utils.register_class(MEDDLE_OT_connect_volume) bpy.types.Scene.selected_folder = StringProperty(name="Selected Folder", description="Path to the selected folder") def unregister(): bpy.utils.unregister_class(VIEW3D_PT_update_meddle_shaders) bpy.utils.unregister_class(MEDDLE_OT_fix_ior) - bpy.utils.unregister_class(MEDDLE_OT_fix_terrain) + bpy.utils.unregister_class(MEDDLE_OT_fix_bg) + bpy.utils.unregister_class(MEDDLE_OT_connect_volume) if __name__ == "__main__": register() \ No newline at end of file diff --git a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs index 90282d5..7bf4591 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs @@ -111,39 +111,6 @@ private DetailSet GetDetail(int detailId, Vector2 size) return new DetailSet(detailD, detailN, detailColor, detailNormalScale, detailColorUvScale, detailNormalUvScale); } } - - private TextureSet GetTextureSet() - { - if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerColorMap0, out var colorMap0Texture)) - { - throw new Exception("ColorMap0 texture not found"); - } - - if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerSpecularMap0, out var specularMap0Texture)) - { - throw new Exception("SpecularMap0 texture not found"); - } - - if (!set.TryGetTextureStrict(dataProvider, TextureUsage.g_SamplerNormalMap0, out var normalMap0Texture)) - { - throw new Exception("NormalMap0 texture not found"); - } - - var sizes = new List {colorMap0Texture.Size, specularMap0Texture.Size, normalMap0Texture.Size}; - var colorMap1 = set.TryGetTexture(dataProvider, TextureUsage.g_SamplerColorMap1, out var colorMap1Texture); - var specularMap1 = set.TryGetTexture(dataProvider, TextureUsage.g_SamplerSpecularMap1, out var specularMap1Texture); - var normalMap1 = set.TryGetTexture(dataProvider, TextureUsage.g_SamplerNormalMap1, out var normalMap1Texture); - - var size = sizes.MaxBy(x => x.X * x.Y); - var colorTex0 = colorMap0Texture.ToTexture(size); - var specularTex0 = specularMap0Texture.ToTexture(size); - var normalTex0 = normalMap0Texture.ToTexture(size); - var colorTex1 = colorMap1 ? colorMap1Texture.ToTexture(size) : null; - var specularTex1 = specularMap1 ? specularMap1Texture.ToTexture(size) : null; - var normalTex1 = normalMap1 ? normalMap1Texture.ToTexture(size) : null; - - return new TextureSet(colorTex0, specularTex0, normalTex0, colorTex1, specularTex1, normalTex1); - } public bool VertexPaint { get; private set; } @@ -163,7 +130,27 @@ private bool GetDiffuseColor(out Vector4 diffuseColor) public override MeddleMaterialBuilder Apply() { var extras = new List<(string, object)>(); - var textureSet = GetTextureSet(); + + if (!set.TryGetImageBuilderStrict(dataProvider, TextureUsage.g_SamplerColorMap0, out var colorMap0Texture)) + { + throw new Exception("ColorMap0 texture not found"); + } + + if (!set.TryGetImageBuilderStrict(dataProvider, TextureUsage.g_SamplerSpecularMap0, out var specularMap0Texture)) + { + throw new Exception("SpecularMap0 texture not found"); + } + + if (!set.TryGetImageBuilderStrict(dataProvider, TextureUsage.g_SamplerNormalMap0, out var normalMap0Texture)) + { + throw new Exception("NormalMap0 texture not found"); + } + + set.TryGetImageBuilder(dataProvider, TextureUsage.g_SamplerColorMap1, out var colorMap1Texture); + set.TryGetImageBuilder(dataProvider, TextureUsage.g_SamplerSpecularMap1, out var specularMap1Texture); + set.TryGetImageBuilder(dataProvider, TextureUsage.g_SamplerNormalMap1, out var normalMap1Texture); + + var alphaType = set.GetShaderKeyOrDefault(DiffuseAlphaKey, 0); if (alphaType == DiffuseAlphaValue) { @@ -181,10 +168,10 @@ public override MeddleMaterialBuilder Apply() VertexPaint = set.ShaderKeys.Any(x => x is {Category: BgVertexPaintKey, Value: BgVertexPaintValue}); extras.Add(("VertexPaint", VertexPaint)); - WithNormal(dataProvider.CacheTexture(textureSet.Normal0, $"Computed/{set.ComputedTextureName("normal")}")); - WithBaseColor(dataProvider.CacheTexture(textureSet.Color0, $"Computed/{set.ComputedTextureName("diffuse")}")); + WithNormal(normalMap0Texture); + WithBaseColor(colorMap0Texture); // only the green/y channel is used here for roughness - WithMetallicRoughness(dataProvider.CacheTexture(textureSet.Specular0, $"Computed/{set.ComputedTextureName("specular")}"), 0.0f, 1.0f); + WithMetallicRoughness(specularMap0Texture, 0.0f, 1.0f); IndexOfRefraction = 1.0f; diff --git a/Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs b/Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs index a357f2c..37580c6 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs @@ -64,6 +64,29 @@ public ShpkFile GetShpkFile(string fullPath) }).Value; } + private ConcurrentDictionary> textureCache = new(); + public ImageBuilder? LookupTexture(string gamePath) + { + gamePath = gamePath.TrimHandlePath(); + if (!gamePath.EndsWith(".tex")) throw new ArgumentException("Texture path must end with .tex", nameof(gamePath)); + if (Path.IsPathRooted(gamePath)) + { + throw new ArgumentException("Texture path must be a game path", nameof(gamePath)); + } + + return textureCache.GetOrAdd(gamePath, key => + { + cancellationToken.ThrowIfCancellationRequested(); + return new Lazy(() => + { + var data = LookupData(key, false); + if (data == null) return null; + var texFile = new TexFile(data); + return CacheTexture(texFile.ToResource().ToTexture(), key); + }, LazyThreadSafetyMode.ExecutionAndPublication); + }).Value; + } + public byte[]? LookupData(string fullPath, bool cacheIfTexture = true) { fullPath = fullPath.TrimHandlePath(); diff --git a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs index 8473628..244beaa 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs @@ -116,6 +116,29 @@ public bool TryGetTextureStrict(DataProvider provider, TextureUsage usage, out T texture = new TexFile(data).ToResource(); return true; } + + public bool TryGetImageBuilderStrict(DataProvider provider, TextureUsage usage, out ImageBuilder builder) + { + if (!TextureUsageDict.TryGetValue(usage, out var path)) + { + throw new Exception($"Texture usage {usage} not found in material set"); + } + + builder = provider.LookupTexture(path.FullPath) ?? throw new Exception($"Texture {path.FullPath} not found"); + return true; + } + + public bool TryGetImageBuilder(DataProvider provider, TextureUsage usage, out ImageBuilder? builder) + { + if (!TextureUsageDict.TryGetValue(usage, out var path)) + { + builder = null; + return false; + } + + builder = provider.LookupTexture(path.FullPath); + return builder != null; + } public bool TryGetTexture(DataProvider provider, TextureUsage usage, out TextureResource texture) { @@ -387,6 +410,7 @@ private MeddleMaterialBuilder GetMaterialBuilder(DataProvider dataProvider) switch (ShpkName) { case "bg.shpk": + case "bguvscroll.shpk": return new BgMaterialBuilder(mtrlName, new BgParams(), this, dataProvider); case "bgcolorchange.shpk": return new BgMaterialBuilder(mtrlName, new BgColorChangeParams(stainColor), this, dataProvider); From 80fb21bc8c7d1bcf398f138ff0ac3e86d26eec71 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Fri, 6 Sep 2024 19:20:29 +1000 Subject: [PATCH 17/44] Only apply volume to skin and iris --- Blender/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Blender/__init__.py b/Blender/__init__.py index 0704ea5..0a0a17b 100644 --- a/Blender/__init__.py +++ b/Blender/__init__.py @@ -26,10 +26,10 @@ def draw(self, context): row.operator("meddle.fix_bg", text="Fix bg.shpk") row = layout.row() - row.operator("meddle.connect_volume", text="Connect Volume") + row.operator("meddle.connect_volume", text="Connect Skin/Iris Volume") class MEDDLE_OT_connect_volume(Operator): - """Connects the volume output to the material output""" + """Connects the volume output to the material output for skin.shpk and iris.shpk""" bl_idname = "meddle.connect_volume" bl_label = "Connect Volume" bl_options = {'REGISTER', 'UNDO'} @@ -40,6 +40,12 @@ def execute(self, context): # Check if the material uses nodes if not mat.use_nodes: continue + + if "ShaderPackage" not in mat: + continue + + if mat["ShaderPackage"] != "skin.shpk" and mat["ShaderPackage"] != "iris.shpk": + continue # Look for the Principled BSDF node principled_bsdf = None From dd483e83a7b27068337e80291274823be14c8cdd Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Fri, 6 Sep 2024 19:25:47 +1000 Subject: [PATCH 18/44] Init refactor to support legacy color table + Make Live character tab use CharacterComposer + Relocate come classes in Meddle.Utils + (Regression) Lost support for exporting with attaches + Enable characterlegacy (uses character logic for now) --- .../Models/Composer/BgMaterialBuilder.cs | 1 - .../Models/Composer/CharacterComposer.cs | 51 ++- .../Composer/CharacterMaterialBuilder.cs | 9 +- .../CharacterTattooMaterialBuilder.cs | 1 - .../Models/Composer/DataProvider.cs | 1 - .../Models/Composer/HairMaterialBuilder.cs | 1 - .../Models/Composer/InstanceComposer.cs | 2 +- .../Models/Composer/IrisMaterialBuilder.cs | 1 - .../Models/Composer/MaterialSet.cs | 17 +- .../Models/Composer/Partitioner.cs | 2 +- .../Models/Composer/SkinMaterialBuilder.cs | 1 - .../Models/Layout/ParsedInstance.cs | 11 +- .../Meddle.Plugin/Services/LayoutService.cs | 201 +++++---- Meddle/Meddle.Plugin/Services/ParseService.cs | 419 +----------------- Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs | 200 +++++---- Meddle/Meddle.Plugin/Utils/UIUtil.cs | 41 +- Meddle/Meddle.UI/PathService.cs | 2 +- Meddle/Meddle.UI/Windows/PathManager.cs | 2 +- Meddle/Meddle.UI/Windows/Views/ExportView.cs | 2 +- Meddle/Meddle.UI/Windows/Views/MdlView.cs | 2 +- Meddle/Meddle.UI/Windows/Views/MtrlView.cs | 30 +- Meddle/Meddle.UI/Windows/Views/ShpkView.cs | 2 +- Meddle/Meddle.UI/Windows/Views/TeraView.cs | 2 +- Meddle/Meddle.Utils/Export/Material.cs | 161 +++---- Meddle/Meddle.Utils/Export/Model.cs | 2 +- Meddle/Meddle.Utils/Files/MtrlFile.cs | 55 +-- .../Files/Structs/Material/ColorDyeTable.cs | 116 ----- .../Structs/Material/ColorDyeTableRow.cs | 89 ++++ .../Files/Structs/Material/ColorTable.cs | 119 ----- .../Files/Structs/Material/ColorTableSet.cs | 144 ++++++ .../Structs/Material/LegacyColorDyeTable.cs | 95 ---- .../Structs/Material/MaterialParameters.cs | 58 --- .../{Models => Helpers}/MaterialUtils.cs | 36 +- .../{Models => Helpers}/ModelUtils.cs | 2 +- .../{Models => Helpers}/ShaderUtils.cs | 2 +- Meddle/Meddle.Utils/ImageUtils.cs | 1 - Meddle/Meddle.Utils/Materials/Bg.cs | 1 - Meddle/Meddle.Utils/Materials/Character.cs | 7 +- Meddle/Meddle.Utils/Materials/Hair.cs | 1 - Meddle/Meddle.Utils/Materials/Iris.cs | 1 - .../Meddle.Utils/Materials/MaterialUtility.cs | 1 - Meddle/Meddle.Utils/Materials/Other.cs | 1 - Meddle/Meddle.Utils/Materials/Skin.cs | 1 - Meddle/Meddle.Utils/MeshBuilder.cs | 100 ----- Meddle/Meddle.Utils/{Models => }/SKTexture.cs | 2 +- .../VertexPositionNormalTangent2.cs | 250 +++++++++++ 46 files changed, 987 insertions(+), 1259 deletions(-) delete mode 100644 Meddle/Meddle.Utils/Files/Structs/Material/ColorDyeTable.cs create mode 100644 Meddle/Meddle.Utils/Files/Structs/Material/ColorDyeTableRow.cs delete mode 100644 Meddle/Meddle.Utils/Files/Structs/Material/ColorTable.cs create mode 100644 Meddle/Meddle.Utils/Files/Structs/Material/ColorTableSet.cs delete mode 100644 Meddle/Meddle.Utils/Files/Structs/Material/LegacyColorDyeTable.cs delete mode 100644 Meddle/Meddle.Utils/Files/Structs/Material/MaterialParameters.cs rename Meddle/Meddle.Utils/{Models => Helpers}/MaterialUtils.cs (66%) rename Meddle/Meddle.Utils/{Models => Helpers}/ModelUtils.cs (98%) rename Meddle/Meddle.Utils/{Models => Helpers}/ShaderUtils.cs (98%) rename Meddle/Meddle.Utils/{Models => }/SKTexture.cs (99%) create mode 100644 Meddle/Meddle.Utils/VertexPositionNormalTangent2.cs diff --git a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs index 7bf4591..eb829b4 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs @@ -5,7 +5,6 @@ using Meddle.Utils.Export; using Meddle.Utils.Files; using Meddle.Utils.Materials; -using Meddle.Utils.Models; using SharpGLTF.Materials; namespace Meddle.Plugin.Models.Composer; diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs index 76721cc..5642c0d 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs @@ -1,10 +1,11 @@ using System.Numerics; using Meddle.Plugin.Models.Layout; +using Meddle.Plugin.Models.Skeletons; using Meddle.Plugin.Utils; using Meddle.Utils; using Meddle.Utils.Export; using Meddle.Utils.Files; -using Meddle.Utils.Models; +using Meddle.Utils.Helpers; using Microsoft.Extensions.Logging; using SharpGLTF.Materials; using SharpGLTF.Scenes; @@ -42,7 +43,8 @@ public CharacterComposer(ILogger log, DataProvider dataProvider) } } - private void HandleModel(ParsedCharacterInfo characterInfo, ParsedModelInfo modelInfo, SceneBuilder scene, List bones, BoneNodeBuilder? rootBone) + private void HandleModel(GenderRace genderRace, CustomizeParameter customizeParameter, CustomizeData customizeData, + ParsedModelInfo modelInfo, SceneBuilder scene, List bones, BoneNodeBuilder? rootBone) { if (modelInfo.Path.GamePath.Contains("b0003_top")) { @@ -73,11 +75,6 @@ private void HandleModel(ParsedCharacterInfo characterInfo, ParsedModelInfo mode log.LogInformation("Loaded material {mtrlPath}", materialInfo.Path.FullPath); var mtrlFile = new MtrlFile(mtrlData); - if (materialInfo.ColorTable != null) - { - mtrlFile.ColorTable = materialInfo.ColorTable.Value; - } - var shpkName = mtrlFile.GetShaderPackageName(); var shpkPath = $"shader/sm5/shpk/{shpkName}"; var shpkData = dataProvider.LookupData(shpkPath); @@ -96,8 +93,12 @@ private void HandleModel(ParsedCharacterInfo characterInfo, ParsedModelInfo mode x.Path.FullPath == handleString.FullPath); return match?.Resource; }); - material.SetCustomizeParameters(characterInfo.CustomizeParameter); - material.SetCustomizeData(characterInfo.CustomizeData); + if (materialInfo.ColorTable != null) + { + material.SetColorTable(materialInfo.ColorTable); + } + material.SetCustomizeParameters(customizeParameter); + material.SetCustomizeData(customizeData); materialBuilders[i] = material.Compose(dataProvider); }); @@ -118,7 +119,7 @@ private void HandleModel(ParsedCharacterInfo characterInfo, ParsedModelInfo mode var parsed = RaceDeformer.ParseRaceCode(modelInfo.Path.GamePath); if (Enum.IsDefined(parsed)) { - deform = (parsed, characterInfo.GenderRace, new RaceDeformer(PbdFile!, bones)); + deform = (parsed, genderRace, new RaceDeformer(PbdFile!, bones)); } else { @@ -162,11 +163,8 @@ private void HandleModel(ParsedCharacterInfo characterInfo, ParsedModelInfo mode } } - public void ComposeCharacterInstance(ParsedCharacterInstance characterInstance, SceneBuilder scene, NodeBuilder root) + public void ComposeCharacterInstance(ParsedCharacterInfo characterInfo, SceneBuilder scene, NodeBuilder root) { - var characterInfo = characterInstance.CharacterInfo; - if (characterInfo == null) return; - var bones = SkeletonUtils.GetBoneMap(characterInfo.Skeleton, true, out var rootBone); if (rootBone != null) { @@ -175,10 +173,33 @@ public void ComposeCharacterInstance(ParsedCharacterInstance characterInstance, foreach (var t in characterInfo.Models) { - HandleModel(characterInfo, t, scene, bones, rootBone); + HandleModel(characterInfo.GenderRace, characterInfo.CustomizeParameter, characterInfo.CustomizeData, + t, scene, bones, rootBone); } } + public void ComposeModels(ParsedModelInfo[] models, GenderRace genderRace, CustomizeParameter customizeParameter, + CustomizeData customizeData, ParsedSkeleton skeleton, SceneBuilder scene, NodeBuilder root) + { + var bones = SkeletonUtils.GetBoneMap(skeleton, true, out var rootBone); + if (rootBone != null) + { + root.AddNode(rootBone); + } + + foreach (var t in models) + { + HandleModel(genderRace, customizeParameter, customizeData, t, scene, bones, rootBone); + } + } + + public void ComposeCharacterInstance(ParsedCharacterInstance characterInstance, SceneBuilder scene, NodeBuilder root) + { + var characterInfo = characterInstance.CharacterInfo; + if (characterInfo == null) return; + ComposeCharacterInstance(characterInfo, scene, root); + } + private void EnsureBonesExist(Model model, List bones, BoneNodeBuilder? root) { foreach (var mesh in model.Meshes) diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs index 0965137..0f3d62e 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs @@ -2,8 +2,8 @@ using Meddle.Utils; using Meddle.Utils.Export; using Meddle.Utils.Files; +using Meddle.Utils.Files.Structs.Material; using Meddle.Utils.Materials; -using Meddle.Utils.Models; using SharpGLTF.Materials; namespace Meddle.Plugin.Models.Composer; @@ -12,11 +12,13 @@ public class CharacterMaterialBuilder : MeddleMaterialBuilder { private readonly MaterialSet set; private readonly DataProvider dataProvider; + private readonly IColorTableSet? colorTableSet; - public CharacterMaterialBuilder(string name, MaterialSet set, DataProvider dataProvider) : base(name) + public CharacterMaterialBuilder(string name, MaterialSet set, DataProvider dataProvider, IColorTableSet? colorTableSet) : base(name) { this.set = set; this.dataProvider = dataProvider; + this.colorTableSet = colorTableSet; } public override MeddleMaterialBuilder Apply() @@ -55,7 +57,8 @@ public override MeddleMaterialBuilder Apply() var mask = maskTexture[x, y].ToVector4(); var indexColor = indexTexture[x, y]; - var blended = set.ColorTable!.Value.GetBlendedPair(indexColor.Red, indexColor.Green); + var blended = ((ColorTableSet?)colorTableSet)!.Value.ColorTable + .GetBlendedPair(indexColor.Red, indexColor.Green); if (textureMode == TextureMode.Compatibility) { var diffuse = diffuseTexture![x, y].ToVector4(); diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs index cef449c..1717e57 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterTattooMaterialBuilder.cs @@ -3,7 +3,6 @@ using Meddle.Utils.Export; using Meddle.Utils.Files; using Meddle.Utils.Materials; -using Meddle.Utils.Models; using SharpGLTF.Materials; namespace Meddle.Plugin.Models.Composer; diff --git a/Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs b/Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs index 37580c6..80d7606 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/DataProvider.cs @@ -4,7 +4,6 @@ using Meddle.Utils; using Meddle.Utils.Files; using Meddle.Utils.Files.SqPack; -using Meddle.Utils.Models; using Microsoft.Extensions.Logging; using SharpGLTF.Materials; using SharpGLTF.Memory; diff --git a/Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs index 9c548c8..a1c3415 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/HairMaterialBuilder.cs @@ -2,7 +2,6 @@ using Meddle.Utils; using Meddle.Utils.Export; using Meddle.Utils.Materials; -using Meddle.Utils.Models; using SharpGLTF.Materials; namespace Meddle.Plugin.Models.Composer; diff --git a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs index d5f9cd1..4e4e150 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs @@ -7,7 +7,7 @@ using Meddle.Utils.Export; using Meddle.Utils.Files; using Meddle.Utils.Files.SqPack; -using Meddle.Utils.Models; +using Meddle.Utils.Helpers; using Microsoft.Extensions.Logging; using SharpGLTF.Materials; using SharpGLTF.Scenes; diff --git a/Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs index 2fa0228..28955ba 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs @@ -3,7 +3,6 @@ using Meddle.Utils.Export; using Meddle.Utils.Files; using Meddle.Utils.Materials; -using Meddle.Utils.Models; using SharpGLTF.Materials; namespace Meddle.Plugin.Models.Composer; diff --git a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs index 244beaa..7af83a1 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs @@ -9,8 +9,8 @@ using Meddle.Utils.Export; using Meddle.Utils.Files; using Meddle.Utils.Files.Structs.Material; +using Meddle.Utils.Helpers; using Meddle.Utils.Materials; -using Meddle.Utils.Models; using SharpGLTF.Materials; using ShaderPackage = Meddle.Utils.Export.ShaderPackage; @@ -176,7 +176,11 @@ public bool TryGetTexture(DataProvider provider, TextureUsage usage, out Texture public bool IsTransparent => (shaderFlagData & (uint)ShaderFlags.EnableTranslucency) != 0; - public readonly ColorTable? ColorTable; + private IColorTableSet? colorTable; + public void SetColorTable(IColorTableSet? table) + { + this.colorTable = table; + } // BgColorChange private Vector4? stainColor; @@ -338,7 +342,7 @@ public MaterialSet(MtrlFile file, string mtrlPath, ShpkFile shpk, string shpkNam this.textureLoader = textureLoader; shaderFlagData = file.ShaderHeader.Flags; var package = new ShaderPackage(shpk, shpkName); - ColorTable = file.ColorTable; + colorTable = file.GetColorTable(); ShaderKeys = new ShaderKey[file.ShaderKeys.Length]; for (var i = 0; i < file.ShaderKeys.Length; i++) @@ -417,7 +421,8 @@ private MeddleMaterialBuilder GetMaterialBuilder(DataProvider dataProvider) case "lightshaft.shpk": return new LightshaftMaterialBuilder(mtrlName, this, dataProvider); case "character.shpk": - return new CharacterMaterialBuilder(mtrlName, this, dataProvider); + case "characterlegacy.shpk": + return new CharacterMaterialBuilder(mtrlName, this, dataProvider, colorTable); case "charactertattoo.shpk": return new CharacterTattooMaterialBuilder(mtrlName, this, dataProvider, customizeParameters ?? new CustomizeParameter()); case "characterocclusion.shpk": @@ -477,8 +482,8 @@ void AddCustomizeParameters() void AddColorTable() { - if (ColorTable == null) return; - extrasDict["ColorTable"] = JsonNode.Parse(JsonSerializer.Serialize(ColorTable, JsonOptions))!; + if (colorTable == null) return; + extrasDict["ColorTable"] = JsonNode.Parse(JsonSerializer.Serialize(colorTable, JsonOptions))!; } void AddCustomizeData() diff --git a/Meddle/Meddle.Plugin/Models/Composer/Partitioner.cs b/Meddle/Meddle.Plugin/Models/Composer/Partitioner.cs index e5f897a..41bdb8e 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/Partitioner.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/Partitioner.cs @@ -1,6 +1,6 @@ using System.Numerics; using System.Runtime.CompilerServices; -using Meddle.Utils.Models; +using Meddle.Utils; namespace Meddle.Plugin.Models.Composer; diff --git a/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs index 539d312..5c2c4dd 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/SkinMaterialBuilder.cs @@ -2,7 +2,6 @@ using Meddle.Utils; using Meddle.Utils.Export; using Meddle.Utils.Materials; -using Meddle.Utils.Models; using AlphaMode = SharpGLTF.Materials.AlphaMode; namespace Meddle.Plugin.Models.Composer; diff --git a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs index a59786e..4c394c4 100644 --- a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs +++ b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs @@ -158,16 +158,17 @@ public class ParsedTextureInfo(string path, string pathFromMaterial, TextureReso public TextureResource Resource { get; } = resource; } -public class ParsedMaterialInfo(string path, string pathFromModel, string shpk, ColorTable? colorTable, IList textures) +public class ParsedMaterialInfo(string path, string pathFromModel, string shpk, IColorTableSet? colorTable, IList textures) { public HandleString Path { get; } = new() { FullPath = path, GamePath = pathFromModel }; public string Shpk { get; } = shpk; - public ColorTable? ColorTable { get; } = colorTable; + public IColorTableSet? ColorTable { get; } = colorTable; public IList Textures { get; } = textures; } -public class ParsedModelInfo(string path, string pathFromCharacter, DeformerCachedStruct? deformer, Model.ShapeAttributeGroup shapeAttributeGroup, IList materials) +public class ParsedModelInfo(nint id, string path, string pathFromCharacter, DeformerCachedStruct? deformer, Model.ShapeAttributeGroup shapeAttributeGroup, IList materials) { + public nint Id { get; } public HandleString Path { get; } = new() { FullPath = path, GamePath = pathFromCharacter }; public DeformerCachedStruct? Deformer { get; } = deformer; public Model.ShapeAttributeGroup ShapeAttributeGroup { get; } = shapeAttributeGroup; @@ -192,8 +193,8 @@ public class ParsedCharacterInfo { public readonly IList Models; public readonly ParsedSkeleton Skeleton; - public readonly CustomizeData CustomizeData; - public readonly CustomizeParameter CustomizeParameter; + public CustomizeData CustomizeData; + public CustomizeParameter CustomizeParameter; public readonly GenderRace GenderRace; public ParsedCharacterInfo(IList models, ParsedSkeleton skeleton, CustomizeData customizeData, CustomizeParameter customizeParameter, GenderRace genderRace) diff --git a/Meddle/Meddle.Plugin/Services/LayoutService.cs b/Meddle/Meddle.Plugin/Services/LayoutService.cs index 912a556..c7b14e9 100644 --- a/Meddle/Meddle.Plugin/Services/LayoutService.cs +++ b/Meddle/Meddle.Plugin/Services/LayoutService.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.Logging; using CustomizeParameter = Meddle.Plugin.Models.Structs.CustomizeParameter; using HousingFurniture = FFXIVClientStructs.FFXIV.Client.Game.HousingFurniture; +using Model = FFXIVClientStructs.FFXIV.Client.Graphics.Render.Model; using Transform = Meddle.Plugin.Models.Transform; namespace Meddle.Plugin.Services; @@ -367,6 +368,78 @@ public unsafe ParsedInstance[] ParseObjects(bool resolveCharacterInfo = false) return objects.ToArray(); } + public unsafe ParsedModelInfo? HandleModel(Pointer cbasePtr, Pointer modelPtr, Dictionary colorTableSets) + { + if (modelPtr == null) return null; + var model = modelPtr.Value; + if (model == null) return null; + var modelPath = model->ModelResourceHandle->ResourceHandle.FileName.ParseString(); + if (cbasePtr == null) return null; + if (cbasePtr.Value == null) return null; + var characterBase = cbasePtr.Value; + var modelPathFromCharacter = characterBase->ResolveMdlPath(model->SlotIndex); + var shapeAttributeGroup = StructExtensions.ParseModelShapeAttributes(model); + + var materials = new List(); + for (var mtrlIdx = 0; mtrlIdx < model->MaterialsSpan.Length; mtrlIdx++) + { + var materialPtr = model->MaterialsSpan[mtrlIdx]; + if (materialPtr == null) continue; + var material = materialPtr.Value; + if (material == null) continue; + + var materialPath = material->MaterialResourceHandle->ResourceHandle.FileName.ParseString(); + var materialPathFromModel = + model->ModelResourceHandle->GetMaterialFileNameBySlotAsString((uint)mtrlIdx); + var shaderName = material->MaterialResourceHandle->ShpkNameString; + IColorTableSet? colorTable = null; + if (colorTableSets.TryGetValue((int)(model->SlotIndex * CharacterBase.MaterialsPerSlot) + mtrlIdx, + out var gpuColorTable)) + { + colorTable = gpuColorTable; + } + else if (material->MaterialResourceHandle->ColorTableSpan.Length == 32) + { + var colorTableRows = material->MaterialResourceHandle->ColorTableSpan; + var colorTableBytes = MemoryMarshal.AsBytes(colorTableRows); + var colorTableBuf = new byte[colorTableBytes.Length]; + colorTableBytes.CopyTo(colorTableBuf); + var reader = new SpanBinaryReader(colorTableBuf); + colorTable = new ColorTableSet + { + ColorTable = new ColorTable(ref reader) + }; + } + + var textures = new List(); + for (var texIdx = 0; texIdx < material->MaterialResourceHandle->TexturesSpan.Length; texIdx++) + { + var texturePtr = material->MaterialResourceHandle->TexturesSpan[texIdx]; + if (texturePtr.TextureResourceHandle == null) continue; + + var texturePath = texturePtr.TextureResourceHandle->FileName.ParseString(); + if (texIdx < material->TextureCount) + { + var texturePathFromMaterial = material->MaterialResourceHandle->TexturePathString(texIdx); + var (resource, stride) = + DXHelper.ExportTextureResource(texturePtr.TextureResourceHandle->Texture); + var textureInfo = new ParsedTextureInfo(texturePath, texturePathFromMaterial, resource); + textures.Add(textureInfo); + } + } + + var materialInfo = + new ParsedMaterialInfo(materialPath, materialPathFromModel, shaderName, colorTable, textures); + materials.Add(materialInfo); + } + + var deform = pbdHooks.TryGetDeformer((nint)cbasePtr.Value, model->SlotIndex); + var modelInfo = + new ParsedModelInfo((nint)model, modelPath, modelPathFromCharacter, deform, shapeAttributeGroup, materials); + + return modelInfo; + } + public unsafe ParsedCharacterInfo? HandleDrawObject(DrawObject* drawObject) { if (drawObject == null) @@ -385,102 +458,50 @@ public unsafe ParsedInstance[] ParseObjects(bool resolveCharacterInfo = false) var models = new List(); foreach (var modelPtr in characterBase->ModelsSpan) { - if (modelPtr == null) continue; - var model = modelPtr.Value; - if (model == null) continue; - var modelPath = model->ModelResourceHandle->ResourceHandle.FileName.ParseString(); - var modelPathFromCharacter = characterBase->ResolveMdlPath(model->SlotIndex); - var shapeAttributeGroup = StructExtensions.ParseModelShapeAttributes(model); - - var materials = new List(); - for (var mtrlIdx = 0; mtrlIdx < model->MaterialsSpan.Length; mtrlIdx++) - { - var materialPtr = model->MaterialsSpan[mtrlIdx]; - if (materialPtr == null) continue; - var material = materialPtr.Value; - if (material == null) continue; - - var materialPath = material->MaterialResourceHandle->ResourceHandle.FileName.ParseString(); - var materialPathFromModel = - model->ModelResourceHandle->GetMaterialFileNameBySlotAsString((uint)mtrlIdx); - var shaderName = material->MaterialResourceHandle->ShpkNameString; - ColorTable? colorTable = null; - if (colorTableTextures.TryGetValue((int)(model->SlotIndex * CharacterBase.MaterialsPerSlot) + mtrlIdx, - out var gpuColorTable)) - { - colorTable = gpuColorTable; - } - else if (material->MaterialResourceHandle->ColorTableSpan.Length == 32) - { - var colorTableRows = material->MaterialResourceHandle->ColorTableSpan; - var colorTableBytes = MemoryMarshal.AsBytes(colorTableRows); - var colorTableBuf = new byte[colorTableBytes.Length]; - colorTableBytes.CopyTo(colorTableBuf); - var reader = new SpanBinaryReader(colorTableBuf); - colorTable = ColorTable.Load(ref reader); - } - - var textures = new List(); - for (var texIdx = 0; texIdx < material->MaterialResourceHandle->TexturesSpan.Length; texIdx++) - { - var texturePtr = material->MaterialResourceHandle->TexturesSpan[texIdx]; - if (texturePtr.TextureResourceHandle == null) continue; - - var texturePath = texturePtr.TextureResourceHandle->FileName.ParseString(); - if (texIdx < material->TextureCount) - { - var texturePathFromMaterial = material->MaterialResourceHandle->TexturePathString(texIdx); - var (resource, stride) = - DXHelper.ExportTextureResource(texturePtr.TextureResourceHandle->Texture); - var textureInfo = new ParsedTextureInfo(texturePath, texturePathFromMaterial, resource); - textures.Add(textureInfo); - } - } - - var materialInfo = - new ParsedMaterialInfo(materialPath, materialPathFromModel, shaderName, colorTable, textures); - materials.Add(materialInfo); - } - - var deform = pbdHooks.TryGetDeformer((nint)characterBase, model->SlotIndex); - var modelInfo = - new ParsedModelInfo(modelPath, modelPathFromCharacter, deform, shapeAttributeGroup, materials); - models.Add(modelInfo); + var modelInfo = HandleModel(characterBase, modelPtr, colorTableTextures); + if (modelInfo != null) + models.Add(modelInfo); } var skeleton = StructExtensions.GetParsedSkeleton(characterBase); + var (customizeParams, customizeData, genderRace) = ParseHuman(characterBase); + + return new ParsedCharacterInfo(models, skeleton, customizeData, customizeParams, genderRace); + } + + public unsafe (Meddle.Utils.Export.CustomizeParameter, CustomizeData, GenderRace) ParseHuman(CharacterBase* characterBase) + { var modelType = characterBase->GetModelType(); - CustomizeData customizeData = new CustomizeData(); - Meddle.Utils.Export.CustomizeParameter customizeParams = new(); - GenderRace genderRace = GenderRace.Unknown; - if (modelType == CharacterBase.ModelType.Human) + if (modelType != CharacterBase.ModelType.Human) { - var human = (Human*)characterBase; - var customizeCBuf = human->CustomizeParameterCBuffer->TryGetBuffer()[0]; - customizeParams = new Meddle.Utils.Export.CustomizeParameter - { - SkinColor = customizeCBuf.SkinColor, - MuscleTone = customizeCBuf.MuscleTone, - SkinFresnelValue0 = customizeCBuf.SkinFresnelValue0, - LipColor = customizeCBuf.LipColor, - MainColor = customizeCBuf.MainColor, - FacePaintUVMultiplier = customizeCBuf.FacePaintUVMultiplier, - HairFresnelValue0 = customizeCBuf.HairFresnelValue0, - MeshColor = customizeCBuf.MeshColor, - FacePaintUVOffset = customizeCBuf.FacePaintUVOffset, - LeftColor = customizeCBuf.LeftColor, - RightColor = customizeCBuf.RightColor, - OptionColor = customizeCBuf.OptionColor - }; - customizeData = new CustomizeData - { - LipStick = human->Customize.Lipstick, - Highlights = human->Customize.Highlights - }; - genderRace = (GenderRace)human->RaceSexId; + return (new Meddle.Utils.Export.CustomizeParameter(), new CustomizeData(), GenderRace.Unknown); } - - return new ParsedCharacterInfo(models, skeleton, customizeData, customizeParams, genderRace); + + var human = (Human*)characterBase; + var customizeCBuf = human->CustomizeParameterCBuffer->TryGetBuffer()[0]; + var customizeParams = new Meddle.Utils.Export.CustomizeParameter + { + SkinColor = customizeCBuf.SkinColor, + MuscleTone = customizeCBuf.MuscleTone, + SkinFresnelValue0 = customizeCBuf.SkinFresnelValue0, + LipColor = customizeCBuf.LipColor, + MainColor = customizeCBuf.MainColor, + FacePaintUVMultiplier = customizeCBuf.FacePaintUVMultiplier, + HairFresnelValue0 = customizeCBuf.HairFresnelValue0, + MeshColor = customizeCBuf.MeshColor, + FacePaintUVOffset = customizeCBuf.FacePaintUVOffset, + LeftColor = customizeCBuf.LeftColor, + RightColor = customizeCBuf.RightColor, + OptionColor = customizeCBuf.OptionColor + }; + var customizeData = new CustomizeData + { + LipStick = human->Customize.Lipstick, + Highlights = human->Customize.Highlights + }; + var genderRace = (GenderRace)human->RaceSexId; + + return (customizeParams, customizeData, genderRace); } private unsafe Furniture[] ParseTerritory(HousingTerritory* territory) diff --git a/Meddle/Meddle.Plugin/Services/ParseService.cs b/Meddle/Meddle.Plugin/Services/ParseService.cs index 940a83e..faa4b15 100644 --- a/Meddle/Meddle.Plugin/Services/ParseService.cs +++ b/Meddle/Meddle.Plugin/Services/ParseService.cs @@ -10,7 +10,7 @@ using Meddle.Utils.Files; using Meddle.Utils.Files.SqPack; using Meddle.Utils.Files.Structs.Material; -using Meddle.Utils.Models; +using Meddle.Utils.Helpers; using Microsoft.Extensions.Logging; using CustomizeParameter = Meddle.Utils.Export.CustomizeParameter; using Material = FFXIVClientStructs.FFXIV.Client.Graphics.Render.Material; @@ -58,10 +58,10 @@ private void OnLog(LogLevel logLevel, string message) OnLogEvent?.Invoke(logLevel, message); } - public unsafe Dictionary ParseColorTableTextures(CharacterBase* characterBase) + public unsafe Dictionary ParseColorTableTextures(CharacterBase* characterBase) { using var activity = ActivitySource.StartActivity(); - var colorTableTextures = new Dictionary(); + var colorTableTextures = new Dictionary(); for (var i = 0; i < characterBase->ColorTableTexturesSpan.Length; i++) { var colorTableTex = characterBase->ColorTableTexturesSpan[i]; @@ -70,31 +70,16 @@ public unsafe Dictionary ParseColorTableTextures(CharacterBase* var colorTableTexture = colorTableTex.Value; if (colorTableTexture != null) { - var textures = ParseColorTableTexture(colorTableTexture); - var cts = new ColorTable - { - Rows = textures - }; - colorTableTextures[i] = cts; + var colorTableSet = ParseColorTableTexture(colorTableTexture); + colorTableTextures[i] = colorTableSet; } } return colorTableTextures; } - public unsafe AttachedModelGroup? ParseDrawObjectAsAttach(DrawObject* drawObject) - { - if (drawObject == null) return null; - if (drawObject->GetObjectType() != ObjectType.CharacterBase) return null; - var drawCharacterBase = (CharacterBase*)drawObject; - var attachGroup = ParseCharacterBase(drawCharacterBase); - var attach = StructExtensions.GetParsedAttach(drawCharacterBase); - return new AttachedModelGroup(attach, attachGroup.MdlGroups, attachGroup.Skeleton); - - } - // Only call from main thread or you will probably crash - public unsafe ColorTableRow[] ParseColorTableTexture(Texture* colorTableTexture) + public unsafe IColorTableSet ParseColorTableTexture(Texture* colorTableTexture) { using var activity = ActivitySource.StartActivity(); var (colorTableRes, stride) = DXHelper.ExportTextureResource(colorTableTexture); @@ -110,8 +95,10 @@ public unsafe ColorTableRow[] ParseColorTableTexture(Texture* colorTableTexture) var stridedData = ImageUtils.AdjustStride(stride, (int)colorTableTexture->Width * 8, (int)colorTableTexture->Height, colorTableRes.Data); var reader = new SpanBinaryReader(stridedData); - var tableData = reader.Read(16); - return tableData.ToArray().Select(x => x.ToNew()).ToArray(); + return new LegacyColorTableSet + { + ColorTable = new LegacyColorTable(ref reader) + }; } if (colorTableTexture->Width == 8 && colorTableTexture->Height == 32) @@ -120,391 +107,13 @@ public unsafe ColorTableRow[] ParseColorTableTexture(Texture* colorTableTexture) var stridedData = ImageUtils.AdjustStride(stride, (int)colorTableTexture->Width * 8, (int)colorTableTexture->Height, colorTableRes.Data); var reader = new SpanBinaryReader(stridedData); - var tableData = reader.Read(32); - return tableData.ToArray(); + return new ColorTableSet + { + ColorTable = new ColorTable(ref reader) + }; } throw new ArgumentException( $"Color table is not 4x16 or 8x32 ({colorTableTexture->Width}x{colorTableTexture->Height})"); } - - /// - /// Parse a character base into a character group excluding attach data and customize data - /// - /// - /// - public unsafe CharacterGroup ParseCharacterBase(CharacterBase* characterBase) - { - var colorTableTextures = ParseColorTableTextures(characterBase); - var models = new List(); - foreach (var modelPtr in characterBase->ModelsSpan) - { - if (modelPtr == null) continue; - var model = modelPtr.Value; - if (model == null) continue; - var modelData = HandleModelPtr(characterBase, (int)model->SlotIndex, colorTableTextures); - if (modelData == null) continue; - models.Add(modelData); - } - var skeleton = StructExtensions.GetParsedSkeleton(characterBase); - return new CharacterGroup(new CustomizeParameter(), new CustomizeData(), GenderRace.Unknown, models.ToArray(), skeleton, []); - } - - public Task ParseFromModelInfo(ParsedModelInfo info) - { - var mdlFileResource = pack.GetFileOrReadFromDisk(info.Path.FullPath); - if (mdlFileResource == null) - { - throw new Exception($"Failed to load model file {info.Path.FullPath}"); - } - - var mdlFile = new MdlFile(mdlFileResource); - var mtrlGroups = new List(); - - foreach (var materialInfo in info.Materials) - { - var mtrlFileResource = pack.GetFileOrReadFromDisk(materialInfo.Path.FullPath); - if (mtrlFileResource == null) - { - logger.LogWarning("Material file {MtrlFileName} not found", materialInfo.Path.FullPath); - mtrlGroups.Add(new MtrlFileStubGroup(materialInfo.Path.GamePath)); - continue; - } - - var mtrlFile = new MtrlFile(mtrlFileResource); - if (materialInfo.ColorTable != null) - { - mtrlFile.ColorTable = materialInfo.ColorTable.Value; - } - - var shpkName = mtrlFile.GetShaderPackageName(); - var shpkFile = HandleShpk(shpkName); - - var texGroups = new List(); - foreach (var textureInfo in materialInfo.Textures) - { - var textureResource = pack.GetFileOrReadFromDisk(textureInfo.Path.FullPath); - if (textureResource == null) - { - logger.LogWarning("Texture file {TexturePath} not found", textureInfo.Path); - continue; - } - - var texGroup = new TexResourceGroup(textureInfo.Path.GamePath, textureInfo.Path.FullPath, textureInfo.Resource); - texGroups.Add(texGroup); - } - - mtrlGroups.Add(new MtrlFileGroup(materialInfo.Path.GamePath, materialInfo.Path.FullPath, mtrlFile, shpkName, shpkFile, - texGroups.ToArray())); - } - - DeformerGroup? deformerGroup = null; - if (info.Deformer != null) - { - deformerGroup = new DeformerGroup(info.Deformer.Value.PbdPath, - info.Deformer.Value.RaceSexId, - info.Deformer.Value.DeformerId); - } - - return Task.FromResult(new MdlFileGroup(info.Path.GamePath, info.Path.FullPath, deformerGroup, mdlFile, mtrlGroups.ToArray(), info.ShapeAttributeGroup)); - } - - public Task ParseFromPath(string mdlPath) - { - var mdlFileResource = pack.GetFileOrReadFromDisk(mdlPath); - if (mdlFileResource == null) - { - throw new Exception($"Failed to load model file {mdlPath}"); - } - - var mdlFile = new MdlFile(mdlFileResource); - var mtrlFileNames = mdlFile.GetMaterialNames().Select(x => x.Value).ToArray(); - var mtrlGroups = new List(); - - foreach (var mtrlFileName in mtrlFileNames) - { - if (!MtrlCache.TryGetValue(mtrlFileName, out var mtrlFile)) - { - var mtrlFileResource = pack.GetFileOrReadFromDisk(mtrlFileName); - if (mtrlFileResource == null) - { - logger.LogWarning("Material file {MtrlFileName} not found", mtrlFileName); - mtrlGroups.Add(new MtrlFileStubGroup(mtrlFileName)); - continue; - } - - mtrlFile = new MtrlFile(mtrlFileResource); - MtrlCache[mtrlFileName] = mtrlFile; - } - - var shpkName = mtrlFile.GetShaderPackageName(); - var shpkFile = HandleShpk(shpkName); - - var texturePaths = mtrlFile.GetTexturePaths().Select(x => x.Value).ToArray(); - var texGroups = new List(); - foreach (var texturePath in texturePaths) - { - if (!TexCache.TryGetValue(texturePath, out var texFile)) - { - var textureResource = pack.GetFileOrReadFromDisk(texturePath); - if (textureResource == null) - { - logger.LogWarning("Texture file {TexturePath} not found", texturePath); - continue; - } - - texFile = new TexFile(textureResource); - TexCache[texturePath] = texFile; - } - - var texRes = texFile.ToResource(); - var texGroup = new TexResourceGroup(texturePath, texturePath, texRes); - texGroups.Add(texGroup); - } - - mtrlGroups.Add(new MtrlFileGroup(mtrlFileName, mtrlFileName, mtrlFile, shpkName, shpkFile, - texGroups.ToArray())); - } - - return Task.FromResult(new MdlFileGroup(mdlPath, mdlPath, null, mdlFile, mtrlGroups.ToArray(), null)); - } - - public unsafe MdlFileGroup? HandleModelPtr(CharacterBase* characterBase, int slotIdx, Dictionary colorTables) - { - using var activity = ActivitySource.StartActivity(); - var modelPtr = characterBase->ModelsSpan[slotIdx]; - if (modelPtr == null || modelPtr.Value == null) - { - //logger.LogWarning("Model Ptr {ModelIndex} is null", modelIdx); - return null; - } - - var model = modelPtr.Value; - - var mdlFileName = model->ModelResourceHandle->ResourceHandle.FileName.ParseString(); - var mdlFileActorName = characterBase->ResolveMdlPath((uint)slotIdx); - activity?.SetTag("mdl", mdlFileName); - var mdlFileResource = pack.GetFileOrReadFromDisk(mdlFileName); - if (mdlFileResource == null) - { - logger.LogWarning("Model file {MdlFileName} not found", mdlFileName); - return null; - } - - var shapeAttributeGroup = StructExtensions.ParseModelShapeAttributes(model); - var mdlFile = new MdlFile(mdlFileResource); - var mtrlFileNames = mdlFile.GetMaterialNames().Select(x => x.Value).ToArray(); - var mtrlGroups = new List(); - for (var j = 0; j < model->MaterialsSpan.Length; j++) - { - var materialPtr = model->MaterialsSpan[j]; - var material = materialPtr.Value; - var mdlMtrlFileName = mtrlFileNames[j]; - if (material == null) - { - logger.LogWarning("Material Ptr {MaterialIndex} is null for {MdlFileName}", j, mdlFileName); - mtrlGroups.Add(new MtrlFileStubGroup(mdlMtrlFileName)); - continue; - } - - var mtrlGroup = ParseMtrl(mdlMtrlFileName, material, slotIdx, j, colorTables); - if (mtrlGroup == null) - { - logger.LogWarning("Failed to parse material {MdlMtrlFileName}", mdlMtrlFileName); - mtrlGroups.Add(new MtrlFileStubGroup(mdlMtrlFileName)); - } - else - { - mtrlGroups.Add(mtrlGroup); - } - } - - var deformerData = pbdHooks.TryGetDeformer((nint)characterBase, (uint)slotIdx); - DeformerGroup? deformerGroup = null; - if (deformerData != null) - { - deformerGroup = new DeformerGroup(deformerData.Value.PbdPath, deformerData.Value.RaceSexId, - deformerData.Value.DeformerId); - } - - return new MdlFileGroup(mdlFileActorName, mdlFileName, deformerGroup, mdlFile, mtrlGroups.ToArray(), - shapeAttributeGroup); - } - - private ShpkFile HandleShpk(string shader) - { - using var activity = ActivitySource.StartActivity(); - activity?.SetTag("shader", shader); - if (ShpkCache.TryGetValue(shader, out var shpk)) - { - return shpk; - } - - var shpkFileResource = pack.GetFileOrReadFromDisk($"shader/sm5/shpk/{shader}"); - if (shpkFileResource == null) - { - throw new Exception($"Failed to load shader package {shader}"); - } - - var shpkFile = new ShpkFile(shpkFileResource); - ShpkCache[shader] = shpkFile; - return shpkFile; - } - - private unsafe MtrlFileGroup? ParseMtrl( - string mdlMtrlPath, - Material* material, int modelIdx, int j, - Dictionary colorTables) - { - using var activity = ActivitySource.StartActivity(); - var mtrlFileName = material->MaterialResourceHandle->ResourceHandle.FileName.ParseString(); - var shader = material->MaterialResourceHandle->ShpkNameString; - activity?.SetTag("mtrl", mtrlFileName); - activity?.SetTag("shader", shader); - - var mtrlFileResource = pack.GetFileOrReadFromDisk(mtrlFileName); - if (mtrlFileResource == null) - { - logger.LogWarning("Material file {MtrlFileName} not found", mtrlFileName); - return null; - } - - var mtrlFile = new MtrlFile(mtrlFileResource); - var colorTable = material->MaterialResourceHandle->ColorTableSpan; - if (colorTable.Length == 32) - { - var colorTableBytes = MemoryMarshal.AsBytes(colorTable); - var colorTableBuf = new byte[colorTableBytes.Length]; - colorTableBytes.CopyTo(colorTableBuf); - var reader = new SpanBinaryReader(colorTableBuf); - var cts = ColorTable.Load(ref reader); - mtrlFile.ColorTable = cts; - } - - if (colorTables.TryGetValue((modelIdx * CharacterBase.MaterialsPerSlot) + j, out var gpuColorTable)) - { - mtrlFile.ColorTable = gpuColorTable; - } - - var shpkFile = HandleShpk(shader); - var texGroups = new List(); - - for (var i = 0; i < material->MaterialResourceHandle->TextureCount; i++) - { - var textureEntry = material->MaterialResourceHandle->TexturesSpan[i]; - if (textureEntry.TextureResourceHandle == null) - { - logger.LogWarning("Texture handle is null on {MtrlFileName}", mtrlFileName); - continue; - } - - var texturePath = material->MaterialResourceHandle->TexturePathString(i); - var resourcePath = textureEntry.TextureResourceHandle->ResourceHandle.FileName.ParseString(); - var data = DXHelper.ExportTextureResource(textureEntry.TextureResourceHandle->Texture); - var texResourceGroup = new TexResourceGroup(texturePath, resourcePath, data.Resource); - texGroups.Add(texResourceGroup); - } - - return new MtrlFileGroup(mdlMtrlPath, mtrlFileName, mtrlFile, shader, shpkFile, texGroups.ToArray()); - } - - /*public CharacterGroup HandleModelPath(string path) - { - var data = pack.GetFileOrReadFromDisk(path); - if (data == null) - { - throw new Exception($"Failed to load model file {path}"); - } - - var mdlFile = new MdlFile(data); - var mtrlFileNames = mdlFile.GetMaterialNames().Select(x => x.Value).ToArray(); - var mtrlGroups = new List(); - - for (var i = 0; i < mtrlFileNames.Length; i++) - { - var mtrlFileName = mtrlFileNames[i]; - var mtrlFileResource = pack.GetFileOrReadFromDisk(mtrlFileName); - if (mtrlFileResource == null) - { - logger.LogWarning("Material file {MtrlFileName} not found", mtrlFileName); - mtrlGroups.Add(new MtrlFileStubGroup(mtrlFileName)); - continue; - } - - var mtrlFile = new MtrlFile(mtrlFileResource); - var texturePaths = mtrlFile.GetTexturePaths().Select(x => x.Value).ToArray(); - var texGroups = new List(); - foreach (var texturePath in texturePaths) - { - var textureResource = pack.GetFileOrReadFromDisk(texturePath); - if (textureResource == null) - { - logger.LogWarning("Texture file {TexturePath} not found", texturePath); - continue; - } - - var texFile = new TexFile(textureResource); - } - - } - - return null; - } - - - public unsafe CharacterGroup HandleCharacterGroup( - CharacterBase* characterBase, - Dictionary colorTableTextures, - Dictionary, Dictionary> attachDict, - CustomizeParameter customizeParams, - CustomizeData customizeData, - GenderRace genderRace) - { - using var activity = ActivitySource.StartActivity(); - var skeleton = new ParsedSkeleton(characterBase->Skeleton); - var mdlGroups = new List(); - for (var i = 0; i < characterBase->SlotCount; i++) - { - var mdlGroup = HandleModelPtr(characterBase, i, colorTableTextures); - if (mdlGroup != null) - { - mdlGroups.Add(mdlGroup); - } - } - - var attachGroups = new List(); - foreach (var (attachBase, attachColorTableTextures) in attachDict) - { - var attachGroup = HandleAttachGroup(attachBase, attachColorTableTextures); - attachGroups.Add(attachGroup); - } - - return new CharacterGroup( - customizeParams, - customizeData, - genderRace, - mdlGroups.ToArray(), - skeleton, - attachGroups.ToArray()); - } - - public unsafe AttachedModelGroup HandleAttachGroup( - Pointer attachBase, Dictionary colorTables) - { - using var activity = ActivitySource.StartActivity(); - var attach = new ParsedAttach(attachBase.GetAttach()); - var models = new List(); - var skeleton = new ParsedSkeleton(attachBase.Value->Skeleton); - for (var i = 0; i < attachBase.Value->ModelsSpan.Length; i++) - { - var mdlGroup = HandleModelPtr(attachBase, i, colorTables); - if (mdlGroup != null) - { - models.Add(mdlGroup); - } - } - - var attachGroup = new AttachedModelGroup(attach, models.ToArray(), skeleton); - return attachGroup; - }*/ } diff --git a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs index 0036709..f802aa4 100644 --- a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs +++ b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs @@ -1,4 +1,5 @@ -using Dalamud.Game.ClientState.Objects.Types; +using System.Diagnostics; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Textures; @@ -9,6 +10,8 @@ using FFXIVClientStructs.Interop; using ImGuiNET; using Meddle.Plugin.Models; +using Meddle.Plugin.Models.Composer; +using Meddle.Plugin.Models.Layout; using Meddle.Plugin.Models.Structs; using Meddle.Plugin.Services; using Meddle.Plugin.Services.UI; @@ -18,6 +21,7 @@ using Meddle.Utils.Files; using Meddle.Utils.Files.SqPack; using Microsoft.Extensions.Logging; +using SharpGLTF.Scenes; using SkiaSharp; using CSCharacter = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; using CSCharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; @@ -32,6 +36,7 @@ public unsafe class LiveCharacterTab : ITab { private readonly CommonUi commonUi; private readonly ExportService exportService; + private readonly LayoutService layoutService; public MenuType MenuType => MenuType.Default; private readonly FileDialogManager fileDialog = new() @@ -54,6 +59,7 @@ public unsafe class LiveCharacterTab : ITab public LiveCharacterTab( ILogger log, ExportService exportService, + LayoutService layoutService, ITextureProvider textureProvider, ParseService parseService, TextureCache textureCache, @@ -63,6 +69,7 @@ public LiveCharacterTab( { this.log = log; this.exportService = exportService; + this.layoutService = layoutService; this.textureProvider = textureProvider; this.parseService = parseService; this.textureCache = textureCache; @@ -168,10 +175,10 @@ private void DrawCharacter(CSCharacter* character, string name, int depth = 0) if (modelType == CSCharacterBase.ModelType.Human) { DrawHumanCharacter((CSHuman*)cBase, out customizeData, out customizeParams, out genderRace); - if (ImGui.Button("Export All Models With Attaches")) - { - ExportAllModelsWithAttaches(character, customizeParams, customizeData, genderRace); - } + // if (ImGui.Button("Export All Models With Attaches")) + // { + // ExportAllModelsWithAttaches(character, customizeParams, customizeData, genderRace); + // } } else { @@ -235,39 +242,50 @@ private void DrawDrawObject(DrawObject* drawObject, CustomizeData? customizeData } ImGui.SameLine(); - var selectedModelCount = cBase->ModelsSpan.ToArray().Count(modelPtr => + var currentSelectedModels = cBase->ModelsSpan.ToArray().Where(modelPtr => { if (modelPtr == null) return false; return selectedModels.ContainsKey((nint)modelPtr.Value) && selectedModels[(nint)modelPtr.Value]; - }); - using (ImRaii.Disabled(selectedModelCount == 0)) + }).ToArray(); + using (ImRaii.Disabled(currentSelectedModels.Length == 0)) { - if (ImGui.Button($"Export Selected Models ({selectedModelCount})") && selectedModelCount > 0) + if (ImGui.Button($"Export Selected Models ({currentSelectedModels.Length})") && currentSelectedModels.Length > 0) { var colorTableTextures = parseService.ParseColorTableTextures(cBase); - var models = new List(); - foreach (var modelPtr in cBase->ModelsSpan) + var models = new List(); + customizeData ??= new CustomizeData(); + customizeParams ??= new CustomizeParameter(); + var skeleton = StructExtensions.GetParsedSkeleton(cBase); + foreach (var currentSelectedModel in currentSelectedModels) { - if (modelPtr == null) continue; - if (!selectedModels.TryGetValue((nint)modelPtr.Value, out var isSelected) || !isSelected) continue; - var model = modelPtr.Value; - if (model == null) continue; - var modelData = parseService.HandleModelPtr(cBase, (int)model->SlotIndex, colorTableTextures); - if (modelData == null) continue; - models.Add(modelData); + var modelInfo = layoutService.HandleModel(cBase, currentSelectedModel.Value, colorTableTextures); + if (modelInfo != null) + { + models.Add(modelInfo); + } } - - var skeleton = StructExtensions.GetParsedSkeleton(cBase); - var cGroup = new CharacterGroup(customizeParams ?? new CustomizeParameter(), - customizeData ?? new CustomizeData(), genderRace, models.ToArray(), - skeleton, []); - - fileDialog.SaveFolderDialog("Save Model", "Character", + + var folder = $"Models-{DateTime.Now:yyyy-MM-dd-HH-mm-ss}"; + fileDialog.SaveFolderDialog("Save Model", folder, (result, path) => { if (!result) return; - - Task.Run(() => { exportService.Export(cGroup, path); }); + + Task.Run(() => + { + var cacheDir = Path.Combine(path, "cache"); + Directory.CreateDirectory(cacheDir); + var composer = new CharacterComposer( + log, + new DataProvider(cacheDir, pack, log, CancellationToken.None)); + var scene = new SceneBuilder(); + var root = new NodeBuilder(); + composer.ComposeModels(models.ToArray(), genderRace, customizeParams, + customizeData, skeleton, scene, root); + scene.AddNode(root); + scene.ToGltf2().SaveGLTF(Path.Combine(path, "character.gltf")); + Process.Start("explorer.exe", path); + }); }, Plugin.TempDirectory); } } @@ -288,7 +306,7 @@ private void DrawDrawObject(DrawObject* drawObject, CustomizeData? customizeData } } - private void ExportAllModelsWithAttaches(CSCharacter* character, CustomizeParameter? customizeParams, CustomizeData? customizeData, GenderRace genderRace) + /*private void ExportAllModelsWithAttaches(CSCharacter* character, CustomizeParameter? customizeParams, CustomizeData? customizeData, GenderRace genderRace) { var drawObject = character->GameObject.DrawObject; if (drawObject == null) @@ -325,26 +343,26 @@ private void ExportAllModelsWithAttaches(CSCharacter* character, CustomizeParame { // hacky workaround since mount is actually a "root" and the character is attached to them // TODO: transform needs to be adjusted to be relative to the mount - /*var playerAttach = StructExtensions.GetParsedAttach(cBase); - var attachPointName = - playerAttach.OwnerSkeleton!.PartialSkeletons[playerAttach.PartialSkeletonIdx].HkSkeleton!.BoneNames[ - (int)playerAttach.BoneIdx]; - - attachGroup.Attach.OwnerSkeleton = playerAttach.TargetSkeleton; - attachGroup.Attach.TargetSkeleton = attachGroup.Skeleton; - for (int i = 0; i < attachGroup.Skeleton.PartialSkeletons.Count; i++) - { - var partial = attachGroup.Skeleton.PartialSkeletons[i]; - for (int j = 0; j < partial.HkSkeleton!.BoneNames.Count; j++) - { - if (partial.HkSkeleton.BoneNames[j] == attachPointName) - { - attachGroup.Attach.BoneIdx = (uint)j; - attachGroup.Attach.PartialSkeletonIdx = (byte)i; - break; - } - } - }*/ + // var playerAttach = StructExtensions.GetParsedAttach(cBase); + // var attachPointName = + // playerAttach.OwnerSkeleton!.PartialSkeletons[playerAttach.PartialSkeletonIdx].HkSkeleton!.BoneNames[ + // (int)playerAttach.BoneIdx]; + // + // attachGroup.Attach.OwnerSkeleton = playerAttach.TargetSkeleton; + // attachGroup.Attach.TargetSkeleton = attachGroup.Skeleton; + // for (int i = 0; i < attachGroup.Skeleton.PartialSkeletons.Count; i++) + // { + // var partial = attachGroup.Skeleton.PartialSkeletons[i]; + // for (int j = 0; j < partial.HkSkeleton!.BoneNames.Count; j++) + // { + // if (partial.HkSkeleton.BoneNames[j] == attachPointName) + // { + // attachGroup.Attach.BoneIdx = (uint)j; + // attachGroup.Attach.PartialSkeletonIdx = (byte)i; + // break; + // } + // } + // } attaches.Add(attachGroup); } @@ -382,25 +400,36 @@ private void ExportAllModelsWithAttaches(CSCharacter* character, CustomizeParame exportService.Export(group, path); }); }, Plugin.TempDirectory); - } + }*/ private void ExportAllModels(CSCharacterBase* cBase, CustomizeParameter? customizeParams, CustomizeData? customizeData, GenderRace genderRace) { - var group = parseService.ParseCharacterBase(cBase) with + var info = layoutService.HandleDrawObject((DrawObject*)cBase); + if (info == null) { - CustomizeParams = customizeParams ?? new CustomizeParameter(), - CustomizeData = customizeData ?? new CustomizeData(), - GenderRace = genderRace - }; + log.LogError("Failed to get character info from draw object"); + return; + } - fileDialog.SaveFolderDialog("Save Model", "Character", + var folderName = $"Character-{DateTime.Now:yyyy-MM-dd-HH-mm-ss}"; + fileDialog.SaveFolderDialog("Save Model", folderName, (result, path) => { if (!result) return; Task.Run(() => { - exportService.Export(group, path); + var cacheDir = Path.Combine(path, "cache"); + Directory.CreateDirectory(cacheDir); + var composer = new CharacterComposer( + log, + new DataProvider(cacheDir, pack, log, CancellationToken.None)); + var scene = new SceneBuilder(); + var root = new NodeBuilder(); + composer.ComposeCharacterInstance(info, scene, root); + scene.AddNode(root); + scene.ToGltf2().SaveGLTF(Path.Combine(path, "character.gltf")); + Process.Start("explorer.exe", path); }); }, Plugin.TempDirectory); } @@ -475,29 +504,43 @@ private void DrawModel( if (ImGui.MenuItem("Export as glTF")) { var folderName = Path.GetFileNameWithoutExtension(fileName); + var characterInfo = layoutService.HandleDrawObject((DrawObject*)cBase); + if (characterInfo == null) + { + log.LogError("Failed to get character info from draw object"); + return; + } + + var colorTableTextures = parseService.ParseColorTableTextures(cBase); + fileDialog.SaveFolderDialog("Save Model", folderName, - (result, path) => - { - if (!result) return; - var colorTableTextures = parseService.ParseColorTableTextures(cBase); - var modelData = - parseService.HandleModelPtr( - cBase, (int)model->SlotIndex, colorTableTextures); - if (modelData == null) - { - log.LogError("Failed to get model data for {FileName}", fileName); - return; - } - - var skeleton = StructExtensions.GetParsedSkeleton(model); - var cGroup = new CharacterGroup( - customizeParams ?? new CustomizeParameter(), - customizeData ?? new CustomizeData(), genderRace, [modelData], - skeleton, []); - - - Task.Run(() => { exportService.Export(cGroup, path); }); - }, Plugin.TempDirectory); + (result, path) => + { + if (!result) return; + + var modelData = layoutService.HandleModel(cBase, model, colorTableTextures); + if (modelData == null) + { + log.LogError("Failed to get model data for {FileName}", fileName); + return; + } + + Task.Run(() => + { + var cacheDir = Path.Combine(path, "cache"); + Directory.CreateDirectory(cacheDir); + var composer = new CharacterComposer( + log, + new DataProvider(cacheDir, pack, log, CancellationToken.None)); + var scene = new SceneBuilder(); + var root = new NodeBuilder(); + composer.ComposeModels([modelData], characterInfo.GenderRace, characterInfo.CustomizeParameter, + characterInfo.CustomizeData, characterInfo.Skeleton, scene, root); + scene.AddNode(root); + scene.ToGltf2().SaveGLTF(Path.Combine(path, "model.gltf")); + Process.Start("explorer.exe", path); + }); + }, Plugin.TempDirectory); } ImGui.EndPopup(); @@ -769,6 +812,7 @@ private void DrawMaterial( var imageData = str.DetachAsData().AsSpan(); File.WriteAllBytes(filePath, imageData.ToArray()); } + Process.Start("explorer.exe", path); }, Plugin.TempDirectory); } diff --git a/Meddle/Meddle.Plugin/Utils/UIUtil.cs b/Meddle/Meddle.Plugin/Utils/UIUtil.cs index 7a1dcb0..71ab6af 100644 --- a/Meddle/Meddle.Plugin/Utils/UIUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/UIUtil.cs @@ -61,12 +61,24 @@ public static void DrawCustomizeData(CustomizeData customize) ImGui.Checkbox("Highlights", ref customize.Highlights); } + public static void DrawColorTable(IColorTableSet table) + { + if (table is ColorTableSet set) + { + DrawColorTable(set.ColorTable, set.ColorDyeTable); + } + else + { + ImGui.Text("Unsupported ColorTableSet"); + } + } + public static void DrawColorTable(ColorTable table, ColorDyeTable? dyeTable = null) { DrawColorTable(table.Rows, dyeTable); } - public static void DrawColorTable(ColorTableRow[] tableRows, ColorDyeTable? dyeTable = null) + public static void DrawColorTable(ReadOnlySpan tableRows, ColorDyeTable? dyeTable = null) { if (ImGui.BeginTable("ColorTable", 9, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable)) { @@ -83,27 +95,14 @@ public static void DrawColorTable(ColorTableRow[] tableRows, ColorDyeTable? dyeT for (var i = 0; i < tableRows.Length; i++) { - DrawRow(i, ref tableRows[i], dyeTable); + DrawRow(i, tableRows[i], dyeTable); } ImGui.EndTable(); } } - public static void DrawColorTable(MtrlFile file) - { - ImGui.Text($"Color Table: {file.HasTable}"); - ImGui.Text($"Dye Table: {file.HasDyeTable}"); - ImGui.Text($"Extended Color Table: {file.LargeColorTable}"); - if (!file.HasTable) - { - return; - } - - DrawColorTable(file.ColorTable, file.HasDyeTable ? file.ColorDyeTable : null); - } - - private static void DrawRow(int i, ref ColorTableRow row, ColorDyeTable? dyeTable) + private static void DrawRow(int i, ColorTableRow row, ColorDyeTable? dyeTable) { ImGui.TableNextRow(); ImGui.TableSetColumnIndex(0); @@ -113,7 +112,7 @@ private static void DrawRow(int i, ref ColorTableRow row, ColorDyeTable? dyeTabl if (dyeTable != null) { ImGui.SameLine(); - var diff = dyeTable.Value[i].Diffuse; + var diff = dyeTable.Value.Rows[i].Diffuse; ImGui.Checkbox("##rowdiff", ref diff); } @@ -122,7 +121,7 @@ private static void DrawRow(int i, ref ColorTableRow row, ColorDyeTable? dyeTabl if (dyeTable != null) { ImGui.SameLine(); - var spec = dyeTable.Value[i].Specular; + var spec = dyeTable.Value.Rows[i].Specular; ImGui.Checkbox("##rowspec", ref spec); } @@ -131,7 +130,7 @@ private static void DrawRow(int i, ref ColorTableRow row, ColorDyeTable? dyeTabl if (dyeTable != null) { ImGui.SameLine(); - var emm = dyeTable.Value[i].Emissive; + var emm = dyeTable.Value.Rows[i].Emissive; ImGui.Checkbox("##rowemm", ref emm); } @@ -142,7 +141,7 @@ private static void DrawRow(int i, ref ColorTableRow row, ColorDyeTable? dyeTabl ImGui.TableSetColumnIndex(6); if (dyeTable != null) { - var specStrength = dyeTable.Value[i].SpecularStrength; + var specStrength = dyeTable.Value.Rows[i].SpecularStrength; ImGui.Checkbox("##rowspecstr", ref specStrength); ImGui.SameLine(); } @@ -151,7 +150,7 @@ private static void DrawRow(int i, ref ColorTableRow row, ColorDyeTable? dyeTabl ImGui.TableSetColumnIndex(7); if (dyeTable != null) { - var gloss = dyeTable.Value[i].Gloss; + var gloss = dyeTable.Value.Rows[i].Gloss; ImGui.Checkbox("##rowgloss", ref gloss); ImGui.SameLine(); } diff --git a/Meddle/Meddle.UI/PathService.cs b/Meddle/Meddle.UI/PathService.cs index a840bb2..f6445e5 100644 --- a/Meddle/Meddle.UI/PathService.cs +++ b/Meddle/Meddle.UI/PathService.cs @@ -2,7 +2,7 @@ using System.IO.Compression; using Meddle.Utils.Files; using Meddle.Utils.Files.SqPack; -using Meddle.Utils.Models; +using Meddle.Utils.Helpers; namespace Meddle.UI; diff --git a/Meddle/Meddle.UI/Windows/PathManager.cs b/Meddle/Meddle.UI/Windows/PathManager.cs index d136c36..9ae5b5a 100644 --- a/Meddle/Meddle.UI/Windows/PathManager.cs +++ b/Meddle/Meddle.UI/Windows/PathManager.cs @@ -1,7 +1,7 @@ using ImGuiNET; using Meddle.Utils.Files; using Meddle.Utils.Files.SqPack; -using Meddle.Utils.Models; +using Meddle.Utils.Helpers; using Microsoft.Extensions.Logging; namespace Meddle.UI.Windows; diff --git a/Meddle/Meddle.UI/Windows/Views/ExportView.cs b/Meddle/Meddle.UI/Windows/Views/ExportView.cs index 5333920..9fe3cc1 100644 --- a/Meddle/Meddle.UI/Windows/Views/ExportView.cs +++ b/Meddle/Meddle.UI/Windows/Views/ExportView.cs @@ -7,8 +7,8 @@ using Meddle.Utils.Export; using Meddle.Utils.Files; using Meddle.Utils.Files.SqPack; +using Meddle.Utils.Helpers; using Meddle.Utils.Materials; -using Meddle.Utils.Models; using Meddle.Utils.Skeletons.Havok; using Meddle.Utils.Skeletons.Havok.Models; using SharpGLTF.Materials; diff --git a/Meddle/Meddle.UI/Windows/Views/MdlView.cs b/Meddle/Meddle.UI/Windows/Views/MdlView.cs index 8482692..3539b3c 100644 --- a/Meddle/Meddle.UI/Windows/Views/MdlView.cs +++ b/Meddle/Meddle.UI/Windows/Views/MdlView.cs @@ -2,7 +2,7 @@ using Meddle.Utils.Files; using Meddle.Utils.Files.SqPack; using Meddle.Utils.Files.Structs.Model; -using Meddle.Utils.Models; +using Meddle.Utils.Helpers; namespace Meddle.UI.Windows.Views; diff --git a/Meddle/Meddle.UI/Windows/Views/MtrlView.cs b/Meddle/Meddle.UI/Windows/Views/MtrlView.cs index e56fa9e..32ded17 100644 --- a/Meddle/Meddle.UI/Windows/Views/MtrlView.cs +++ b/Meddle/Meddle.UI/Windows/Views/MtrlView.cs @@ -6,7 +6,7 @@ using Meddle.Utils.Files; using Meddle.Utils.Files.SqPack; using Meddle.Utils.Files.Structs.Material; -using Meddle.Utils.Models; +using Meddle.Utils.Helpers; namespace Meddle.UI.Windows.Views; @@ -315,23 +315,23 @@ private void DrawColorTable() ImGui.Text("Tile Set"); ImGui.NextColumn(); - for (var i = 0; i < file.ColorTable.Rows.Length; i++) - { - if (file.LargeColorTable) - { - DrawRow(i); - } - else - { - ImGui.Text($"Legacy Row {i}"); - DrawRow(i); - } - } + // for (var i = 0; i < file.ColorTable.Rows.Length; i++) + // { + // if (file.LargeColorTable) + // { + // DrawRow(i); + // } + // else + // { + // ImGui.Text($"Legacy Row {i}"); + // DrawRow(i); + // } + // } ImGui.Columns(1); } - private void DrawRow(int i) + /*private void DrawRow(int i) { ref var row = ref file.ColorTable.Rows[i]; ImGui.Text($"{i}"); @@ -387,5 +387,5 @@ private void DrawRow(int i) ImGui.NextColumn(); ImGui.Text($"{row.TileIndex}"); ImGui.NextColumn(); - } + }*/ } diff --git a/Meddle/Meddle.UI/Windows/Views/ShpkView.cs b/Meddle/Meddle.UI/Windows/Views/ShpkView.cs index b6a8c33..c3c5069 100644 --- a/Meddle/Meddle.UI/Windows/Views/ShpkView.cs +++ b/Meddle/Meddle.UI/Windows/Views/ShpkView.cs @@ -5,7 +5,7 @@ using Meddle.Utils; using Meddle.Utils.Export; using Meddle.Utils.Files; -using Meddle.Utils.Models; +using Meddle.Utils.Helpers; namespace Meddle.UI.Windows.Views; diff --git a/Meddle/Meddle.UI/Windows/Views/TeraView.cs b/Meddle/Meddle.UI/Windows/Views/TeraView.cs index 4a67067..f48f5ec 100644 --- a/Meddle/Meddle.UI/Windows/Views/TeraView.cs +++ b/Meddle/Meddle.UI/Windows/Views/TeraView.cs @@ -5,8 +5,8 @@ using Meddle.Utils; using Meddle.Utils.Export; using Meddle.Utils.Files; +using Meddle.Utils.Helpers; using Meddle.Utils.Materials; -using Meddle.Utils.Models; using SharpGLTF.Materials; using SharpGLTF.Scenes; using SqPack = Meddle.Utils.Files.SqPack.SqPack; diff --git a/Meddle/Meddle.Utils/Export/Material.cs b/Meddle/Meddle.Utils/Export/Material.cs index a4aa946..c81929b 100644 --- a/Meddle/Meddle.Utils/Export/Material.cs +++ b/Meddle/Meddle.Utils/Export/Material.cs @@ -4,7 +4,7 @@ using System.Text.Json.Serialization; using Meddle.Utils.Files; using Meddle.Utils.Files.Structs.Material; -using Meddle.Utils.Models; +using Meddle.Utils.Helpers; namespace Meddle.Utils.Export; @@ -61,13 +61,13 @@ public enum SpecularMode : uint public enum MaterialConstant : uint { g_AlphaThreshold = 0x29AC0223, - g_ShaderID = 0x59BDA0B1, + g_ShaderID = 0x59BDA0B1, // ??? Set to 0: disable SSS, add a metallic effect. Set to 1: SSS enabled. Set to 6 (hroth) disable SSS, fur parallax enabled g_DiffuseColor = 0x2C2A34DD, g_SpecularColor = 0x141722D5, g_SpecularColorMask = 0xCB0338DC, g_LipRoughnessScale = 0x3632401A, g_WhiteEyeColor = 0x11C90091, - g_SphereMapIndex = 0x074953E9, + g_SphereMapIndex = 0x074953E9, // array index for chara/common/texture/sphere_d_array.tex g_EmissiveColor = 0x38A64362, g_SSAOMask = 0xB7FA33E2, g_TileIndex = 0x4255F2F4, @@ -77,9 +77,9 @@ public enum MaterialConstant : uint g_SheenRate = 0x800EE35F, g_SheenTintRate = 0x1F264897, g_SheenAperture = 0xF490F76E, - g_IrisRingColor = 0x50E36D56, + g_IrisRingColor = 0x50E36D56, // doesn't appear to do anything g_IrisRingEmissiveIntensity = 0x7DABA471, - g_IrisThickness = 0x66C93D3E, + g_IrisThickness = 0x66C93D3E, // SSS Thickness on eyes? g_IrisOptionColorRate = 0x29253809, g_AlphaAperture = 0xD62BF368, g_AlphaOffset = 0xD07A6A65, @@ -96,73 +96,84 @@ public enum MaterialConstant : uint g_ShadowAlphaThreshold = 0xD925FF32, g_NearClip = 0x17A52926, g_AngleClip = 0x71DBDA81, - g_CausticsReflectionPowerBright = 0x0CC09E67, // 213950055 - g_CausticsReflectionPowerDark = 0xC295EA6C, // 3264604780 - g_HeightMapScale = 0xA320B199, // 2736828825 - g_HeightMapUVScale = 0x5B99505D, // 1536774237 - g_MultiWaveScale = 0x37363FDD, // 926302173 - g_WaveSpeed = 0xE4C68FF3, // 3838218227 - g_WaveTime = 0x8EB9D2A6, // 2394542758 - g_AlphaMultiParam = 0x07EDA444, // 133014596 - g_AmbientOcclusionMask = 0x575ABFB2, // 1465565106 - g_ColorUVScale = 0xA5D02C52, // 2781883474 - g_DetailColorUvScale = 0xC63D9716, // 3325925142 - g_DetailID = 0x8981D4D9, // 2306987225 - g_DetailNormalScale = 0x9F42EDA2, // 2671963554 - g_EnvMapPower = 0xEEF5665F, // 4009059935 - g_FresnelValue0 = 0x62E44A4F, // 1659128399 - g_HeightScale = 0x8F8B0070, // 2408251504 - g_InclusionAperture = 0xBCA22FD4, // 3164745684 - g_IrisRingForceColor = 0x58DE06E2, // 1490945762 - g_LayerDepth = 0xA9295FEF, // 2838061039 - g_LayerIrregularity = 0x0A00B0A1, // 167817377 - g_LayerScale = 0xBFCC6602, // 3217843714 - g_LayerVelocity = 0x72181E22, // 1914183202 - g_LipFresnelValue0 = 0x174BB64E, // 390837838 - g_LipShininess = 0x878B272C, // 2274043692 - g_MultiDetailColor = 0x11FD4221, // 301810209 - g_MultiDiffuseColor = 0x3F8AC211, // 1066058257 - g_MultiEmissiveColor = 0xAA676D0F, // 2858904847 - g_MultiHeightScale = 0x43E59A68, // 1139120744 - g_MultiNormalScale = 0x793AC5A3, // 2033894819 - g_MultiSpecularColor = 0x86D60CB8, // 2262174904 - g_MultiSSAOMask = 0x926E860D, // 2456716813 - g_MultiWhitecapDistortion = 0x93504F3B, // 2471513915 - g_MultiWhitecapScale = 0x312B69C1, // 824928705 - g_NormalScale1 = 0x0DD83E61, // 232275553 - g_NormalUVScale = 0xBB99CF76, // 3147419510 - g_PrefersFailure = 0x5394405B, // 1402224731 - g_ReflectionPower = 0x223A3329, // 574239529 - g_ScatteringLevel = 0xB500BB24, // 3036724004 - g_ShadowOffset = 0x96D2B53D, // 2530391357 - g_ShadowPosOffset = 0x5351646E, // 1397843054 - g_SpecularMask = 0x36080AD0, // 906496720 - g_SpecularPower = 0xD9CB6B9C, // 3653987228 - g_SpecularUVScale = 0x8D03A782, // 2365826946 - g_ToonIndex = 0xDF15112D, // 3742699821 - g_ToonLightScale = 0x3CCE9E4C, // 1020173900 - g_ToonReflectionScale = 0xD96FAF7A, // 3647975290 - g_ToonSpecIndex = 0x00A680BC, // 10911932 - g_TransparencyDistance = 0x1624F841, // 371521601 - g_WaveletDistortion = 0x3439B378, // 876196728 - g_WaveletNoiseParam = 0x1279815C, // 309952860 - g_WaveletOffset = 0x9BE8354A, // 2615686474 - g_WaveletScale = 0xD62C681E, // 3593234462 - g_WaveTime1 = 0x6EE5BF35, // 1860550453 - g_WhitecapDistance = 0x5D26B262, // 1562817122 - g_WhitecapDistortion = 0x61053025, // 1627729957 - g_WhitecapNoiseScale = 0x0FF95B0C, // 268000012 - g_WhitecapScale = 0xA3EA47AC, // 2750039980 - g_WhitecapSpeed = 0x408A9CDE, // 1082825950 - g_Fresnel = 0xE3AA427A, // 3819586170 - g_Gradation = 0x94B40EEE, // 2494828270 - g_Intensity = 0xBCBA70E1, // 3166335201 - g_Shininess = 0x992869AB, // 2569562539 - g_DetailColor = 0xDD93D839, // 3717453881 - g_LayerColor = 0x35DC0B6F, // 903613295 - g_RefractionColor = 0xBA163700, // 3122018048 - g_WhitecapColor = 0x29FA2AC1, // 704260801 - g_DetailNormalUvScale = 0x025A9BEE, // 39492590 + g_CausticsReflectionPowerBright = 0x0CC09E67, + g_CausticsReflectionPowerDark = 0xC295EA6C, + + g_HeightScale = 0x8F8B0070, + g_HeightMapScale = 0xA320B199, + g_HeightMapUVScale = 0x5B99505D, + g_MultiWaveScale = 0x37363FDD, + g_WaveSpeed = 0xE4C68FF3, + g_WaveTime = 0x8EB9D2A6, + g_AlphaMultiParam = 0x07EDA444, + g_AmbientOcclusionMask = 0x575ABFB2, + g_ColorUVScale = 0xA5D02C52, + + g_DetailID = 0x8981D4D9, // Index into bgcommon/nature/detail/texture/detail_d_array.tex and bgcommon/nature/detail/texture/detail_n_array.tex + g_DetailNormalScale = 0x9F42EDA2, + g_DetailColorUvScale = 0xC63D9716, + g_DetailColor = 0xDD93D839, + g_DetailNormalUvScale = 0x025A9BEE, + + g_EnvMapPower = 0xEEF5665F, + g_FresnelValue0 = 0x62E44A4F, + g_InclusionAperture = 0xBCA22FD4, + g_IrisRingForceColor = 0x58DE06E2, // seems to adjust the colour or specular of the iris ring + g_LayerDepth = 0xA9295FEF, + g_LayerIrregularity = 0x0A00B0A1, + g_LayerScale = 0xBFCC6602, + g_LayerVelocity = 0x72181E22, + g_LipFresnelValue0 = 0x174BB64E, + g_LipShininess = 0x878B272C, + g_MultiDetailColor = 0x11FD4221, + g_MultiDiffuseColor = 0x3F8AC211, + g_MultiEmissiveColor = 0xAA676D0F, + g_MultiHeightScale = 0x43E59A68, + g_MultiNormalScale = 0x793AC5A3, + g_MultiSpecularColor = 0x86D60CB8, + g_MultiSSAOMask = 0x926E860D, + g_MultiWhitecapDistortion = 0x93504F3B, + g_MultiWhitecapScale = 0x312B69C1, + g_NormalScale1 = 0x0DD83E61, + g_NormalUVScale = 0xBB99CF76, + g_PrefersFailure = 0x5394405B, + g_ReflectionPower = 0x223A3329, + g_ScatteringLevel = 0xB500BB24, + g_ShadowOffset = 0x96D2B53D, + g_ShadowPosOffset = 0x5351646E, + g_SpecularMask = 0x36080AD0, + g_SpecularPower = 0xD9CB6B9C, + g_SpecularUVScale = 0x8D03A782, + g_ToonIndex = 0xDF15112D, + g_ToonLightScale = 0x3CCE9E4C, + g_ToonReflectionScale = 0xD96FAF7A, + g_ToonSpecIndex = 0x00A680BC, + g_TransparencyDistance = 0x1624F841, + g_WaveletDistortion = 0x3439B378, + g_WaveletNoiseParam = 0x1279815C, + g_WaveletOffset = 0x9BE8354A, + g_WaveletScale = 0xD62C681E, + g_WaveTime1 = 0x6EE5BF35, + g_WhitecapDistance = 0x5D26B262, + g_WhitecapDistortion = 0x61053025, + g_WhitecapNoiseScale = 0x0FF95B0C, + g_WhitecapScale = 0xA3EA47AC, + g_WhitecapSpeed = 0x408A9CDE, + g_Fresnel = 0xE3AA427A, + g_Gradation = 0x94B40EEE, + g_Intensity = 0xBCBA70E1, + g_Shininess = 0x992869AB, + g_LayerColor = 0x35DC0B6F, + g_RefractionColor = 0xBA163700, + g_WhitecapColor = 0x29FA2AC1, + + // The following are have unknown names but their usage is generally known + unk_LimbalRingRange = 0xE18398AE, // from centre of iris, the start and end point of the limbal ring + unk_LimbalRingFade = 0x5B608CFE, // inner and outer fade of limbal ring + unk_IrisParallaxDepth = 0x37DEA328, // Iris parallax depth (needs to be >0) + unk_IrisEmissiveOverride = 0x8EA14846, // Override iris emissive color with feature color + unk_IrisEmissiveOverrideOpacity = 0x7918D232, // Opacity of the Feature Color Emissive Override. + unk_TileSharpening = 0x6421DD30, // Positive value "smoothes" the texture, negative value "sharpens" the texture } public class Material @@ -246,7 +257,7 @@ private void InitFromFile(MtrlFile file) } MtrlConstants = materialConstantDict; - ColorTable = file.ColorTable; + ColorTable = file.GetColorTable(); } public string HandlePath { get; private set; } @@ -268,8 +279,6 @@ public float ComputeAlpha(float alpha) return 1.0f; } - - public IReadOnlyList ShaderKeys { get; private set; } public Dictionary MtrlConstants { get; private set; } @@ -277,7 +286,7 @@ public float ComputeAlpha(float alpha) public IReadOnlyList Textures { get; private set; } [JsonIgnore] - public ColorTable ColorTable { get; private set; } + public IColorTableSet ColorTable { get; private set; } public bool TryGetTexture(TextureUsage usage, out Texture texture) { diff --git a/Meddle/Meddle.Utils/Export/Model.cs b/Meddle/Meddle.Utils/Export/Model.cs index 30f453e..9bde789 100644 --- a/Meddle/Meddle.Utils/Export/Model.cs +++ b/Meddle/Meddle.Utils/Export/Model.cs @@ -1,5 +1,5 @@ using Meddle.Utils.Files; -using Meddle.Utils.Models; +using Meddle.Utils.Helpers; namespace Meddle.Utils.Export; diff --git a/Meddle/Meddle.Utils/Files/MtrlFile.cs b/Meddle/Meddle.Utils/Files/MtrlFile.cs index 9152f2b..2eb4c90 100644 --- a/Meddle/Meddle.Utils/Files/MtrlFile.cs +++ b/Meddle/Meddle.Utils/Files/MtrlFile.cs @@ -7,23 +7,23 @@ public class MtrlFile public const uint MtrlMagic = 0x1030000; private readonly byte[] _data; - public byte[] AdditionalData; - public ColorDyeTable ColorDyeTable; - public ColorSet[] ColorSets; - - public ColorTable ColorTable; - public Constant[] Constants; public MaterialFileHeader FileHeader; - public Sampler[] Samplers; - + public TextureOffset[] TextureOffsets; + public UvColorSet[] UvColorSets; + public ColorSet[] ColorSets; + public byte[] Strings; + public byte[] AdditionalData; + public byte[] DataSet; + + //public ColorTable ColorTable; + //public ColorDyeTable ColorDyeTable; + public MaterialShaderHeader ShaderHeader; - public ShaderKey[] ShaderKeys; + public Constant[] Constants; + public Sampler[] Samplers; public uint[] ShaderValues; - public byte[] Strings; - public TextureOffset[] TextureOffsets; - public UvColorSet[] UvColorSets; public MtrlFile(byte[] data) : this((ReadOnlySpan)data) { } @@ -49,24 +49,25 @@ public MtrlFile(ReadOnlySpan data) if (FileHeader.DataSetSize > 0) { - var dataSet = reader.Read(FileHeader.DataSetSize).ToArray(); - var dataSetReader = new SpanBinaryReader(dataSet); - if (LargeColorTable) - { - ColorTable = HasTable ? ColorTable.Load(ref dataSetReader) : ColorTable.Default(); - if (HasDyeTable) - ColorDyeTable = dataSetReader.Read(); - } - else - { - ColorTable = HasTable ? ColorTable.LoadLegacy(ref dataSetReader) : ColorTable.DefaultLegacy(); - if (HasDyeTable) - ColorDyeTable = new ColorDyeTable(dataSetReader.Read()); - } + DataSet = reader.Read(FileHeader.DataSetSize).ToArray(); + // var dataSetReader = new SpanBinaryReader(DataSet); + // if (LargeColorTable) + // { + // ColorTable = HasTable ? ColorTable.Load(ref dataSetReader) : ColorTable.Default(); + // if (HasDyeTable) + // ColorDyeTable = dataSetReader.Read(); + // } + // else + // { + // ColorTable = HasTable ? ColorTable.LoadLegacy(ref dataSetReader) : ColorTable.DefaultLegacy(); + // if (HasDyeTable) + // ColorDyeTable = new ColorDyeTable(dataSetReader.Read()); + // } } else { - ColorTable = ColorTable.Default(); + DataSet = Array.Empty(); + //ColorTable = ColorTable.Default(); } ShaderHeader = reader.Read(); diff --git a/Meddle/Meddle.Utils/Files/Structs/Material/ColorDyeTable.cs b/Meddle/Meddle.Utils/Files/Structs/Material/ColorDyeTable.cs deleted file mode 100644 index acfb5e5..0000000 --- a/Meddle/Meddle.Utils/Files/Structs/Material/ColorDyeTable.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Collections; - -namespace Meddle.Utils.Files.Structs.Material; - -public unsafe struct ColorDyeTable : IEnumerable -{ - public struct Row - { - public const int Size = 4; - private uint _data; - - public ushort Template - { - get => (ushort)(_data >> 5); - set => _data = (_data & 0x1Fu) | ((uint)value << 5); - } - - public bool Diffuse - { - get => (_data & 0x01) != 0; - set => _data = value ? _data | 0x01u : _data & 0xFFFEu; - } - - public bool Specular - { - get => (_data & 0x02) != 0; - set => _data = value ? _data | 0x02u : _data & 0xFFFDu; - } - - public bool Emissive - { - get => (_data & 0x04) != 0; - set => _data = value ? _data | 0x04u : _data & 0xFFFBu; - } - - public bool Gloss - { - get => (_data & 0x08) != 0; - set => _data = value ? _data | 0x08u : _data & 0xFFF7u; - } - - public bool SpecularStrength - { - get => (_data & 0x10) != 0; - set => _data = value ? _data | 0x10u : _data & 0xFFEFu; - } - } - - public const int NumRows = 32; - private fixed uint _rowData[NumRows]; - - public ref Row this[int i] - { - get - { - fixed (uint* ptr = _rowData) - { - return ref ((Row*)ptr)[i]; - } - } - } - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < NumRows; ++i) - { - yield return this[i]; - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public ReadOnlySpan AsBytes() - { - fixed (uint* ptr = _rowData) - { - return new ReadOnlySpan(ptr, NumRows * sizeof(ushort)); - } - } - - internal LegacyColorDyeTable ToLegacy() - { - var table = new LegacyColorDyeTable(); - for (var i = 0; i < LegacyColorDyeTable.NumRows; ++i) - { - var oldRow = table[i]; - ref var row = ref this[i]; - oldRow.Template = row.Template; - oldRow.Diffuse = row.Diffuse; - oldRow.Specular = row.Specular; - oldRow.Emissive = row.Emissive; - oldRow.Gloss = row.Gloss; - oldRow.SpecularStrength = row.SpecularStrength; - } - - return table; - } - - internal ColorDyeTable(in LegacyColorDyeTable oldTable) - { - for (var i = 0; i < LegacyColorDyeTable.NumRows; ++i) - { - var oldRow = oldTable[i]; - ref var row = ref this[i]; - row.Template = oldRow.Template; - row.Diffuse = oldRow.Diffuse; - row.Specular = oldRow.Specular; - row.Emissive = oldRow.Emissive; - row.Gloss = oldRow.Gloss; - row.SpecularStrength = oldRow.SpecularStrength; - } - } -} diff --git a/Meddle/Meddle.Utils/Files/Structs/Material/ColorDyeTableRow.cs b/Meddle/Meddle.Utils/Files/Structs/Material/ColorDyeTableRow.cs new file mode 100644 index 0000000..64ad76a --- /dev/null +++ b/Meddle/Meddle.Utils/Files/Structs/Material/ColorDyeTableRow.cs @@ -0,0 +1,89 @@ +using System.Runtime.InteropServices; + +namespace Meddle.Utils.Files.Structs.Material; + +[StructLayout(LayoutKind.Explicit, Size = 4)] +public struct ColorDyeTableRow +{ + [FieldOffset(0)] + public uint Data; + + public ushort Template + { + get => (ushort)(Data >> 5); + set => Data = (Data & 0x1Fu) | ((uint)value << 5); + } + + public bool Diffuse + { + get => (Data & 0x01) != 0; + set => Data = value ? Data | 0x01u : Data & 0xFFFEu; + } + + public bool Specular + { + get => (Data & 0x02) != 0; + set => Data = value ? Data | 0x02u : Data & 0xFFFDu; + } + + public bool Emissive + { + get => (Data & 0x04) != 0; + set => Data = value ? Data | 0x04u : Data & 0xFFFBu; + } + + public bool Gloss + { + get => (Data & 0x08) != 0; + set => Data = value ? Data | 0x08u : Data & 0xFFF7u; + } + + public bool SpecularStrength + { + get => (Data & 0x10) != 0; + set => Data = value ? Data | 0x10u : Data & 0xFFEFu; + } +} + +[StructLayout(LayoutKind.Explicit, Size = 2)] +public struct LegacyColorDyeTableRow +{ + [FieldOffset(0)] + public ushort Data; + + public ushort Template + { + get => (ushort)(Data >> 5); + set => Data = (ushort)((Data & 0x1F) | (value << 5)); + } + + public bool Diffuse + { + get => (Data & 0x01) != 0; + set => Data = (ushort)(value ? Data | 0x01 : Data & 0xFFFE); + } + + public bool Specular + { + get => (Data & 0x02) != 0; + set => Data = (ushort)(value ? Data | 0x02 : Data & 0xFFFD); + } + + public bool Emissive + { + get => (Data & 0x04) != 0; + set => Data = (ushort)(value ? Data | 0x04 : Data & 0xFFFB); + } + + public bool Gloss + { + get => (Data & 0x08) != 0; + set => Data = (ushort)(value ? Data | 0x08 : Data & 0xFFF7); + } + + public bool SpecularStrength + { + get => (Data & 0x10) != 0; + set => Data = (ushort)(value ? Data | 0x10 : Data & 0xFFEF); + } +} diff --git a/Meddle/Meddle.Utils/Files/Structs/Material/ColorTable.cs b/Meddle/Meddle.Utils/Files/Structs/Material/ColorTable.cs deleted file mode 100644 index a993f81..0000000 --- a/Meddle/Meddle.Utils/Files/Structs/Material/ColorTable.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System.Numerics; - -namespace Meddle.Utils.Files.Structs.Material; - -public unsafe struct ColorTable -{ - public ColorTableRow[] Rows; - public const int RowSize = 32; - public const int NumRows = 32; - public const int LegacyRowSize = 16; - public const int LegacyNumRows = 16; - - public (ColorTableRow row0, ColorTableRow row1) GetPair(int weight) - { - var weightArr = new byte[] - { - 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, - 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF - }; - - var nearestPair = weightArr.MinBy(v => Math.Abs(v - weight)); - var pairIdx = Array.IndexOf(weightArr, nearestPair) * 2; - var pair0 = Rows[pairIdx]; - var pair1 = Rows[pairIdx + 1]; - - return (pair0, pair1); - } - - public ColorTableRow GetBlendedPair(int weight, int blend) - { - var (row0, row1) = GetPair(weight); - var prioRow = weight < 128 ? row1 : row0; - - var blendAmount = blend / 255f; - var row = new ColorTableRow - { - Diffuse = Vector3.Clamp(Vector3.Lerp(row1.Diffuse, row0.Diffuse, blendAmount), Vector3.Zero, Vector3.One), - Specular = - Vector3.Clamp(Vector3.Lerp(row1.Specular, row0.Specular, blendAmount), Vector3.Zero, Vector3.One), - Emissive = - Vector3.Clamp(Vector3.Lerp(row1.Emissive, row0.Emissive, blendAmount), Vector3.Zero, Vector3.One), - MaterialRepeat = prioRow.MaterialRepeat, - MaterialSkew = prioRow.MaterialSkew, - SpecularStrength = float.Clamp(float.Lerp(row1.SpecularStrength, row0.SpecularStrength, blendAmount), 0, 1), - GlossStrength = float.Clamp(float.Lerp(row1.GlossStrength, row0.GlossStrength, blendAmount), 0, 1), - TileIndex = prioRow.TileIndex - }; - - return row; - } - - public static ColorTable Load(ref SpanBinaryReader reader) - { - var table = new ColorTable - { - Rows = reader.Read(NumRows).ToArray() - }; - - return table; - } - - public static ColorTable Default() - { - var table = new ColorTable - { - Rows = new ColorTableRow[NumRows] - }; - - return table; - } - - public ColorTable LoadLegacy(ref SpanBinaryReader dataSetReader) - { - var buf = dataSetReader.Read(LegacyNumRows); - var upgraded = buf.ToArray().Select(x => x.ToNew()).ToArray(); - - var table = new ColorTable - { - Rows = upgraded - }; - - return table; - } - - public ColorTable DefaultLegacy() - { - var table = new ColorTable - { - Rows = new ColorTableRow[LegacyNumRows] - }; - - return table; - } -} - -// Old Color Table blending -public struct TableRow -{ - public int Stepped; - public int Previous; - public int Next; - public float Weight; - - public static TableRow GetTableRowIndices(float index) - { - var vBase = index * 15f; - var vOffFilter = index * 7.5f % 1.0f; - var smoothed = float.Lerp(vBase, float.Floor(vBase + 0.5f), vOffFilter * 2); - var stepped = float.Floor(smoothed + 0.5f); - - return new TableRow - { - Stepped = (int)stepped, - Previous = (int)MathF.Floor(smoothed), - Next = (int)MathF.Ceiling(smoothed), - Weight = smoothed % 1 - }; - } -} diff --git a/Meddle/Meddle.Utils/Files/Structs/Material/ColorTableSet.cs b/Meddle/Meddle.Utils/Files/Structs/Material/ColorTableSet.cs new file mode 100644 index 0000000..3014837 --- /dev/null +++ b/Meddle/Meddle.Utils/Files/Structs/Material/ColorTableSet.cs @@ -0,0 +1,144 @@ +using System.Numerics; + +namespace Meddle.Utils.Files.Structs.Material; + +public interface IColorTableSet; + +public struct ColorTableSet : IColorTableSet +{ + public ColorTable ColorTable; + public ColorDyeTable? ColorDyeTable; +} + +public struct LegacyColorTableSet : IColorTableSet +{ + public LegacyColorTable ColorTable; + public LegacyColorDyeTable? ColorDyeTable; +} + +public readonly struct LegacyColorDyeTable +{ + private readonly LegacyColorDyeTableRow[] rows; + public ReadOnlySpan Rows => new(rows); + + public LegacyColorDyeTable(ref SpanBinaryReader reader) + { + rows = reader.Read(LegacyColorTable.LegacyNumRows).ToArray(); + } +} + +public readonly struct ColorDyeTable +{ + private readonly ColorDyeTableRow[] rows; + public ReadOnlySpan Rows => new(rows); + + public ColorDyeTable(ref SpanBinaryReader reader) + { + rows = reader.Read(ColorTable.NumRows).ToArray(); + } +} + + +public readonly struct LegacyColorTable +{ + public const int LegacyNumRows = 16; + private readonly LegacyColorTableRow[] rows; + public ReadOnlySpan Rows => new(rows); + public LegacyColorTable(ref SpanBinaryReader reader) + { + rows = reader.Read(LegacyNumRows).ToArray(); + } + + // normal pixel A channel on legacy normal as a float from 0-1 + public LegacyColorTableRow GetBlendedPair(float normalPixelW) + { + var indices = TableRow.GetTableRowIndices(normalPixelW); + var row0 = Rows[indices.Previous]; + var row1 = Rows[indices.Next]; + var stepped = Rows[indices.Stepped]; + + return new LegacyColorTableRow + { + Diffuse = Vector3.Clamp(Vector3.Lerp(row0.Diffuse, row1.Diffuse, indices.Weight), Vector3.Zero, Vector3.One), + Specular = Vector3.Clamp(Vector3.Lerp(row0.Specular, row1.Specular, indices.Weight), Vector3.Zero, Vector3.One), + SpecularStrength = float.Lerp(row0.SpecularStrength, row1.SpecularStrength, indices.Weight), + Emissive = Vector3.Clamp(Vector3.Lerp(row0.Emissive, row1.Emissive, indices.Weight), Vector3.Zero, Vector3.One), + GlossStrength = float.Lerp(row0.GlossStrength, row1.GlossStrength, indices.Weight), + MaterialRepeat = stepped.MaterialRepeat, + MaterialSkew = stepped.MaterialSkew, + TileIndex = stepped.TileIndex + }; + } + + public struct TableRow + { + public int Stepped; + public int Previous; + public int Next; + public float Weight; + + public static TableRow GetTableRowIndices(float index) + { + var vBase = index * 15f; + var vOffFilter = index * 7.5f % 1.0f; + var smoothed = float.Lerp(vBase, float.Floor(vBase + 0.5f), vOffFilter * 2); + var stepped = float.Floor(smoothed + 0.5f); + + return new TableRow + { + Stepped = (int)stepped, + Previous = (int)MathF.Floor(smoothed), + Next = (int)MathF.Ceiling(smoothed), + Weight = smoothed % 1 + }; + } + } +} + +public readonly struct ColorTable +{ + public const int NumRows = 32; + private readonly ColorTableRow[] rows; + public ReadOnlySpan Rows => new(rows); + public ColorTable(ref SpanBinaryReader reader) + { + rows = reader.Read(NumRows).ToArray(); + } + + public (ColorTableRow row0, ColorTableRow row1) GetPair(int weight) + { + var weightArr = new byte[] + { + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF + }; + + var nearestPair = weightArr.MinBy(v => Math.Abs(v - weight)); + var pairIdx = Array.IndexOf(weightArr, nearestPair) * 2; + var pair0 = rows[pairIdx]; + var pair1 = rows[pairIdx + 1]; + + return (pair0, pair1); + } + + public ColorTableRow GetBlendedPair(int weight, int blend) + { + var (row0, row1) = GetPair(weight); + var prioRow = weight < 128 ? row1 : row0; + + var blendAmount = blend / 255f; + var row = new ColorTableRow + { + Diffuse = Vector3.Clamp(Vector3.Lerp(row1.Diffuse, row0.Diffuse, blendAmount), Vector3.Zero, Vector3.One), + Specular = Vector3.Clamp(Vector3.Lerp(row1.Specular, row0.Specular, blendAmount), Vector3.Zero, Vector3.One), + Emissive = Vector3.Clamp(Vector3.Lerp(row1.Emissive, row0.Emissive, blendAmount), Vector3.Zero, Vector3.One), + SpecularStrength = float.Lerp(row1.SpecularStrength, row0.SpecularStrength, blendAmount), + GlossStrength = float.Lerp(row1.GlossStrength, row0.GlossStrength, blendAmount), + MaterialRepeat = prioRow.MaterialRepeat, + MaterialSkew = prioRow.MaterialSkew, + TileIndex = prioRow.TileIndex + }; + + return row; + } +} diff --git a/Meddle/Meddle.Utils/Files/Structs/Material/LegacyColorDyeTable.cs b/Meddle/Meddle.Utils/Files/Structs/Material/LegacyColorDyeTable.cs deleted file mode 100644 index 575b539..0000000 --- a/Meddle/Meddle.Utils/Files/Structs/Material/LegacyColorDyeTable.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Collections; - -namespace Meddle.Utils.Files.Structs.Material; - -internal unsafe struct LegacyColorDyeTable : IEnumerable -{ - public struct Row - { - public const int Size = 2; - - private ushort _data; - - public ushort Template - { - get => (ushort)(_data >> 5); - set => _data = (ushort)((_data & 0x1F) | (value << 5)); - } - - public bool Diffuse - { - get => (_data & 0x01) != 0; - set => _data = (ushort)(value ? _data | 0x01 : _data & 0xFFFE); - } - - public bool Specular - { - get => (_data & 0x02) != 0; - set => _data = (ushort)(value ? _data | 0x02 : _data & 0xFFFD); - } - - public bool Emissive - { - get => (_data & 0x04) != 0; - set => _data = (ushort)(value ? _data | 0x04 : _data & 0xFFFB); - } - - public bool Gloss - { - get => (_data & 0x08) != 0; - set => _data = (ushort)(value ? _data | 0x08 : _data & 0xFFF7); - } - - public bool SpecularStrength - { - get => (_data & 0x10) != 0; - set => _data = (ushort)(value ? _data | 0x10 : _data & 0xFFEF); - } - } - - public const int NumRows = 16; - private fixed ushort _rowData[NumRows]; - - public ref Row this[int i] - { - get - { - fixed (ushort* ptr = _rowData) - { - return ref ((Row*)ptr)[i]; - } - } - } - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < NumRows; ++i) - yield return this[i]; - } - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public ReadOnlySpan AsBytes() - { - fixed (ushort* ptr = _rowData) - { - return new ReadOnlySpan(ptr, NumRows * sizeof(ushort)); - } - } - - internal LegacyColorDyeTable(in ColorDyeTable newTable) - { - for (var i = 0; i < NumRows; ++i) - { - var newRow = newTable[i]; - ref var row = ref this[i]; - row.Template = newRow.Template; - row.Diffuse = newRow.Diffuse; - row.Specular = newRow.Specular; - row.Emissive = newRow.Emissive; - row.Gloss = newRow.Gloss; - row.SpecularStrength = newRow.SpecularStrength; - } - } -} diff --git a/Meddle/Meddle.Utils/Files/Structs/Material/MaterialParameters.cs b/Meddle/Meddle.Utils/Files/Structs/Material/MaterialParameters.cs deleted file mode 100644 index 5623244..0000000 --- a/Meddle/Meddle.Utils/Files/Structs/Material/MaterialParameters.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Numerics; - -namespace Meddle.Utils.Files.Structs.Material; - -public struct MaterialParameters -{ - public static readonly IReadOnlyList ValidShaders = new [] - { - "hair.shpk", - "iris.shpk", - "skin.shpk", - "character.shpk", - "characterglass.shpk", - }; - - public MaterialParameters(ReadOnlySpan m) - { - if (m.Length != 6) return; - - DiffuseColor = Normalize(new Vector3(m[0].X, m[0].Y, m[0].Z)); - AlphaThreshold = m[0].W == 0 ? 0.5f : m[0].W; - FresnelValue0 = Normalize(new Vector3(m[1].X, m[1].Y, m[1].Z)); - SpecularMask = m[1].W; - LipFresnelValue0 = Normalize(new Vector3(m[2].X, m[2].Y, m[2].Z)); - Shininess = m[2].W / 255f; - EmissiveColor = Normalize(new Vector3(m[3].X, m[3].Y, m[3].Z)); - LipShininess = m[3].W / 255f; - TileScale = new Vector2(m[4].X, m[4].Y); - AmbientOcclusionMask = m[4].Z; - TileIndex = m[4].W; - ScatteringLevel = m[5].X; - UNK_15B70E35 = m[5].Y; - NormalScale = m[5].Z; - } - - public Vector3 DiffuseColor; - public float AlphaThreshold; - public Vector3 FresnelValue0; - public float SpecularMask; - public Vector3 LipFresnelValue0; - public float Shininess; - public Vector3 EmissiveColor; - public float LipShininess; - public Vector2 TileScale; - public float AmbientOcclusionMask; - public float TileIndex; - public float ScatteringLevel; - public float UNK_15B70E35; - public float NormalScale; - - private static Vector3 Normalize(Vector3 v) - { - var len = v.Length(); - if (len == 0) - return Vector3.Zero; - return v / len; - } -} diff --git a/Meddle/Meddle.Utils/Models/MaterialUtils.cs b/Meddle/Meddle.Utils/Helpers/MaterialUtils.cs similarity index 66% rename from Meddle/Meddle.Utils/Models/MaterialUtils.cs rename to Meddle/Meddle.Utils/Helpers/MaterialUtils.cs index 19f0df6..50320d3 100644 --- a/Meddle/Meddle.Utils/Models/MaterialUtils.cs +++ b/Meddle/Meddle.Utils/Helpers/MaterialUtils.cs @@ -1,7 +1,11 @@ -using System.Text; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; using Meddle.Utils.Files; +using Meddle.Utils.Files.Structs.Material; +using Vector2 = FFXIVClientStructs.FFXIV.Common.Math.Vector2; -namespace Meddle.Utils.Models; +namespace Meddle.Utils.Helpers; public static class MaterialUtils { @@ -64,4 +68,32 @@ public static Dictionary GetColorSetStrings(this MtrlFile file) } return colorSetStrings; } + + public static IColorTableSet? GetColorTable(this MtrlFile file) + { + if (file.FileHeader.DataSetSize == 0) + { + return null; + } + + if (!file.HasTable) + { + return null; + } + + var dataSetReader = new SpanBinaryReader(file.DataSet); + return file.LargeColorTable switch + { + true => new ColorTableSet + { + ColorTable = new ColorTable(ref dataSetReader), + ColorDyeTable = file.HasDyeTable ? new ColorDyeTable(ref dataSetReader) : null + }, + false => new LegacyColorTableSet + { + ColorTable = new LegacyColorTable(ref dataSetReader), + ColorDyeTable = file.HasDyeTable ? new LegacyColorDyeTable(ref dataSetReader) : null + } + }; + } } diff --git a/Meddle/Meddle.Utils/Models/ModelUtils.cs b/Meddle/Meddle.Utils/Helpers/ModelUtils.cs similarity index 98% rename from Meddle/Meddle.Utils/Models/ModelUtils.cs rename to Meddle/Meddle.Utils/Helpers/ModelUtils.cs index e0a7c96..b1f31a6 100644 --- a/Meddle/Meddle.Utils/Models/ModelUtils.cs +++ b/Meddle/Meddle.Utils/Helpers/ModelUtils.cs @@ -1,7 +1,7 @@ using System.Text; using Meddle.Utils.Files; -namespace Meddle.Utils.Models; +namespace Meddle.Utils.Helpers; public static class ModelUtils { diff --git a/Meddle/Meddle.Utils/Models/ShaderUtils.cs b/Meddle/Meddle.Utils/Helpers/ShaderUtils.cs similarity index 98% rename from Meddle/Meddle.Utils/Models/ShaderUtils.cs rename to Meddle/Meddle.Utils/Helpers/ShaderUtils.cs index 88ea436..5ad8de3 100644 --- a/Meddle/Meddle.Utils/Models/ShaderUtils.cs +++ b/Meddle/Meddle.Utils/Helpers/ShaderUtils.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Meddle.Utils.Models; +namespace Meddle.Utils.Helpers; public static class ShaderUtils { diff --git a/Meddle/Meddle.Utils/ImageUtils.cs b/Meddle/Meddle.Utils/ImageUtils.cs index ddc9d10..bb4bc15 100644 --- a/Meddle/Meddle.Utils/ImageUtils.cs +++ b/Meddle/Meddle.Utils/ImageUtils.cs @@ -1,7 +1,6 @@ using System.Numerics; using Meddle.Utils.Export; using Meddle.Utils.Files; -using Meddle.Utils.Models; using OtterTex; using SkiaSharp; diff --git a/Meddle/Meddle.Utils/Materials/Bg.cs b/Meddle/Meddle.Utils/Materials/Bg.cs index d567453..5588b28 100644 --- a/Meddle/Meddle.Utils/Materials/Bg.cs +++ b/Meddle/Meddle.Utils/Materials/Bg.cs @@ -1,6 +1,5 @@ using System.Numerics; using Meddle.Utils.Export; -using Meddle.Utils.Models; using SharpGLTF.Materials; namespace Meddle.Utils.Materials; diff --git a/Meddle/Meddle.Utils/Materials/Character.cs b/Meddle/Meddle.Utils/Materials/Character.cs index 515662b..1c95746 100644 --- a/Meddle/Meddle.Utils/Materials/Character.cs +++ b/Meddle/Meddle.Utils/Materials/Character.cs @@ -1,6 +1,6 @@ using System.Numerics; using Meddle.Utils.Export; -using Meddle.Utils.Models; +using Meddle.Utils.Files.Structs.Material; using SharpGLTF.Materials; using CustomizeParameter = Meddle.Utils.Export.CustomizeParameter; @@ -71,7 +71,8 @@ public static MaterialBuilder BuildCharacter(Material material, string name) var maskPixel = maskTexture[x, y].ToVector4(); var indexPixel = indexTexture[x, y]; - var blended = material.ColorTable.GetBlendedPair(indexPixel.Red, indexPixel.Green); + + var blended = ((ColorTableSet)material.ColorTable).ColorTable.GetBlendedPair(indexPixel.Red, indexPixel.Green); if (texMode == TextureMode.Compatibility) { var diffusePixel = diffuseTexture![x, y].ToVector4(); @@ -253,7 +254,7 @@ public static MaterialBuilder BuildCharacterLegacy(Material material, string nam var normalPixel = normal[x, y].ToVector4(); var indexPixel = indexTexture[x, y]; - var blended = material.ColorTable.GetBlendedPair(indexPixel.Red, indexPixel.Green); + var blended = ((LegacyColorTableSet)material.ColorTable).ColorTable.GetBlendedPair(normalPixel.W); if (texMode == TextureMode.Compatibility) { var diffusePixel = diffuseTexture![x, y].ToVector4(); diff --git a/Meddle/Meddle.Utils/Materials/Hair.cs b/Meddle/Meddle.Utils/Materials/Hair.cs index 932c137..8f16b71 100644 --- a/Meddle/Meddle.Utils/Materials/Hair.cs +++ b/Meddle/Meddle.Utils/Materials/Hair.cs @@ -1,6 +1,5 @@ using System.Numerics; using Meddle.Utils.Export; -using Meddle.Utils.Models; using SharpGLTF.Materials; using CustomizeParameter = Meddle.Utils.Export.CustomizeParameter; diff --git a/Meddle/Meddle.Utils/Materials/Iris.cs b/Meddle/Meddle.Utils/Materials/Iris.cs index 42cb1c3..bc83160 100644 --- a/Meddle/Meddle.Utils/Materials/Iris.cs +++ b/Meddle/Meddle.Utils/Materials/Iris.cs @@ -1,7 +1,6 @@ using System.Numerics; using Meddle.Utils.Export; using Meddle.Utils.Files; -using Meddle.Utils.Models; using SharpGLTF.Materials; using CustomizeParameter = Meddle.Utils.Export.CustomizeParameter; diff --git a/Meddle/Meddle.Utils/Materials/MaterialUtility.cs b/Meddle/Meddle.Utils/Materials/MaterialUtility.cs index a168ff2..6db8218 100644 --- a/Meddle/Meddle.Utils/Materials/MaterialUtility.cs +++ b/Meddle/Meddle.Utils/Materials/MaterialUtility.cs @@ -2,7 +2,6 @@ using System.Reflection.Metadata; using FFXIVClientStructs.FFXIV.Shader; using Meddle.Utils.Export; -using Meddle.Utils.Models; using SharpGLTF.Materials; using SharpGLTF.Memory; using SkiaSharp; diff --git a/Meddle/Meddle.Utils/Materials/Other.cs b/Meddle/Meddle.Utils/Materials/Other.cs index c23488b..8accc5d 100644 --- a/Meddle/Meddle.Utils/Materials/Other.cs +++ b/Meddle/Meddle.Utils/Materials/Other.cs @@ -1,6 +1,5 @@ using System.Numerics; using Meddle.Utils.Export; -using Meddle.Utils.Models; using SharpGLTF.Materials; namespace Meddle.Utils.Materials; diff --git a/Meddle/Meddle.Utils/Materials/Skin.cs b/Meddle/Meddle.Utils/Materials/Skin.cs index 196b04c..8a3fe3e 100644 --- a/Meddle/Meddle.Utils/Materials/Skin.cs +++ b/Meddle/Meddle.Utils/Materials/Skin.cs @@ -1,7 +1,6 @@ using System.Numerics; using Meddle.Utils.Export; using Meddle.Utils.Files; -using Meddle.Utils.Models; using SharpGLTF.Materials; using CustomizeParameter = Meddle.Utils.Export.CustomizeParameter; diff --git a/Meddle/Meddle.Utils/MeshBuilder.cs b/Meddle/Meddle.Utils/MeshBuilder.cs index b5cf8d9..b8fe427 100644 --- a/Meddle/Meddle.Utils/MeshBuilder.cs +++ b/Meddle/Meddle.Utils/MeshBuilder.cs @@ -7,8 +7,6 @@ using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.Materials; -using SharpGLTF.Memory; -using SharpGLTF.Schema2; using Mesh = Meddle.Utils.Export.Mesh; namespace Meddle.Utils; @@ -328,7 +326,6 @@ private static Type GetVertexMaterialType(IReadOnlyList vertex) var hasColor = vertex[0].Color != null; var hasUv = vertex[0].UV != null; - if (hasColor && hasUv) { return typeof(VertexColor1Texture2); @@ -389,100 +386,3 @@ private static Type GetVertexSkinningType(IReadOnlyList vertex, bool isS private static Vector3 ToVec3(Vector4 v) => new(v.X, v.Y, v.Z); private static (Vector2 XY, Vector2 ZW) ToVec2(Vector4 v) => (new(v.X, v.Y), new(v.Z, v.W)); } - -/*public struct VertexPositionNormalTangent2 : IVertexGeometry, IEquatable -{ - public VertexPositionNormalTangent2(in Vector3 p, in Vector3 n, in Vector4 t, in Vector4 t2) - { - this.Position = p; - this.Normal = n; - this.Tangent = t; - this.Tangent2 = t2; - } - - public static implicit operator VertexPositionNormalTangent2(in (Vector3 Pos, Vector3 Nrm, Vector4 Tgt, Vector4 Tgt2) tuple) - { - return new VertexPositionNormalTangent2(tuple.Pos, tuple.Nrm, tuple.Tgt, tuple.Tgt2); - } - - #region data - - public Vector3 Position; - public Vector3 Normal; - public Vector4 Tangent; - public Vector4 Tangent2; - - IEnumerable> IVertexReflection.GetEncodingAttributes() - { - yield return new KeyValuePair("POSITION", new AttributeFormat(DimensionType.VEC3)); - yield return new KeyValuePair("NORMAL", new AttributeFormat(DimensionType.VEC3)); - yield return new KeyValuePair("TANGENT", new AttributeFormat(DimensionType.VEC4)); - yield return new KeyValuePair("TANGENT2", new AttributeFormat(DimensionType.VEC4)); - } - - public override readonly int GetHashCode() { return Position.GetHashCode(); } - - /// - public override readonly bool Equals(object obj) { return obj is VertexPositionNormalTangent2 other && AreEqual(this, other); } - - /// - public readonly bool Equals(VertexPositionNormalTangent2 other) { return AreEqual(this, other); } - public static bool operator ==(in VertexPositionNormalTangent2 a, in VertexPositionNormalTangent2 b) { return AreEqual(a, b); } - public static bool operator !=(in VertexPositionNormalTangent2 a, in VertexPositionNormalTangent2 b) { return !AreEqual(a, b); } - public static bool AreEqual(in VertexPositionNormalTangent2 a, in VertexPositionNormalTangent2 b) - { - return a.Position == b.Position && a.Normal == b.Normal && a.Tangent == b.Tangent && a.Tangent2 == b.Tangent2; - } - - #endregion - - #region API - - void IVertexGeometry.SetPosition(in Vector3 position) { this.Position = position; } - - void IVertexGeometry.SetNormal(in Vector3 normal) { this.Normal = normal; } - - void IVertexGeometry.SetTangent(in Vector4 tangent) { this.Tangent = tangent; } - - void SetTangent2(in Vector4 tangent2) { this.Tangent2 = tangent2; } - - /// - public readonly VertexGeometryDelta Subtract(IVertexGeometry baseValue) - { - var baseVertex = (VertexPositionNormalTangent2)baseValue; - var tangentDelta = this.Tangent - baseVertex.Tangent; - - return new VertexGeometryDelta( - this.Position - baseVertex.Position, - this.Normal - baseVertex.Normal, - new Vector3(tangentDelta.X, tangentDelta.Y, tangentDelta.Z)); - } - - public void Add(in VertexGeometryDelta delta) - { - this.Position += delta.PositionDelta; - this.Normal += delta.NormalDelta; - this.Tangent += new Vector4(delta.TangentDelta, 0); - } - - public readonly Vector3 GetPosition() { return this.Position; } - public readonly bool TryGetNormal(out Vector3 normal) { normal = this.Normal; return true; } - public readonly bool TryGetTangent(out Vector4 tangent) { tangent = this.Tangent; return true; } - public readonly bool TryGetTangent2(out Vector4 tangent2) { tangent2 = this.Tangent2; return true; } - - /// - public void ApplyTransform(in Matrix4x4 xform) - { - Position = Vector3.Transform(Position, xform); - Normal = Vector3.Normalize(Vector3.TransformNormal(Normal, xform)); - - var txyz = Vector3.Normalize(Vector3.TransformNormal(new Vector3(Tangent.X, Tangent.Y, Tangent.Z), xform)); - Tangent = new Vector4(txyz, Tangent.W); - - var t2xyz = Vector3.Normalize(Vector3.TransformNormal(new Vector3(Tangent2.X, Tangent2.Y, Tangent2.Z), xform)); - Tangent2 = new Vector4(t2xyz, Tangent2.W); - } - - #endregion -} -*/ diff --git a/Meddle/Meddle.Utils/Models/SKTexture.cs b/Meddle/Meddle.Utils/SKTexture.cs similarity index 99% rename from Meddle/Meddle.Utils/Models/SKTexture.cs rename to Meddle/Meddle.Utils/SKTexture.cs index 0c6b12a..938ad6d 100644 --- a/Meddle/Meddle.Utils/Models/SKTexture.cs +++ b/Meddle/Meddle.Utils/SKTexture.cs @@ -2,7 +2,7 @@ using System.Runtime.InteropServices; using SkiaSharp; -namespace Meddle.Utils.Models; +namespace Meddle.Utils; public sealed class SKTexture { diff --git a/Meddle/Meddle.Utils/VertexPositionNormalTangent2.cs b/Meddle/Meddle.Utils/VertexPositionNormalTangent2.cs new file mode 100644 index 0000000..a57154c --- /dev/null +++ b/Meddle/Meddle.Utils/VertexPositionNormalTangent2.cs @@ -0,0 +1,250 @@ +using System.Numerics; +using SharpGLTF.Geometry.VertexTypes; +using SharpGLTF.Memory; +using SharpGLTF.Schema2; + +namespace Meddle.Utils; + +// --------- CHARACTER --------- +// VertexColor1 when VertexColorMode is MASK +// R = Specular Mask +// G = Roughness +// B = Diffuse Mask +// A = Opacity +// VertexColor1 when VertexColorMode is COLOR +// RGBA = Color +// other modes unknown + +// VertexColor2 +// R = Faux-Wind influence +// G = Faux-Wind Multplier +// BA = unknown + +// UV1 +// UV + +// UV2 +// OFF = No Decals +// COLOR = Color Decal Placement +// ALPHA = Alpha Decal Placement + +// --------- CHARACTERLEGACY --------- +// VertexColor1 when VertexColorMode is MASK +// R = Specular Mask +// G = Roughness +// B = Diffuse Mask +// A = Opacity +// VertexColor1 when VertexColorMode is COLOR +// RGBA = Color +// other modes unknown + +// UV1 +// UV + +// UV2 (FC Crests etc.) +// OFF = No Decals +// COLOR = Color Decal Placement +// ALPHA = Alpha Decal Placement + +// --------- SKIN --------- +// VertexColor1 when VertexColorMode is MASK +// R = Muscle slider influence +// G = Unused +// B = ?? +// A = Shadow casting on/off +// VertexColor1 when VertexColorMode is COLOR +// RGBA = Color +// other modes unknown + +// VertexColor2 +// R = Faux-Wind influence +// G = Faux-Wind Multplier +// BA = unknown + +// UV1 +// UV + +// UV2 (Legacy Mark) +// OFF = No Decals +// COLOR = Color Decal Placement +// ALPHA = Alpha Decal Placement + +// --------- HAIR --------- +// VertexColor1 +// RGB = unknown +// A = Shadow casting on/off + +// VertexColor2 +// R = Faux-Wind influence +// G = Faux-Wind Multplier +// BA = unknown + +// VertexColor3 +// R = Tangent Space Anisotropic Flow U +// G = Tangent Space Anisotropic Flow V +// BA = unknown + +// UV1 +// UV + +// UV2 +// Opacity mapping for miqote? + +// --------- IRIS --------- +// VertexColor1 +// R = Eye left/right selection +// G = Eye left/right selection +// BA = unknown + +// UV1 +// UV + +// --------- CHARACTERTATTOO --------- +// UV1 +// UV + +// UV2 +// OFF = No Decals +// COLOR = Color Decal Placement +// ALPHA = Alpha Decal Placement + +// --------- CHARACTEROCCLUSION --------- +// VertexColor1 +// R = Standard Tangent Space Normal Map +// G = Standard Tangent Space Normal Map +// B = unknown +// A = unused? maybe mask/color stuff + +// UV1 +// UV + +// UV2 +// OFF = No Decals +// COLOR = Color Decal Placement +// ALPHA = Alpha Decal Placement + +// --------- CHARACTERGLASS --------- +// VertexColor1 +// R = Specular Mask +// G = Roughness +// B = Diffuse Mask +// A = Opacity + +// VertexColor2 +// R = Faux-Wind influence +// G = Faux-Wind Multplier +// BA = unknown + +// UV1 +// UV + +// UV2 +// OFF = No Decals +// COLOR = Color Decal Placement +// ALPHA = Alpha Decal Placement + +// --------- BG/BGColorChange --------- +// VertexColor1 when BGVertexPaint is set and value is also set +// RGBA = Color +// VertexColor1 otherwise +// A = Map0/Map1 blend + +// UV1 +// UV + + +/*public struct VertexPositionNormalTangent2 : IVertexGeometry, IEquatable +{ + public VertexPositionNormalTangent2(in Vector3 p, in Vector3 n, in Vector4 t, in Vector4 t2) + { + this.Position = p; + this.Normal = n; + this.Tangent = t; + this.Tangent2 = t2; + } + + public static implicit operator VertexPositionNormalTangent2(in (Vector3 Pos, Vector3 Nrm, Vector4 Tgt, Vector4 Tgt2) tuple) + { + return new VertexPositionNormalTangent2(tuple.Pos, tuple.Nrm, tuple.Tgt, tuple.Tgt2); + } + + #region data + + public Vector3 Position; + public Vector3 Normal; + public Vector4 Tangent; + public Vector4 Tangent2; + + IEnumerable> IVertexReflection.GetEncodingAttributes() + { + yield return new KeyValuePair("POSITION", new AttributeFormat(DimensionType.VEC3)); + yield return new KeyValuePair("NORMAL", new AttributeFormat(DimensionType.VEC3)); + yield return new KeyValuePair("TANGENT", new AttributeFormat(DimensionType.VEC4)); + yield return new KeyValuePair("TANGENT2", new AttributeFormat(DimensionType.VEC4)); + } + + public override readonly int GetHashCode() { return Position.GetHashCode(); } + + /// + public override readonly bool Equals(object obj) { return obj is VertexPositionNormalTangent2 other && AreEqual(this, other); } + + /// + public readonly bool Equals(VertexPositionNormalTangent2 other) { return AreEqual(this, other); } + public static bool operator ==(in VertexPositionNormalTangent2 a, in VertexPositionNormalTangent2 b) { return AreEqual(a, b); } + public static bool operator !=(in VertexPositionNormalTangent2 a, in VertexPositionNormalTangent2 b) { return !AreEqual(a, b); } + public static bool AreEqual(in VertexPositionNormalTangent2 a, in VertexPositionNormalTangent2 b) + { + return a.Position == b.Position && a.Normal == b.Normal && a.Tangent == b.Tangent && a.Tangent2 == b.Tangent2; + } + + #endregion + + #region API + + void IVertexGeometry.SetPosition(in Vector3 position) { this.Position = position; } + + void IVertexGeometry.SetNormal(in Vector3 normal) { this.Normal = normal; } + + void IVertexGeometry.SetTangent(in Vector4 tangent) { this.Tangent = tangent; } + + void SetTangent2(in Vector4 tangent2) { this.Tangent2 = tangent2; } + + /// + public readonly VertexGeometryDelta Subtract(IVertexGeometry baseValue) + { + var baseVertex = (VertexPositionNormalTangent2)baseValue; + var tangentDelta = this.Tangent - baseVertex.Tangent; + + return new VertexGeometryDelta( + this.Position - baseVertex.Position, + this.Normal - baseVertex.Normal, + new Vector3(tangentDelta.X, tangentDelta.Y, tangentDelta.Z)); + } + + public void Add(in VertexGeometryDelta delta) + { + this.Position += delta.PositionDelta; + this.Normal += delta.NormalDelta; + this.Tangent += new Vector4(delta.TangentDelta, 0); + } + + public readonly Vector3 GetPosition() { return this.Position; } + public readonly bool TryGetNormal(out Vector3 normal) { normal = this.Normal; return true; } + public readonly bool TryGetTangent(out Vector4 tangent) { tangent = this.Tangent; return true; } + public readonly bool TryGetTangent2(out Vector4 tangent2) { tangent2 = this.Tangent2; return true; } + + /// + public void ApplyTransform(in Matrix4x4 xform) + { + Position = Vector3.Transform(Position, xform); + Normal = Vector3.Normalize(Vector3.TransformNormal(Normal, xform)); + + var txyz = Vector3.Normalize(Vector3.TransformNormal(new Vector3(Tangent.X, Tangent.Y, Tangent.Z), xform)); + Tangent = new Vector4(txyz, Tangent.W); + + var t2xyz = Vector3.Normalize(Vector3.TransformNormal(new Vector3(Tangent2.X, Tangent2.Y, Tangent2.Z), xform)); + Tangent2 = new Vector4(t2xyz, Tangent2.W); + } + + #endregion +}*/ From cab07275c20157ede5a836552c2638e99e715ad8 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Fri, 6 Sep 2024 23:02:16 +1000 Subject: [PATCH 19/44] Add param for swapping bg channels --- Blender/__init__.py | 32 ++++++++++++++++++++------ Meddle/Meddle.Utils/Export/Material.cs | 3 ++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/Blender/__init__.py b/Blender/__init__.py index 0a0a17b..fac2e7d 100644 --- a/Blender/__init__.py +++ b/Blender/__init__.py @@ -226,10 +226,19 @@ def execute(self, context): g_SamplerColorMap1Node.label = "BASE COLOR 1" g_SamplerColorMap1Node.image = bpy.data.images.load(self.directory + g_SamplerColorMap1 + ".png") - mat.node_tree.links.new(g_SamplerColorMap1Node.outputs['Color'], mix_color.inputs['Color2']) - # use base_color - mat.node_tree.links.new(g_SamplerColorMap0Node.outputs['Color'], mix_color.inputs['Color1']) + if "CategoryTextureType" in mat: + if mat["CategoryTextureType"] == "0x1DF2985C" or mat["CategoryTextureType"] == "SwapMapPriority": + # swap color1 and color2 + print("Swapping color1 and color2") + mat.node_tree.links.new(g_SamplerColorMap1Node.outputs['Color'], mix_color.inputs['Color1']) + mat.node_tree.links.new(g_SamplerColorMap0Node.outputs['Color'], mix_color.inputs['Color2']) + else: + mat.node_tree.links.new(g_SamplerColorMap1Node.outputs['Color'], mix_color.inputs['Color2']) + mat.node_tree.links.new(g_SamplerColorMap0Node.outputs['Color'], mix_color.inputs['Color1']) + else: + mat.node_tree.links.new(g_SamplerColorMap1Node.outputs['Color'], mix_color.inputs['Color2']) + mat.node_tree.links.new(g_SamplerColorMap0Node.outputs['Color'], mix_color.inputs['Color1']) # organize nodes g_SamplerColorMap1Node.location = (g_SamplerColorMap0Node.location.x, g_SamplerColorMap0Node.location.y - 150) @@ -264,10 +273,19 @@ def execute(self, context): gSamplerNormalMap1Node = mat.node_tree.nodes.new('ShaderNodeTexImage') gSamplerNormalMap1Node.label = "NORMAL MAP 1" gSamplerNormalMap1Node.image = bpy.data.images.load(self.directory + g_SamplerNormalMap1 + ".png") - mat.node_tree.links.new(gSamplerNormalMap1Node.outputs['Color'], mix_normal.inputs['Color2']) - # use base_normal - mat.node_tree.links.new(g_SamplerNormalMap0Node.outputs['Color'], mix_normal.inputs['Color1']) + if "CategoryTextureType" in mat: + if mat["CategoryTextureType"] == "0x1DF2985C" or mat["CategoryTextureType"] == "SwapMapPriority": + # swap color1 and color2 + print("Swapping color1 and color2") + mat.node_tree.links.new(gSamplerNormalMap1Node.outputs['Color'], mix_normal.inputs['Color1']) + mat.node_tree.links.new(g_SamplerNormalMap0Node.outputs['Color'], mix_normal.inputs['Color2']) + else: + mat.node_tree.links.new(gSamplerNormalMap1Node.outputs['Color'], mix_normal.inputs['Color2']) + mat.node_tree.links.new(g_SamplerNormalMap0Node.outputs['Color'], mix_normal.inputs['Color1']) + else: + mat.node_tree.links.new(gSamplerNormalMap1Node.outputs['Color'], mix_normal.inputs['Color2']) + mat.node_tree.links.new(g_SamplerNormalMap0Node.outputs['Color'], mix_normal.inputs['Color1']) # organize nodes gSamplerNormalMap1Node.location = (g_SamplerNormalMap0Node.location.x, g_SamplerNormalMap0Node.location.y - 150) @@ -294,4 +312,4 @@ def unregister(): bpy.utils.unregister_class(MEDDLE_OT_connect_volume) if __name__ == "__main__": - register() \ No newline at end of file + register() diff --git a/Meddle/Meddle.Utils/Export/Material.cs b/Meddle/Meddle.Utils/Export/Material.cs index c81929b..fdb32b7 100644 --- a/Meddle/Meddle.Utils/Export/Material.cs +++ b/Meddle/Meddle.Utils/Export/Material.cs @@ -48,7 +48,8 @@ public enum TextureMode : uint { Default = 0x5CC605B5, // Default mask texture Compatibility = 0x600EF9DF, // Used to enable diffuse texture - Simple = 0x22A4AABF // meh + Simple = 0x22A4AABF, // meh + SwapMapPriority = 0x1DF2985C, // seems for bg.shpk to determine the order to mix Map0 and Map1 textures } public enum SpecularMode : uint From e7c5eeae7eda943278bfb899a04f357b08ef34fd Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Fri, 6 Sep 2024 23:26:00 +1000 Subject: [PATCH 20/44] Kill blue channel in bg normals --- Blender/__init__.py | 64 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/Blender/__init__.py b/Blender/__init__.py index fac2e7d..958f084 100644 --- a/Blender/__init__.py +++ b/Blender/__init__.py @@ -296,6 +296,70 @@ def execute(self, context): continue + for mat in bpy.data.materials: + # Check if the material uses nodes + if not mat.use_nodes: + continue + + if "ShaderPackage" not in mat: + continue + + if mat["ShaderPackage"] != "bg.shpk": + continue + + # Look for the Principled BSDF node + principled_bsdf = None + for node in mat.node_tree.nodes: + if node.type == 'BSDF_PRINCIPLED': + principled_bsdf = node + break + + if not principled_bsdf: + continue + + # get the node connected to the bsdf "Normal" input + normal_map = None + for node in mat.node_tree.nodes: + if node.type == 'NORMAL_MAP': + normal_map = node + break + + if normal_map is None: + continue + + # create node to remove the blue channel + + separate_rgb = None + for node in mat.node_tree.nodes: + if node.type == 'SEPARATE_COLOR' and node.name == "Separate Normal Map": + separate_rgb = node + break + + if separate_rgb is None: + separate_rgb = mat.node_tree.nodes.new('ShaderNodeSeparateColor') + separate_rgb.location = (normal_map.location.x + 300, normal_map.location.y) + separate_rgb.name = "Separate Normal Map" + + mat.node_tree.links.new(normal_map.outputs['Normal'], separate_rgb.inputs['Color']) + + # create node to add the blue channel + + combine_rgb = None + for node in mat.node_tree.nodes: + if node.type == 'COMBINE_COLOR' and node.name == "Combine Normal Map": + combine_rgb = node + break + + if combine_rgb is None: + combine_rgb = mat.node_tree.nodes.new('ShaderNodeCombineColor') + combine_rgb.location = (separate_rgb.location.x + 300, separate_rgb.location.y) + combine_rgb.name = "Combine Normal Map" + + combine_rgb.inputs['Blue'].default_value = 1.0 + mat.node_tree.links.new(separate_rgb.outputs['Red'], combine_rgb.inputs['Red']) + mat.node_tree.links.new(separate_rgb.outputs['Green'], combine_rgb.inputs['Green']) + mat.node_tree.links.new(combine_rgb.outputs['Color'], principled_bsdf.inputs['Normal']) + return {'FINISHED'} def register(): From e1ed9b0b36b4a65f7bff2322635bd9d03402e7b2 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sat, 7 Sep 2024 23:46:23 +1000 Subject: [PATCH 21/44] Include spec and remove swap based on key --- Blender/__init__.py | 145 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 113 insertions(+), 32 deletions(-) diff --git a/Blender/__init__.py b/Blender/__init__.py index 958f084..5ecdb42 100644 --- a/Blender/__init__.py +++ b/Blender/__init__.py @@ -180,6 +180,20 @@ def execute(self, context): normal_tangent = node break + g_SamplerSpecularMap1 = None + if "g_SamplerSpecularMap1" in mat: + g_SamplerSpecularMap1 = mat["g_SamplerSpecularMap1"] + print(f"Found custom property 'g_SamplerSpecularMap1' in material '{mat.name}' with value {g_SamplerSpecularMap1}.") + else: + print(f"Material '{mat.name}' does not have the custom property 'g_SamplerSpecularMap1'.") + + g_SamplerSpecularMap0Node = None + for node in mat.node_tree.nodes: + if node.label == "METALLIC ROUGHNESS": + g_SamplerSpecularMap0Node = node + break + + # specular_map = None #if "g_SamplerSpecularMap1" in mat: # specular_map = mat["g_SamplerSpecularMap1"] @@ -195,6 +209,15 @@ def execute(self, context): if vertex_color_node is None: continue + #mix_map_1 = False + #if "CategoryTextureType" in mat: + # if mat["CategoryTextureType"] == "MixMap1" or mat["CategoryTextureType"] == "0x1DF2985C": + # mix_map_1 = True + + if "VertexPaint" in mat: + if mat["VertexPaint"] == True: + print(f"Material '{mat.name}' has VertexPaint enabled.") + try: if g_SamplerColorMap1 is not None and g_SamplerColorMap0Node is not None: mix_color = None @@ -206,10 +229,7 @@ def execute(self, context): mix_color = mat.node_tree.nodes.new('ShaderNodeMixRGB') mix_color.label = "MIX COLOR" - if "dummy_" in g_SamplerColorMap1: - mix_color.blend_type = 'MULTIPLY' - else: - mix_color.blend_type = 'MIX' + mix_color.blend_type = 'MIX' mix_color.inputs['Fac'].default_value = 1.0 mat.node_tree.links.new(mix_color.outputs['Color'], principled_bsdf.inputs['Base Color']) mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_color.inputs['Fac']) @@ -227,23 +247,17 @@ def execute(self, context): g_SamplerColorMap1Node.image = bpy.data.images.load(self.directory + g_SamplerColorMap1 + ".png") - if "CategoryTextureType" in mat: - if mat["CategoryTextureType"] == "0x1DF2985C" or mat["CategoryTextureType"] == "SwapMapPriority": - # swap color1 and color2 - print("Swapping color1 and color2") - mat.node_tree.links.new(g_SamplerColorMap1Node.outputs['Color'], mix_color.inputs['Color1']) - mat.node_tree.links.new(g_SamplerColorMap0Node.outputs['Color'], mix_color.inputs['Color2']) - else: - mat.node_tree.links.new(g_SamplerColorMap1Node.outputs['Color'], mix_color.inputs['Color2']) - mat.node_tree.links.new(g_SamplerColorMap0Node.outputs['Color'], mix_color.inputs['Color1']) - else: - mat.node_tree.links.new(g_SamplerColorMap1Node.outputs['Color'], mix_color.inputs['Color2']) - mat.node_tree.links.new(g_SamplerColorMap0Node.outputs['Color'], mix_color.inputs['Color1']) + + mat.node_tree.links.new(g_SamplerColorMap0Node.outputs['Color'], mix_color.inputs['Color1']) + mat.node_tree.links.new(g_SamplerColorMap1Node.outputs['Color'], mix_color.inputs['Color2']) # organize nodes g_SamplerColorMap1Node.location = (g_SamplerColorMap0Node.location.x, g_SamplerColorMap0Node.location.y - 150) mix_color.location = (g_SamplerColorMap0Node.location.x + 300, g_SamplerColorMap0Node.location.y) + except Exception as e: + print(f"Error: {e}") + try: if g_SamplerNormalMap1 is not None and g_SamplerNormalMap0Node is not None and normal_tangent is not None: mix_normal = None for node in mat.node_tree.nodes: @@ -254,10 +268,7 @@ def execute(self, context): mix_normal = mat.node_tree.nodes.new('ShaderNodeMixRGB') mix_normal.label = "MIX NORMAL" - if "dummy_" in g_SamplerNormalMap1: - mix_normal.blend_type = 'MULTIPLY' - else: - mix_normal.blend_type = 'MIX' + mix_normal.blend_type = 'MIX' mix_normal.inputs['Fac'].default_value = 1.0 mat.node_tree.links.new(mix_normal.outputs['Color'], normal_tangent.inputs['Color']) mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_normal.inputs['Fac']) @@ -274,23 +285,79 @@ def execute(self, context): gSamplerNormalMap1Node.label = "NORMAL MAP 1" gSamplerNormalMap1Node.image = bpy.data.images.load(self.directory + g_SamplerNormalMap1 + ".png") - if "CategoryTextureType" in mat: - if mat["CategoryTextureType"] == "0x1DF2985C" or mat["CategoryTextureType"] == "SwapMapPriority": - # swap color1 and color2 - print("Swapping color1 and color2") - mat.node_tree.links.new(gSamplerNormalMap1Node.outputs['Color'], mix_normal.inputs['Color1']) - mat.node_tree.links.new(g_SamplerNormalMap0Node.outputs['Color'], mix_normal.inputs['Color2']) - else: - mat.node_tree.links.new(gSamplerNormalMap1Node.outputs['Color'], mix_normal.inputs['Color2']) - mat.node_tree.links.new(g_SamplerNormalMap0Node.outputs['Color'], mix_normal.inputs['Color1']) - else: - mat.node_tree.links.new(gSamplerNormalMap1Node.outputs['Color'], mix_normal.inputs['Color2']) - mat.node_tree.links.new(g_SamplerNormalMap0Node.outputs['Color'], mix_normal.inputs['Color1']) + mat.node_tree.links.new(g_SamplerNormalMap0Node.outputs['Color'], mix_normal.inputs['Color1']) + mat.node_tree.links.new(gSamplerNormalMap1Node.outputs['Color'], mix_normal.inputs['Color2']) # organize nodes gSamplerNormalMap1Node.location = (g_SamplerNormalMap0Node.location.x, g_SamplerNormalMap0Node.location.y - 150) mix_normal.location = (g_SamplerNormalMap0Node.location.x + 300, g_SamplerNormalMap0Node.location.y) normal_tangent.location = (g_SamplerNormalMap0Node.location.x + 600, g_SamplerNormalMap0Node.location.y) + + if g_SamplerSpecularMap1 is not None and g_SamplerSpecularMap0Node is not None: + mix_specular = None + for node in mat.node_tree.nodes: + if node.label == "MIX SPECULAR": + mix_specular = node + break + if mix_specular is None: + mix_specular = mat.node_tree.nodes.new('ShaderNodeMixRGB') + mix_specular.label = "MIX SPECULAR" + + mix_specular.blend_type = 'MIX' + mix_specular.inputs['Fac'].default_value = 1.0 + mat.node_tree.links.new(mix_specular.outputs['Color'], principled_bsdf.inputs['Metallic']) + mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_specular.inputs['Fac']) + + # load specular texture using the selected folder + specular_map + ".png" + g_SamplerSpecularMap1Node = None + for node in mat.node_tree.nodes: + if node.label == "METALLIC ROUGHNESS 1": + g_SamplerSpecularMap1Node = node + break + + if g_SamplerSpecularMap1Node is None: + g_SamplerSpecularMap1Node = mat.node_tree.nodes.new('ShaderNodeTexImage') + g_SamplerSpecularMap1Node.label = "METALLIC ROUGHNESS 1" + g_SamplerSpecularMap1Node.image = bpy.data.images.load(self.directory + g_SamplerSpecularMap1 + ".png") + + metallic_factor_node = None + for node in mat.node_tree.nodes: + if node.label == "Metallic Factor": + metallic_factor_node = node + break + + if metallic_factor_node is None: + metallic_factor_node = mat.node_tree.nodes.new('ShaderNodeMath') + metallic_factor_node.operation = 'MULTIPLY' + metallic_factor_node.label = "Metallic Factor" + metallic_factor_node.inputs['Value'].default_value = 0.0 + + # Specular -> Mix/Multiply -> Separate Color -> [Green -> Roughness] [Blue -> Metallic Factor -> Metallic] + + separate_color = None + for node in mat.node_tree.nodes: + if node.type == 'SEPARATE_COLOR' and node.name == "Separate Metallic Roughness": + separate_color = node + break + + if separate_color is None: + separate_color = mat.node_tree.nodes.new('ShaderNodeSeparateColor') + separate_color.location = (g_SamplerSpecularMap0Node.location.x + 300, g_SamplerSpecularMap0Node.location.y) + separate_color.name = "Separate Metallic Roughness" + + mat.node_tree.links.new(g_SamplerSpecularMap0Node.outputs['Color'], mix_specular.inputs['Color1']) + mat.node_tree.links.new(g_SamplerSpecularMap1Node.outputs['Color'], mix_specular.inputs['Color2']) + + mat.node_tree.links.new(mix_specular.outputs['Color'], separate_color.inputs['Color']) + mat.node_tree.links.new(separate_color.outputs['Green'], principled_bsdf.inputs['Roughness']) + mat.node_tree.links.new(separate_color.outputs['Blue'], metallic_factor_node.inputs['Value']) + mat.node_tree.links.new(metallic_factor_node.outputs['Value'], principled_bsdf.inputs['Metallic']) + + # organize nodes + g_SamplerSpecularMap1Node.location = (g_SamplerSpecularMap0Node.location.x, g_SamplerSpecularMap0Node.location.y - 150) + mix_specular.location = (g_SamplerSpecularMap0Node.location.x + 300, g_SamplerSpecularMap0Node.location.y) + separate_color.location = (g_SamplerSpecularMap0Node.location.x + 600, g_SamplerSpecularMap0Node.location.y) + metallic_factor_node.location = (g_SamplerSpecularMap0Node.location.x + 900, g_SamplerSpecularMap0Node.location.y) except Exception as e: print(f"Error: {e}") continue @@ -360,6 +427,20 @@ def execute(self, context): mat.node_tree.links.new(separate_rgb.outputs['Green'], combine_rgb.inputs['Green']) mat.node_tree.links.new(combine_rgb.outputs['Color'], principled_bsdf.inputs['Normal']) + # get material output node + material_output = None + for node in mat.node_tree.nodes: + if node.type == 'OUTPUT_MATERIAL': + material_output = node + break + + if material_output is None: + continue + + # connect separate blue channel to material output + mat.node_tree.links.new(separate_rgb.outputs['Blue'], material_output.inputs['Displacement']) + + return {'FINISHED'} def register(): From 39cb0acd18d7bc21926a40abd8eff515c54d0801 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sat, 7 Sep 2024 23:46:42 +1000 Subject: [PATCH 22/44] Always include color if paintbuilder is used --- Meddle/Meddle.Utils/MeshBuilder.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Meddle/Meddle.Utils/MeshBuilder.cs b/Meddle/Meddle.Utils/MeshBuilder.cs index b8fe427..f272d70 100644 --- a/Meddle/Meddle.Utils/MeshBuilder.cs +++ b/Meddle/Meddle.Utils/MeshBuilder.cs @@ -233,11 +233,13 @@ private IVertexBuilder BuildVertex(Vertex vertex) Vector4 vertexColor = new Vector4(1, 1, 1, 1); if (MaterialBuilder is IVertexPaintMaterialBuilder paintBuilder) { - vertexColor = paintBuilder.VertexPaint switch + // Even if vertex paint is false, it's probably a good idea to keep the channels + // for bg.shpk, the alpha channel is used to mix between Map0 and Map1 textures + vertexColor = vertex.Color!.Value; /*paintBuilder.VertexPaint switch { true => vertex.Color!.Value, false => new Vector4(1, 1, 1, 1) - }; + };*/ } materialParamCache.Insert(0, vertexColor); From 5950c8f045611139bae35888ebb33ae29bdf4468 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sat, 7 Sep 2024 23:47:36 +1000 Subject: [PATCH 23/44] Bump SharpGLTF to 1.0.2 (fixes IOR issues) --- Meddle/Meddle.Plugin/packages.lock.json | 20 ++++++++++---------- Meddle/Meddle.Utils/Meddle.Utils.csproj | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Meddle/Meddle.Plugin/packages.lock.json b/Meddle/Meddle.Plugin/packages.lock.json index f8911e9..0178878 100644 --- a/Meddle/Meddle.Plugin/packages.lock.json +++ b/Meddle/Meddle.Plugin/packages.lock.json @@ -414,23 +414,23 @@ }, "SharpGLTF.Core": { "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "ykeV1oNHcJrEJE7s0pGAsf/nYGYY7wqF9nxCMxJUjp/WdW+UUgR1cGdbAa2lVZPkiXEwLzWenZ5wPz7yS0Gj9w==" + "resolved": "1.0.2", + "contentHash": "FhTVtpzhlL7J836t+BuGr7kMylpV1KoEFMfuSRGJ4ut/Hw7dMJYon2yvs9/jEaTCstZa0Nam4lp3CkizSz5/+w==" }, "SharpGLTF.Runtime": { "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "KsgEBKLfsEnu2IPeKaWp4Ih97+kby17IohrAB6Ev8gET18iS80nKMW/APytQWpenMmcWU06utInpANqyrwRlDg==", + "resolved": "1.0.2", + "contentHash": "IVWirVtBX1nOUwDCYwDwbNsRUzNfMTJSkszTvte9X11whr2JhrxtRnuWQIhGiY3hkrvDXW6eCQ+GumVOIAuKgg==", "dependencies": { - "SharpGLTF.Core": "1.0.1" + "SharpGLTF.Core": "1.0.2" } }, "SharpGLTF.Toolkit": { "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "LYBjHdHW5Z8R1oT1iI04si3559tWdZ3jTdHfDEu0jqhuyU8w3oJRLFUoDfVeCOI5zWXlVQPtlpjhH9XTfFFAcA==", + "resolved": "1.0.2", + "contentHash": "N71Oa5+V/ByOTiOemQG77Uvo7zlROLdqJik2LTkYcrRObe23fIB8Qoj2Cn/Xi0o4ZSCpwtEmxH+dz/Hg4/x/SQ==", "dependencies": { - "SharpGLTF.Runtime": "1.0.1" + "SharpGLTF.Runtime": "1.0.2" } }, "SkiaSharp": { @@ -512,8 +512,8 @@ "meddle.utils": { "type": "Project", "dependencies": { - "SharpGLTF.Core": "[1.0.1, )", - "SharpGLTF.Toolkit": "[1.0.1, )", + "SharpGLTF.Core": "[1.0.2, )", + "SharpGLTF.Toolkit": "[1.0.2, )", "SkiaSharp": "[2.88.8, )", "System.IO.Hashing": "[8.0.0, )" } diff --git a/Meddle/Meddle.Utils/Meddle.Utils.csproj b/Meddle/Meddle.Utils/Meddle.Utils.csproj index 3f0cf7e..871b4ee 100644 --- a/Meddle/Meddle.Utils/Meddle.Utils.csproj +++ b/Meddle/Meddle.Utils/Meddle.Utils.csproj @@ -29,8 +29,8 @@ - - + + From 3b229331e026f0b747dbe3db1381ef06c8ea41dc Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sat, 7 Sep 2024 23:48:05 +1000 Subject: [PATCH 24/44] More keys + set IOR on all materials --- .../Models/Composer/BgMaterialBuilder.cs | 23 +++++++++++-------- .../Models/Composer/GenericMaterialBuilder.cs | 1 + .../Models/Composer/IrisMaterialBuilder.cs | 1 + .../Composer/LightshaftMaterialBuilder.cs | 3 ++- .../Meddle.Plugin/UI/MaterialParameterTab.cs | 9 +++++++- Meddle/Meddle.Utils/Export/Material.cs | 11 +++++++-- 6 files changed, 35 insertions(+), 13 deletions(-) diff --git a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs index eb829b4..f023ab8 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs @@ -30,10 +30,6 @@ public class BgMaterialBuilder : MeddleMaterialBuilder, IVertexPaintMaterialBuil private readonly IBgMaterialBuilderParams bgParams; private readonly MaterialSet set; private readonly DataProvider dataProvider; - private const uint BgVertexPaintKey = 0x4F4F0636; - private const uint BgVertexPaintValue = 0xBD94649A; - private const uint DiffuseAlphaKey = 0xA9A3EE25; - private const uint DiffuseAlphaValue = 0x72AAA9AE; // if present, alpha channel on diffuse texture is used?? and should respect g_AlphaThreshold public BgMaterialBuilder(string name, IBgMaterialBuilderParams bgParams, MaterialSet set, DataProvider dataProvider) : base(name) { this.bgParams = bgParams; @@ -150,8 +146,8 @@ public override MeddleMaterialBuilder Apply() set.TryGetImageBuilder(dataProvider, TextureUsage.g_SamplerNormalMap1, out var normalMap1Texture); - var alphaType = set.GetShaderKeyOrDefault(DiffuseAlphaKey, 0); - if (alphaType == DiffuseAlphaValue) + var alphaType = set.GetShaderKeyOrDefault(ShaderCategory.CategoryDiffuseAlpha, DiffuseAlpha.Default); + if (alphaType == DiffuseAlpha.UseDiffuseAlphaAsOpacity) { var alphaThreshold = set.GetConstantOrThrow(MaterialConstant.g_AlphaThreshold); WithAlpha(AlphaMode.MASK, alphaThreshold); // TODO: which mode? @@ -164,9 +160,18 @@ public override MeddleMaterialBuilder Apply() extras.Add(("DiffuseColor", diffuseColor.AsFloatArray())); } - VertexPaint = set.ShaderKeys.Any(x => x is {Category: BgVertexPaintKey, Value: BgVertexPaintValue}); - extras.Add(("VertexPaint", VertexPaint)); - + var vertexPaintValue = set.GetShaderKeyOrDefault(ShaderCategory.CategoryBgVertexPaint, BgVertexPaint.Default); + if (vertexPaintValue == BgVertexPaint.Enable) + { + VertexPaint = true; + extras.Add(("VertexPaint", true)); + } + else + { + VertexPaint = false; + extras.Add(("VertexPaint", false)); + } + WithNormal(normalMap0Texture); WithBaseColor(colorMap0Texture); // only the green/y channel is used here for roughness diff --git a/Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs index 0851e8b..b8da2c7 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/GenericMaterialBuilder.cs @@ -39,6 +39,7 @@ public override MeddleMaterialBuilder Apply() } } + IndexOfRefraction = set.GetConstantOrDefault(MaterialConstant.g_GlassIOR, 1.0f); Extras = set.ComposeExtrasNode(); return this; } diff --git a/Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs index 28955ba..fc788a7 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/IrisMaterialBuilder.cs @@ -80,6 +80,7 @@ public override MeddleMaterialBuilder Apply() WithSpecularColor(dataProvider.CacheTexture(specularTexture, $"Computed/{set.ComputedTextureName("specular")}")); WithEmissive(dataProvider.CacheTexture(emissiveTexture, $"Computed/{set.ComputedTextureName("emissive")}")); + IndexOfRefraction = set.GetConstantOrThrow(MaterialConstant.g_GlassIOR); var alphaThreshold = set.GetConstantOrThrow(MaterialConstant.g_AlphaThreshold); if (alphaThreshold > 0) WithAlpha(AlphaMode.MASK, alphaThreshold); diff --git a/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs index 454b406..fef6050 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/LightshaftMaterialBuilder.cs @@ -19,7 +19,8 @@ public override MeddleMaterialBuilder Apply() base.Apply(); this.WithBaseColor(new Vector4(1, 1, 1, 0)); WithAlpha(AlphaMode.BLEND, 0.5f); - + + IndexOfRefraction = 1.0f; Extras = set.ComposeExtrasNode(); return this; } diff --git a/Meddle/Meddle.Plugin/UI/MaterialParameterTab.cs b/Meddle/Meddle.Plugin/UI/MaterialParameterTab.cs index b26c0e6..7fc1118 100644 --- a/Meddle/Meddle.Plugin/UI/MaterialParameterTab.cs +++ b/Meddle/Meddle.Plugin/UI/MaterialParameterTab.cs @@ -58,7 +58,14 @@ public void Draw() commonUi.DrawCharacterSelect(ref selectedCharacter); - if (selectedCharacter != null) + if (ImGui.Button("Clear Cache")) + { + shpkCache.Clear(); + mtrlConstantCache.Clear(); + materialCache.Clear(); + } + + if (selectedCharacter != null) { DrawCharacter(selectedCharacter); } diff --git a/Meddle/Meddle.Utils/Export/Material.cs b/Meddle/Meddle.Utils/Export/Material.cs index fdb32b7..a4f47ad 100644 --- a/Meddle/Meddle.Utils/Export/Material.cs +++ b/Meddle/Meddle.Utils/Export/Material.cs @@ -15,7 +15,14 @@ public enum ShaderCategory : uint CategoryTextureType = 0xB616DC5A, // DEFAULT, COMPATIBILITY, SIMPLE CategorySpecularType = 0xC8BD1DEF, // MASK, DEFAULT CategoryFlowMapType = 0x40D1481E, // STANDARD, FLOW - CategoryDiffuseAlpha = 0xA9A3EE25 + CategoryDiffuseAlpha = 0xA9A3EE25, // Alpha channel on diffuse texture is used + CategoryBgVertexPaint = 0x4F4F0636, // Enable vertex paint +} + +public enum BgVertexPaint : uint +{ + Default = 0x0, + Enable = 0xBD94649A } public enum DiffuseAlpha : uint @@ -49,7 +56,7 @@ public enum TextureMode : uint Default = 0x5CC605B5, // Default mask texture Compatibility = 0x600EF9DF, // Used to enable diffuse texture Simple = 0x22A4AABF, // meh - SwapMapPriority = 0x1DF2985C, // seems for bg.shpk to determine the order to mix Map0 and Map1 textures + MixMap1 = 0x1DF2985C, // seems to indicate the presence of dummy textures for bg.shpk map1 } public enum SpecularMode : uint From e0ccc1fcd6622ac852dc1293ec3a50c1e6d2b264 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:03:04 +1000 Subject: [PATCH 25/44] Correct some names --- .../Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs | 4 ++-- .../Models/Composer/CharacterMaterialBuilder.cs | 2 +- Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs | 2 +- Meddle/Meddle.Utils/Export/Material.cs | 10 +++++++--- Meddle/Meddle.Utils/Materials/Character.cs | 8 ++++---- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs index f023ab8..cddd5fb 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs @@ -160,8 +160,8 @@ public override MeddleMaterialBuilder Apply() extras.Add(("DiffuseColor", diffuseColor.AsFloatArray())); } - var vertexPaintValue = set.GetShaderKeyOrDefault(ShaderCategory.CategoryBgVertexPaint, BgVertexPaint.Default); - if (vertexPaintValue == BgVertexPaint.Enable) + var vertexPaintValue = set.GetShaderKeyOrDefault(ShaderCategory.CategoryBgVertexPaint, BgVertexPaint.Off); + if (vertexPaintValue == BgVertexPaint.On) { VertexPaint = true; extras.Add(("VertexPaint", true)); diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs index 0f3d62e..172f42c 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterMaterialBuilder.cs @@ -23,7 +23,7 @@ public CharacterMaterialBuilder(string name, MaterialSet set, DataProvider dataP public override MeddleMaterialBuilder Apply() { - var textureMode = set.GetShaderKeyOrDefault(ShaderCategory.CategoryTextureType, TextureMode.Default); + var textureMode = set.GetShaderKeyOrDefault(ShaderCategory.GetValuesTextureType, TextureMode.Default); var specularMode = set.GetShaderKeyOrDefault(ShaderCategory.CategorySpecularType, SpecularMode.Default); // TODO: is default actually default var flowType = set.GetShaderKeyOrDefault(ShaderCategory.CategoryFlowMapType, FlowType.Standard); diff --git a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs index 7af83a1..f95ec05 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs @@ -512,7 +512,7 @@ void AddShaderKeys() ShaderCategory.CategorySkinType => IsDefinedOrHex((SkinType)value), ShaderCategory.CategoryDiffuseAlpha => IsDefinedOrHex((DiffuseAlpha)value), ShaderCategory.CategorySpecularType => IsDefinedOrHex((SpecularMode)value), - ShaderCategory.CategoryTextureType => IsDefinedOrHex((TextureMode)value), + ShaderCategory.GetValuesTextureType => IsDefinedOrHex((TextureMode)value), ShaderCategory.CategoryFlowMapType => IsDefinedOrHex((FlowType)value), _ => $"0x{value:X8}" }; diff --git a/Meddle/Meddle.Utils/Export/Material.cs b/Meddle/Meddle.Utils/Export/Material.cs index a4f47ad..a7045a3 100644 --- a/Meddle/Meddle.Utils/Export/Material.cs +++ b/Meddle/Meddle.Utils/Export/Material.cs @@ -12,7 +12,7 @@ public enum ShaderCategory : uint { CategorySkinType = 0x380CAED0, CategoryHairType = 0x24826489, - CategoryTextureType = 0xB616DC5A, // DEFAULT, COMPATIBILITY, SIMPLE + GetValuesTextureType = 0xB616DC5A, // DEFAULT, COMPATIBILITY, SIMPLE CategorySpecularType = 0xC8BD1DEF, // MASK, DEFAULT CategoryFlowMapType = 0x40D1481E, // STANDARD, FLOW CategoryDiffuseAlpha = 0xA9A3EE25, // Alpha channel on diffuse texture is used @@ -21,8 +21,8 @@ public enum ShaderCategory : uint public enum BgVertexPaint : uint { - Default = 0x0, - Enable = 0xBD94649A + Off = 0x7C6FA05B, // Default off + On = 0xBD94649A } public enum DiffuseAlpha : uint @@ -56,7 +56,11 @@ public enum TextureMode : uint Default = 0x5CC605B5, // Default mask texture Compatibility = 0x600EF9DF, // Used to enable diffuse texture Simple = 0x22A4AABF, // meh + + // BG.shpk + Ox669A451B = 0x669A451B, MixMap1 = 0x1DF2985C, // seems to indicate the presence of dummy textures for bg.shpk map1 + Ox941820BE = 0x941820BE } public enum SpecularMode : uint diff --git a/Meddle/Meddle.Utils/Materials/Character.cs b/Meddle/Meddle.Utils/Materials/Character.cs index 1c95746..44ef236 100644 --- a/Meddle/Meddle.Utils/Materials/Character.cs +++ b/Meddle/Meddle.Utils/Materials/Character.cs @@ -11,9 +11,9 @@ public static partial class MaterialUtility public static MaterialBuilder BuildCharacter(Material material, string name) { TextureMode texMode; - if (material.ShaderKeys.Any(x => x.Category == (uint)ShaderCategory.CategoryTextureType)) + if (material.ShaderKeys.Any(x => x.Category == (uint)ShaderCategory.GetValuesTextureType)) { - var key = material.ShaderKeys.First(x => x.Category == (uint)ShaderCategory.CategoryTextureType); + var key = material.ShaderKeys.First(x => x.Category == (uint)ShaderCategory.GetValuesTextureType); texMode = (TextureMode)key.Value; } else @@ -209,9 +209,9 @@ public static MaterialBuilder BuildCharacterLegacy(Material material, string nam { return BuildCharacter(material, name); TextureMode texMode; - if (material.ShaderKeys.Any(x => x.Category == (uint)ShaderCategory.CategoryTextureType)) + if (material.ShaderKeys.Any(x => x.Category == (uint)ShaderCategory.GetValuesTextureType)) { - var key = material.ShaderKeys.First(x => x.Category == (uint)ShaderCategory.CategoryTextureType); + var key = material.ShaderKeys.First(x => x.Category == (uint)ShaderCategory.GetValuesTextureType); texMode = (TextureMode)key.Value; } else From 870af1b923ebe39570e58a8a6237ead06ea2a6c8 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:03:42 +1000 Subject: [PATCH 26/44] Tangent auto correction - tbd what actually causes this and if there is a better solution --- Meddle/Meddle.Utils/GLTFExtensions.cs | 72 +++++++++++++++++++++++++++ Meddle/Meddle.Utils/MeshBuilder.cs | 8 ++- 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 Meddle/Meddle.Utils/GLTFExtensions.cs diff --git a/Meddle/Meddle.Utils/GLTFExtensions.cs b/Meddle/Meddle.Utils/GLTFExtensions.cs new file mode 100644 index 0000000..dc3d598 --- /dev/null +++ b/Meddle/Meddle.Utils/GLTFExtensions.cs @@ -0,0 +1,72 @@ +using System.Numerics; + +namespace Meddle.Utils; + +public static class GLTFExtensions +{ + // https://github.com/vpenades/SharpGLTF/blob/2073cf3cd671f8ecca9667f9a8c7f04ed865d3ac/src/Shared/_Extensions.cs#L158 + private const float _UnitLengthThresholdVec3 = 0.00674f; + private const float _UnitLengthThresholdVec4 = 0.00769f; + + + internal static bool _IsFinite(this float value) + { + return float.IsFinite(value); + } + + internal static bool _IsFinite(this Vector2 v) + { + return v.X._IsFinite() && v.Y._IsFinite(); + } + + internal static bool _IsFinite(this Vector3 v) + { + return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite(); + } + + internal static bool _IsFinite(this in Vector4 v) + { + return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite() && v.W._IsFinite(); + } + + internal static Boolean IsNormalized(this Vector3 normal) + { + if (!normal._IsFinite()) return false; + + return Math.Abs(normal.Length() - 1) <= _UnitLengthThresholdVec3; + } + + internal static void ValidateNormal(this Vector3 normal, string msg) + { + if (!normal._IsFinite()) throw new NotFiniteNumberException($"{msg} is invalid."); + + if (!normal.IsNormalized()) throw new ArithmeticException($"{msg} is not unit length."); + } + + internal static void ValidateTangent(this Vector4 tangent, string msg) + { + if (tangent.W != 1 && tangent.W != -1) throw new ArithmeticException(msg); + + new Vector3(tangent.X, tangent.Y, tangent.Z).ValidateNormal(msg); + } + + internal static Vector3 SanitizeNormal(this Vector3 normal) + { + if (normal == Vector3.Zero) return Vector3.UnitX; + return normal.IsNormalized() ? normal : Vector3.Normalize(normal); + } + + internal static bool IsValidTangent(this Vector4 tangent) + { + if (tangent.W != 1 && tangent.W != -1) return false; + + return new Vector3(tangent.X, tangent.Y, tangent.Z).IsNormalized(); + } + + internal static Vector4 SanitizeTangent(this Vector4 tangent) + { + var n = new Vector3(tangent.X, tangent.Y, tangent.Z).SanitizeNormal(); + var s = float.IsNaN(tangent.W) ? 1 : tangent.W; + return new Vector4(n, s > 0 ? 1 : -1); + } +} diff --git a/Meddle/Meddle.Utils/MeshBuilder.cs b/Meddle/Meddle.Utils/MeshBuilder.cs index f272d70..cdcd338 100644 --- a/Meddle/Meddle.Utils/MeshBuilder.cs +++ b/Meddle/Meddle.Utils/MeshBuilder.cs @@ -217,7 +217,13 @@ private IVertexBuilder BuildVertex(Vertex vertex) // ReSharper disable CompareOfFloatsByEqualityOperator if (GeometryT == typeof(VertexPositionNormalTangent)) { - geometryParamCache.Add(vertex.Tangent1!.Value with { W = vertex.Tangent1.Value.W == 1 ? 1 : -1 }); + var tangent = vertex.Tangent1!.Value with {W = vertex.Tangent1.Value.W == 1 ? 1 : -1}; + if (!tangent.IsValidTangent()) + { + tangent = tangent.SanitizeTangent(); + } + + geometryParamCache.Add(tangent); } // if (GeometryT == typeof(VertexPositionNormalTangent2)) // { From 9e3833f9851ac23219122e59b8ffb354277b7f44 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:51:31 +1000 Subject: [PATCH 27/44] Found light colour influence :D --- .../Models/Composer/InstanceComposer.cs | 7 ++++++- .../Meddle.Plugin/Models/Layout/ParsedInstance.cs | 5 ++++- .../Meddle.Plugin/Models/Structs/LightInstance.cs | 7 +++++++ Meddle/Meddle.Plugin/Services/LayoutService.cs | 6 +++++- Meddle/Meddle.Plugin/UI/Layout/Instance.cs | 5 +++++ Meddle/Meddle.Plugin/UI/Layout/Overlay.cs | 13 +++++++++++-- 6 files changed, 38 insertions(+), 5 deletions(-) diff --git a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs index 4e4e150..8ae103b 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs @@ -141,7 +141,12 @@ public void Compose(SceneBuilder scene) { // TODO: Probably can fill some parts here given more info root.Name = $"{lightInstance.Type}_{lightInstance.Id}"; - var lightBuilder = new LightBuilder.Point(); + var lightBuilder = new LightBuilder.Point + { + // honestly just guesswork + Color = new Vector3(lightInstance.Color.X, lightInstance.Color.Y, lightInstance.Color.Z), + Intensity = lightInstance.Color.W, + }; scene.AddLight(lightBuilder, root); wasAdded = true; } diff --git a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs index 4c394c4..b298f24 100644 --- a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs +++ b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs @@ -132,8 +132,11 @@ public interface IStainableInstance public class ParsedLightInstance : ParsedInstance { - public ParsedLightInstance(nint id, Transform transform) : base(id, ParsedInstanceType.Light, transform) + public Vector4 Color { get; set; } + + public ParsedLightInstance(nint id, Transform transform, Vector4 color) : base(id, ParsedInstanceType.Light, transform) { + Color = color; } } diff --git a/Meddle/Meddle.Plugin/Models/Structs/LightInstance.cs b/Meddle/Meddle.Plugin/Models/Structs/LightInstance.cs index bbba8b9..95359b3 100644 --- a/Meddle/Meddle.Plugin/Models/Structs/LightInstance.cs +++ b/Meddle/Meddle.Plugin/Models/Structs/LightInstance.cs @@ -2,6 +2,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.LayoutEngine; using FFXIVClientStructs.FFXIV.Client.LayoutEngine.Layer; +using FFXIVClientStructs.FFXIV.Common.Math; namespace Meddle.Plugin.Models.Structs; @@ -25,5 +26,11 @@ public unsafe struct Light public unsafe struct LightItem { [FieldOffset(0x20)] public Transform* Transform; + + /// + /// Note, channels define RGBA but values can be greater than 1, indicating a higher intensity? + /// + [FieldOffset(0x28)] public Vector4 Color; + [FieldOffset(0x8C)] public float UnkFloat; } diff --git a/Meddle/Meddle.Plugin/Services/LayoutService.cs b/Meddle/Meddle.Plugin/Services/LayoutService.cs index c7b14e9..adf3156 100644 --- a/Meddle/Meddle.Plugin/Services/LayoutService.cs +++ b/Meddle/Meddle.Plugin/Services/LayoutService.cs @@ -73,6 +73,7 @@ public unsafe void ResolveInstance(ParsedInstance instance) if (objects.Any(o => o.Id == instance.Id)) { var gameObject = (GameObject*)instance.Id; + var characterInfo = HandleDrawObject(gameObject->DrawObject); characterInstance.CharacterInfo = characterInfo; } @@ -238,8 +239,11 @@ private unsafe ParsedInstanceSet[] Parse(LayoutManager* activeLayout, ParseCtx c var light = lightPtr.Value; if (light->Id.Type != InstanceType.Light) return null; + + var typedInstance = (LightLayoutInstance*)light; + var color = typedInstance->LightPtr->LightItem->Color; - return new ParsedLightInstance((nint)light, new Transform(*light->GetTransformImpl())); + return new ParsedLightInstance((nint)light, new Transform(*light->GetTransformImpl()), color); } private unsafe ParsedInstance? ParseSharedGroup(Pointer sharedGroupPtr, ParseCtx ctx) diff --git a/Meddle/Meddle.Plugin/UI/Layout/Instance.cs b/Meddle/Meddle.Plugin/UI/Layout/Instance.cs index f162a8e..2846372 100644 --- a/Meddle/Meddle.Plugin/UI/Layout/Instance.cs +++ b/Meddle/Meddle.Plugin/UI/Layout/Instance.cs @@ -81,6 +81,11 @@ private void DrawInstance(ParsedInstance instance, Stack stack, UiUtil.Text($"Game Path: {pathedInstance.Path.GamePath}", pathedInstance.Path.GamePath); } + if (instance is ParsedLightInstance light) + { + ImGui.ColorButton("Color", light.Color); + } + if (instance is ParsedHousingInstance ho) { ImGui.Text($"Kind: {ho.Kind}"); diff --git a/Meddle/Meddle.Plugin/UI/Layout/Overlay.cs b/Meddle/Meddle.Plugin/UI/Layout/Overlay.cs index c0e2e49..97eb06f 100644 --- a/Meddle/Meddle.Plugin/UI/Layout/Overlay.cs +++ b/Meddle/Meddle.Plugin/UI/Layout/Overlay.cs @@ -113,9 +113,18 @@ private void DrawTooltip(ParsedInstance instance) } } - if (instance is ParsedBgPartsInstance bgInstance) + if (instance is ParsedLightInstance lightInstance) { - ImGui.Text($"Path: {bgInstance.Path}"); + ImGui.ColorButton("Color", lightInstance.Color); + } + + if (instance is IPathInstance pathInstance) + { + ImGui.Text($"Path: {pathInstance.Path.FullPath}"); + if (pathInstance.Path.GamePath != pathInstance.Path.FullPath) + { + ImGui.Text($"Game Path: {pathInstance.Path.GamePath}"); + } } if (instance is ParsedCharacterInstance characterInstance) From 321675bda7af59249e044cc0f9a36288c1e10068 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:36:14 +1000 Subject: [PATCH 28/44] Lighting init --- AttachStuff.rcnet | Bin 18186 -> 18980 bytes .../Models/Composer/InstanceComposer.cs | 142 ++++++++++++++++-- .../Models/Layout/ParsedInstance.cs | 6 +- .../Models/Structs/LightInstance.cs | 91 +++++++++-- .../Meddle.Plugin/Services/LayoutService.cs | 7 +- Meddle/Meddle.Plugin/UI/Layout/Config.cs | 14 +- Meddle/Meddle.Plugin/UI/Layout/Instance.cs | 2 +- Meddle/Meddle.Plugin/UI/Layout/Overlay.cs | 98 ++++++++---- 8 files changed, 301 insertions(+), 59 deletions(-) diff --git a/AttachStuff.rcnet b/AttachStuff.rcnet index fe592b162d3d2d73c127194d99ed5b83e741b273..2948c3eff222b04f88d69d156db8830dc21454a3 100644 GIT binary patch literal 18980 zcmeIa2V7Izwk{4xQ4rWFf>Lc1r7F_9C5_U{ZpGi0HAIn;D;%gZ&y}%w@hEne9Dog`*ks9xL0XqWKP! z=v1@1t`Ir&OjX48<9Q{zyIOM@%fpy2YFG57q#j=q zh?l`1jmXjOPRwrxuH8|r&%?U})_5H{fO0N5cvBF#{pb`6hI#CrW5;^bGmiavd}Xx{ zDs}Q1|LKa07wiZp(}!OfPE=mUFLTxS9XC!>+yeS8?N4*??~k$7%c#unpJ> zda%B{aR+=dZ+pD=$!eZ0dpdo5xnkmQ-m^wn3ciO$bDrc2x5UcJK6=NVPlV22BJmEC^Zp`u>Bj*5gSFB#IM5y+}vK#UwP=Ovzs<179pQk@^wn=8M)Y*zNP6A7r7-e3>%S0Z2DC}E9p70m?O=3 zsJD<5RVeHYx_d<5|G39QT%KG<=3IU{@o--d##(k)FyrXD2JT{R0C@`Hx`q;UZWg(Y z*13Cm#BoDG2(gE+@E%;1$blqiW+W% zJ^=MIk^C;tI(dg95}60Xj!)qC#-@M;Oj2&JR#1t$w)fFT^P^6FzN;@2P5jW3GTzv2 z%kL|!6|_;b-GfI{ooFK6uyS-r{_O9doRz_EK*|X@A6(8#o3iUQQ`mOST8dS4WwYUV zGzBK%H_+=z?3^Q9973`wjNZAS67pOVYK?lvKO$s%<4ME}C>QMSzAw-I zl_HmuL!O$(=;mYheL3&<6^yuAp9#;MWtTQ|_oIR8OMTSQuoHmW`pf2(!L`$QxeiLe zY{Cqwa%iOn_4p$p8>-<9UY-B4!kG~e{LfWL?C_an{1I727^Mx>)$jE82(6Jd$PKF0 z_?HlNv&waOIW8c#!LrET2x}l&1ru zkA`T_>)>o^G%nBtqnqClB}%#aI`cQ8GelYwQWHf|v%>ZNh!AisYUqZN2#!#88*=}F zvRa(kfEA~Fr^zj#P}Fb@N@70ruTTm|hxq?|h50SHD)yl05>zt}i>PMV(#2?D?7PI` ziIu)ep?z%&OQneY;0q9&v-c^QYMQ~HQ7BX#azE42s#-X>rrualqPQNk!C)@vN z>cz0pPX*WzeRqa$`878HD6&!v-)mh@xx#9yKcIwiO&EvxsWA!UyDFha*>bpni~w7W z`iFmb*Z*B#fP9FbFsc*pb_E?-%Hkem1YFmsznE+C3p3dqz2Q%=nGiS`!#7^|4R9lw zGZo)!11Gu35UT$h6j*5T(HjTe`Kwj=BLg`)b(|4{#2ZzRikn+kH#+LJE^_hd*yu79Ku4&7?7(qJ9E z*m>?>MAq8%WGLH%l9xN;S?rDN#m0J3&Hancj_Vt4y_HBO>8UoAaU>&+C$|VQ762BV zEShwJ8&u6Y!CabW!Hch&QJvJ`OmCTJMCktW&mX!>e7PnB+{mpb4mo1_yU?T_GI`)2 z8Bru5X*cSMUdXE{@yA!WuaLVAwyGPI1ZqWx(m0+~5} z>iZ`Ds*L=4J!iFT^mxJ*!1sruOLk7(!!w@>8t?0c_BcNrWmf!+sOUln9%S_BhDLfk z9JNsly{|;wdPnG5?2zd*tv{)ZyiuaBgD(#_nfEZjT=qJaJYd?c6?#*J8oXuW6ss7TEXg>z z&_BZwO)ld75Z;m5sLFaq{h7M{J9gur>6C&n9&RMbF8AMXYj;J)2hWDUM^E7u*YTPA zc*P(7e*xP<4p+M)nA3Y~?66nJY*L40+R@^vcR zPZD?kA0}Qslf9T2bnOB0eG8Ho?WEt{UwD1<%1`nfH~xP@D3M?oUf+xrzgE_C7VKSV zsXGhyexm2N!ShU3|5Fe|fY{88q^XJYW!Q!DR9AjUoNH>jc%Dk|22VF(tA87XaJyc7 z4=EWYIW!?(2)zCtZz+44K>R<-|MbCs(|M3c2z>v^vrO(Zf$Y$Pej)HVp@M%G1xZNj z{ClifFr#b)9b5>OrM;NRBnNF2%$8Qm?8rrRTzxVjX&=Y>Pg}}ob_%4b=lmE}D zv#9akqb2_x%_wQbAMIl->!n!n2ZJz~6X8@3iNKT7LXk9n7!o=6&B#c`u2#6@tF_ri z8=Fjky4xTtl%I2q%f4PW<N{3((;A&j2%r2rc!PvTP#JtP5Dss33 z;OOIYtw^1gZ^6_MSI6VyA}3Kuz6N2eXtw)KQ}D;i%BUHG4bqH%#X5GDh@mh;(pvDg zKCWnKVRhNfpIb~x$73@FwbzQvCLgdm?Za)3)N2VaLQUx3eOlxy>zdtJL)%qiN)(&+ zJF~yPrroU*ZcU&dvsOFpMY*C~YM9(p5*o2z-=$i4IvyX*cbGU4FO@uvycCvPNM++e z|9zFmy}LJTMEKLQ@dS~p{PquN)OKc3PWAgv`=g?E#YSn^)AMHV z0%Z*U_w7KW`cU}bTJIVMN*mszeVPfFjizo$<-`$#_@g_s&!dBK;o(Oq73pc zeZR-&UO3mHSG+@AwiL%5Zj0O2qj^|FWdmgRUcr}PYa}~Z9rV?mSapRY2}$-}BMdd_ z|8#9|T3+{OD=m)R*_FFVj^2y8utZpGc-NHw@u;2|KjW@iIx13iefhv=87BE%nm@ze zSoXL!XwRE?ZJT5qO;*uIWs}bEe~-u?L#q@Hv{;2bLN}{NSJ;WJSR+2w+)B9XPs#%aRB=|}@khUURF_+8J~rPG+xv6#dG zJHIJOhl}H>Q*S3^z#MDksc~LbUw7oMUU4haDdP9<1g;3`PImnXPpSx$uq>(pBUg3q#skmf=kW=Usj1lxH6(0Z4 zfkQqP!&cen`UQ>kOfFi>naXPsGsRGjzSn7Zl|>?a=PKl8UJ$hsnf=py{~R^U7!&`@ zG@@-SdOIk(Z=~#sKEygUNGkabG28vK(oJ__d5jikma$|ouTg())jK_KN=jmb@pM(k z_F|)*$XHApQcaN^M1kERzAC`SZ)#}V0Iv;l_9mX+B57SDd)GyEEr9{z`!NWWNxI6a z3u>&&h$lWbOny;^iC`tJQM@j|;n47@g4le3{GHO@Y$^5)PGx!YYJ7R6U70qq-@IV4h{Tkym7iuy;PpqZ`SaLkcQ|#J zF&tl!2K&S+rVe%3R4z@D$h;j+7UU@;qjY#g!{IJW6jk2U;Zy+uMqW2if-<7(vw%GY z@*Vc2h&`JDhyB3GS|QY(ZTD#eO+czUa_)r?Hn!=7yPC;t*>kO=crJZm?i)?)0m}|$ z?OfWMT!UJU77mILvGFo)f>eH?MX{NgtA1^Fj0Ddc<9Y|*){=8w0yn~F$x$_XSrW|5 z7W1=iZiz)OiqwEtawn5R@%d&rm8GxQ$?H)5#N_xA#y-O`r;>8#QU;T6h@$Z0kzP1T z+`fbd^eEq_Ii9?s&Moreyq>d1oMEQPXthq2rti|bsnzN}MTMGbs5{C}GCj@9Gdvv; zWFqDt5&lTG;QCOAit`{yF(EZ_Y8Hb3G|Q^I8^P)#i0(0}pSZO9@Kfb@&v09$@lkbI zUHJ}HR)Z6+>(xe{-CtBfO={--^5B?d0^;uYoRcT8<&)U1Ud?R5$oexxmQ-Ce%?Nuk zW=-r8QQARPEs0a^l`+B$wGK-4Qg zP>6DhAtdP%A`IH_Ly4#rM}F@mq6m=e)rcRx3XHNjCr7Tm6oIb z?S6`V(L2Vp>uIUe$F5(m#+DU`&cuIpY<$JHNViVN`!Q+7srwM7mhGP|hEnJGL2ky; z3TJIKzS5B%ecwsJq;+U2pTFfE{qvTD@;$a8ezu_!qF;euKxrXV=@#3w%ZH{h?G&+( zr9a)-TDkLv5)^zyw&ZzYcbFPuYmTm3dAX_FyaHukrtX6omoL2Nz4t=K*)1VOc-)Q) z71KO*p2;rI)y*B_7JFsgdoC5*(L9-!jU%Npa2gH7myK<;DD|y#kIKkbxT|-CNd3fF zY>qLgO^+Fw!#*$y2N=RNzpiY&RmmULXo;~Cax2YZ98J#WfO<8~MY|Lv{!nJEZ?v3o z6c(YKP&zJi-Z=0w6&~4N(vfG<5vm}bD7+5o0kZHsG2?~wB%P@n0AbT)p=Ro>@t{Z@ zYOeZplGCo?Ib*E~vWdq)h-%hJBXRzK6jm_z*f(hg9rqr)i zTq7nS%z&qV=L~VF=q43PynvQJga!z|u&I zsmCJ}-2L8!37<1cA}XZ1Y^htJRj z$l7W1aMYNniw}2SICi$j$d^vR^@=}!uxWW99I;wqm6WB5B8qiW2TaO&dI!x>CE?VlF)Zr}I6 zZbDq=jl}W>H@K-H@@R_&&T#clB*i; z91I0JcQn)F3z_kN`Yu@^pb;BRHTz|QQexLjn2X6_uAv>)>U(A3Mm_OHE~B3d>#%9{ z?Gd+GBv{gmy!0?Hz^#`KR>z91Ks;irEMj}=DbRVS1^dHTk1eOeq3P5bSSV@gG+9U4 zc8#_xOybkaWLS~V*aVDe-po%r`hZ4;*dtzY6~btqvj7QKN7QVuxVepirdcfuj#p^+ zVZD>8mji&(hI^2LqRF_TN~uXlruyxH)l^AIr7Zzzqp{Tlbu#VEAFd3dpK@%%NT6#UN)S}%B-+57)m8}t?z>A!slc)Rc+t$ms1 z=&$Jf{#L+BtFpO3&Yces>l$D;p>Y#!BZy61Cov2PVQ3j!zB_Ba;&KY%^{p)Eq3FV{ zS>?Ffj8y8y*tpP%q3mnR@5?AF+^nX&teY8~!0h47KSx{7sh}tKMO^9`CCFLukZZ|m zOS8LYA@fsmhvI#a*ytDF9?7>=Lfc%kfLmW)8oYVk={w?AN)NkyNjLdax3Hz?@#6~w zg?&=_@yDQ}xS%85sn-i<*E5OI(xs;lqAwHN-WYGvnC{!}Em((dCNi8&qAY|f-jIe0JzQMgo3s}JXROmZi-EH{`E)$0YWfXP zV2&E3)TI9uXxc$|iFQIUp={1>6J2&{%e^^xkTr|o2+tgj*xTUgYPEKa-4RtV**AA` zVF-<_xkk74OTd#d;wC%y5zXj=SyWFh)RJi|;POSpZ9z|NPQD|Lbe#yJIOe^z07pD# z>%jSkTP&np0!8U4T)afju0>(Q`K(}eqN|&vXS-*jCMQC(>=u(Et#(eSkM{%EOnmi^ ze0yNz3m`oQ_sgH^@M&y3-O&}YI}%Q74RoSiIx{@l!R61$dLLZqEnM{g?LX@102{#Ic zpOG-gZwX(|RW=ayha$`NdOLa=6=I6jO;W>qvCVtU{`e+`fGMKRHUk&5C_k)F^W@OO zNO)fySK-Mp7P&HLDgv|(m;!67J+0Q-cIm3V6$NZN=5KCG-9{o+ec8u7IB3fr1diUl zUZaygZeW+FS&JBBo?@Fe+{b#XWtmGb7z-ZNau$44slK)4(uED$NzTKMOHHWwqE>Qh zl4COqjOW4FDL_3(UBEJ=;Il%fN5i`DWuevDofK5;(pa;%D+F-a;Dy5;sTf zI7toGbW~h)3dt(RFEsva%28t)YXVNDJ#M(JxOFCpTrec>wHbz%w}6CObIDIX*3Y$3kJOVeQEO!QTqM8N-Y z&|jzw&V4~ctE#jAkv2)Rvp}A~DMBPwMf{_KT2>uQ+T;!5U8+B3|C1jE zZ7+!5M0fOnoR6-$q)pb+=a~G53h1f?6t*nuwI6D215*P0IXL$-)jkzkiVjIduV`50%O!BhyZI0``YHP zJDhJ!$FWcJ+0@p}ES1}I8~NwGbV+OPX_@QTUVT_clRX6~%LOb9AH+CPSTQi`v5eSi z7Ku+DGy#QPg%G~L$fj*w$7{x3`ua`xO30$_nQqTPthVRKaE;r*E^(i51X5mo&7J1T z8_%Jxh3@Mjv`dFGHz_Yt$=%jD&-nS#_w!dn6moxh_$Vyn(mm2hjhi+vDGRQDR%WGI z#4PV-R9OPUJ^L7Xnv3M?2Th1vN>n3_bM=Rj(<(H_FY4p))4t;BmA#(55|L1)6DI*$ zmwn7E3u zXv1yznP)IITqog_o}mdikDUqsjcB?4m@tOYgi0LTgom<){GSD2qLDxGh(0=cy&j!D zW1wK@H}$5Wa_-V7K$2wQt)i*H=6QQ=)?kO0+mIqJ(#osRew0v3cp7$HZ8S{Q3wg~bA{f7;0Ux=mYhao&yLBY{a&iV1gx86%E z*o|yk(Jmj5uubt0&cq?m!AbIcqw4(&>}Fh*K=GR+6So@(Ax*%R$sgGz+$$2U))V_0JMY$=MP=)ORC8~kqRbg4 zZ7;R;BbteiSi{flNZC)Nj?6g)1(=Vu-9l8q$(dLlQ!evBN~#XMsJ2QtuWVSXw5NN;PPHM#xln96n}cX3mWBd6gK}u~;(2 zaXeD&G!Fmft5rUantAdJxJfp zq%9!x{SkDEk~eSErxn-UtXg`-?%l>I;{YLRt&Vd*zmkC3ee(li-X70kU)ggow|D8@ zvb4y(@*3Rn9k;UZxZ!S@00(14-;aAz8SK+-6X zqlF`&we$yY7hjM!?}qc@9&!sfGY@odELte#O6&4VVfw7%61+Gi5+d%M-*iTC1_Jb{ z{0w;y;J^)M=*&EZzvhmfe0Dvp|D?)Ni=vLWRp7kVl0i2h0`cuR^dqfUw3IkNS| zAAKI#tGqMpBF#M2!_z|=iZ${bV4*=OG%w@&RXv@0_y#_Ps$vkg6c^=A67?2Zm#V8} z#=XYv*NlI-TmNv6mWLhPv+ID}^KGE4({+Xpc<(=h@~38h*D>08zW!3P!gVVnwg~ON zzF~`Nr|OeQ%|6`TS&Sol&z>n8|BmGO14@eyuD>Yx?J;*Xv*(zw^VIL^#>2>KE#X9= zBz4<+|4@vE4&7m#&Ty|1w~0U*W$VeCGZCT41qdC%3xeylB|EAp;@ z$k&F+Pb9A9l-<8j@?U@p&pPv)e+u1~mHA7f=GzRK?Ef zqQbX!BM<+R$v{yQ+>enul|d<41hlEL{d9?*M5e|lA>cDNtQP6_d0V9|BT+Bzcd|S> z)7w0U0L$BtdmMGAK;g%LC$MYQB!G+y`nnH^^$N*4ZzibfTrGdY9__g)wdudOH1U>t z*Pg#S;})&lp_}c}&1?3l%q*5@36A9?xebhG8eqZwZfvXZvGldbaefoL*7Rw475{we zBFHzPd3mCbNo_Mp^#f6rv+m2bk>2{`j}shLEaBFiit`iU6It$Gid(0)9q?`vUPrZ* z4?U@a!aj2mp=pRkJ&C4iZu3p0jm#OB-=fh-|C?+g=kFtCIJiaQkv>8~@rcHw7McXktRUr7VuNRB2+Z>b^m(1l>B`gs&`ec=_5_8G$K#Z6}Qr6TBG2- zuheD>%nb7ncCA`$#Ymkz04FX7-~F{pL=Zm?c8;*422vQIE~^&y5<2c=STnLmlx5E& zSsnJEO7z-360LV}bk(B5&0|1n5{W<}J2gQgf}33)U({ndQe=dcpUzntgU7NkNHwq0 z%PeUgF-5y;MZI1*P!EA{D!^tAVmf=odQNNJ5-}mf=Q-%V$^NDH=-FzVk}KiJh{$wRiB&vgot3cijD z>Z9FrH{f>f-h@Hz_6!XS3tMFY`h{x6RT~W$sgDt$VT~m+%2DPzL@A5=dRYKred}F= zVC4`k;uA2p276_Q375SVwlPI>6ghKLyI+=_6E)!vD5{zIP%eC7gX^glb)#-6{r=SZ zhw%MZS&ayhJJeXj&Dm?|hmv8*={Z6PE%Y?anuw~EIbhsSX2tO6>Cq8#3Kg{t@&4$6 zOEv~+_pFiLp)~4LhJ)J)gDqgG`X(CMU{D1VS%~R6`Ce}V(@Q{gKbMg5tF2nj<6h}A zu~Pcp#WQKArj?TnGUL^VqxRxvBIMs@itL~2E&*IGm#^+QY<_&vUr6aM++blu!m)B$ z;CaH367niyb7-^UYWH-iIDeCO(UaQ{?Vf#I7h35<3dHfwX6dqhDq%VDwBc(*scr|3 zY(7d=B_k)71(%2f+f>gwLP#)YNqMV7>nEjnq_;m$VGH%h-Pd+F;o+a3^ND4Ibzla*e|hL9 zIw2&WmTqT0d-%kQeXtc(G)F6##EEi&()?zV?Mm1BL}xp`v?*tPUH z;vA^l*EAiq(xu9TdP}}X$mwEPS)?b&AKFTN+wU`;U#Nmk9H(@;UkN8IMlZ$-*&2HD zkNM2d)@J!KIqY&q-teD1IRVcG?1l0Bj!r&!W=zE-fC9$vHUUS2mO1`v*d{26BTeMp zwe-TKOHc^0U`ZJDCvRSwkPl8>0^Yp#4c#EKb5>`+{W)SOZ9j`x40#4r;rbS5@iiZP zBH=Nsh=?akglx{-ix1ws8uh9t=*XsIg1{J-9k51*gSw z_I7=faOY3wfVCy3Bj&-EKd2?vG9SEt(nTWU;oIt}uID4tA@7o!nkCg{_;5+Rk+XNU zEGL5z19_(=R4uvowI>n>sC=;Y?3MM{kw+jFGDvRA+tbAh>H+GZYX+&^r)(Wi#jQQi z&|NSyA@2r^R?~GG9j4z?rjZy$B(mB{O{tHgmZlV%i%WjY4Ri?al`_?!p8?b}-64?+mMwMTA3*l|6fnFW43 zp-{oCv}ik<5kp3fwY~Cr!qeOFE=ToR4iZd;>fnZw3(98D3yW>Q2yv!o3aG|i&EvVj zXEWxA^kHe_@sncX1jp0v1&n97ogDxm{$WrpGP744hX8A(Dvu`B_)rWsX89(tc$jq6 z0UB+S`1kjFWGVHiKbts_!NOUhpf|vI1Af-LUQkUct?{Q;d+}b9e$u`J3~e}o$Bq+A z{mUW?&askcpgw6~d#lKeoJXD3;$7)AE`6ijRTI~~jbV|qmV=4K4}Gk|=L?;G^z7#A zGirY}->`iRx;P)PWAD>w4B&jD%(*jAdJ z_^aP-pmz9n-H%N>w9JpnV~N9bkCNinv8>fEIwedr6k{@)+Y>j=Pl+tnv~_g`Z$}K* zB_1zr*_RCZUYjrjiA%E?8b32h-tSXsZ~W4R{pxSxgRR>jt&cle6g+A1XaKHFb&*9#A*K?!iX{z01o!gfvv^cH76;!%3jE(#O#g8Oy zSV`{ml^TqTE29fi6Oh(+>OmvMHyPIJV2M#84kmgjyW+|QRjfyzD#&XeczGH5GLkME4nAm`^9UZwdu z?=$Us$y|B3+hg=eHpW4gN4n6*#Cg_?cS1hPtMp)gZd>t5el=98{@}RoaoxAb^4v*S z%PXI*K{E4u=V6|*aD+A$*hHU-i7^E@v)=^#G$iKky%GU3lb0uq4~$#D(Ip#WstM*E zkl9jHExi@C{VxS!D8ZsVaW-%e=bFe#;ebJQZZYB@|FDwH=}E(tG5%^bze$~BFdLhU zv}&xxRYO%z1&YvPYf-QnKRbI00Let_$S+Hv!q1@;|! zdOJf3UVPuBZ&;!;jdM^FP(ODvBIP~dE~h@z_g23xvN_qq`rsw&!Zdqn=1Hl!(flSq z4}jB5i*U2Q0VI4qGYfD@b}KH2+IuCaD)4_id{MbH;WKnx$ENA+I2u^RN(0Hm?Q1{h zq!Bi;_jsNt9w)d`sME2|boGb(VbzX4!lDDNyZY+DHz%#Z@8>@;C(YgD#>)%YH7j=KE127RGh)f0|*b)*Wh->s0uMSn({;h z^9g4LGPc{PdM+8-J1zzsM&VE`9Fez}4EimVl_L{}{p?*ujuMv0*saBDO0BK?Q7Gds zDIf32TKx~F7P#%2M#_R;~g=w0n2OhFbFzpW;My2#zO2gkR#}qVEP%Qo$Ev;G8BcZ%_mvf- zsB5g1)MEU+iu|VONaw3c=*XNxf z_4gTVq~PjE(g}kdQB}V9E^4IIFG{lK6KL)lHP-y3v* zX_{ZpHDz3iUl$k(OXAY&Obf1#6^GSkc6MihGjLOby^YP@F{#Fw-tw_)7SXPd)NF4z zK3HDN2(VHfxS4S3b$JB=Wnd8=QROIU;q8SRiX_(3!$jv@P!?Oj6oa9~>W&2{Tm+P` z!Vx<{VttW0{6+~`?=?}HU>7kOIb*7n`%cUuXXEA11cssQX8T#z*!ld{S=RWMl%6V0 zqiNo}V_4(qXO$ED&C3gI1tyJD%7y-l12eoY_v3JTS&iP5G34mG zPca)zgb6_5oR?H$OHjLZtDD?HcO%8mv2?=NH1G@B*M2hr>;9!%RynQgjv1ZKelD4HtXA>?*#hFIT~yvi(-+D{sk z(mtx~ootNtWl}Za<1_Y|dx+b>I$NWRcxyrf@4o4W$S@bRQM|rW=~3x%+KJy9QF8Hm zsq@ZL-Q#q8OiC1pF96_!$k=0NHhz>Kr%b|YiZb2p6JW+RHsD&{zQlElH7$k3gtyu5 zcDy#25$t}rp588NPLo_o>#fdXcHE~ECd)NbCnHx;<3ej8HxDd{pNzF^wxbqco1316 zUN@@AS3zV=^B9*_^t|kc@O;ue2_>!NVYtPTM&T@5+sh1Oty%DlKNI{&h_lXBTtl7c zG*Iw5f0aP1q=}dVzc>iGCg++7G`(qYEdhDt@Z)eJLPi^G9|9=#r( zV1sS9+u_}zi;HzB6vwB1V-xA82=UG-d=H*PLxq@xj_8ly>_m9E(=Y!1<2_FrDrDqm RW@5rem>@?9?}8#C`d@6jz=r?; literal 18186 zcmeHv2Ut_vwk|E8R13vTw}FBbrFT#P5u_73p-At9-iwIR1?g3a^j-o4h=2hp0i=cA zJA@z|V&KJn_BnT-xA%T`pL5T9@4oMyFh0ik=by}PWv;nq#+YNQrK(6sM2&}scL@(u z&!ii#!+UDtYQpVlZ->g5a9Qdc%SVgl%(bCHa3_Vh*-<-qt6yxNuNCcUpr*IilxI z&%gF1DT|hMJLO$`KR-UMz@9km_?#c3PiyA(*I?S1^Oni&V@bPnW$>d-NzA}`mej=t z78-z!0L+zRzq_NBAao$@&uVy)R~W0~H~q(l-zz z<&Q*4+4iSzXJrj!e%Z(t=-w&CcCQa~nLu!Zk=36P1DknK0)`C3#6mV^PP%4}P=ct) zfcO3|+k#;SOw8QO@|D4yya6OV%vqrXfzhJh<71B#%JvsjU_B^~`F{8BrlR!M3*cT*?QVXU-_MH5+dusyX6JK4{h1 zQ7=gJF|DQ)S#cI%bRc-sQs%HG`%G6A`=HRDma#=wRe`H8k5*HijH3vUFcO?AL=rvA zSO}sV;UTe!CKTujN?nphIA^a4tAZH|MJPuYf7N_h8u5#KdZ>Z7sr{eIrA_UBNSH<{ zL<>P~s<6#$|4|uiambk#$zciQruH8aM4Q_GkdSX`|3gBssr?TL>8AFdm!6HJJz=am za{;Jl=|Isd-T!{fFwctfS-!Y3?0wh&iX_=gA~;kGI?I>Fs%QJ(SA|g*f=EYr0Kd~* zoa%q--3nZfuY1N-s=7rd!j?~VXv)^*T9n12onHeRTbHAnDf>3LHLRH_xsu$v*AW`b zyWin4yB}YXrEgNJ-sFcMYK1+ zX8~ruu!}@qLQ;71Tbo)8RkXLS)5uatE;Gvr&u^q6{B{DoP8wSrbf!gkSVFw1O)*kI zUkDOVh1uZF{T~rQ>0AJpdg?C|>a3QEzXrX5$|{7S5)|tP{Mp09e*m=7GhxZk9>Jpr zQr_mdakiFc*#V-~XG^Fm2I?^}VE{gzMzOZb@A2(F5Z8v%57OF5>lgl)fr2!a;GgS| zVDd4bHk8zgjAoFQBb!STcQk?Zs<;FCmk^q@q1@%T2}69S7;=^`gB{BDr5z?7 zn9^<0*#?~f=rsZSg``%hG=te3*)m$vA;ngzFx>w@Vl8}%VuIh&|1e>rv%`FLya}Pr zmpbR4s;JH24-=D@fNW(rl*4h0{QRbX#7PNg1A{J9K{=go0`>N1FV55})B*0F)r{pC zf2$}D-UiROQI8wS2mODdQfvVhb??+v{fs%ic9JQ(JeSVeCFU3K#y-^&h2Vfr)ixLI26g z`s361dki~nM*V(=E9#tp4mtGsLZ7!0mSdw-oINZI1=XL)Vh}vbDt)U+$>2Abu^zJX zl*N_(EL-d=$;?~Hb>#yYj~uF|>;IOwH z*L1$YeuHA;s&kSHVu^btwac(aAjXFpN2(cUOy@RyY>;<;;T*G=y2uO{fJ=nihT}-; z<9p?Q>?H9Qg6Kzhu8e>PWv~MOp*Tr4d2R@1b}3WE*zUB8`E4>MvwAvAZ#p@QpIBS_ zPMDZAj(!V9w2lnKlnhOPY+ZB1cE&tWht?b_0rp4zPNKqFEv%tw6! z#+j5vb7e=vKd)Mr#f;`aLYf}lP)Q8|TDE??PaFQjz2AI!vBCmV2^lW<23N43w#rO# z(#kno&%vM~`?fnbr+5N3E(L9&1)Q)g#N{yLr^93VhI1oAt$vrlqj z#E}a0ylmE=lJoG*;{GJ~_+07HF&shtnt`*mfu=^=!e~14cY>89KW9GUFGGPh6$xKI zptw=Z|1SbhTK$6mM4$66@%T>?iOF^8o!)f?Mu-y|EI*vT6@yRbMc(km?2{}rA@|;m zAJm`d>tA%_|K)Me;C`uFOQ$V)EOpEQ0~lz)OR z$+<5&-xmAU@GiqM-Ty5voccutMyligluz*JIpF1c(l7UDV%0cUb2X*qivFvF;`P`h zHddY&(nhI)&HVUY)&zS)M9cY?zx!5b{HtmBb9^c!n@j8{-Zm!4i{p%(j7!dQAbZ(R24*h$(dKXC9l{)YsD$DDvyK+;QP)K~QGv(~Er zAyF*N3C8*QT5n+AZE%|v(%X4~;|lmy@dT#qfX~XLZ!)N_h}~y(SO5Q*AWsRr@d*Fn zO9Im`M4w|Xzqxt+$`dx$*Gkg=j}Y;=OAU9#TD&hCaaSS-Tex2pZk=F#pd_u699YSV zZ(>5w-cIx?^ztRL>sM&meofH63e~+rYayeP3@|bIZvzAhLGakhicx}75bbh+ZtR7* z%y|;v-{9Y4@V{vuOyI7de{PnoNS`MWo`NWs14Lpk{@VcMkTbv@jPc8b3+qL1(03k^ ze}jLI!T-j2@MELspPFSAc*_9*)(cL&QxN`_3sUKGZM;1gLF@%h;P?Og6vZapf4gJ) z*9}e2HyAF*w<*-M#1e3J6gKzq=R8~1?*wxfCre%Er$Pv|8i9?-O#~#I4>oECk*mM%Rv$sN|;65iX0{(mSPW+m1H* zCXzhSaTtcNXQ&0adlyit#EC}^00n(idbbxG*1YzPcMLU~N2F>zG=?Qxy(JtS4i3z* zYKMA+VDH9}Sy`bOe++uEVzCIBg7vmL;ek|%&TPJ*ipe?LSjcCL?gb@zd2m~W31l;k z1-k1HPMT2sA$UO}woQN#-E=$4=ynPu1L~yLq+BgMAM0QKzG3IbDe^*dZ7&}@#c-VD z9lP`LVwxquFRA@&;c4~!TiwW8wiE@9StS$2F#l_<+T5??d<&23EPb6-omS`3vl0;f zshL7l3;B_t`8LY+7*Zj{{2jn*Oknd21VhL7kcNX^I(pAb2oejTq+p;t?1GlbMD85TbAFLJG+?gbJh?b(1YiPYl&a<(X`3{#ldjiGT235 z&__4?7h43WV*vg#GAlj0!CLNYYeVVbrojIa5kY)N_NP1C8dQhZW?m#}Tj^JvBx*-` zr+M}ISRL-9u?H0&=cu8#bsg=_RM?SjCc7V((i*8VoC)>|R8q*y3 ze~eJ?mjoFn^b2t=i?aB8=V>8)F18dYt?^O21d|JZ+72?SFuF>n`ao1={&TTyM)V;@ z>&T;_$KH8=pkmU!b-VGL;kfi9r3NkId+%%37UsrtDap=zxtDHy-n^N1pd*kcGdPNk25*$?=A5%=!y?gWs6a zC6)~}n)FYeEhQtvLi$1adM{J4DCpPG<&3w@-N1ZOj3`1A0~ZXa0B%1xH|aVo##knB zyVLa>@b9^dh?dw(mI*2Igjr5~i=KYrPMLTxKpXiUxD=65GnqZHQ?`EItvnwr84V^G%P{p4LBpoN# z{3Mu7P>)YjNl~b2O_&PX(-^;0JNZN|oG*^gyGn2}p&L8))a&fApypBF#Mb(Xyo~V^ zb$o_6((zc$Pp|U`1nP~559`S$!RwLy=g%Yp@braq)tu=o#)U=r0$W;#yTMydb!m3pW@eDGxVk4TzS4lm zUa}iWbiCX=#s)^88kz&0z3`Xz2s&2)+st>3kMH3_<+W0s`isolr0W2LqqN)1=50G_ zw2=z>ylnPu$+ZBpp})3~v+MJw+x3RVlMz>*4xnO|0%MLh;|O)gR+%3p`&1h5v;{Yk z{2kEeK2~+oXWTx)ruE)yRkjz}?)en9R zwufFnNNAQ6%uGA(Saw|*OW8;5`zsF2(Q?Zt98RRB))`2;=dXd~jb$aak#C<;p@__U+N5^}OJ zLQ`yVsPS>Q9e&(Xpi}^_>EtUYZJnEKn2zCDnjP!F+WJh}F$!oW3fxR+F(xB428qh| zko5>dN&CO@*cld&7fOJiIvySDR|!n=*Oz-R^L#8gpZRLZGp|iKw%qS*oiQMFOa360t6AP6-I!utDcvLn8Yy7?K3YSc)u6MSM~0lqDv(d49u z`JwEd!ULOD2IMrw9r?k6+*I@v7vBpaUT&El=NRgzeWnUi86A-8$2`_~m;tWmO=zYtIFH2BEKsxp#Fvk70 zrW)?FBnCs|jN^m($%|OHp7E37MHw_MmrPMaP0Ni+8j7zRwE~Ia1$;uWGM39q0Pc^#CFpd?CQ4a7O!#PwU~wK z?U+u@#}JX}S55?A$L&tP=dB(8w4HTxvN+7Vjs6Xt_QOLiBb9C&XYG`t$DI9!_xQ9t z;eb(&LQ|PK(k2~MitQI#@AR6D_vANR*qVZ;90y555E+PEiy^yFN){F>)au!Jy3LJ< z;@4HiX#!;Rx+TH+U&;XWx-~zbQ4g(MjYVTvbCvOJEwUbTLEwc;T|*M}(kH=ply=uQ zeo@j`mu!C#Rap2G?a#Y5@aRCEF@nQh+>RZ2=9wvO2zqp}&v7ke>30O@?^NNlzoigX zuWkc5T73(qcOTQXp<%mIhVUg_;}NqX+CL@kTVvqOa_nhbBUMIZ z9DiTlS_6)6F$+?uP@;y<&U@?#F$*((;6Xp|9ZELD&V2m-%L%Y1JSo|hk2m!4?(_N^ zD&dwdEq$U$3RIRN;wKxvit{ZLKRx^^3-PV^Hhr5&GOzb1A+qws-%-SsU`6imuU8%|npxBKM4yDW$>3s;Do&xrz z>NF-UpW5Ssx6B#Op2U?E>GV zMT+IYgxLAv&l^K&E!&kAsd*|%bciGhl`)mGV+)V9aYuyVR9k^pd%h5K#H`G?W_U_& zNpN}_#9!~Q)a}+q*58f<&?$sF2nu890&4>B|Fmiwl9@}J?AVD4SjnFqnbSq;*F!|T zlEL-F_j|5W_5|gV3Svx650=z?FQ%c_{l=PgCyqMO8p9M$iU^nD(wGu$+jT}Vb7wco z498;XeR!ZV!^7WtzS?+-9rK=UcN@9a^iEWh2|~F`Dtzd8_7)YUUUaQ-o+mV=GdRD( z_o?ljpeGab(N+%Da74Z2(k+@&;PbMQI6Pi0NAb#Zd6-mhpX~I_qA1u75mTM1+bj)H z-*{V-<$aM-`0k0*+ukCb%54@@S>7IW$c@5L+gWB0Ak`Slj$=z>MBcgenbQP6eqqiq@nFyY;*w2_9b)Bo&9_Ghh%qB(YMidj*&EdM*RRPzohkevZz7yI&4C|8%UYr`Mo}3r z2R3qB*08HH7WV!%gsvQ%+;joln_2bOq@q-8&uv}!^;+yG2gEYIoldh#M1MtMZ!k{7 z=jXYJWyO9i-0W6>q&+|M`l*el2cL~yquv_p<*@Ur4CwpoQLQuVE2i3jG(Vp~u?CwA zamp44bw~6I$fgj3HaPxTQCR==k1M+7DE@6Mn(OkXhxcoqJSu8 zT4HW8ZD_a4%~xpHen{x)JK8rpgh)!Ipj;(D^dlPK9?kiC8vg zijsAtbfUivD0ry;XVla5;M7cOQ#+NA>qd8E|p0Ma6YP|cvn#g#HnKr#j*OI!v3SLtP~ z3ggFB9lBr&N%e*O>c*;%`nwSea&hGMFb*a0o#pmLpuz-m@N0(KtIhwzDyX(GV<&z>B)>@&TCXsCd8u&eC|}mH$LlozAdA+sUenVE~Nil z;%ay)pQX>z>{t5>?sq0j+Yh5eh3G24$oj`in!1hVOPbmPbnbU4Vt~yKmO4;a(^WJO zVPAiwdiU#!kpphIn{Js+H^}fzn(dwJb-tKqV=8AJkOvS70*GA-J6sqyE@F1>8g;ZYBxb6+eFNzj=d;vdzl*l3c_SUOmR( zjrc1{S+9Pf*1NIfNxJ2@jJHI7okWoQ4G z{a<`=S-jCGV}IGAP8eGZ&YUi#EdaUo$ES@h9+VhyPw7?vjFVDvc#BWOxlu3Oe^dly z1B@81XzJR~&Zr6TcI&519VIx7s0p#a4btLvHg4j=gvG3YxMlF~C=N>!E|=u(-?+R> zyk5OwvD{HjftIqLYHrQzz9UshkW*gTYSQ#6eNaquO5?s~-!UDGDqa*tlxT%AWa*c* z=dwb8p%DVdo?1TSJkt*zm?Vr6WN{epFM7jQ;bdnamUW442b4ZhVVRSTEO)P=i<7z= zg=R(4*FtbLbUIA@k4I&0YyPfkNxxb=U#~i;R6!!9U+?UC?Ar*gb)&AukV)?(6n!P5 zDVhH6(`Er;=C8guRiW!1gYU>f>jmGkP_D`B_zN%sXG6sIl2%Oa(lM0iZ$Y}IW+%@* zM__)<$vZ3v76+5Y)0IvZS&5rFnNwvbhmj*AVxP1}NA69SDZa(Vc&r7>%uA;gC}p`X zvMm8E+OTIxqTRZs1#AUR`HztyTLZ?@R_TF@G-3`)kF0i2+Yb+>g{JGOZ4GTz{#^Co`yu`b0Bz38#un$5+Sn+}rs#BG0s= z#x$T+L+%+gk`-`6E;=CYZ$ui^)G z8!0F%Vq|l zXk|!r$Xlrm`@X9wn!DX$b=wwD^O?8$6EOy1NmS789)v;wcxAzK*uL*uMX-uyUUgaq zq%e`@Gu^1BDq}*5uI83FXigp}hI!$LD*Ob2uwyRUW~1OG_qz)1?jffFhl?BSF(($e42 zW5X`&HPh(L@-?AJhU_TZL+lltFNM#?&Da2aze%k1xXIE7a|}K5HGpzZ-5!`JspQ=U zI#(DhAVg}kd2BPVz42#8+p^k_jHsE8-mopgmlY@SpLwj@_+A-_w^7+3Ds=5f6-LkeEpXIYCqrF4|dD3`ue>#&gheykov??0!M8Jd#)0| zZ29!09+0fru(zy?W}5cYEG5S5w(E0l*J+HL&RvxReH>M?Jm6v*1SuhO)FzoNr))M1 zF)vu&Bk5ovLB8|=1FoghJaYoFw&?~DS^gc+ikmm64Xk-*BxTQHAa#&>o|q@c{kj91 zQk3E}*kY^P)X_F+CEOy_x>&jq-La2Hn1Z7NIYxxPe%ronrIR|DLBrTVDX8%wVD(o( z>yZ%9Nq?DYQIeAM>*=>k+f}C#_fRmVR>AUj1n;mqG>$wjI)(np`_xB}%| z<2+(;AGWGnsV%GWCfrAB+BR&eq**bN51(_8sPuVipyC+8N^CD%aooh`;3=_-)DubY zzJ2HGCJ3E{>btaT&jx<*b@F=Um>4lSsNlw=%n5yM7I4|^etf6@z9eH*Di3I`$nj!g zx_`aF)1g8jz17cXe{aohkLX#K`Bi3+*tzKa`WJnolmjX;T@WgW!jh(86n1Uue8TC&uhY|1k6SYgxOpl$m@miVW44Nfy{-lL0W47eqbf`qLFID<{gz&f@kj(drOQjDD zxDr!J>*3gyj^Ki_RUQYI(h6!Mj_$4bG~J?8ty zY$C*b-4Bx$qT7^Q47lSBShIvOkS!ZEmS+mJE-iwYlArbUv5jF`4-%?|$b@{XeH_kT z9GWTH{Jb^kr;R}X(UpA6%JOCX-r&IA18CFGIvtrSCM3>gGV1#ixY<-9VO%{xX z6vRVoc*^2=*2&*9W!q3i0P0-T^N&}3Yhstw1Rq4sCSHGQGv{qr9gl{3WcYgw{-jY)knk#$;A94fE9wQnRLAqiLqu{bquLH-(dNBM5;K0lDClez zMDX&rd(YG<+q9}_k7r{?8jfG*b+!oI5!h+Dxo{(ELKx?%70?vGX(eCh+L$5Xb| zcFOf;q9N|?2cF%!rt98?-lQH|Sn;cLB#n@8X zdkQOh!S71}&##>jzYBJ}a{u>~b)(Q5T&!|40T8Cis(+`fnk_{i=TMVvYA4hU;lA{b z{ls5x`P)n){CO|Y4u$=(mnbb|M0)m3eJ~~|aes-1oZ+~B?6AQ^*?V@Q*LC`jkF&)@9#rH&$r1y?lq0p$)&VMpGFcM zY&F(Zl6+Kd_)9~djR{rX59WV1NZOwL_ANnYaQLkn-0(VAJ=mbl!S0BB zlOYEfc<84nXlVL*h33=_c{#h#yhE_Mj)U}R)mTnHalD#`lizgncm8W9I%vzfm_z?} zB|`8BDZa+DmJg|u2?_4Y0vcOI`|bg-wD$$q1XufK7hHKaX5FHjDrBJv;N;a_tlPub zb}gB8y7V@bNtzkg8*a^1bSVJ#$VP^gp}WuMT8#X_n#$^4-rPE)ltCBIpEoHf5(L#nrhy5{~8I$j%kR_(pN}+Gtpf}5R zZ-DYFhZT=%y%jWtpI!Lty%-(dybE{#pzFT%Jh`>BXEYpnsFD?zf!so~i9Wo@xoJ zVi5ee6d|+aa@GXCXnw``pb6!WdK}udL*}uoUj&-%0F`=OZI8$=tw0qw6Ti-bAfH<<)j-C15E8vG8jk6 z;CwC(&<$<@cgVfN-60P2eq$YnxEgI%e1nKJH~6s&q;i{RUt75=nkMX8 zZ~hbEHq@@IwU_ocs^p64zD>|80{u|cUBQ?&$rlM+G89HSHU(nI(Iu$0Tb=KVlTl6) z&$aXeb((cY-JKUKxTc=m38+3j1kHhMP1M)4R?2wFLnY{|^~Q5=mV|OeThvv;zn0Qk?0m5?S&jb1 zcru0az-ke=CE`cw3piW>D3s$VdW1~E&|e@Bb+f0k$hR>OHp8g+_^twLmxA_^jh%HT zr#UR|b%Wh$1n3<`ksR6XCAfPQ_lg^w@MFiS;TCavXpON1oBHub>IIqUka#}ct2 z)zm^3WzgOF-TkN1VE(pZL=PNeuTe!cRM7+U9OlwUOrFEAro^j3W{Nz?{nhFgZ=T3% z?Ob|!D2jj%CHV)rJB&r24ZG6Y``A{*V0)8RU#=~>m0O3EyXux;-gY{J zd*lFwlwtfMlZRZcg-4)LavvHT%D$Zb)K9ya<5DlqCD2p=My7rKw#cE@(D>xStb?QV z9(SphNj_HvkS5mDfvdd5wmzQb z^Q3eyzVEF15&Jzj-dW`uIQyc~=z9Yxvl{J)Bo+t1MX+BmQU`EChZ(hbg}IqIxW$fZ zKR>IVA5no!`Xj|i-_uFVWFfuHAgPiF!>S^=it+a~&E|E&peK5a@b9Kv{Ybnc)c&Vd zKjv}B03qhc%Xm(bYI|xe$PXi4>6k_G_vBs75lwfwkV6(KlV#>EPgL2)N}Ul*0kme3 zExFW^lwcUWiwb(>NOi~8hjn3!@bVn$nvSaM^J~Xkp<=%jwW2lbryte@b5{=4*sQnSY|;ieIozz>3yd zhwTa`hn4qgP2bc|SIU=dvuq`^=~A1Fv$bK*3N4rZ%Z8ofQb5X}5^%T4?LEGVYs~ z)b*k8q3dUeru0aZPI};Uxs^ry(Z~IcCu#yq0-)N5JGFCU+F5-m6(=tmifQ^9b7+L0 zsjU#-2ecnndbxV*KqFNcV8=#3@Q9sHwf7x+~T`!f^HK>KZzYBkSAvh zleYew$@;^w>bm*mhhyb@srrZC#fXb!3qg!wzoN9>J*jNKHR7Hr5&rf4&w&;b?#3g9 zXyUYGtKTSYbSx@C-%$L#q5dQ@!() + { + { "LightType", light.LightType.ToString() }, + { "Range", light.Range }, + { "FalloffType", light.FalloffType }, + { "Falloff", light.Falloff }, + { "ShadowFar", light.ShadowFar }, + { "ShadowNear", light.ShadowNear }, + { "CharaShadowRange", light.CharaShadowRange }, + { "LightAngle", light.LightAngle }, + { "FalloffAngle", light.FalloffAngle }, + { "AreaAngle", light.AreaAngle }, + { "ColorHDR", light.Color._vec3 }, + { "ColorRGB", light.Color.Rgb }, + { "Intensity", light.Color.Intensity }, + { "BoundsMin", light.Bounds.Min }, + { "BoundsMax", light.Bounds.Max }, + { "Flags", light.Flags }, + }; + + foreach (var lightFlag in Enum.GetValues()) + { + extras.Add(lightFlag.ToString(), light.Flags.HasFlag(lightFlag)); + } + + // doesn't appear to set extras on the light itself + lightBuilder.Extras = JsonNode.Parse(JsonSerializer.Serialize(extras, MaterialSet.JsonOptions)); + + root.Extras = JsonNode.Parse(JsonSerializer.Serialize(extras, MaterialSet.JsonOptions)); + scene.AddLight(lightBuilder, root); + wasAdded = true; + } } if (parsedInstance is ParsedTerrainInstance terrainInstance) @@ -176,12 +257,53 @@ public void Compose(SceneBuilder scene) if (wasAdded) { - root.SetLocalTransform(parsedInstance.Transform.AffineTransform, true); + root.SetLocalTransform(transform.AffineTransform, true); return root; } return null; } + + private (float outer, float inner) FixSpotLightAngles(float outerConeAngle, float innerConeAngle) + { + // inner must be less than or equal to outer + // outer (due to blender bug and sharpgltf removing if the value is equal to the default) needs to be greater than inner and not equal to pi / 4 + // TODO: https://github.com/KhronosGroup/glTF-Blender-IO/issues/2349 + if (innerConeAngle > outerConeAngle) + { + throw new Exception("Inner cone angle must be less than or equal to outer cone angle"); + } + + if (MathF.Abs(outerConeAngle - (MathF.PI / 4f)) < 0.0001f) + { + outerConeAngle = (MathF.PI / 4f) - 0.0001f; + } + + if (innerConeAngle > outerConeAngle) + { + innerConeAngle = outerConeAngle; + } + + return (outerConeAngle, innerConeAngle); + } + + + // directional lights use illuminance in lux (lm/ m2) + private float LuxIntensity(float intensity) + { + return intensity; + } + + // Point and spot lights use luminous intensity in candela (lm/ sr) + private float CandelaIntensity(float intensity) + { + return intensity * 100f; + } + + private float DegreesToRadians(float degrees) + { + return degrees * (MathF.PI / 180f); + } private void ComposeTerrainInstance(ParsedTerrainInstance terrainInstance, SceneBuilder scene, NodeBuilder root) { diff --git a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs index b298f24..3b51a26 100644 --- a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs +++ b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs @@ -132,11 +132,11 @@ public interface IStainableInstance public class ParsedLightInstance : ParsedInstance { - public Vector4 Color { get; set; } + public RenderLight Light { get; } - public ParsedLightInstance(nint id, Transform transform, Vector4 color) : base(id, ParsedInstanceType.Light, transform) + public unsafe ParsedLightInstance(nint id, Transform transform, RenderLight* light) : base(id, ParsedInstanceType.Light, transform) { - Color = color; + Light = *light; } } diff --git a/Meddle/Meddle.Plugin/Models/Structs/LightInstance.cs b/Meddle/Meddle.Plugin/Models/Structs/LightInstance.cs index 95359b3..1c27069 100644 --- a/Meddle/Meddle.Plugin/Models/Structs/LightInstance.cs +++ b/Meddle/Meddle.Plugin/Models/Structs/LightInstance.cs @@ -1,8 +1,9 @@ -using System.Runtime.InteropServices; +using System.Numerics; +using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.LayoutEngine; using FFXIVClientStructs.FFXIV.Client.LayoutEngine.Layer; -using FFXIVClientStructs.FFXIV.Common.Math; +using SharpGLTF.Scenes; namespace Meddle.Plugin.Models.Structs; @@ -18,19 +19,85 @@ public unsafe struct LightLayoutInstance public unsafe struct Light { [FieldOffset(0x00)] public DrawObject DrawObject; - [FieldOffset(0x90)] public LightItem* LightItem; + [FieldOffset(0x90)] public RenderLight* LightItem; } -// at least 0x90, now sure if this is correct -[StructLayout(LayoutKind.Explicit, Size = 0x90)] -public unsafe struct LightItem +[StructLayout(LayoutKind.Explicit, Size = sizeof(float) * 10)] +public struct LightBounds { - [FieldOffset(0x20)] public Transform* Transform; + // hard limits, relative to the light's position + [FieldOffset(0x00)] public float _pad0; + [FieldOffset(0x04)] public float _pad1; + [FieldOffset(0x08)] public float West; + [FieldOffset(0x0C)] public float Down; + [FieldOffset(0x10)] public float North; + [FieldOffset(0x14)] public float _pad2; + [FieldOffset(0x18)] public float East; + [FieldOffset(0x1C)] public float Up; + [FieldOffset(0x20)] public float South; + [FieldOffset(0x24)] public float _pad3; + + public Vector3 Min => new Vector3(West, Down, North); + public Vector3 Max => new Vector3(East, Up, South); +} + +// https://github.com/ktisis-tools/Ktisis/blob/57391bf9eeb432b296d6ea22956df7868a37d069/Ktisis/Structs/Lights/RenderLight.cs +[Flags] +public enum LightFlags : uint { + Reflection = 0x01, + Dynamic = 0x02, + CharaShadow = 0x04, + ObjectShadow = 0x08 +} + +public enum LightType : uint { + Directional = 1, + PointLight = 2, + SpotLight = 3, + AreaLight = 4, + CapsuleLight = 5 +} + +public enum FalloffType : uint { + Linear = 0, + Quadratic = 1, + Cubic = 2 +} + +[StructLayout(LayoutKind.Explicit, Size = 0xA0)] +public struct RenderLight { + [FieldOffset(0x18)] public LightFlags Flags; + [FieldOffset(0x1C)] public LightType LightType; + [FieldOffset(0x20)] public unsafe Transform* Transform; + [FieldOffset(0x28)] public ColorHdr Color; + [FieldOffset(0x38)] public LightBounds Bounds; + [FieldOffset(0x60)] public float ShadowNear; + [FieldOffset(0x64)] public float ShadowFar; + [FieldOffset(0x68)] public FalloffType FalloffType; + [FieldOffset(0x70)] public Vector2 AreaAngle; + //[FieldOffset(0x78)] public float _unk0; + [FieldOffset(0x80)] public float Falloff; + [FieldOffset(0x84)] public float LightAngle; // 0-90deg + [FieldOffset(0x88)] public float FalloffAngle; // 0-90deg + [FieldOffset(0x8C)] public float Range; + [FieldOffset(0x90)] public float CharaShadowRange; +} + +[StructLayout(LayoutKind.Explicit, Size = sizeof(float) * 4)] +public struct ColorHdr { + [FieldOffset(0x00)] public Vector3 _vec3; + + [FieldOffset(0x00)] public float Red; + [FieldOffset(0x04)] public float Green; + [FieldOffset(0x08)] public float Blue; + [FieldOffset(0x0C)] public float Intensity; + + public Vector3 Rgb => HdrToRgb(_vec3); - /// - /// Note, channels define RGBA but values can be greater than 1, indicating a higher intensity? - /// - [FieldOffset(0x28)] public Vector4 Color; + public float HdrIntensity => Intensity * _vec3.Length(); - [FieldOffset(0x8C)] public float UnkFloat; + private static Vector3 HdrToRgb(Vector3 hdr) { + var len = hdr.Length(); + return hdr / (1.0f + len); + } } diff --git a/Meddle/Meddle.Plugin/Services/LayoutService.cs b/Meddle/Meddle.Plugin/Services/LayoutService.cs index adf3156..d1bcc7b 100644 --- a/Meddle/Meddle.Plugin/Services/LayoutService.cs +++ b/Meddle/Meddle.Plugin/Services/LayoutService.cs @@ -2,6 +2,7 @@ using System.Runtime.InteropServices; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.LayoutEngine; @@ -20,6 +21,7 @@ using Meddle.Utils.Files.SqPack; using Meddle.Utils.Files.Structs.Material; using Microsoft.Extensions.Logging; +using CustomizeData = Meddle.Utils.Export.CustomizeData; using CustomizeParameter = Meddle.Plugin.Models.Structs.CustomizeParameter; using HousingFurniture = FFXIVClientStructs.FFXIV.Client.Game.HousingFurniture; using Model = FFXIVClientStructs.FFXIV.Client.Graphics.Render.Model; @@ -241,9 +243,10 @@ private unsafe ParsedInstanceSet[] Parse(LayoutManager* activeLayout, ParseCtx c return null; var typedInstance = (LightLayoutInstance*)light; - var color = typedInstance->LightPtr->LightItem->Color; + if (typedInstance->LightPtr == null || typedInstance->LightPtr->LightItem == null) + return null; - return new ParsedLightInstance((nint)light, new Transform(*light->GetTransformImpl()), color); + return new ParsedLightInstance((nint)light, new Transform(*light->GetTransformImpl()), typedInstance->LightPtr->LightItem); } private unsafe ParsedInstance? ParseSharedGroup(Pointer sharedGroupPtr, ParseCtx ctx) diff --git a/Meddle/Meddle.Plugin/UI/Layout/Config.cs b/Meddle/Meddle.Plugin/UI/Layout/Config.cs index bcab66f..93a1c6d 100644 --- a/Meddle/Meddle.Plugin/UI/Layout/Config.cs +++ b/Meddle/Meddle.Plugin/UI/Layout/Config.cs @@ -16,11 +16,18 @@ public enum ExportType // ReSharper restore InconsistentNaming } - private const ParsedInstanceType DefaultDrawTypes = ParsedInstanceType.Character | ParsedInstanceType.Housing | ParsedInstanceType.Terrain | ParsedInstanceType.BgPart | ParsedInstanceType.SharedGroup; + private const ParsedInstanceType DefaultDrawTypes = ParsedInstanceType.Character | + ParsedInstanceType.Housing | + ParsedInstanceType.Terrain | + ParsedInstanceType.BgPart | + ParsedInstanceType.Light | + ParsedInstanceType.SharedGroup; private const ExportType DefaultExportType = ExportType.GLTF; private ParsedInstanceType drawTypes = DefaultDrawTypes; private ExportType exportType = DefaultExportType; private bool drawOverlay = true; + private bool drawChildren; + private bool traceToParent = true; private bool orderByDistance = true; private bool traceToHovered = true; private bool hideOffscreenCharacters = true; @@ -44,6 +51,11 @@ private void DrawOptions() } ImGui.Checkbox("Draw Overlay", ref drawOverlay); + ImGui.Checkbox("Draw Children", ref drawChildren); + if (drawChildren) + { + ImGui.Checkbox("Trace to Parent", ref traceToParent); + } ImGui.Checkbox("Order by Distance", ref orderByDistance); ImGui.Checkbox("Trace to Hovered", ref traceToHovered); ImGui.Checkbox("Hide Offscreen Characters", ref hideOffscreenCharacters); diff --git a/Meddle/Meddle.Plugin/UI/Layout/Instance.cs b/Meddle/Meddle.Plugin/UI/Layout/Instance.cs index 2846372..fa53cb2 100644 --- a/Meddle/Meddle.Plugin/UI/Layout/Instance.cs +++ b/Meddle/Meddle.Plugin/UI/Layout/Instance.cs @@ -83,7 +83,7 @@ private void DrawInstance(ParsedInstance instance, Stack stack, if (instance is ParsedLightInstance light) { - ImGui.ColorButton("Color", light.Color); + ImGui.ColorButton("Color", new Vector4(light.Light.Color.Rgb, light.Light.Color.Intensity)); } if (instance is ParsedHousingInstance ho) diff --git a/Meddle/Meddle.Plugin/UI/Layout/Overlay.cs b/Meddle/Meddle.Plugin/UI/Layout/Overlay.cs index 97eb06f..8e29f87 100644 --- a/Meddle/Meddle.Plugin/UI/Layout/Overlay.cs +++ b/Meddle/Meddle.Plugin/UI/Layout/Overlay.cs @@ -1,6 +1,8 @@ using System.Numerics; +using Dalamud.Interface.Utility.Raii; using ImGuiNET; using Meddle.Plugin.Models.Layout; +using Meddle.Plugin.Models.Structs; using Meddle.Plugin.Utils; namespace Meddle.Plugin.UI.Layout; @@ -23,31 +25,7 @@ private void DrawOverlayWindow(out List hovered, out List 0 && hovered.Any(x => drawTypes.HasFlag(x.Type))) - { - ImGui.BeginTooltip(); - foreach (var instance in hovered) - { - DrawTooltip(instance); - } - ImGui.EndTooltip(); - } - - if (traceToHovered) - { - var first = hovered.FirstOrDefault(); - if (first != null) - { - if (WorldToScreen(first.Transform.Translation, out var screenPos, out var inView) && - WorldToScreen(currentPos, out var currentScreenPos, out var currentInView)) - { - var bg = ImGui.GetBackgroundDrawList(); - bg.AddLine(currentScreenPos, screenPos, ImGui.GetColorU32(config.WorldDotColor), 2); - } - } - } - + DrawLayers(currentLayout, null, out hovered, out selected); } } finally @@ -56,13 +34,13 @@ private void DrawOverlayWindow(out List hovered, out List hovered, out List selected) + private void DrawLayers(ParsedInstance[] instances, ParsedInstance? parent, out List hovered, out List selected) { hovered = new List(); selected = new List(); foreach (var instance in instances) { - var state = DrawInstanceOverlay(instance); + var state = DrawInstanceOverlay(instance, parent); if (state == InstanceSelectState.Hovered) { hovered.Add(instance); @@ -71,6 +49,15 @@ private void DrawLayers(ParsedInstance[] instances, out List hov { selected.Add(instance); } + + if (instance is ParsedSharedInstance shared && drawChildren) + { + foreach (var child in shared.Children) + { + DrawLayers([child], shared, out var childHovered, out var childSelected); + hovered.AddRange(childHovered); + } + } } } @@ -115,7 +102,15 @@ private void DrawTooltip(ParsedInstance instance) if (instance is ParsedLightInstance lightInstance) { - ImGui.ColorButton("Color", lightInstance.Color); + ImGui.Text($"Light Type: {lightInstance.Light.LightType}"); + ImGui.ColorButton("Color", new Vector4(lightInstance.Light.Color.Rgb, lightInstance.Light.Color.Intensity)); + ImGui.SameLine(); + ImGui.Text($"HDR: {lightInstance.Light.Color._vec3.ToFormatted()}"); + ImGui.SameLine(); + ImGui.Text($"RGB: {lightInstance.Light.Color.Rgb.ToFormatted()}"); + ImGui.SameLine(); + ImGui.Text($"Intensity: {lightInstance.Light.Color.Intensity}"); + ImGui.Text($"Range: {lightInstance.Light.Range}"); } if (instance is IPathInstance pathInstance) @@ -154,7 +149,7 @@ private enum InstanceSelectState None } - private InstanceSelectState DrawInstanceOverlay(ParsedInstance obj) + private InstanceSelectState DrawInstanceOverlay(ParsedInstance obj, ParsedInstance? parent) { var localPos = sigUtil.GetLocalPosition(); if (Vector3.Abs(obj.Transform.Translation - localPos).Length() > config.WorldCutoffDistance) @@ -177,10 +172,53 @@ private InstanceSelectState DrawInstanceOverlay(ParsedInstance obj) var screenPosVec = new Vector2(screenPos.X, screenPos.Y); var bg = ImGui.GetBackgroundDrawList(); - bg.AddCircleFilled(screenPosVec, 5, ImGui.GetColorU32(config.WorldDotColor)); + var dotColor = config.WorldDotColor; + if (selectedInstances.ContainsKey(obj.Id) || (parent != null && selectedInstances.ContainsKey(parent.Id))) + { + dotColor = new Vector4(1f, 1f, 1f, 0.5f); + } + + // if obj is light instance, draw a short line in the direction of the light + if (obj is ParsedLightInstance light) + { + dotColor = new Vector4(light.Light.Color.Rgb, 0.5f); + + if (light.Light.LightType != LightType.PointLight) + { + var range = Math.Min(light.Light.Range, 1); + var lightDir = Vector3.Transform(new Vector3(0, 0, range), obj.Transform.Rotation); + WorldToScreen(obj.Transform.Translation + lightDir, out var endPos, out _); + bg.AddLine(screenPosVec, endPos, ImGui.GetColorU32(dotColor), 2); + } + + bg.AddCircle(screenPosVec, 5.1f, ImGui.GetColorU32(config.WorldDotColor)); + } + + bg.AddCircleFilled(screenPosVec, 5, ImGui.GetColorU32(dotColor)); + if (ImGui.IsMouseHoveringRect(screenPosVec - new Vector2(5, 5), screenPosVec + new Vector2(5, 5)) && !ImGui.IsWindowHovered(ImGuiHoveredFlags.AnyWindow)) { + if (traceToHovered) + { + if (WorldToScreen(currentPos, out var currentScreenPos, out var currentInView)) + { + bg.AddLine(currentScreenPos, screenPos, ImGui.GetColorU32(config.WorldDotColor), 2); + } + } + + if (drawChildren && traceToParent && parent != null) + { + if (WorldToScreen(parent.Transform.Translation, out var parentScreenPos, out var parentInView)) + { + bg.AddLine(screenPos, parentScreenPos, ImGui.GetColorU32(config.WorldDotColor), 2); + } + } + + using (var tt = ImRaii.Tooltip()) + { + DrawTooltip(obj); + } ImGui.SetNextFrameWantCaptureMouse(true); if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { From 0088a566c49d63238aec89220c1516d891271ac9 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:36:18 +1000 Subject: [PATCH 29/44] Update Material.cs --- Meddle/Meddle.Utils/Export/Material.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meddle/Meddle.Utils/Export/Material.cs b/Meddle/Meddle.Utils/Export/Material.cs index a7045a3..eac80be 100644 --- a/Meddle/Meddle.Utils/Export/Material.cs +++ b/Meddle/Meddle.Utils/Export/Material.cs @@ -22,7 +22,7 @@ public enum ShaderCategory : uint public enum BgVertexPaint : uint { Off = 0x7C6FA05B, // Default off - On = 0xBD94649A + On = 0xBD94649A // Treat vertex color as diffuse map0, and map0 as map1??? } public enum DiffuseAlpha : uint @@ -36,7 +36,7 @@ public enum SkinType : uint Body = 0x2BDB45F1, Face = 0xF5673524, Hrothgar = 0x57FF3B64, - Default = 0 + Default = 0x0 } public enum HairType : uint From f3e775d9cbad7c434c51480f408f1d966b864e82 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:41:09 +1000 Subject: [PATCH 30/44] Add ktisis credit for lighting structs --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1c34da2..283c699 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ Much of this code is from or based on the following projects and wouldn't have b - [PathFinder](https://github.com/chirpxiv/ffxiv-pathfinder) [[MIT](https://github.com/chirpxiv/ffxiv-pathfinder/blob/main/LICENSE)] - 🐇 - reference for world overlay logic +- [Ktisis](https://github.com/ktisis-tools/Ktisis) [[GNU GPL v3](https://github.com/ktisis-tools/Ktisis/blob/main/LICENSE)] + - lighting structs Important contributors: - [WorkingRobot](https://github.com/WorkingRobot) From 15eed8b07213aed976d6fa88f2369a2e0b147ba9 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Fri, 13 Sep 2024 17:36:26 +1000 Subject: [PATCH 31/44] Attach support returns - handle character attaches - handle if character *is* an attach (ie. mounted) - fix related to Air-Wheeler A9 having multiple root bones --- .../Models/Composer/CharacterComposer.cs | 147 ++++++++++++++++-- .../Models/Layout/ParsedInstance.cs | 6 +- .../Meddle.Plugin/Services/LayoutService.cs | 99 ++++++++++-- Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs | 118 +++++--------- Meddle/Meddle.Plugin/Utils/SkeletonUtils.cs | 43 +++-- 5 files changed, 292 insertions(+), 121 deletions(-) diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs index 5642c0d..77e09ea 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs @@ -44,7 +44,9 @@ public CharacterComposer(ILogger log, DataProvider dataProvider) } private void HandleModel(GenderRace genderRace, CustomizeParameter customizeParameter, CustomizeData customizeData, - ParsedModelInfo modelInfo, SceneBuilder scene, List bones, BoneNodeBuilder? rootBone) + ParsedModelInfo modelInfo, SceneBuilder scene, List bones, + BoneNodeBuilder? rootBone, + Matrix4x4 transform) { if (modelInfo.Path.GamePath.Contains("b0003_top")) { @@ -133,11 +135,11 @@ private void HandleModel(GenderRace genderRace, CustomizeParameter customizePara InstanceBuilder instance; if (bones.Count > 0) { - instance = scene.AddSkinnedMesh(mesh.Mesh, Matrix4x4.Identity, bones.Cast().ToArray()); + instance = scene.AddSkinnedMesh(mesh.Mesh, transform, bones.Cast().ToArray()); } else { - instance = scene.AddRigidMesh(mesh.Mesh, Matrix4x4.Identity); + instance = scene.AddRigidMesh(mesh.Mesh, transform); } if (model.Shapes.Count != 0 && mesh.Shapes != null) @@ -163,21 +165,146 @@ private void HandleModel(GenderRace genderRace, CustomizeParameter customizePara } } - public void ComposeCharacterInstance(ParsedCharacterInfo characterInfo, SceneBuilder scene, NodeBuilder root) + private int attachSuffix; + private readonly object attachLock = new(); + + public (List bones, BoneNodeBuilder root)? ComposeCharacterInfo(ParsedCharacterInfo characterInfo, (ParsedCharacterInfo Owner, List OwnerBones, ParsedAttach Attach)? attachData, SceneBuilder scene, NodeBuilder root) { var bones = SkeletonUtils.GetBoneMap(characterInfo.Skeleton, true, out var rootBone); - if (rootBone != null) + if (rootBone == null) + { + log.LogWarning("Root bone not found"); + return null; + } + bool rootParented = false; + + Matrix4x4 transform = Matrix4x4.Identity; + if (attachData != null) + { + var attach = attachData.Value.Attach; + if (rootBone == null) throw new InvalidOperationException("Root bone not found"); + var attachName = attachData.Value.Owner.Skeleton.PartialSkeletons[attach.PartialSkeletonIdx] + .HkSkeleton!.BoneNames[(int)attach.BoneIdx]; + log.LogInformation("Attaching {AttachName} to {RootBone}", attachName, rootBone.BoneName); + lock (attachLock) + { + Interlocked.Increment(ref attachSuffix); + rootBone.SetSuffixRecursively(attachSuffix); + } + + if (attach.OffsetTransform is { } ct) + { + rootBone.WithLocalScale(ct.Scale); + rootBone.WithLocalRotation(ct.Rotation); + rootBone.WithLocalTranslation(ct.Translation); + if (rootBone.AnimationTracksNames.Contains("pose")) + { + rootBone.UseScale().UseTrackBuilder("pose").WithPoint(0, ct.Scale); + rootBone.UseRotation().UseTrackBuilder("pose").WithPoint(0, ct.Rotation); + rootBone.UseTranslation().UseTrackBuilder("pose").WithPoint(0, ct.Translation); + } + } + + var attachPointBone = attachData.Value.OwnerBones.FirstOrDefault(x => x.BoneName.Equals(attachName, StringComparison.Ordinal)); + if (attachPointBone == null) + { + scene.AddNode(rootBone); + rootParented = true; + } + else + { + attachPointBone.AddNode(rootBone); + rootParented = true; + } + + NodeBuilder? c = rootBone; + while (c != null) + { + transform *= c.LocalMatrix; + c = c.Parent; + } + } + else if (characterInfo.Attach.ExecuteType != 0) + { + var rootAttach = characterInfo.Attaches.FirstOrDefault(x => x.Attach.ExecuteType == 0); + if (rootAttach == null) + { + log.LogWarning("Root attach not found"); + } + else + { + log.LogWarning("Root attach found"); + // handle root first, then attach this to the root + var rootAttachData = ComposeCharacterInfo(rootAttach, null, scene, root); + if (rootAttachData != null) + { + var attachName = rootAttach.Skeleton.PartialSkeletons[characterInfo.Attach.PartialSkeletonIdx].HkSkeleton! + .BoneNames[(int)characterInfo.Attach.BoneIdx]; + if (rootBone == null) throw new InvalidOperationException("Root bone not found"); + var attachRoot = rootAttachData.Value.root; + lock (attachLock) + { + Interlocked.Increment(ref attachSuffix); + attachRoot.SetSuffixRecursively(attachSuffix); + } + + if (rootAttach.Attach.OffsetTransform is { } ct) + { + attachRoot.WithLocalScale(ct.Scale); + attachRoot.WithLocalRotation(ct.Rotation); + attachRoot.WithLocalTranslation(ct.Translation); + if (attachRoot.AnimationTracksNames.Contains("pose")) + { + attachRoot.UseScale().UseTrackBuilder("pose").WithPoint(0, ct.Scale); + attachRoot.UseRotation().UseTrackBuilder("pose").WithPoint(0, ct.Rotation); + attachRoot.UseTranslation().UseTrackBuilder("pose").WithPoint(0, ct.Translation); + } + } + + var attachPointBone = rootAttachData.Value.bones.FirstOrDefault(x => x.BoneName.Equals(attachName, StringComparison.Ordinal)); + if (attachPointBone == null) + { + scene.AddNode(rootBone); + rootParented = true; + } + else + { + attachPointBone.AddNode(rootBone); + rootParented = true; + } + + NodeBuilder? c = rootBone; + while (c != null) + { + transform *= c.LocalMatrix; + c = c.Parent; + } + + rootBone = attachRoot; + } + } + } + + if (!rootParented) { root.AddNode(rootBone); } foreach (var t in characterInfo.Models) { - HandleModel(characterInfo.GenderRace, characterInfo.CustomizeParameter, characterInfo.CustomizeData, - t, scene, bones, rootBone); + HandleModel(characterInfo.GenderRace, characterInfo.CustomizeParameter, characterInfo.CustomizeData, t, scene, bones, rootBone, transform); } + + for (var i = 0; i < characterInfo.Attaches.Length; i++) + { + var attach = characterInfo.Attaches[i]; + if (attach.Attach.ExecuteType == 0) continue; + ComposeCharacterInfo(attach, (characterInfo, bones, attach.Attach), scene, root); + } + + return (bones, rootBone); } - + public void ComposeModels(ParsedModelInfo[] models, GenderRace genderRace, CustomizeParameter customizeParameter, CustomizeData customizeData, ParsedSkeleton skeleton, SceneBuilder scene, NodeBuilder root) { @@ -189,7 +316,7 @@ public void ComposeModels(ParsedModelInfo[] models, GenderRace genderRace, Custo foreach (var t in models) { - HandleModel(genderRace, customizeParameter, customizeData, t, scene, bones, rootBone); + HandleModel(genderRace, customizeParameter, customizeData, t, scene, bones, rootBone, Matrix4x4.Identity); } } @@ -197,7 +324,7 @@ public void ComposeCharacterInstance(ParsedCharacterInstance characterInstance, { var characterInfo = characterInstance.CharacterInfo; if (characterInfo == null) return; - ComposeCharacterInstance(characterInfo, scene, root); + ComposeCharacterInfo(characterInfo, null, scene, root); } private void EnsureBonesExist(Model model, List bones, BoneNodeBuilder? root) diff --git a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs index 3b51a26..903bef7 100644 --- a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs +++ b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs @@ -199,14 +199,17 @@ public class ParsedCharacterInfo public CustomizeData CustomizeData; public CustomizeParameter CustomizeParameter; public readonly GenderRace GenderRace; + public readonly ParsedAttach Attach; + public ParsedCharacterInfo[] Attaches = []; - public ParsedCharacterInfo(IList models, ParsedSkeleton skeleton, CustomizeData customizeData, CustomizeParameter customizeParameter, GenderRace genderRace) + public ParsedCharacterInfo(IList models, ParsedSkeleton skeleton, ParsedAttach attach, CustomizeData customizeData, CustomizeParameter customizeParameter, GenderRace genderRace) { Models = models; Skeleton = skeleton; CustomizeData = customizeData; CustomizeParameter = customizeParameter; GenderRace = genderRace; + Attach = attach; } } @@ -217,7 +220,6 @@ public class ParsedCharacterInstance : ParsedInstance, IResolvableInstance, ICha public ObjectKind Kind; public bool Visible; - public ParsedCharacterInstance(nint id, string name, ObjectKind kind, Transform transform, bool visible) : base(id, ParsedInstanceType.Character, transform) { Name = name; diff --git a/Meddle/Meddle.Plugin/Services/LayoutService.cs b/Meddle/Meddle.Plugin/Services/LayoutService.cs index d1bcc7b..9ca13de 100644 --- a/Meddle/Meddle.Plugin/Services/LayoutService.cs +++ b/Meddle/Meddle.Plugin/Services/LayoutService.cs @@ -14,6 +14,7 @@ using Lumina.Excel.GeneratedSheets; using Meddle.Plugin.Models; using Meddle.Plugin.Models.Layout; +using Meddle.Plugin.Models.Skeletons; using Meddle.Plugin.Models.Structs; using Meddle.Plugin.Utils; using Meddle.Utils; @@ -66,6 +67,21 @@ public void ResolveInstances(ParsedInstance[] instances) } } + private bool IsCharacterKind(ObjectKind kind) + { + return kind switch + { + ObjectKind.Pc => true, + ObjectKind.Mount => true, + ObjectKind.Companion => true, + ObjectKind.Retainer => true, + ObjectKind.BattleNpc => true, + ObjectKind.EventNpc => true, + ObjectKind.Ornament => true, + _ => false + }; + } + public unsafe void ResolveInstance(ParsedInstance instance) { if (instance is ParsedCharacterInstance {CharacterInfo: null} characterInstance) @@ -75,9 +91,20 @@ public unsafe void ResolveInstance(ParsedInstance instance) if (objects.Any(o => o.Id == instance.Id)) { var gameObject = (GameObject*)instance.Id; - - var characterInfo = HandleDrawObject(gameObject->DrawObject); - characterInstance.CharacterInfo = characterInfo; + if (IsCharacterKind(gameObject->ObjectKind)) + { + var characterInfo = HandleCharacter((Character*)gameObject); + characterInstance.CharacterInfo = characterInfo; + } + else + { + var characterInfo = HandleDrawObject(gameObject->DrawObject); + characterInstance.CharacterInfo = characterInfo; + } + } + else + { + logger.LogWarning("Character instance {Id} no longer exists", instance.Id); } } @@ -338,8 +365,8 @@ private unsafe ParsedInstanceSet[] Parse(LayoutManager* activeLayout, ParseCtx c return new ParsedBgPartsInstance((nint)bgPartPtr.Value, new Transform(*bgPart->GetTransformImpl()), path); } - - public unsafe ParsedInstance[] ParseObjects(bool resolveCharacterInfo = false) + + public unsafe ParsedInstance[] ParseObjects() { var gameObjectManager = sigUtil.GetGameObjectManager(); @@ -358,18 +385,8 @@ public unsafe ParsedInstance[] ParseObjects(bool resolveCharacterInfo = false) if (drawObject == null) continue; - ParsedCharacterInfo? characterInfo = null; - if (resolveCharacterInfo) - { - characterInfo = HandleDrawObject(drawObject); - } - var transform = new Transform(drawObject->Position, drawObject->Rotation, drawObject->Scale); - objects.Add( - new ParsedCharacterInstance((nint)obj, obj->NameString, type, transform, drawObject->IsVisible) - { - CharacterInfo = characterInfo - }); + objects.Add(new ParsedCharacterInstance((nint)obj, obj->NameString, type, transform, drawObject->IsVisible)); } return objects.ToArray(); @@ -447,6 +464,54 @@ public unsafe ParsedInstance[] ParseObjects(bool resolveCharacterInfo = false) return modelInfo; } + public unsafe ParsedCharacterInfo? HandleCharacter(Character* character) + { + if (character == null) + { + return null; + } + + var drawObject = character->DrawObject; + if (drawObject == null) + { + return null; + } + + var characterInfo = HandleDrawObject(drawObject); + if (characterInfo == null) + { + return null; + } + + var attaches = new List(); + var mountInfo = HandleCharacter(character->Mount.MountObject); + if (mountInfo != null) + { + attaches.Add(mountInfo); + } + + var ornamentInfo = HandleCharacter((Character*)character->OrnamentData.OrnamentObject); + if (ornamentInfo != null) + { + attaches.Add(ornamentInfo); + } + + foreach (var weapon in character->DrawData.WeaponData) + { + var weaponInfo = HandleDrawObject(weapon.DrawObject); + if (weaponInfo != null) + { + attaches.Add(weaponInfo); + } + } + + characterInfo.Attaches = attaches.ToArray(); + + return characterInfo; + } + + + public unsafe ParsedCharacterInfo? HandleDrawObject(DrawObject* drawObject) { if (drawObject == null) @@ -473,7 +538,7 @@ public unsafe ParsedInstance[] ParseObjects(bool resolveCharacterInfo = false) var skeleton = StructExtensions.GetParsedSkeleton(characterBase); var (customizeParams, customizeData, genderRace) = ParseHuman(characterBase); - return new ParsedCharacterInfo(models, skeleton, customizeData, customizeParams, genderRace); + return new ParsedCharacterInfo(models, skeleton, StructExtensions.GetParsedAttach(characterBase), customizeData, customizeParams, genderRace); } public unsafe (Meddle.Utils.Export.CustomizeParameter, CustomizeData, GenderRace) ParseHuman(CharacterBase* characterBase) diff --git a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs index f802aa4..bfe8dc0 100644 --- a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs +++ b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs @@ -175,10 +175,10 @@ private void DrawCharacter(CSCharacter* character, string name, int depth = 0) if (modelType == CSCharacterBase.ModelType.Human) { DrawHumanCharacter((CSHuman*)cBase, out customizeData, out customizeParams, out genderRace); - // if (ImGui.Button("Export All Models With Attaches")) - // { - // ExportAllModelsWithAttaches(character, customizeParams, customizeData, genderRace); - // } + if (ImGui.Button("Export All Models With Attaches")) + { + ExportAllModelsWithAttaches(character, customizeParams, customizeData, genderRace); + } } else { @@ -306,101 +306,47 @@ private void DrawDrawObject(DrawObject* drawObject, CustomizeData? customizeData } } - /*private void ExportAllModelsWithAttaches(CSCharacter* character, CustomizeParameter? customizeParams, CustomizeData? customizeData, GenderRace genderRace) + private void ExportAllModelsWithAttaches(CSCharacter* character, CustomizeParameter? customizeParams, CustomizeData? customizeData, GenderRace genderRace) { - var drawObject = character->GameObject.DrawObject; - if (drawObject == null) + var info = layoutService.HandleCharacter(character); + if (info == null) { - log.LogError("Draw object is null"); + log.LogError("Failed to get character info from draw object"); return; } - var cBase = (CSCharacterBase*)drawObject; - var group = parseService.ParseCharacterBase(cBase) with - { - CustomizeParams = customizeParams ?? new CustomizeParameter(), - CustomizeData = customizeData ?? new CustomizeData(), - GenderRace = genderRace - }; - - var attaches = new List(); - if (character->OrnamentData.OrnamentObject != null) + if (customizeParams != null) { - var draw = character->OrnamentData.OrnamentObject->GetDrawObject(); - var attachGroup = parseService.ParseDrawObjectAsAttach(draw); - if (attachGroup != null) - { - attaches.Add(attachGroup); - } + info.CustomizeParameter = customizeParams; } - if (character->Mount.MountObject != null) - { - var draw = character->Mount.MountObject->GetDrawObject(); - var attachGroup = parseService.ParseDrawObjectAsAttach(draw); - - if (attachGroup != null) - { - // hacky workaround since mount is actually a "root" and the character is attached to them - // TODO: transform needs to be adjusted to be relative to the mount - // var playerAttach = StructExtensions.GetParsedAttach(cBase); - // var attachPointName = - // playerAttach.OwnerSkeleton!.PartialSkeletons[playerAttach.PartialSkeletonIdx].HkSkeleton!.BoneNames[ - // (int)playerAttach.BoneIdx]; - // - // attachGroup.Attach.OwnerSkeleton = playerAttach.TargetSkeleton; - // attachGroup.Attach.TargetSkeleton = attachGroup.Skeleton; - // for (int i = 0; i < attachGroup.Skeleton.PartialSkeletons.Count; i++) - // { - // var partial = attachGroup.Skeleton.PartialSkeletons[i]; - // for (int j = 0; j < partial.HkSkeleton!.BoneNames.Count; j++) - // { - // if (partial.HkSkeleton.BoneNames[j] == attachPointName) - // { - // attachGroup.Attach.BoneIdx = (uint)j; - // attachGroup.Attach.PartialSkeletonIdx = (byte)i; - // break; - // } - // } - // } - - attaches.Add(attachGroup); - } - } - - if (character->CompanionData.CompanionObject != null) - { - var draw = character->CompanionData.CompanionObject->GetDrawObject(); - var attachGroup = parseService.ParseDrawObjectAsAttach(draw); - if (attachGroup != null) - { - attaches.Add(attachGroup); - } - } - - foreach (var weaponData in character->DrawData.WeaponData) + if (customizeData != null) { - if (weaponData.DrawObject == null) continue; - var draw = weaponData.DrawObject; - var attachGroup = parseService.ParseDrawObjectAsAttach(draw); - if (attachGroup != null) - { - attaches.Add(attachGroup); - } + info.CustomizeData = customizeData; } - group = group with { AttachedModelGroups = attaches.ToArray() }; - fileDialog.SaveFolderDialog("Save Model", "Character", + var folderName = $"Character-{DateTime.Now:yyyy-MM-dd-HH-mm-ss}"; + fileDialog.SaveFolderDialog("Save Model", folderName, (result, path) => { if (!result) return; Task.Run(() => { - exportService.Export(group, path); + var cacheDir = Path.Combine(path, "cache"); + Directory.CreateDirectory(cacheDir); + var composer = new CharacterComposer( + log, + new DataProvider(cacheDir, pack, log, CancellationToken.None)); + var scene = new SceneBuilder(); + var root = new NodeBuilder(); + composer.ComposeCharacterInfo(info, null, scene, root); + scene.AddNode(root); + scene.ToGltf2().SaveGLTF(Path.Combine(path, "character.gltf")); + Process.Start("explorer.exe", path); }); }, Plugin.TempDirectory); - }*/ + } private void ExportAllModels(CSCharacterBase* cBase, CustomizeParameter? customizeParams, CustomizeData? customizeData, GenderRace genderRace) { @@ -410,6 +356,16 @@ private void ExportAllModels(CSCharacterBase* cBase, CustomizeParameter? customi log.LogError("Failed to get character info from draw object"); return; } + + if (customizeParams != null) + { + info.CustomizeParameter = customizeParams; + } + + if (customizeData != null) + { + info.CustomizeData = customizeData; + } var folderName = $"Character-{DateTime.Now:yyyy-MM-dd-HH-mm-ss}"; fileDialog.SaveFolderDialog("Save Model", folderName, @@ -426,7 +382,7 @@ private void ExportAllModels(CSCharacterBase* cBase, CustomizeParameter? customi new DataProvider(cacheDir, pack, log, CancellationToken.None)); var scene = new SceneBuilder(); var root = new NodeBuilder(); - composer.ComposeCharacterInstance(info, scene, root); + composer.ComposeCharacterInfo(info, null, scene, root); scene.AddNode(root); scene.ToGltf2().SaveGLTF(Path.Combine(path, "character.gltf")); Process.Start("explorer.exe", path); diff --git a/Meddle/Meddle.Plugin/Utils/SkeletonUtils.cs b/Meddle/Meddle.Plugin/Utils/SkeletonUtils.cs index 8936c8a..aefe5c3 100644 --- a/Meddle/Meddle.Plugin/Utils/SkeletonUtils.cs +++ b/Meddle/Meddle.Plugin/Utils/SkeletonUtils.cs @@ -7,11 +7,10 @@ namespace Meddle.Plugin.Utils; public static class SkeletonUtils { - public static List GetBoneMap( - IReadOnlyList partialSkeletons, bool includePose, out BoneNodeBuilder? root) + public static (List List, BoneNodeBuilder Root)[] GetBoneMaps(IReadOnlyList partialSkeletons, bool includePose) { List boneMap = new(); - root = null; + List rootList = new(); for (var partialIdx = 0; partialIdx < partialSkeletons.Count; partialIdx++) { @@ -65,9 +64,7 @@ public static List GetBoneMap( skeleBones[parentIdx].AddNode(bone); else { - if (root != null) - throw new InvalidOperationException($"Multiple root bones found {root.BoneName} and {name}"); - root = bone; + rootList.Add(bone); } skeleBones[i] = bone; @@ -75,18 +72,42 @@ public static List GetBoneMap( } } - if (!NodeBuilder.IsValidArmature(boneMap)) + // create separate lists based on each root + var boneMapList = new List<(List List, BoneNodeBuilder root)>(); + foreach (var root in rootList) { - throw new InvalidOperationException( - $"Joints are not valid, {string.Join(", ", boneMap.Select(x => x.Name))}"); + var bones = NodeBuilder.Flatten(root).Cast().ToList(); + if (!NodeBuilder.IsValidArmature(bones)) + { + throw new InvalidOperationException($"Armature is invalid, {string.Join(", ", bones.Select(x => x.BoneName))}"); + } + + boneMapList.Add((bones, root)); } - return boneMap; + return boneMapList.ToArray(); } public static List GetBoneMap(ParsedSkeleton skeleton, bool includePose, out BoneNodeBuilder? root) { - return GetBoneMap(skeleton.PartialSkeletons, includePose, out root); + var maps = GetBoneMaps(skeleton.PartialSkeletons, includePose); + if (maps.Length == 0) + { + throw new InvalidOperationException("No roots were found"); + } + + // only known instance of this thus far is the Air-Wheeler A9 mount + // contains two roots, one for the mount and an n_pluslayer which contains an additional skeleton + var rootMap = maps.FirstOrDefault(x => x.Root.BoneName.Equals("n_root", StringComparison.OrdinalIgnoreCase)); + if (rootMap != default) + { + root = rootMap.Root; + return rootMap.List; + } + + var map0 = maps[0]; + root = map0.Root; + return map0.List; } public record AttachGrouping(List Bones, BoneNodeBuilder? Root, List<(DateTime Time, AttachSet Attach)> Timeline); From 23b5e3f6d3ea3f937abc3817c4bb08411f4f3c38 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Fri, 13 Sep 2024 22:22:05 +1000 Subject: [PATCH 32/44] skip lights with 0 range --- Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs index 8a8fd44..6d3b9ae 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs @@ -144,6 +144,12 @@ public void Compose(SceneBuilder scene) if (parsedInstance is ParsedLightInstance lightInstance) { + if (lightInstance.Light.Range <= 0) + { + log.LogWarning("Light {LightId} has a range of 0 or less ({Range})", lightInstance.Id, lightInstance.Light.Range); + return null; + } + // idk if its blender, sharpgltf or game engine stuff but flip the rotation for lights (only tested spot though) var rotation = transform.Rotation; rotation *= Quaternion.CreateFromAxisAngle(Vector3.UnitY, MathF.PI); From 83a9db767dcfcc8c41afbd30a7d8957f73adf635 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Fri, 13 Sep 2024 22:22:31 +1000 Subject: [PATCH 33/44] Staining support & blender plugin cleanup --- Blender/__init__.py | 665 +++++++++++------- .../Models/Composer/BgMaterialBuilder.cs | 28 +- .../Models/Composer/MaterialSet.cs | 6 +- 3 files changed, 417 insertions(+), 282 deletions(-) diff --git a/Blender/__init__.py b/Blender/__init__.py index 5ecdb42..7bb9865 100644 --- a/Blender/__init__.py +++ b/Blender/__init__.py @@ -25,6 +25,9 @@ def draw(self, context): row = layout.row() row.operator("meddle.fix_bg", text="Fix bg.shpk") + row = layout.row() + row.operator("meddle.stain_housing", text="Stain Housing") + row = layout.row() row.operator("meddle.connect_volume", text="Connect Skin/Iris Volume") @@ -109,6 +112,179 @@ def execute(self, context): return {'FINISHED'} +class MEDDLE_OT_stain_housing(Operator): + """Applies stain colors to housing materials""" + bl_idname = "meddle.stain_housing" + bl_label = "Stain Housing" + bl_options = {'REGISTER', 'UNDO'} + + def discardNormalBlueChannel(self, mat): + principled_bsdf = self.getBsdfPrincipled(mat) + + # get the node connected to the bsdf "Normal" input + normal_map = None + for node in mat.node_tree.nodes: + if node.label == 'NORMAL MAP': + normal_map = node + break + + if normal_map is None: + return + + # create node to remove the blue channel + separate_rgb = None + for node in mat.node_tree.nodes: + if node.type == 'SEPARATE_COLOR' and node.name == "Separate Normal Map": + separate_rgb = node + break + + if separate_rgb is None: + separate_rgb = mat.node_tree.nodes.new('ShaderNodeSeparateColor') + separate_rgb.location = (normal_map.location.x + 300, normal_map.location.y) + separate_rgb.name = "Separate Normal Map" + + mat.node_tree.links.new(normal_map.outputs['Color'], separate_rgb.inputs['Color']) + + # create node to add the blue channel + combine_rgb = None + for node in mat.node_tree.nodes: + if node.type == 'COMBINE_COLOR' and node.name == "Combine Normal Map": + combine_rgb = node + break + + if combine_rgb is None: + combine_rgb = mat.node_tree.nodes.new('ShaderNodeCombineColor') + combine_rgb.location = (separate_rgb.location.x + 300, separate_rgb.location.y) + combine_rgb.name = "Combine Normal Map" + + combine_rgb.inputs['Blue'].default_value = 1.0 + mat.node_tree.links.new(separate_rgb.outputs['Red'], combine_rgb.inputs['Red']) + mat.node_tree.links.new(separate_rgb.outputs['Green'], combine_rgb.inputs['Green']) + mat.node_tree.links.new(combine_rgb.outputs['Color'], principled_bsdf.inputs['Normal']) + + normal_map_node = None + for node in mat.node_tree.nodes: + if node.type == 'NORMAL_MAP': + normal_map_node = node + break + + if normal_map_node is None: + return + + mat.node_tree.links.new(combine_rgb.outputs['Color'], normal_map_node.inputs['Color']) + mat.node_tree.links.new(normal_map_node.outputs['Normal'], principled_bsdf.inputs['Normal']) + + # organize nodes + combine_rgb.location = (normal_map.location.x, normal_map.location.y - 150) + separate_rgb.location = (normal_map.location.x + 300, normal_map.location.y) + normal_map_node.location = (normal_map.location.x + 600, normal_map.location.y) + + + + def getBsdfPrincipled(self, mat): + # Look for the Principled BSDF node + principled_bsdf = None + for node in mat.node_tree.nodes: + if node.type == 'BSDF_PRINCIPLED': + principled_bsdf = node + break + + return principled_bsdf + + def mixColor(self, mat): + principled_bsdf = self.getBsdfPrincipled(mat) + + # if DiffuseColor is not found, then skip + if "DiffuseColor" not in mat: + return + + diffuse_color = mat["DiffuseColor"] + print(f"Found custom property 'DiffuseColor' in material '{mat.name}' with value {diffuse_color}.") + + # create a new RGB node + rgb_node = None + for node in mat.node_tree.nodes: + if node.type == 'RGB': + rgb_node = node + break + + if rgb_node is None: + rgb_node = mat.node_tree.nodes.new('ShaderNodeRGB') + rgb_node.location = (-300, 0) + + rgb_node.outputs['Color'].default_value = diffuse_color + + power_node = None + for node in mat.node_tree.nodes: + if node.label == "POWER": + power_node = node + break + + if power_node is None: + power_node = mat.node_tree.nodes.new('ShaderNodeMath') + power_node.operation = 'POWER' + power_node.label = "POWER" + + mat.node_tree.links.new(rgb_node.outputs['Color'], power_node.inputs['Value']) + power_node.inputs[1].default_value = 2.0 + + # mix the color with "BASE COLOR" node + base_color = None + for node in mat.node_tree.nodes: + if node.label == "BASE COLOR": + base_color = node + break + + if base_color is None: + print(f"Material '{mat.name}' does not have a 'BASE COLOR' node.") + return + + mix_color = None + for node in mat.node_tree.nodes: + if node.label == "MULTIPLY COLOR": + mix_color = node + break + + if mix_color is None: + mix_color = mat.node_tree.nodes.new('ShaderNodeMixRGB') + mix_color.label = "MULTIPLY COLOR" + + mix_color.blend_type = 'MULTIPLY' + mix_color.inputs['Fac'].default_value = 1.0 + mat.node_tree.links.new(mix_color.outputs['Color'], principled_bsdf.inputs['Base Color']) + mat.node_tree.links.new(base_color.outputs['Color'], mix_color.inputs['Color1']) + mat.node_tree.links.new(power_node.outputs['Value'], mix_color.inputs['Color2']) + mat.node_tree.links.new(base_color.outputs['Alpha'], mix_color.inputs['Fac']) + + # organize nodes + rgb_node.location = (base_color.location.x - 300, base_color.location.y) + mix_color.location = (base_color.location.x, base_color.location.y - 150) + power_node.location = (base_color.location.x + 300, base_color.location.y) + + def handleMaterial(self, mat): + # Check if the material uses nodes + if not mat.use_nodes: + return + + if "ShaderPackage" not in mat: + return + + if mat["ShaderPackage"] != "bgcolorchange.shpk": + return + + self.mixColor(mat) + self.discardNormalBlueChannel(mat) + + + def execute(self, context): + # Iterate all materials in the scene + # if ShaderPackage is bgcolorchange.shpk, then read DiffuseColor property + + for mat in bpy.data.materials: + self.handleMaterial(mat) + + return {'FINISHED'} + class MEDDLE_OT_fix_bg(Operator): """Looks up the g_SamplerXXXMap1 values on bg materials and creates the relevant texture nodes""" bl_idname = "meddle.fix_bg" @@ -121,248 +297,242 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - def execute(self, context): - context.scene.selected_folder = self.directory - print(f"Folder selected: {self.directory}") - # Iterate all materials in the scene - for mat in bpy.data.materials: - # Check if the material uses nodes - if not mat.use_nodes: - continue + def getPrincipalBsdf(self, mat): + principled_bsdf = None + for node in mat.node_tree.nodes: + if node.type == 'BSDF_PRINCIPLED': + principled_bsdf = node + break + return principled_bsdf + + def handleNormalChannels(self, mat, vertex_color_node, principled_bsdf): + g_SamplerNormalMap1 = None + if "g_SamplerNormalMap1" in mat: + g_SamplerNormalMap1 = mat["g_SamplerNormalMap1"] + print(f"Found custom property 'g_SamplerNormalMap1' in material '{mat.name}' with value {g_SamplerNormalMap1}.") + else: + print(f"Material '{mat.name}' does not have the custom property 'g_SamplerNormalMap1'.") + + g_SamplerNormalMap0Node = None + for node in mat.node_tree.nodes: + if node.label == "NORMAL MAP": + g_SamplerNormalMap0Node = node + break + + normal_tangent = None + for node in mat.node_tree.nodes: + if node.name == "Normal Map": + normal_tangent = node + break + + if g_SamplerNormalMap1 is not None and g_SamplerNormalMap0Node is not None and normal_tangent is not None and vertex_color_node is not None: + mix_normal = None + for node in mat.node_tree.nodes: + if node.label == "MIX NORMAL": + mix_normal = node + break + if mix_normal is None: + mix_normal = mat.node_tree.nodes.new('ShaderNodeMixRGB') + mix_normal.label = "MIX NORMAL" - if "ShaderPackage" not in mat: - continue + mix_normal.blend_type = 'MIX' + mix_normal.inputs['Fac'].default_value = 1.0 + mat.node_tree.links.new(mix_normal.outputs['Color'], normal_tangent.inputs['Color']) + mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_normal.inputs['Fac']) - if mat["ShaderPackage"] != "bg.shpk": - continue - - # Look for the Principled BSDF node - principled_bsdf = None + # load normal texture using the selected folder + normal_map + ".png" + gSamplerNormalMap1Node = None for node in mat.node_tree.nodes: - if node.type == 'BSDF_PRINCIPLED': - principled_bsdf = node + if node.label == "NORMAL MAP 1": + gSamplerNormalMap1Node = node break - - if not principled_bsdf: - continue - # look for g_SamplerColorMap1, g_SamplerNormalMap1, g_SamplerSpecularMap1 - g_SamplerColorMap1 = None - if "g_SamplerColorMap1" in mat: - g_SamplerColorMap1 = mat["g_SamplerColorMap1"] - print(f"Found custom property 'g_SamplerColorMap1' in material '{mat.name}' with value {g_SamplerColorMap1}.") - else: - print(f"Material '{mat.name}' does not have the custom property 'g_SamplerColorMap1'.") - - g_SamplerColorMap0Node = None + if gSamplerNormalMap1Node is None: + gSamplerNormalMap1Node = mat.node_tree.nodes.new('ShaderNodeTexImage') + gSamplerNormalMap1Node.label = "NORMAL MAP 1" + gSamplerNormalMap1Node.image = bpy.data.images.load(self.directory + g_SamplerNormalMap1 + ".png") + + mat.node_tree.links.new(g_SamplerNormalMap0Node.outputs['Color'], mix_normal.inputs['Color1']) + mat.node_tree.links.new(gSamplerNormalMap1Node.outputs['Color'], mix_normal.inputs['Color2']) + + # organize nodes + gSamplerNormalMap1Node.location = (g_SamplerNormalMap0Node.location.x, g_SamplerNormalMap0Node.location.y - 150) + mix_normal.location = (g_SamplerNormalMap0Node.location.x + 300, g_SamplerNormalMap0Node.location.y) + normal_tangent.location = (g_SamplerNormalMap0Node.location.x + 600, g_SamplerNormalMap0Node.location.y) + self.discardNormalBlueChannel(mat, mix_normal, normal_tangent, principled_bsdf) + else: + self.discardNormalBlueChannel(mat, g_SamplerNormalMap0Node, normal_tangent, principled_bsdf) + + def discardNormalBlueChannel(self, mat, normal_source_node, normal_map_node, principled_bsdf): + # create node to remove the blue channel + separate_rgb = None + for node in mat.node_tree.nodes: + if node.type == 'SEPARATE_COLOR' and node.name == "Separate Normal Map": + separate_rgb = node + break + + if separate_rgb is None: + separate_rgb = mat.node_tree.nodes.new('ShaderNodeSeparateColor') + separate_rgb.location = (normal_source_node.location.x + 300, normal_source_node.location.y) + separate_rgb.name = "Separate Normal Map" + + mat.node_tree.links.new(normal_source_node.outputs['Color'], separate_rgb.inputs['Color']) + + # create node to add the blue channel + combine_rgb = None + for node in mat.node_tree.nodes: + if node.type == 'COMBINE_COLOR' and node.name == "Combine Normal Map": + combine_rgb = node + break + + if combine_rgb is None: + combine_rgb = mat.node_tree.nodes.new('ShaderNodeCombineColor') + combine_rgb.location = (separate_rgb.location.x + 300, separate_rgb.location.y) + combine_rgb.name = "Combine Normal Map" + + combine_rgb.inputs['Blue'].default_value = 1.0 + mat.node_tree.links.new(separate_rgb.outputs['Red'], combine_rgb.inputs['Red']) + mat.node_tree.links.new(separate_rgb.outputs['Green'], combine_rgb.inputs['Green']) + mat.node_tree.links.new(combine_rgb.outputs['Color'], principled_bsdf.inputs['Normal']) + + mat.node_tree.links.new(combine_rgb.outputs['Color'], normal_map_node.inputs['Color']) + mat.node_tree.links.new(normal_map_node.outputs['Normal'], principled_bsdf.inputs['Normal']) + + def handleColorChannels(self, mat, vertex_color_node, principled_bsdf): + g_SamplerColorMap0Node = None + for node in mat.node_tree.nodes: + if node.label == "BASE COLOR": + g_SamplerColorMap0Node = node + break + + g_SamplerColorMap1 = None + if "g_SamplerColorMap1" in mat: + g_SamplerColorMap1 = mat["g_SamplerColorMap1"] + print(f"Found custom property 'g_SamplerColorMap1' in material '{mat.name}' with value {g_SamplerColorMap1}.") + else: + print(f"Material '{mat.name}' does not have the custom property 'g_SamplerColorMap1'.") + + if g_SamplerColorMap1 is not None and g_SamplerColorMap0Node is not None and vertex_color_node is not None: + mix_color = None for node in mat.node_tree.nodes: - if node.label == "BASE COLOR": - g_SamplerColorMap0Node = node + if node.label == "MIX COLOR": + mix_color = node break + if mix_color is None: + mix_color = mat.node_tree.nodes.new('ShaderNodeMixRGB') + mix_color.label = "MIX COLOR" - g_SamplerNormalMap1 = None - if "g_SamplerNormalMap1" in mat: - g_SamplerNormalMap1 = mat["g_SamplerNormalMap1"] - print(f"Found custom property 'g_SamplerNormalMap1' in material '{mat.name}' with value {g_SamplerNormalMap1}.") - else: - print(f"Material '{mat.name}' does not have the custom property 'g_SamplerNormalMap1'.") + mix_color.blend_type = 'MIX' + mix_color.inputs['Fac'].default_value = 1.0 + mat.node_tree.links.new(mix_color.outputs['Color'], principled_bsdf.inputs['Base Color']) + mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_color.inputs['Fac']) - g_SamplerNormalMap0Node = None + # load color texture using the selected folder + color_map + ".png" + g_SamplerColorMap1Node = None for node in mat.node_tree.nodes: - if node.label == "NORMAL MAP": - g_SamplerNormalMap0Node = node + if node.label == "BASE COLOR 1": + g_SamplerColorMap1Node = node break - - normal_tangent = None + + if g_SamplerColorMap1Node is None: + g_SamplerColorMap1Node = mat.node_tree.nodes.new('ShaderNodeTexImage') + g_SamplerColorMap1Node.label = "BASE COLOR 1" + + g_SamplerColorMap1Node.image = bpy.data.images.load(self.directory + g_SamplerColorMap1 + ".png") + + + mat.node_tree.links.new(g_SamplerColorMap0Node.outputs['Color'], mix_color.inputs['Color1']) + mat.node_tree.links.new(g_SamplerColorMap1Node.outputs['Color'], mix_color.inputs['Color2']) + + # organize nodes + g_SamplerColorMap1Node.location = (g_SamplerColorMap0Node.location.x, g_SamplerColorMap0Node.location.y - 150) + mix_color.location = (g_SamplerColorMap0Node.location.x + 300, g_SamplerColorMap0Node.location.y) + + def handleSpecularChannels(self, mat, vertex_color_node, principled_bsdf): + g_SamplerSpecularMap1 = None + if "g_SamplerSpecularMap1" in mat: + g_SamplerSpecularMap1 = mat["g_SamplerSpecularMap1"] + print(f"Found custom property 'g_SamplerSpecularMap1' in material '{mat.name}' with value {g_SamplerSpecularMap1}.") + else: + print(f"Material '{mat.name}' does not have the custom property 'g_SamplerSpecularMap1'.") + + g_SamplerSpecularMap0Node = None + for node in mat.node_tree.nodes: + if node.label == "METALLIC ROUGHNESS": + g_SamplerSpecularMap0Node = node + break + + if g_SamplerSpecularMap1 is not None and g_SamplerSpecularMap0Node is not None and vertex_color_node is not None: + mix_specular = None for node in mat.node_tree.nodes: - if node.name == "Normal Map": - normal_tangent = node + if node.label == "MIX SPECULAR": + mix_specular = node break + if mix_specular is None: + mix_specular = mat.node_tree.nodes.new('ShaderNodeMixRGB') + mix_specular.label = "MIX SPECULAR" - g_SamplerSpecularMap1 = None - if "g_SamplerSpecularMap1" in mat: - g_SamplerSpecularMap1 = mat["g_SamplerSpecularMap1"] - print(f"Found custom property 'g_SamplerSpecularMap1' in material '{mat.name}' with value {g_SamplerSpecularMap1}.") - else: - print(f"Material '{mat.name}' does not have the custom property 'g_SamplerSpecularMap1'.") + mix_specular.blend_type = 'MIX' + mix_specular.inputs['Fac'].default_value = 1.0 + mat.node_tree.links.new(mix_specular.outputs['Color'], principled_bsdf.inputs['Metallic']) + mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_specular.inputs['Fac']) - g_SamplerSpecularMap0Node = None + # load specular texture using the selected folder + specular_map + ".png" + g_SamplerSpecularMap1Node = None for node in mat.node_tree.nodes: - if node.label == "METALLIC ROUGHNESS": - g_SamplerSpecularMap0Node = node + if node.label == "METALLIC ROUGHNESS 1": + g_SamplerSpecularMap1Node = node break - - # specular_map = None - #if "g_SamplerSpecularMap1" in mat: - # specular_map = mat["g_SamplerSpecularMap1"] - # print(f"Found custom property 'g_SamplerSpecularMap1' in material '{mat.name}' with value {specular_map}.") + if g_SamplerSpecularMap1Node is None: + g_SamplerSpecularMap1Node = mat.node_tree.nodes.new('ShaderNodeTexImage') + g_SamplerSpecularMap1Node.label = "METALLIC ROUGHNESS 1" + g_SamplerSpecularMap1Node.image = bpy.data.images.load(self.directory + g_SamplerSpecularMap1 + ".png") - # get vertex color node - vertex_color_node = None + metallic_factor_node = None for node in mat.node_tree.nodes: - if node.type == 'VERTEX_COLOR': - vertex_color_node = node + if node.label == "Metallic Factor": + metallic_factor_node = node break - if vertex_color_node is None: - continue + if metallic_factor_node is None: + metallic_factor_node = mat.node_tree.nodes.new('ShaderNodeMath') + metallic_factor_node.operation = 'MULTIPLY' + metallic_factor_node.label = "Metallic Factor" + metallic_factor_node.inputs['Value'].default_value = 0.0 - #mix_map_1 = False - #if "CategoryTextureType" in mat: - # if mat["CategoryTextureType"] == "MixMap1" or mat["CategoryTextureType"] == "0x1DF2985C": - # mix_map_1 = True + # Specular -> Mix/Multiply -> Separate Color -> [Green -> Roughness] [Blue -> Metallic Factor -> Metallic] + separate_color = None + for node in mat.node_tree.nodes: + if node.type == 'SEPARATE_COLOR' and node.name == "Separate Metallic Roughness": + separate_color = node + break - if "VertexPaint" in mat: - if mat["VertexPaint"] == True: - print(f"Material '{mat.name}' has VertexPaint enabled.") + if separate_color is None: + separate_color = mat.node_tree.nodes.new('ShaderNodeSeparateColor') + separate_color.location = (g_SamplerSpecularMap0Node.location.x + 300, g_SamplerSpecularMap0Node.location.y) + separate_color.name = "Separate Metallic Roughness" - try: - if g_SamplerColorMap1 is not None and g_SamplerColorMap0Node is not None: - mix_color = None - for node in mat.node_tree.nodes: - if node.label == "MIX COLOR": - mix_color = node - break - if mix_color is None: - mix_color = mat.node_tree.nodes.new('ShaderNodeMixRGB') - mix_color.label = "MIX COLOR" - - mix_color.blend_type = 'MIX' - mix_color.inputs['Fac'].default_value = 1.0 - mat.node_tree.links.new(mix_color.outputs['Color'], principled_bsdf.inputs['Base Color']) - mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_color.inputs['Fac']) - - # load color texture using the selected folder + color_map + ".png" - g_SamplerColorMap1Node = None - for node in mat.node_tree.nodes: - if node.label == "BASE COLOR 1": - g_SamplerColorMap1Node = node - break - - if g_SamplerColorMap1Node is None: - g_SamplerColorMap1Node = mat.node_tree.nodes.new('ShaderNodeTexImage') - g_SamplerColorMap1Node.label = "BASE COLOR 1" - - g_SamplerColorMap1Node.image = bpy.data.images.load(self.directory + g_SamplerColorMap1 + ".png") - - - mat.node_tree.links.new(g_SamplerColorMap0Node.outputs['Color'], mix_color.inputs['Color1']) - mat.node_tree.links.new(g_SamplerColorMap1Node.outputs['Color'], mix_color.inputs['Color2']) - - # organize nodes - g_SamplerColorMap1Node.location = (g_SamplerColorMap0Node.location.x, g_SamplerColorMap0Node.location.y - 150) - mix_color.location = (g_SamplerColorMap0Node.location.x + 300, g_SamplerColorMap0Node.location.y) - except Exception as e: - print(f"Error: {e}") + mat.node_tree.links.new(g_SamplerSpecularMap0Node.outputs['Color'], mix_specular.inputs['Color1']) + mat.node_tree.links.new(g_SamplerSpecularMap1Node.outputs['Color'], mix_specular.inputs['Color2']) - try: - if g_SamplerNormalMap1 is not None and g_SamplerNormalMap0Node is not None and normal_tangent is not None: - mix_normal = None - for node in mat.node_tree.nodes: - if node.label == "MIX NORMAL": - mix_normal = node - break - if mix_normal is None: - mix_normal = mat.node_tree.nodes.new('ShaderNodeMixRGB') - mix_normal.label = "MIX NORMAL" - - mix_normal.blend_type = 'MIX' - mix_normal.inputs['Fac'].default_value = 1.0 - mat.node_tree.links.new(mix_normal.outputs['Color'], normal_tangent.inputs['Color']) - mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_normal.inputs['Fac']) - - # load normal texture using the selected folder + normal_map + ".png" - gSamplerNormalMap1Node = None - for node in mat.node_tree.nodes: - if node.label == "NORMAL MAP 1": - gSamplerNormalMap1Node = node - break - - if gSamplerNormalMap1Node is None: - gSamplerNormalMap1Node = mat.node_tree.nodes.new('ShaderNodeTexImage') - gSamplerNormalMap1Node.label = "NORMAL MAP 1" - gSamplerNormalMap1Node.image = bpy.data.images.load(self.directory + g_SamplerNormalMap1 + ".png") - - mat.node_tree.links.new(g_SamplerNormalMap0Node.outputs['Color'], mix_normal.inputs['Color1']) - mat.node_tree.links.new(gSamplerNormalMap1Node.outputs['Color'], mix_normal.inputs['Color2']) - - # organize nodes - gSamplerNormalMap1Node.location = (g_SamplerNormalMap0Node.location.x, g_SamplerNormalMap0Node.location.y - 150) - mix_normal.location = (g_SamplerNormalMap0Node.location.x + 300, g_SamplerNormalMap0Node.location.y) - normal_tangent.location = (g_SamplerNormalMap0Node.location.x + 600, g_SamplerNormalMap0Node.location.y) - - if g_SamplerSpecularMap1 is not None and g_SamplerSpecularMap0Node is not None: - mix_specular = None - for node in mat.node_tree.nodes: - if node.label == "MIX SPECULAR": - mix_specular = node - break - if mix_specular is None: - mix_specular = mat.node_tree.nodes.new('ShaderNodeMixRGB') - mix_specular.label = "MIX SPECULAR" - - mix_specular.blend_type = 'MIX' - mix_specular.inputs['Fac'].default_value = 1.0 - mat.node_tree.links.new(mix_specular.outputs['Color'], principled_bsdf.inputs['Metallic']) - mat.node_tree.links.new(vertex_color_node.outputs['Alpha'], mix_specular.inputs['Fac']) - - # load specular texture using the selected folder + specular_map + ".png" - g_SamplerSpecularMap1Node = None - for node in mat.node_tree.nodes: - if node.label == "METALLIC ROUGHNESS 1": - g_SamplerSpecularMap1Node = node - break - - if g_SamplerSpecularMap1Node is None: - g_SamplerSpecularMap1Node = mat.node_tree.nodes.new('ShaderNodeTexImage') - g_SamplerSpecularMap1Node.label = "METALLIC ROUGHNESS 1" - g_SamplerSpecularMap1Node.image = bpy.data.images.load(self.directory + g_SamplerSpecularMap1 + ".png") - - metallic_factor_node = None - for node in mat.node_tree.nodes: - if node.label == "Metallic Factor": - metallic_factor_node = node - break - - if metallic_factor_node is None: - metallic_factor_node = mat.node_tree.nodes.new('ShaderNodeMath') - metallic_factor_node.operation = 'MULTIPLY' - metallic_factor_node.label = "Metallic Factor" - metallic_factor_node.inputs['Value'].default_value = 0.0 - - # Specular -> Mix/Multiply -> Separate Color -> [Green -> Roughness] [Blue -> Metallic Factor -> Metallic] - - separate_color = None - for node in mat.node_tree.nodes: - if node.type == 'SEPARATE_COLOR' and node.name == "Separate Metallic Roughness": - separate_color = node - break - - if separate_color is None: - separate_color = mat.node_tree.nodes.new('ShaderNodeSeparateColor') - separate_color.location = (g_SamplerSpecularMap0Node.location.x + 300, g_SamplerSpecularMap0Node.location.y) - separate_color.name = "Separate Metallic Roughness" - - mat.node_tree.links.new(g_SamplerSpecularMap0Node.outputs['Color'], mix_specular.inputs['Color1']) - mat.node_tree.links.new(g_SamplerSpecularMap1Node.outputs['Color'], mix_specular.inputs['Color2']) - - mat.node_tree.links.new(mix_specular.outputs['Color'], separate_color.inputs['Color']) - mat.node_tree.links.new(separate_color.outputs['Green'], principled_bsdf.inputs['Roughness']) - mat.node_tree.links.new(separate_color.outputs['Blue'], metallic_factor_node.inputs['Value']) - mat.node_tree.links.new(metallic_factor_node.outputs['Value'], principled_bsdf.inputs['Metallic']) - - # organize nodes - g_SamplerSpecularMap1Node.location = (g_SamplerSpecularMap0Node.location.x, g_SamplerSpecularMap0Node.location.y - 150) - mix_specular.location = (g_SamplerSpecularMap0Node.location.x + 300, g_SamplerSpecularMap0Node.location.y) - separate_color.location = (g_SamplerSpecularMap0Node.location.x + 600, g_SamplerSpecularMap0Node.location.y) - metallic_factor_node.location = (g_SamplerSpecularMap0Node.location.x + 900, g_SamplerSpecularMap0Node.location.y) - except Exception as e: - print(f"Error: {e}") - continue + mat.node_tree.links.new(mix_specular.outputs['Color'], separate_color.inputs['Color']) + mat.node_tree.links.new(separate_color.outputs['Green'], principled_bsdf.inputs['Roughness']) + mat.node_tree.links.new(separate_color.outputs['Blue'], metallic_factor_node.inputs['Value']) + mat.node_tree.links.new(metallic_factor_node.outputs['Value'], principled_bsdf.inputs['Metallic']) + # organize nodes + g_SamplerSpecularMap1Node.location = (g_SamplerSpecularMap0Node.location.x, g_SamplerSpecularMap0Node.location.y - 150) + mix_specular.location = (g_SamplerSpecularMap0Node.location.x + 300, g_SamplerSpecularMap0Node.location.y) + separate_color.location = (g_SamplerSpecularMap0Node.location.x + 600, g_SamplerSpecularMap0Node.location.y) + metallic_factor_node.location = (g_SamplerSpecularMap0Node.location.x + 900, g_SamplerSpecularMap0Node.location.y) + def execute(self, context): + context.scene.selected_folder = self.directory + print(f"Folder selected: {self.directory}") + + # Iterate all materials in the scene for mat in bpy.data.materials: # Check if the material uses nodes if not mat.use_nodes: @@ -384,62 +554,27 @@ def execute(self, context): if not principled_bsdf: continue - # get the node connected to the bsdf "Normal" input - normal_map = None - for node in mat.node_tree.nodes: - if node.type == 'NORMAL_MAP': - normal_map = node - break - - if normal_map is None: - continue - - # create node to remove the blue channel - - separate_rgb = None - for node in mat.node_tree.nodes: - if node.type == 'SEPARATE_COLOR' and node.name == "Separate Normal Map": - separate_rgb = node - break - - if separate_rgb is None: - separate_rgb = mat.node_tree.nodes.new('ShaderNodeSeparateColor') - separate_rgb.location = (normal_map.location.x + 300, normal_map.location.y) - separate_rgb.name = "Separate Normal Map" - - mat.node_tree.links.new(normal_map.outputs['Normal'], separate_rgb.inputs['Color']) - - # create node to add the blue channel - - combine_rgb = None - for node in mat.node_tree.nodes: - if node.type == 'COMBINE_COLOR' and node.name == "Combine Normal Map": - combine_rgb = node - break - - if combine_rgb is None: - combine_rgb = mat.node_tree.nodes.new('ShaderNodeCombineColor') - combine_rgb.location = (separate_rgb.location.x + 300, separate_rgb.location.y) - combine_rgb.name = "Combine Normal Map" - - combine_rgb.inputs['Blue'].default_value = 1.0 - mat.node_tree.links.new(separate_rgb.outputs['Red'], combine_rgb.inputs['Red']) - mat.node_tree.links.new(separate_rgb.outputs['Green'], combine_rgb.inputs['Green']) - mat.node_tree.links.new(combine_rgb.outputs['Color'], principled_bsdf.inputs['Normal']) - - # get material output node - material_output = None + # get vertex color node + vertex_color_node = None for node in mat.node_tree.nodes: - if node.type == 'OUTPUT_MATERIAL': - material_output = node + if node.type == 'VERTEX_COLOR': + vertex_color_node = node break - if material_output is None: - continue + try: + self.handleColorChannels(mat, vertex_color_node, principled_bsdf) + except Exception as e: + print(f"Error: {e}") - # connect separate blue channel to material output - mat.node_tree.links.new(separate_rgb.outputs['Blue'], material_output.inputs['Displacement']) + try: + self.handleNormalChannels(mat, vertex_color_node, principled_bsdf) + except Exception as e: + print(f"Error: {e}") + try: + self.handleSpecularChannels(mat, vertex_color_node, principled_bsdf) + except Exception as e: + print(f"Error: {e}") return {'FINISHED'} @@ -448,6 +583,7 @@ def register(): bpy.utils.register_class(MEDDLE_OT_fix_ior) bpy.utils.register_class(MEDDLE_OT_fix_bg) bpy.utils.register_class(MEDDLE_OT_connect_volume) + bpy.utils.register_class(MEDDLE_OT_stain_housing) bpy.types.Scene.selected_folder = StringProperty(name="Selected Folder", description="Path to the selected folder") def unregister(): @@ -455,6 +591,7 @@ def unregister(): bpy.utils.unregister_class(MEDDLE_OT_fix_ior) bpy.utils.unregister_class(MEDDLE_OT_fix_bg) bpy.utils.unregister_class(MEDDLE_OT_connect_volume) + bpy.utils.unregister_class(MEDDLE_OT_stain_housing) if __name__ == "__main__": register() diff --git a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs index cddd5fb..b91f448 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/BgMaterialBuilder.cs @@ -126,20 +126,9 @@ public override MeddleMaterialBuilder Apply() { var extras = new List<(string, object)>(); - if (!set.TryGetImageBuilderStrict(dataProvider, TextureUsage.g_SamplerColorMap0, out var colorMap0Texture)) - { - throw new Exception("ColorMap0 texture not found"); - } - - if (!set.TryGetImageBuilderStrict(dataProvider, TextureUsage.g_SamplerSpecularMap0, out var specularMap0Texture)) - { - throw new Exception("SpecularMap0 texture not found"); - } - - if (!set.TryGetImageBuilderStrict(dataProvider, TextureUsage.g_SamplerNormalMap0, out var normalMap0Texture)) - { - throw new Exception("NormalMap0 texture not found"); - } + var colorMap0Texture = set.GetImageBuilderStrict(dataProvider, TextureUsage.g_SamplerColorMap0); + var specularMap0Texture = set.GetImageBuilderStrict(dataProvider, TextureUsage.g_SamplerSpecularMap0); + var normalMap0Texture = set.GetImageBuilderStrict(dataProvider, TextureUsage.g_SamplerNormalMap0); set.TryGetImageBuilder(dataProvider, TextureUsage.g_SamplerColorMap1, out var colorMap1Texture); set.TryGetImageBuilder(dataProvider, TextureUsage.g_SamplerSpecularMap1, out var specularMap1Texture); @@ -156,7 +145,16 @@ public override MeddleMaterialBuilder Apply() if (bgParams is BgColorChangeParams bgColorChangeParams && GetDiffuseColor(out var bgColorChangeDiffuseColor)) { - var diffuseColor = bgColorChangeParams.StainColor ?? bgColorChangeDiffuseColor; + Vector4 diffuseColor; + if (bgColorChangeParams.StainColor != null && bgColorChangeParams.StainColor != Vector4.Zero) + { + diffuseColor = bgColorChangeParams.StainColor.Value with { W = 1.0f }; + } + else + { + diffuseColor = bgColorChangeDiffuseColor; + } + extras.Add(("DiffuseColor", diffuseColor.AsFloatArray())); } diff --git a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs index f95ec05..a68bf11 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs @@ -117,15 +117,15 @@ public bool TryGetTextureStrict(DataProvider provider, TextureUsage usage, out T return true; } - public bool TryGetImageBuilderStrict(DataProvider provider, TextureUsage usage, out ImageBuilder builder) + public ImageBuilder GetImageBuilderStrict(DataProvider provider, TextureUsage usage) { if (!TextureUsageDict.TryGetValue(usage, out var path)) { throw new Exception($"Texture usage {usage} not found in material set"); } - builder = provider.LookupTexture(path.FullPath) ?? throw new Exception($"Texture {path.FullPath} not found"); - return true; + var builder = provider.LookupTexture(path.FullPath) ?? throw new Exception($"Texture {path.FullPath} not found"); + return builder; } public bool TryGetImageBuilder(DataProvider provider, TextureUsage usage, out ImageBuilder? builder) From da1957756415000ee5425fd03a47633d78b663da Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:19:31 +1000 Subject: [PATCH 34/44] Allow animation exports to specify directory --- Meddle/Meddle.Plugin/Services/ExportService.cs | 7 ++++--- Meddle/Meddle.Plugin/UI/AnimationTab.cs | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Meddle/Meddle.Plugin/Services/ExportService.cs b/Meddle/Meddle.Plugin/Services/ExportService.cs index 4eb2d52..90e2850 100644 --- a/Meddle/Meddle.Plugin/Services/ExportService.cs +++ b/Meddle/Meddle.Plugin/Services/ExportService.cs @@ -98,15 +98,16 @@ public static void ExportTexture(SKBitmap bitmap, string path) Process.Start("explorer.exe", folder); } - public void ExportAnimation( - List<(DateTime, AttachSet[])> frames, bool includePositionalData, CancellationToken token = default) + public void ExportAnimation(List<(DateTime, AttachSet[])> frames, bool includePositionalData, string path, CancellationToken token = default) { try { using var activity = ActivitySource.StartActivity(); var boneSets = SkeletonUtils.GetAnimatedBoneMap(frames.ToArray()); var startTime = frames.Min(x => x.Item1); - var folder = GetPathForOutput(); + //var folder = GetPathForOutput(); + var folder = path; + Directory.CreateDirectory(folder); foreach (var (id, (bones, root, timeline)) in boneSets) { var scene = new SceneBuilder(); diff --git a/Meddle/Meddle.Plugin/UI/AnimationTab.cs b/Meddle/Meddle.Plugin/UI/AnimationTab.cs index fcbc0ac..2ee548c 100644 --- a/Meddle/Meddle.Plugin/UI/AnimationTab.cs +++ b/Meddle/Meddle.Plugin/UI/AnimationTab.cs @@ -1,4 +1,5 @@ using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; @@ -24,7 +25,11 @@ public class AnimationTab : ITab private bool includePositionalData; private ICharacter? selectedCharacter; public MenuType MenuType => MenuType.Default; - + private readonly FileDialogManager fileDialog = new() + { + AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking + }; + public AnimationTab( IFramework framework, ILogger logger, ExportService exportService, @@ -74,7 +79,14 @@ public void Draw() if (ImGui.Button("Export")) { - exportService.ExportAnimation(frames, includePositionalData); + var folderName = $"Animation-{DateTime.Now:yyyy-MM-dd-HH-mm-ss}"; + fileDialog.SaveFolderDialog("Save Animation", folderName, (result, path) => + { + if (!result) return; + exportService.ExportAnimation(frames, includePositionalData, path); + }, Plugin.TempDirectory); + + } ImGui.SameLine(); @@ -82,6 +94,8 @@ public void Draw() ImGui.Separator(); DrawSelectedCharacter(); + + fileDialog.Draw(); } public void Dispose() From 82c9cf4680a53a91a87a22693f160d9920f44873 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:19:49 +1000 Subject: [PATCH 35/44] More shader keys --- .../Meddle.Plugin/Models/Composer/MaterialSet.cs | 1 + Meddle/Meddle.Utils/Export/Material.cs | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs index a68bf11..f0bdb95 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/MaterialSet.cs @@ -514,6 +514,7 @@ void AddShaderKeys() ShaderCategory.CategorySpecularType => IsDefinedOrHex((SpecularMode)value), ShaderCategory.GetValuesTextureType => IsDefinedOrHex((TextureMode)value), ShaderCategory.CategoryFlowMapType => IsDefinedOrHex((FlowType)value), + ShaderCategory.CategoryBgVertexPaint => IsDefinedOrHex((BgVertexPaint)value), _ => $"0x{value:X8}" }; diff --git a/Meddle/Meddle.Utils/Export/Material.cs b/Meddle/Meddle.Utils/Export/Material.cs index eac80be..b56ba08 100644 --- a/Meddle/Meddle.Utils/Export/Material.cs +++ b/Meddle/Meddle.Utils/Export/Material.cs @@ -17,12 +17,19 @@ public enum ShaderCategory : uint CategoryFlowMapType = 0x40D1481E, // STANDARD, FLOW CategoryDiffuseAlpha = 0xA9A3EE25, // Alpha channel on diffuse texture is used CategoryBgVertexPaint = 0x4F4F0636, // Enable vertex paint + CategoryBgTextureMode = 0x36F72D5F, // Number of textures in BG shader +} + +public enum BgTextureMode : uint +{ + Map0 = 0x1E314009, + Map1 = 0x9807BAC4 } public enum BgVertexPaint : uint { Off = 0x7C6FA05B, // Default off - On = 0xBD94649A // Treat vertex color as diffuse map0, and map0 as map1??? + On = 0xBD94649A // Doesn't actually seem to mean use vertex color, from what I can tell, just use color0 texture and ignore rest } public enum DiffuseAlpha : uint @@ -58,9 +65,9 @@ public enum TextureMode : uint Simple = 0x22A4AABF, // meh // BG.shpk - Ox669A451B = 0x669A451B, - MixMap1 = 0x1DF2985C, // seems to indicate the presence of dummy textures for bg.shpk map1 - Ox941820BE = 0x941820BE + BG_UNK0 = 0x669A451B, + BG_UNK1 = 0x1DF2985C, + BG_UNK2 = 0x941820BE, // Mix both textures? } public enum SpecularMode : uint From daa8b38bb066397ccdc8560f33908c042c402ef9 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:23:54 +1000 Subject: [PATCH 36/44] Update blender addon buttons --- Blender/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Blender/__init__.py b/Blender/__init__.py index 7bb9865..4aca19a 100644 --- a/Blender/__init__.py +++ b/Blender/__init__.py @@ -19,17 +19,17 @@ class VIEW3D_PT_update_meddle_shaders(Panel): def draw(self, context): layout = self.layout - row = layout.row() - row.operator("meddle.fix_ior", text="Fix ior") + #row = layout.row() + #row.operator("meddle.fix_ior", text="Fix ior") row = layout.row() row.operator("meddle.fix_bg", text="Fix bg.shpk") row = layout.row() - row.operator("meddle.stain_housing", text="Stain Housing") + row.operator("meddle.stain_housing", text="Fix bgcolorchange.shpk") row = layout.row() - row.operator("meddle.connect_volume", text="Connect Skin/Iris Volume") + row.operator("meddle.connect_volume", text="Fix skin.shpk/iris.shpk") class MEDDLE_OT_connect_volume(Operator): """Connects the volume output to the material output for skin.shpk and iris.shpk""" @@ -286,7 +286,7 @@ def execute(self, context): return {'FINISHED'} class MEDDLE_OT_fix_bg(Operator): - """Looks up the g_SamplerXXXMap1 values on bg materials and creates the relevant texture nodes""" + """Looks up the g_SamplerXXXMap1 values on bg materials and creates the relevant texture nodes, select the 'cache' directory from your meddle export folder""" bl_idname = "meddle.fix_bg" bl_label = "Fix bg.shpk" bl_options = {'REGISTER', 'UNDO'} @@ -448,6 +448,7 @@ def handleColorChannels(self, mat, vertex_color_node, principled_bsdf): # organize nodes g_SamplerColorMap1Node.location = (g_SamplerColorMap0Node.location.x, g_SamplerColorMap0Node.location.y - 150) mix_color.location = (g_SamplerColorMap0Node.location.x + 300, g_SamplerColorMap0Node.location.y) + def handleSpecularChannels(self, mat, vertex_color_node, principled_bsdf): g_SamplerSpecularMap1 = None From d98e4a8424eeb20db23131261866826848ab87ae Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:35:13 +1000 Subject: [PATCH 37/44] Create blender.yml --- .github/workflows/blender.yml | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/blender.yml diff --git a/.github/workflows/blender.yml b/.github/workflows/blender.yml new file mode 100644 index 0000000..b9feb4b --- /dev/null +++ b/.github/workflows/blender.yml @@ -0,0 +1,36 @@ +name: Blender Release + +# Add a concurrency group incase a tag is created, deleted, and then recreated while a release is in progress. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + paths: + - 'Blender/**' + +permissions: + contents: write + +jobs: + BlenderRelease: + if: github.event.pull_request.draft == false # Ignore draft PRs + runs-on: ubuntu-latest + defaults: + run: + working-directory: Blender/ + shell: bash + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Zip Blender + run: | + zip -r blender.zip Blender/ + + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: Release Artifacts + path: blender.zip From 0f59b37f69a7ec0fab6a2fe617ac1fe9d42ec95c Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:40:10 +1000 Subject: [PATCH 38/44] Bump --- .github/workflows/blender.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/blender.yml b/.github/workflows/blender.yml index b9feb4b..73ebed1 100644 --- a/.github/workflows/blender.yml +++ b/.github/workflows/blender.yml @@ -30,7 +30,7 @@ jobs: zip -r blender.zip Blender/ - name: Upload Artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: Release Artifacts path: blender.zip From 40ad6db105757f1b0a4c06e1e23e118821c25397 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:41:11 +1000 Subject: [PATCH 39/44] Update blender.yml --- .github/workflows/blender.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/blender.yml b/.github/workflows/blender.yml index 73ebed1..2c3808a 100644 --- a/.github/workflows/blender.yml +++ b/.github/workflows/blender.yml @@ -9,6 +9,7 @@ on: push: paths: - 'Blender/**' + - '.github/workflows/blender.yml' permissions: contents: write From c7d8dd4b06033f83990d7216cbc196d0787d4588 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:42:09 +1000 Subject: [PATCH 40/44] Update blender.yml --- .github/workflows/blender.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/blender.yml b/.github/workflows/blender.yml index 2c3808a..10e7924 100644 --- a/.github/workflows/blender.yml +++ b/.github/workflows/blender.yml @@ -20,7 +20,6 @@ jobs: runs-on: ubuntu-latest defaults: run: - working-directory: Blender/ shell: bash steps: - name: Checkout Repository From e5bce517dfe75b9ec94f47bc09b6fea91e763919 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:59:54 +1000 Subject: [PATCH 41/44] Add manifest --- .github/workflows/blender.yml | 10 ++--- Blender/__init__.py | 22 ++++++++++- Blender/blender_manifest.toml | 73 +++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 Blender/blender_manifest.toml diff --git a/.github/workflows/blender.yml b/.github/workflows/blender.yml index 10e7924..1e910ee 100644 --- a/.github/workflows/blender.yml +++ b/.github/workflows/blender.yml @@ -24,13 +24,9 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v3 - - - name: Zip Blender - run: | - zip -r blender.zip Blender/ - + - name: Upload Artifact uses: actions/upload-artifact@v3 with: - name: Release Artifacts - path: blender.zip + name: MeddleTools + path: Blender/ diff --git a/Blender/__init__.py b/Blender/__init__.py index 4aca19a..9b96ae1 100644 --- a/Blender/__init__.py +++ b/Blender/__init__.py @@ -1,6 +1,24 @@ -bl_info = { - "name": "Meddle Utils", +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +bl_info = { + "name": "Meddle Tools", + "author": "PassiveModding", + "description": "", "blender": (4, 0, 0), + "version": (0, 0, 1), + "location": "3D View > Meddle Utils", + "warning": "", "category": "3D View", } diff --git a/Blender/blender_manifest.toml b/Blender/blender_manifest.toml new file mode 100644 index 0000000..e04dc87 --- /dev/null +++ b/Blender/blender_manifest.toml @@ -0,0 +1,73 @@ +schema_version = "1.0.0" + +# Example of manifest file for a Blender extension +# Change the values according to your extension +id = "meddle_tools" +version = "1.0.0" +name = "Meddle Tools" +tagline = "This is another extension" +maintainer = "PassiveModding" +# Supported types: "add-on", "theme" +type = "add-on" + +# Optional link to documentation, support, source files, etc +# website = "https://extensions.blender.org/add-ons/my-example-package/" + +# Optional list defined by Blender and server, see: +# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html +tags = ["Material"] + +blender_version_min = "4.2.0" +# # Optional: Blender version that the extension does not support, earlier versions are supported. +# # This can be omitted and defined later on the extensions platform if an issue is found. +# blender_version_max = "5.1.0" + +# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix) +# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html +license = [ + "SPDX:GPL-2.0-or-later", +] +# Optional: required by some licenses. +# copyright = [ +# "2002-2024 Developer Name", +# "1998 Company Name", +# ] + +# Optional list of supported platforms. If omitted, the extension will be available in all operating systems. +# platforms = ["windows-x64", "macos-arm64", "linux-x64"] +# Other supported platforms: "windows-arm64", "macos-x64" + +# Optional: bundle 3rd party Python modules. +# https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html +# wheels = [ +# "./wheels/hexdump-3.3-py3-none-any.whl", +# "./wheels/jsmin-3.0.1-py3-none-any.whl", +# ] + +# Optional: add-ons can list which resources they will require: +# * files (for access of any filesystem operations) +# * network (for internet access) +# * clipboard (to read and/or write the system clipboard) +# * camera (to capture photos and videos) +# * microphone (to capture audio) +# +# If using network, remember to also check `bpy.app.online_access` +# https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access +# +# For each permission it is important to also specify the reason why it is required. +# Keep this a single short sentence without a period (.) at the end. +# For longer explanations use the documentation or detail page. +# +[permissions] +# network = "Need to sync motion-capture data to server" +files = "Import files from meddle cache directory" +# clipboard = "Copy and paste bone transforms" + +# Optional: build settings. +# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build +# [build] +# paths_exclude_pattern = [ +# "__pycache__/", +# "/.git/", +# "/*.zip", +# ] \ No newline at end of file From 4be3af22bbfa4755bc1bf918c3e66fb7508e9bcf Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sun, 15 Sep 2024 19:05:55 +1000 Subject: [PATCH 42/44] Bump pipeline versions --- .github/workflows/blender.yml | 2 +- .github/workflows/release.yml | 8 ++++---- .github/workflows/test.yml | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/blender.yml b/.github/workflows/blender.yml index 1e910ee..6c968a4 100644 --- a/.github/workflows/blender.yml +++ b/.github/workflows/blender.yml @@ -23,7 +23,7 @@ jobs: shell: bash steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Upload Artifact uses: actions/upload-artifact@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8714ccb..1ec95fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,12 +28,12 @@ jobs: IsCI: true steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: true # Grab any submodules that may be required - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: 7.0.x @@ -66,7 +66,7 @@ jobs: fail_on_unmatched_files: true # If the files arent found, fail the workflow and abort the release. - name: Upload Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Release Artifacts path: | @@ -92,4 +92,4 @@ jobs: git config --local user.email "github-actions@users.noreply.github.com" git commit -m "Update repo.json for ${{ github.ref_name }}" - git push origin HEAD:main \ No newline at end of file + git push origin HEAD:main diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6b9e4c7..9470518 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,13 +31,13 @@ jobs: IsCI: true steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 # We don't need the history for testing builds, so we can save some time by not fetching it submodules: true # Grab any submodules that may be required - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ matrix.dotnet-version }} @@ -56,7 +56,7 @@ jobs: run: dotnet build -c Release --no-restore --nologo - name: Upload Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Build Artifacts path: Meddle/Meddle.Plugin/bin/ From 27d5ce83009d9f81519ec40a4047b14e33eed2a4 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sun, 15 Sep 2024 19:06:45 +1000 Subject: [PATCH 43/44] Update blender.yml --- .github/workflows/blender.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/blender.yml b/.github/workflows/blender.yml index 6c968a4..b0aaa2b 100644 --- a/.github/workflows/blender.yml +++ b/.github/workflows/blender.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@v4 - name: Upload Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: MeddleTools path: Blender/ From d6cfb0dcedd6cb88f18e4496091485ec6d24ab6d Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sun, 15 Sep 2024 19:15:53 +1000 Subject: [PATCH 44/44] Include blender plugin in release --- .github/workflows/release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ec95fb..fdab78b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,6 +52,11 @@ jobs: working-directory: Meddle/Meddle.Plugin/bin/Release/Meddle.Plugin run: | sha512sum latest.zip >> checksums.sha512 + + - name: Zip Blender Plugin + working-directory: Blender/ + run: | + zip -r MeddleTools.zip . - name: Create GitHub Release uses: softprops/action-gh-release@v1 @@ -59,6 +64,7 @@ jobs: files: | Meddle/Meddle.Plugin/bin/Release/Meddle.Plugin/latest.zip Meddle/Meddle.Plugin/bin/Release/Meddle.Plugin/checksums.sha512 + Blender/MeddleTools.zip prerelease: false # Releases cant be marked as prereleases as Dalamud wont be able to find them append_body: true # Append the release notes to the release body body_path: .github/release-notices.md # These notes are automatically added to the release body every time.