diff --git a/AttachStuff.rcnet b/AttachStuff.rcnet index dfd9b09..fe592b1 100644 Binary files a/AttachStuff.rcnet and b/AttachStuff.rcnet differ diff --git a/Meddle/Meddle.Plugin/Models/Composer/InstanceSet.cs b/Meddle/Meddle.Plugin/Models/Composer/InstanceSet.cs new file mode 100644 index 0000000..28e2380 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/InstanceSet.cs @@ -0,0 +1,252 @@ +using System.Collections.Concurrent; +using System.Numerics; +using Dalamud.Plugin.Services; +using Meddle.Plugin.Models.Layout; +using Meddle.Utils; +using Meddle.Utils.Export; +using Meddle.Utils.Files; +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 static class InstanceCache +{ + public static ConcurrentDictionary ShpkCache { get; } = new(); +} + +public class InstanceSet +{ + public InstanceSet(ILogger log, IDataManager manager, 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; + this.progress = progress; + this.cancellationToken = cancellationToken; + this.size = instances.Select(x => x.Flatten().Length).Sum(); + this.count = instances.Length; + } + + private readonly ILogger log; + private readonly IDataManager dataManager; + private readonly Action? progress; + private readonly CancellationToken cancellationToken; + private readonly int size; + private readonly int count; + private int sizeProgress; + private int countProgress; + public string CacheDir { get; } + private readonly ParsedInstance[] instances; + private readonly Dictionary imageCache = new(); + + public void Compose(SceneBuilder scene) + { + progress?.Invoke(new ProgressEvent(0, count)); + foreach (var instance in instances) + { + var node = ComposeInstance(scene, instance); + if (node != null) + { + scene.AddNode(node); + } + + countProgress++; + progress?.Invoke(new ProgressEvent(countProgress, count)); + } + } + + public NodeBuilder? ComposeInstance(SceneBuilder scene, ParsedInstance parsedInstance) + { + if (cancellationToken.IsCancellationRequested) return null; + + var root = new NodeBuilder(); + if (parsedInstance is ParsedHousingInstance housingInstance) + { + root.Name = housingInstance.Name; + } + else + { + root.Name = $"{parsedInstance.Type}_{parsedInstance.Id}"; + } + root.SetLocalTransform(parsedInstance.Transform.AffineTransform, false); + bool added = false; + + if (parsedInstance is ParsedBgPartsInstance {Path: not null} bgPartsInstance) + { + var meshes = ComposeBgPartsInstance(bgPartsInstance); + foreach (var mesh in meshes) + { + scene.AddRigidMesh(mesh.Mesh, root, Matrix4x4.Identity); + } + + added = true; + } + else if (parsedInstance is ParsedLightInstance lightInstance) + { + sizeProgress++; + return null; + var lightBuilder = new LightBuilder.Point + { + Name = $"light_{lightInstance.Id}" + }; + scene.AddLight(lightBuilder, root); + } + else if (parsedInstance is ParsedCharacterInstance characterInstance) + { + sizeProgress++; + return null; + } + else + { + sizeProgress++; + return null; + } + + foreach (var child in parsedInstance.Children) + { + var childNode = ComposeInstance(scene, child); + if (childNode != null) + { + root.AddNode(childNode); + added = true; + } + } + + sizeProgress++; + + if (!added) return null; + return root; + } + + private IReadOnlyList ComposeBgPartsInstance(ParsedBgPartsInstance bgPartsInstance) + { + if (bgPartsInstance.Path == null) + { + return []; + } + + var mdlData = dataManager.GetFile(bgPartsInstance.Path); + if (mdlData == null) + { + log.LogWarning("Failed to load model file: {bgPartsInstance.Path}", bgPartsInstance.Path); + return []; + } + + var mdlFile = new MdlFile(mdlData.Data); + var materials = mdlFile.GetMaterialNames().Select(x => x.Value).ToArray(); + + var materialBuilders = new List(); + foreach (var mtrlPath in materials) + { + var mtrlData = dataManager.GetFile(mtrlPath); + if (mtrlData == null) + { + log.LogWarning("Failed to load material file: {materialPath}", mtrlPath); + // TODO: Stub material + continue; + } + + var mtrlFile = new MtrlFile(mtrlData.Data); + var texturePaths = mtrlFile.GetTexturePaths(); + var shpkPath = $"shader/sm5/shpk/{mtrlFile.GetShaderPackageName()}"; + if (!InstanceCache.ShpkCache.TryGetValue(shpkPath, out var shaderPackage)) + { + var shpkData = dataManager.GetFile(shpkPath); + if (shpkData == null) + throw new Exception($"Failed to load shader package file: {shpkPath}"); + var shpkFile = new ShpkFile(shpkData.Data); + shaderPackage = new ShaderPackage(shpkFile, null!); + InstanceCache.ShpkCache.TryAdd(shpkPath, shaderPackage); + log.LogInformation("Loaded shader package {shpkPath}", shpkPath); + } + else + { + log.LogDebug("Reusing shader package {shpkPath}", shpkPath); + } + + var output = new MaterialBuilder(Path.GetFileNameWithoutExtension(mtrlPath)) + .WithMetallicRoughnessShader() + .WithBaseColor(Vector4.One); + + foreach (var (offset, texPath) in texturePaths) + { + if (imageCache.ContainsKey(texPath)) continue; + ComposeTexture(texPath); + } + + var setTypes = new HashSet(); + foreach (var sampler in mtrlFile.Samplers) + { + if (sampler.TextureIndex == byte.MaxValue) continue; + var textureInfo = mtrlFile.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 (!shaderPackage.TextureLookup.TryGetValue(sampler.SamplerId, out var usage)) + { + log.LogWarning("Unknown texture usage for texture {texturePath} ({textureUsage})", texturePath, (TextureUsage)sampler.SamplerId); + continue; + } + + var channel = MaterialUtility.MapTextureUsageToChannel(usage); + if (channel != null && setTypes.Add(usage)) + { + var fileName = $"{Path.GetFileNameWithoutExtension(texturePath)}_{usage}_{shaderPackage.Name}"; + var imageBuilder = ImageBuilder.From(tex.MemoryImage, fileName); + imageBuilder.AlternateWriteFileName = $"{fileName}.*"; + output.WithChannelImage(channel.Value, imageBuilder); + } + else if (channel != null) + { + log.LogWarning("Ignoring texture {texturePath} with usage {usage}", texturePath, usage); + } + else + { + log.LogWarning("Unknown texture usage {usage} for texture {texturePath}", usage, texturePath); + } + } + + materialBuilders.Add(output); + } + + var model = new Model(bgPartsInstance.Path, mdlFile, null); + var meshes = ModelBuilder.BuildMeshes(model, materialBuilders, [], null); + return meshes; + } + + private void ComposeTexture(string texPath) + { + var texData = dataManager.GetFile(texPath); + if (texData == null) throw new Exception($"Failed to load texture file: {texPath}"); + log.LogInformation("Loaded texture {texPath}", texPath); + var texFile = new TexFile(texData.Data); + var diskPath = Path.Combine(CacheDir, Path.GetDirectoryName(texPath) ?? "", + Path.GetFileNameWithoutExtension(texPath)) + ".png"; + var texture = Texture.GetResource(texFile).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)))); + } +} diff --git a/Meddle/Meddle.Plugin/Models/Composer/ProgressEvent.cs b/Meddle/Meddle.Plugin/Models/Composer/ProgressEvent.cs new file mode 100644 index 0000000..d877c10 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/ProgressEvent.cs @@ -0,0 +1,3 @@ +namespace Meddle.Plugin.Models.Composer; + +public record ProgressEvent(int Progress, int Total); diff --git a/Meddle/Meddle.Plugin/Models/Composer/TerrainSet.cs b/Meddle/Meddle.Plugin/Models/Composer/TerrainSet.cs new file mode 100644 index 0000000..5aafddc --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Composer/TerrainSet.cs @@ -0,0 +1,188 @@ +using System.Numerics; +using Dalamud.Plugin.Services; +using Meddle.Utils; +using Meddle.Utils.Export; +using Meddle.Utils.Files; +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 TerrainSet +{ + public TerrainSet(ILogger log, IDataManager manager, string terrainDir, string? cacheDir = null, Action? progress = null, CancellationToken cancellationToken = default) + { + CacheDir = cacheDir ?? Path.GetTempPath(); + Directory.CreateDirectory(CacheDir); + TerrainDir = terrainDir; + this.log = log; + dataManager = manager; + this.progress = progress; + this.cancellationToken = cancellationToken; + var teraPath = $"{TerrainDir}/bgplate/terrain.tera"; + var teraData = dataManager.GetFile(teraPath); + if (teraData == null) throw new Exception($"Failed to load terrain file: {teraPath}"); + teraFile = new TeraFile(teraData.Data); + } + + private readonly ILogger log; + private readonly IDataManager dataManager; + private readonly Action? progress; + private readonly CancellationToken cancellationToken; + public int PlateCount => (int)teraFile.Header.PlateCount; + public int Progress { get; private set; } + + public string TerrainDir { get; } + public string CacheDir { get; } + + private readonly Dictionary imageCache = new(); + private TeraFile teraFile; + + public void Compose(SceneBuilder scene) + { + progress?.Invoke(new ProgressEvent(0, PlateCount)); + var terrainRoot = new NodeBuilder(TerrainDir); + scene.AddNode(terrainRoot); + + for (var i = 0; i < teraFile.Header.PlateCount; i++) + { + if (cancellationToken.IsCancellationRequested) return; + log.LogInformation("Parsing plate {i}", i); + var platePos = teraFile.GetPlatePosition(i); + var plateTransform = + new Transform(new Vector3(platePos.X, 0, platePos.Y), Quaternion.Identity, Vector3.One); + var meshes = ComposePlate(i); + var plateRoot = new NodeBuilder($"Plate{i:D4}"); + foreach (var mesh in meshes) + { + scene.AddRigidMesh(mesh.Mesh, plateRoot, plateTransform.AffineTransform); + } + + terrainRoot.AddNode(plateRoot); + Progress = i; + progress?.Invoke(new ProgressEvent(i, PlateCount)); + } + } + + private IReadOnlyList ComposePlate(int i) + { + var mdlPath = $"{TerrainDir}/bgplate/{i:D4}.mdl"; + var mdlData = dataManager.GetFile(mdlPath); + if (mdlData == null) throw new Exception($"Failed to load model file: {mdlPath}"); + log.LogInformation("Loaded model {mdlPath}", mdlPath); + var mdlFile = new MdlFile(mdlData.Data); + var materials = mdlFile.GetMaterialNames().Select(x => x.Value).ToArray(); + + var materialBuilders = new List(); + foreach (var mtrlPath in materials) + { + var materialBuilder = ComposeMaterial(mtrlPath); + materialBuilders.Add(materialBuilder); + } + + var model = new Model(mdlPath, mdlFile, null); + var meshes = ModelBuilder.BuildMeshes(model, materialBuilders, [], null); + return meshes; + } + + private MaterialBuilder ComposeMaterial(string path) + { + var mtrlData = dataManager.GetFile(path); + if (mtrlData == null) throw new Exception($"Failed to load material file: {path}"); + log.LogInformation("Loaded material {path}", path); + + var mtrlFile = new MtrlFile(mtrlData.Data); + var texturePaths = mtrlFile.GetTexturePaths(); + var shpkPath = $"shader/sm5/shpk/{mtrlFile.GetShaderPackageName()}"; + if (!InstanceCache.ShpkCache.TryGetValue(shpkPath, out var shaderPackage)) + { + var shpkData = dataManager.GetFile(shpkPath); + if (shpkData == null) + throw new Exception($"Failed to load shader package file: {shpkPath}"); + var shpkFile = new ShpkFile(shpkData.Data); + shaderPackage = new ShaderPackage(shpkFile, null!); + InstanceCache.ShpkCache.TryAdd(shpkPath, shaderPackage); + log.LogInformation("Loaded shader package {shpkPath}", shpkPath); + } + else + { + log.LogDebug("Reusing shader package {shpkPath}", shpkPath); + } + + var output = new MaterialBuilder(Path.GetFileNameWithoutExtension(path)) + .WithMetallicRoughnessShader() + .WithBaseColor(Vector4.One); + + foreach (var (offset, texPath) in texturePaths) + { + if (imageCache.ContainsKey(texPath)) continue; + ComposeTexture(texPath); + } + + var setTypes = new HashSet(); + foreach (var sampler in mtrlFile.Samplers) + { + if (sampler.TextureIndex == byte.MaxValue) continue; + var textureInfo = mtrlFile.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 (!shaderPackage.TextureLookup.TryGetValue(sampler.SamplerId, out var usage)) + { + log.LogWarning("Unknown texture usage for texture {texturePath} ({textureUsage})", texturePath, (TextureUsage)sampler.SamplerId); + continue; + } + + var channel = MaterialUtility.MapTextureUsageToChannel(usage); + if (channel != null && setTypes.Add(usage)) + { + var fileName = $"{Path.GetFileNameWithoutExtension(texturePath)}_{usage}_{shaderPackage.Name}"; + var imageBuilder = ImageBuilder.From(tex.MemoryImage, fileName); + imageBuilder.AlternateWriteFileName = $"{fileName}.*"; + output.WithChannelImage(channel.Value, imageBuilder); + } + else if (channel != null) + { + log.LogWarning("Ignoring texture {texturePath} with usage {usage}", texturePath, usage); + } + else + { + log.LogWarning("Unknown texture usage {usage} for texture {texturePath}", usage, texturePath); + } + } + + return output; + } + + private void ComposeTexture(string texPath) + { + var texData = dataManager.GetFile(texPath); + if (texData == null) throw new Exception($"Failed to load texture file: {texPath}"); + log.LogInformation("Loaded texture {texPath}", texPath); + var texFile = new TexFile(texData.Data); + var diskPath = Path.Combine(CacheDir, Path.GetDirectoryName(texPath) ?? "", + Path.GetFileNameWithoutExtension(texPath)) + ".png"; + var texture = Texture.GetResource(texFile).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)))); + } +} diff --git a/Meddle/Meddle.Plugin/Services/LayoutService.cs b/Meddle/Meddle.Plugin/Services/LayoutService.cs index d4fcfec..d550a17 100644 --- a/Meddle/Meddle.Plugin/Services/LayoutService.cs +++ b/Meddle/Meddle.Plugin/Services/LayoutService.cs @@ -1,4 +1,6 @@ -using System.Runtime.InteropServices; +using System.Collections.Concurrent; +using System.Numerics; +using System.Runtime.InteropServices; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Object; @@ -14,10 +16,18 @@ using Meddle.Plugin.Utils; using Meddle.Utils; using Meddle.Utils.Export; +using Meddle.Utils.Files; +using Meddle.Utils.Files.SqPack; using Meddle.Utils.Files.Structs.Material; +using Meddle.Utils.Materials; +using Meddle.Utils.Models; using Microsoft.Extensions.Logging; +using SharpGLTF.Materials; +using SharpGLTF.Scenes; +using SkiaSharp; using CustomizeParameter = Meddle.Plugin.Models.Structs.CustomizeParameter; using HousingFurniture = FFXIVClientStructs.FFXIV.Client.Game.HousingFurniture; +using Material = Meddle.Utils.Export.Material; using Transform = Meddle.Plugin.Models.Transform; namespace Meddle.Plugin.Services; @@ -26,18 +36,23 @@ public class LayoutService : IService { private readonly Dictionary itemDict; private readonly ILogger logger; + private readonly IDataManager dataManager; + private readonly SqPack pack; private readonly ParseService parseService; private readonly PbdHooks pbdHooks; private readonly SigUtil sigUtil; private readonly Dictionary stainDict; public LayoutService( - SigUtil sigUtil, ILogger logger, - IDataManager dataManager, + SigUtil sigUtil, ILogger logger, + IDataManager dataManager, + SqPack pack, ParseService parseService, PbdHooks pbdHooks) { this.sigUtil = sigUtil; this.logger = logger; + this.dataManager = dataManager; + this.pack = pack; this.parseService = parseService; this.pbdHooks = pbdHooks; stainDict = dataManager.GetExcelSheet()!.ToDictionary(row => row.RowId, row => row); @@ -52,7 +67,7 @@ public ParsedInstance[] ResolveInstances(ParsedInstance[] instances) { ResolveInstance(instance); } - + return instances; } @@ -67,12 +82,12 @@ public unsafe ParsedInstance ResolveInstance(ParsedInstance instance) characterInstance.CharacterInfo = characterInfo; } } - + foreach (var child in instance.Children) { ResolveInstance(child); } - + return instance; } @@ -96,10 +111,10 @@ public unsafe ParsedInstance ResolveInstance(ParsedInstance instance) layers.AddRange(loadedLayers.SelectMany(x => x.Instances)); layers.AddRange(globalLayers.SelectMany(x => x.Instances)); layers.AddRange(objects); - + return layers.ToArray(); } - + private unsafe HousingTerritory* GetCurrentTerritory() { var housingManager = sigUtil.GetHousingManager(); @@ -295,7 +310,7 @@ private unsafe ParsedLayer[] Parse(LayoutManager* activeLayout, ParseCtx ctx) Path = path }; } - + public unsafe ParsedInstance[] ParseObjects(bool resolveCharacterInfo = false) { var gameObjectManager = sigUtil.GetGameObjectManager(); @@ -305,16 +320,16 @@ public unsafe ParsedInstance[] ParseObjects(bool resolveCharacterInfo = false) { if (objectPtr == null || objectPtr.Value == null) continue; - + var obj = objectPtr.Value; if (objects.Any(o => o.Id == (nint)obj)) continue; - + var type = obj->GetObjectKind(); var drawObject = obj->DrawObject; if (drawObject == null) continue; - + ParsedCharacterInfo? characterInfo = null; if (resolveCharacterInfo) { @@ -341,13 +356,13 @@ public unsafe ParsedInstance[] ParseObjects(bool resolveCharacterInfo = false) { return null; } - + var objectType = drawObject->Object.GetObjectType(); if (objectType != ObjectType.CharacterBase) { return null; } - + var characterBase = (CharacterBase*)drawObject; var colorTableTextures = parseService.ParseColorTableTextures(characterBase); var models = new List(); @@ -369,7 +384,8 @@ public unsafe ParsedInstance[] ParseObjects(bool resolveCharacterInfo = false) if (material == null) continue; var materialPath = material->MaterialResourceHandle->ResourceHandle.FileName.ParseString(); - var materialPathFromModel = model->ModelResourceHandle->GetMaterialFileNameBySlotAsString((uint)mtrlIdx); + var materialPathFromModel = + model->ModelResourceHandle->GetMaterialFileNameBySlotAsString((uint)mtrlIdx); var shaderName = material->MaterialResourceHandle->ShpkNameString; ColorTable? colorTable = null; if (colorTableTextures.TryGetValue((int)(model->SlotIndex * CharacterBase.MaterialsPerSlot) + mtrlIdx, @@ -397,21 +413,24 @@ public unsafe ParsedInstance[] ParseObjects(bool resolveCharacterInfo = false) if (texIdx < material->TextureCount) { var texturePathFromMaterial = material->MaterialResourceHandle->TexturePathString(texIdx); - var (resource, stride) = DXHelper.ExportTextureResource(texturePtr.TextureResourceHandle->Texture); + 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); + + 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); + var modelInfo = + new ParsedModelInfo(modelPath, modelPathFromCharacter, deform, shapeAttributeGroup, materials); models.Add(modelInfo); } - + var skeleton = StructExtensions.GetParsedSkeleton(characterBase); var modelType = characterBase->GetModelType(); CustomizeData customizeData = new CustomizeData(); diff --git a/Meddle/Meddle.Plugin/Services/SigUtil.cs b/Meddle/Meddle.Plugin/Services/SigUtil.cs index ed280ab..83f6c11 100644 --- a/Meddle/Meddle.Plugin/Services/SigUtil.cs +++ b/Meddle/Meddle.Plugin/Services/SigUtil.cs @@ -74,7 +74,6 @@ public unsafe Vector3 GetLocalPosition() return layoutWorld; } - public unsafe Camera* GetCamera() { var manager = CameraManager.Instance(); diff --git a/Meddle/Meddle.Plugin/UI/TerrainTab.cs b/Meddle/Meddle.Plugin/UI/TerrainTab.cs new file mode 100644 index 0000000..e3a29a6 --- /dev/null +++ b/Meddle/Meddle.Plugin/UI/TerrainTab.cs @@ -0,0 +1,247 @@ +using System.Diagnostics; +using System.Numerics; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin.Services; +using ImGuiNET; +using Meddle.Plugin.Models; +using Meddle.Plugin.Models.Composer; +using Meddle.Plugin.Services; +using Microsoft.Extensions.Logging; +using SharpGLTF.Scenes; +using SharpGLTF.Schema2; + +namespace Meddle.Plugin.UI; + +public class TerrainTab : ITab +{ + private readonly LayoutService layoutService; + private readonly ILogger logger; + private readonly SigUtil sigUtil; + private readonly IDataManager dataManager; + + private readonly FileDialogManager fileDialog = new() + { + AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking + }; + + public void Dispose() + { + // TODO release managed resources here + } + + public string Name => "Terrain"; + public int Order => 0; + public MenuType MenuType => MenuType.Debug; + + public TerrainTab( + LayoutService layoutService, + ILogger logger, + SigUtil sigUtil, + IDataManager dataManager) + { + this.layoutService = layoutService; + this.logger = logger; + this.sigUtil = sigUtil; + this.dataManager = dataManager; + cts = new CancellationTokenSource(); + } + + + private CancellationTokenSource cts; + private Task task = Task.CompletedTask; + private ExportType exportType = ExportType.GLTF; + public enum ExportType + { + GLTF, + OBJ, + GLB + } + + public void DrawInner() + { + ImGui.Text("Note: textures exported from here are not manipulated in any way at this stage."); + ImGui.Text("To combine with world objects, use the layout tab and export with origin mode set to 'Zero'."); + + if (ImGui.BeginCombo("Export Type", exportType.ToString())) + { + foreach (ExportType type in Enum.GetValues(typeof(ExportType))) + { + var isSelected = type == exportType; + if (ImGui.Selectable(type.ToString(), isSelected)) + { + exportType = type; + } + + if (isSelected) + { + ImGui.SetItemDefaultFocus(); + } + } + + ImGui.EndCombo(); + } + + if (task.IsFaulted) + { + var ex = task.Exception; + ImGui.TextWrapped($"Error: {ex}"); + } + + if (task.IsCompleted) + { + /*using (var disabled = ImRaii.Disabled()) + { + if (ImGui.Button("Export Full Layout")) + { + var dirname = $"Export-{DateTime.Now:yyyy-MM-dd-HH-mm-ss}"; + fileDialog.SaveFolderDialog("Select folder to dump to", dirname, + (result, path) => + { + cts = new CancellationTokenSource(); + if (!result) return; + task = ComposeAll(path, exportType, cts.Token); + }, Plugin.TempDirectory); + } + }*/ + if (ImGui.Button($"Export Terrain")) + { + var dirname = $"Export-{DateTime.Now:yyyy-MM-dd-HH-mm-ss}"; + fileDialog.SaveFolderDialog("Select folder to dump to", dirname, + (result, path) => + { + cts = new CancellationTokenSource(); + if (!result) return; + task = ComposeTerrain(path, exportType, cts.Token); + }, Plugin.TempDirectory); + } + } + else + { + if (LastProgress != null) + { + var (progress, total) = LastProgress; + ImGui.ProgressBar(progress / (float)total, new Vector2(-1, 0), $"Progress: {progress}/{total}"); + } + + if (ImGui.Button("Cancel")) + { + cts.Cancel(); + } + } + } + + public void Draw() + { + DrawInner(); + fileDialog.Draw(); + } + + private ProgressEvent? LastProgress { get; set; } + + public unsafe Task ComposeTerrain(string outDir, ExportType exportFormat, CancellationToken cancellationToken = default) + { + logger.LogInformation("Parsing terrain"); + var layoutWorld = sigUtil.GetLayoutWorld(); + if (layoutWorld == null) return Task.CompletedTask; + var activeLayout = layoutWorld->ActiveLayout; + if (activeLayout == null) return Task.CompletedTask; + + var scene = new SceneBuilder(); + Directory.CreateDirectory(outDir); + var cacheDir = Path.Combine(outDir, "cache"); + Directory.CreateDirectory(cacheDir); + + var teraFiles = new HashSet(); + foreach (var (_, terrainPtr) in activeLayout->Terrains) + { + if (terrainPtr == null || terrainPtr.Value == null) continue; + var terrain = terrainPtr.Value; + var terrainDir = terrain->PathString; + teraFiles.Add(terrainDir); + } + + return Task.Run(() => + { + foreach (var dir in teraFiles) + { + var terrainSet = new TerrainSet(logger, dataManager, dir, cacheDir, x => LastProgress = x, cancellationToken); + terrainSet.Compose(scene); + } + + var sceneGraph = scene.ToGltf2(); + switch (exportFormat) + { + case ExportType.GLTF: + sceneGraph.SaveGLTF(Path.Combine(outDir, "composed.gltf")); + break; + case ExportType.OBJ: + sceneGraph.SaveAsWavefront(Path.Combine(outDir, "composed.obj")); + break; + case ExportType.GLB: + sceneGraph.SaveGLB(Path.Combine(outDir, "composed.glb")); + break; + } + + Process.Start("explorer.exe", outDir); + GC.Collect(); + }, cancellationToken); + } + + public unsafe Task ComposeAll(string outDir, ExportType exportFormat, CancellationToken cancellationToken = default) + { + logger.LogInformation("Parsing terrain"); + var layoutWorld = sigUtil.GetLayoutWorld(); + if (layoutWorld == null) return Task.CompletedTask; + var activeLayout = layoutWorld->ActiveLayout; + if (activeLayout == null) return Task.CompletedTask; + + var scene = new SceneBuilder(); + Directory.CreateDirectory(outDir); + var cacheDir = Path.Combine(outDir, "cache"); + Directory.CreateDirectory(cacheDir); + + var teraFiles = new HashSet(); + foreach (var (_, terrainPtr) in activeLayout->Terrains) + { + if (terrainPtr == null || terrainPtr.Value == null) continue; + var terrain = terrainPtr.Value; + var terrainDir = terrain->PathString; + teraFiles.Add(terrainDir); + } + + var layout = layoutService.GetWorldState(); + + return Task.Run(() => + { + foreach (var dir in teraFiles) + { + var terrainSet = new TerrainSet(logger, dataManager, dir, cacheDir, x => LastProgress = x, cancellationToken); + terrainSet.Compose(scene); + } + + if (layout != null) + { + var instanceSet = new InstanceSet(logger, dataManager, layout, cacheDir, x => LastProgress = x, cancellationToken); + instanceSet.Compose(scene); + } + + var sceneGraph = scene.ToGltf2(); + switch (exportFormat) + { + case ExportType.GLTF: + sceneGraph.SaveGLTF(Path.Combine(outDir, "composed.gltf")); + break; + case ExportType.OBJ: + sceneGraph.SaveAsWavefront(Path.Combine(outDir, "composed.obj")); + break; + case ExportType.GLB: + sceneGraph.SaveGLB(Path.Combine(outDir, "composed.glb")); + break; + } + + Process.Start("explorer.exe", outDir); + GC.Collect(); + }, cancellationToken); + } +} diff --git a/Meddle/Meddle.Utils/Materials/MaterialUtility.cs b/Meddle/Meddle.Utils/Materials/MaterialUtility.cs index 753944f..281c97a 100644 --- a/Meddle/Meddle.Utils/Materials/MaterialUtility.cs +++ b/Meddle/Meddle.Utils/Materials/MaterialUtility.cs @@ -2,6 +2,7 @@ using Meddle.Utils.Export; using Meddle.Utils.Models; using SharpGLTF.Materials; +using SharpGLTF.Memory; using SkiaSharp; namespace Meddle.Utils.Materials; @@ -49,7 +50,8 @@ public static MaterialBuilder BuildFallback(Material material, string name) TextureUsage.g_SamplerGradationMap => "gradationMap", TextureUsage.g_SamplerNormal2 => "normal2", TextureUsage.g_SamplerWrinklesMask => "wrinklesMask", - _ => throw new ArgumentOutOfRangeException() + // _ => throw new ArgumentOutOfRangeException($"Unknown texture usage: {usage}") + _ => $"unknown_{usage}" }; KnownChannel knownChannel = usage switch @@ -95,6 +97,29 @@ 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; @@ -125,7 +150,7 @@ public static Vector4 Clamp(this Vector4 v, float min, float max) public static SKColor ToSkColor(this Vector3 color) => new((byte)(color.X * 255), (byte)(color.Y * 255), (byte)(color.Z * 255), byte.MaxValue); - public static ImageBuilder BuildImage(SKTexture texture, string materialName, string suffix) + /*public static ImageBuilder BuildImage(SKTexture texture, string materialName, string suffix) { var name = $"{Path.GetFileNameWithoutExtension(materialName)}_{suffix}"; @@ -139,6 +164,24 @@ public static ImageBuilder BuildImage(SKTexture texture, string materialName, st var imageBuilder = ImageBuilder.From(textureBytes, name); imageBuilder.AlternateWriteFileName = $"{name}.*"; return imageBuilder; + }*/ + public static ImageBuilder BuildImage(SKTexture texture, string materialName, string suffix) + { + var name = $"{Path.GetFileNameWithoutExtension(materialName)}_{suffix}"; + + byte[] textureBytes; + using (var memoryStream = new MemoryStream()) + { + texture.Bitmap.Encode(memoryStream, SKEncodedImageFormat.Png, 100); + textureBytes = memoryStream.ToArray(); + } + + var tempPath = Path.GetTempFileName(); + File.WriteAllBytes(tempPath, textureBytes); + + var imageBuilder = ImageBuilder.From(new MemoryImage(() => File.ReadAllBytes(tempPath)), name); + imageBuilder.AlternateWriteFileName = $"{name}.*"; + return imageBuilder; } public static SKTexture MultiplyBitmaps(SKTexture target, SKTexture multiplier, bool preserveTargetAlpha = true)