mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Resource Tree: Improve mtrl and sklb support
This commit is contained in:
parent
ecfe88faa6
commit
ccc0b51a99
5 changed files with 129 additions and 43 deletions
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData;
|
||||
|
|
@ -23,17 +24,17 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
|
|||
CharacterArmor Equipment)
|
||||
{
|
||||
private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true);
|
||||
private ResourceNode? CreateNodeFromShpk(nint sourceAddress, ByteString gamePath, bool @internal)
|
||||
private unsafe ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath, bool @internal)
|
||||
{
|
||||
if (gamePath.IsEmpty)
|
||||
return null;
|
||||
if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false))
|
||||
return null;
|
||||
|
||||
return CreateNodeFromGamePath(ResourceType.Shpk, sourceAddress, path, @internal);
|
||||
return CreateNodeFromGamePath(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->Handle, path, @internal);
|
||||
}
|
||||
|
||||
private ResourceNode? CreateNodeFromTex(nint sourceAddress, ByteString gamePath, bool @internal, bool dx11)
|
||||
private unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool @internal, bool dx11)
|
||||
{
|
||||
if (dx11)
|
||||
{
|
||||
|
|
@ -59,13 +60,19 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
|
|||
if (!Utf8GamePath.FromByteString(gamePath, out var path))
|
||||
return null;
|
||||
|
||||
return CreateNodeFromGamePath(ResourceType.Tex, sourceAddress, path, @internal);
|
||||
return CreateNodeFromGamePath(ResourceType.Tex, (nint)resourceHandle->KernelTexture, &resourceHandle->Handle, path, @internal);
|
||||
}
|
||||
|
||||
private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddress, Utf8GamePath gamePath, bool @internal)
|
||||
=> new(null, type, sourceAddress, gamePath, FilterFullPath(Collection.ResolvePath(gamePath) ?? new FullPath(gamePath)), @internal);
|
||||
private unsafe ResourceNode CreateNodeFromGamePath(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, Utf8GamePath gamePath, bool @internal)
|
||||
{
|
||||
var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(resourceHandle), out var p) ? new FullPath(p) : FullPath.Empty;
|
||||
if (fullPath.InternalName.IsEmpty)
|
||||
fullPath = Collection.ResolvePath(gamePath) ?? new FullPath(gamePath);
|
||||
|
||||
private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal,
|
||||
return new(null, type, objectAddress, (nint)resourceHandle, gamePath, FilterFullPath(fullPath), GetResourceHandleLength(resourceHandle), @internal);
|
||||
}
|
||||
|
||||
private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint objectAddress, ResourceHandle* handle, bool @internal,
|
||||
bool withName)
|
||||
{
|
||||
var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty;
|
||||
|
|
@ -79,13 +86,14 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
|
|||
gamePaths = Filter(gamePaths);
|
||||
|
||||
if (gamePaths.Count == 1)
|
||||
return new ResourceNode(withName ? GuessNameFromPath(gamePaths[0]) : null, type, sourceAddress, gamePaths[0], fullPath, @internal);
|
||||
return new ResourceNode(withName ? GuessNameFromPath(gamePaths[0]) : null, type, objectAddress, (nint)handle, gamePaths[0], fullPath,
|
||||
GetResourceHandleLength(handle), @internal);
|
||||
|
||||
Penumbra.Log.Information($"Found {gamePaths.Count} game paths while reverse-resolving {fullPath} in {Collection.Name}:");
|
||||
foreach (var gamePath in gamePaths)
|
||||
Penumbra.Log.Information($"Game path: {gamePath}");
|
||||
|
||||
return new ResourceNode(null, type, sourceAddress, gamePaths.ToArray(), fullPath, @internal);
|
||||
return new ResourceNode(null, type, objectAddress, (nint)handle, gamePaths.ToArray(), fullPath, GetResourceHandleLength(handle), @internal);
|
||||
}
|
||||
public unsafe ResourceNode? CreateHumanSkeletonNode(GenderRace gr)
|
||||
{
|
||||
|
|
@ -95,12 +103,12 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
|
|||
if (!Utf8GamePath.FromString(path, out var gamePath))
|
||||
return null;
|
||||
|
||||
return CreateNodeFromGamePath(ResourceType.Sklb, 0, gamePath, false);
|
||||
return CreateNodeFromGamePath(ResourceType.Sklb, 0, null, gamePath, false);
|
||||
}
|
||||
|
||||
public unsafe ResourceNode? CreateNodeFromImc(ResourceHandle* imc)
|
||||
{
|
||||
var node = CreateNodeFromResourceHandle(ResourceType.Imc, (nint) imc, imc, true, false);
|
||||
var node = CreateNodeFromResourceHandle(ResourceType.Imc, 0, imc, true, false);
|
||||
if (node == null)
|
||||
return null;
|
||||
|
||||
|
|
@ -113,8 +121,8 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
|
|||
return node;
|
||||
}
|
||||
|
||||
public unsafe ResourceNode? CreateNodeFromTex(ResourceHandle* tex)
|
||||
=> CreateNodeFromResourceHandle(ResourceType.Tex, (nint) tex, tex, false, WithNames);
|
||||
public unsafe ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex)
|
||||
=> CreateNodeFromResourceHandle(ResourceType.Tex, (nint)tex->KernelTexture, &tex->Handle, false, WithNames);
|
||||
|
||||
public unsafe ResourceNode? CreateNodeFromRenderModel(RenderModel* mdl)
|
||||
{
|
||||
|
|
@ -145,6 +153,38 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
|
|||
|
||||
private unsafe ResourceNode? CreateNodeFromMaterial(Material* mtrl)
|
||||
{
|
||||
static ushort GetTextureIndex(ushort texFlags)
|
||||
{
|
||||
if ((texFlags & 0x001F) != 0x001F)
|
||||
return (ushort)(texFlags & 0x001F);
|
||||
else if ((texFlags & 0x03E0) != 0x03E0)
|
||||
return (ushort)((texFlags >> 5) & 0x001F);
|
||||
else
|
||||
return (ushort)((texFlags >> 10) & 0x001F);
|
||||
}
|
||||
static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle)
|
||||
{
|
||||
var textures = mtrl->Textures;
|
||||
for (var i = 0; i < mtrl->TextureCount; ++i)
|
||||
{
|
||||
if (textures[i].ResourceHandle == handle)
|
||||
return textures[i].Id;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
static uint? GetSamplerCrcById(ShaderPackage* shpk, uint id)
|
||||
{
|
||||
var samplers = (ShaderPackageUtility.Sampler*)shpk->Samplers;
|
||||
for (var i = 0; i < shpk->SamplerCount; ++i)
|
||||
{
|
||||
if (samplers[i].Id == id)
|
||||
return samplers[i].Crc;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mtrl == null)
|
||||
return null;
|
||||
|
||||
|
|
@ -153,23 +193,36 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
|
|||
if (node == null)
|
||||
return null;
|
||||
|
||||
var mtrlFile = WithNames ? TreeBuildCache.ReadMaterial(node.FullPath) : null;
|
||||
|
||||
var shpkNode = CreateNodeFromShpk(nint.Zero, new ByteString(resource->ShpkString), false);
|
||||
var shpkNode = CreateNodeFromShpk(resource->ShpkResourceHandle, new ByteString(resource->ShpkString), false);
|
||||
if (shpkNode != null)
|
||||
node.Children.Add(WithNames ? shpkNode.WithName("Shader Package") : shpkNode);
|
||||
var shpkFile = WithNames && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null;
|
||||
var samplers = WithNames ? mtrlFile?.GetSamplersByTexture(shpkFile) : null;
|
||||
var shpk = WithNames && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null;
|
||||
|
||||
for (var i = 0; i < resource->NumTex; i++)
|
||||
{
|
||||
var texNode = CreateNodeFromTex(nint.Zero, new ByteString(resource->TexString(i)), false, resource->TexIsDX11(i));
|
||||
var texNode = CreateNodeFromTex(resource->TexSpace[i].ResourceHandle, new ByteString(resource->TexString(i)), false, resource->TexIsDX11(i));
|
||||
if (texNode == null)
|
||||
continue;
|
||||
|
||||
if (WithNames)
|
||||
{
|
||||
var name = samplers != null && i < samplers.Length ? samplers[i].ShpkSampler?.Name : null;
|
||||
string? name = null;
|
||||
if (shpk != null)
|
||||
{
|
||||
var index = GetTextureIndex(resource->TexSpace[i].Flags);
|
||||
uint? samplerId;
|
||||
if (index != 0x001F)
|
||||
samplerId = mtrl->Textures[index].Id;
|
||||
else
|
||||
samplerId = GetTextureSamplerId(mtrl, resource->TexSpace[i].ResourceHandle);
|
||||
if (samplerId.HasValue)
|
||||
{
|
||||
var samplerCrc = GetSamplerCrcById(shpk, samplerId.Value);
|
||||
if (samplerCrc.HasValue)
|
||||
name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}";
|
||||
}
|
||||
}
|
||||
node.Children.Add(texNode.WithName(name ?? $"Texture #{i}"));
|
||||
}
|
||||
else
|
||||
|
|
@ -181,6 +234,14 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
|
|||
return node;
|
||||
}
|
||||
|
||||
public unsafe ResourceNode? CreateNodeFromPartialSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton* sklb)
|
||||
{
|
||||
if (sklb->SkeletonResourceHandle == null)
|
||||
return null;
|
||||
|
||||
return CreateNodeFromResourceHandle(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, false, WithNames);
|
||||
}
|
||||
|
||||
private FullPath FilterFullPath(FullPath fullPath)
|
||||
{
|
||||
if (!fullPath.IsRooted)
|
||||
|
|
@ -294,4 +355,12 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
|
|||
|
||||
return name;
|
||||
}
|
||||
|
||||
static unsafe ulong GetResourceHandleLength(ResourceHandle* handle)
|
||||
{
|
||||
if (handle == null)
|
||||
return 0;
|
||||
|
||||
return ResourceHandle.GetLength(handle);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,37 +9,43 @@ public class ResourceNode
|
|||
{
|
||||
public readonly string? Name;
|
||||
public readonly ResourceType Type;
|
||||
public readonly nint SourceAddress;
|
||||
public readonly nint ObjectAddress;
|
||||
public readonly nint ResourceHandle;
|
||||
public readonly Utf8GamePath GamePath;
|
||||
public readonly Utf8GamePath[] PossibleGamePaths;
|
||||
public readonly FullPath FullPath;
|
||||
public readonly ulong Length;
|
||||
public readonly bool Internal;
|
||||
public readonly List<ResourceNode> Children;
|
||||
|
||||
public ResourceNode(string? name, ResourceType type, nint sourceAddress, Utf8GamePath gamePath, FullPath fullPath, bool @internal)
|
||||
public ResourceNode(string? name, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath gamePath, FullPath fullPath, ulong length, bool @internal)
|
||||
{
|
||||
Name = name;
|
||||
Type = type;
|
||||
SourceAddress = sourceAddress;
|
||||
GamePath = gamePath;
|
||||
Name = name;
|
||||
Type = type;
|
||||
ObjectAddress = objectAddress;
|
||||
ResourceHandle = resourceHandle;
|
||||
GamePath = gamePath;
|
||||
PossibleGamePaths = new[]
|
||||
{
|
||||
gamePath,
|
||||
};
|
||||
FullPath = fullPath;
|
||||
Length = length;
|
||||
Internal = @internal;
|
||||
Children = new List<ResourceNode>();
|
||||
}
|
||||
|
||||
public ResourceNode(string? name, ResourceType type, nint sourceAddress, Utf8GamePath[] possibleGamePaths, FullPath fullPath,
|
||||
bool @internal)
|
||||
public ResourceNode(string? name, ResourceType type, nint objectAddress, nint resourceHandle, Utf8GamePath[] possibleGamePaths, FullPath fullPath,
|
||||
ulong length, bool @internal)
|
||||
{
|
||||
Name = name;
|
||||
Type = type;
|
||||
SourceAddress = sourceAddress;
|
||||
ObjectAddress = objectAddress;
|
||||
ResourceHandle = resourceHandle;
|
||||
GamePath = possibleGamePaths.Length == 1 ? possibleGamePaths[0] : Utf8GamePath.Empty;
|
||||
PossibleGamePaths = possibleGamePaths;
|
||||
FullPath = fullPath;
|
||||
Length = length;
|
||||
Internal = @internal;
|
||||
Children = new List<ResourceNode>();
|
||||
}
|
||||
|
|
@ -48,10 +54,12 @@ public class ResourceNode
|
|||
{
|
||||
Name = name;
|
||||
Type = originalResourceNode.Type;
|
||||
SourceAddress = originalResourceNode.SourceAddress;
|
||||
ObjectAddress = originalResourceNode.ObjectAddress;
|
||||
ResourceHandle = originalResourceNode.ResourceHandle;
|
||||
GamePath = originalResourceNode.GamePath;
|
||||
PossibleGamePaths = originalResourceNode.PossibleGamePaths;
|
||||
FullPath = originalResourceNode.FullPath;
|
||||
Length = originalResourceNode.Length;
|
||||
Internal = originalResourceNode.Internal;
|
||||
Children = originalResourceNode.Children;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
|
@ -57,7 +58,9 @@ public class ResourceTree
|
|||
var mdlNode = context.CreateNodeFromRenderModel(mdl);
|
||||
if (mdlNode != null)
|
||||
Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode);
|
||||
}
|
||||
}
|
||||
|
||||
AddSkeleton(Nodes, globalContext.CreateContext(EquipSlot.Unknown, default), model->Skeleton);
|
||||
|
||||
if (character->GameObject.GetObjectKind() == (byte)ObjectKind.Pc)
|
||||
AddHumanResources(globalContext, (HumanExt*)model);
|
||||
|
|
@ -95,7 +98,9 @@ public class ResourceTree
|
|||
subObjectNodes.Add(globalContext.WithNames
|
||||
? mdlNode.WithName(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}")
|
||||
: mdlNode);
|
||||
}
|
||||
}
|
||||
|
||||
AddSkeleton(subObjectNodes, subObjectContext, subObject->Skeleton, $"{subObjectNamePrefix} #{subObjectIndex}, ");
|
||||
|
||||
subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject;
|
||||
++subObjectIndex;
|
||||
|
|
@ -106,16 +111,25 @@ public class ResourceTree
|
|||
|
||||
var context = globalContext.CreateContext(EquipSlot.Unknown, default);
|
||||
|
||||
var skeletonNode = context.CreateHumanSkeletonNode((GenderRace)human->Human.RaceSexId);
|
||||
if (skeletonNode != null)
|
||||
Nodes.Add(globalContext.WithNames ? skeletonNode.WithName(skeletonNode.Name ?? "Skeleton") : skeletonNode);
|
||||
|
||||
var decalNode = context.CreateNodeFromTex(human->Decal);
|
||||
var decalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->Decal);
|
||||
if (decalNode != null)
|
||||
Nodes.Add(globalContext.WithNames ? decalNode.WithName(decalNode.Name ?? "Face Decal") : decalNode);
|
||||
|
||||
var legacyDecalNode = context.CreateNodeFromTex(human->LegacyBodyDecal);
|
||||
var legacyDecalNode = context.CreateNodeFromTex((TextureResourceHandle*)human->LegacyBodyDecal);
|
||||
if (legacyDecalNode != null)
|
||||
Nodes.Add(globalContext.WithNames ? legacyDecalNode.WithName(legacyDecalNode.Name ?? "Legacy Body Decal") : legacyDecalNode);
|
||||
}
|
||||
|
||||
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, Skeleton* skeleton, string prefix = "")
|
||||
{
|
||||
if (skeleton == null)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
|
||||
{
|
||||
var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i]);
|
||||
if (sklbNode != null)
|
||||
nodes.Add(context.WithNames ? sklbNode.WithName($"{prefix}Skeleton #{i}") : sklbNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ namespace Penumbra.Interop.ResourceTree;
|
|||
internal class TreeBuildCache
|
||||
{
|
||||
private readonly IDataManager _dataManager;
|
||||
private readonly Dictionary<FullPath, MtrlFile?> _materials = new();
|
||||
private readonly Dictionary<FullPath, ShpkFile?> _shaderPackages = new();
|
||||
public readonly List<Character> Characters;
|
||||
public readonly Dictionary<uint, Character> CharactersById;
|
||||
|
|
@ -27,10 +26,6 @@ internal class TreeBuildCache
|
|||
.ToDictionary(c => c.Key, c => c.First());
|
||||
}
|
||||
|
||||
/// <summary> Try to read a material file from the given path and cache it on success. </summary>
|
||||
public MtrlFile? ReadMaterial(FullPath path)
|
||||
=> ReadFile(_dataManager, path, _materials, bytes => new MtrlFile(bytes));
|
||||
|
||||
/// <summary> Try to read a shpk file from the given path and cache it on success. </summary>
|
||||
public ShpkFile? ReadShaderPackage(FullPath path)
|
||||
=> ReadFile(_dataManager, path, _shaderPackages, bytes => new ShpkFile(bytes));
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ public class ResourceTreeViewer
|
|||
|
||||
if (debugMode)
|
||||
ImGuiUtil.HoverTooltip(
|
||||
$"Resource Type: {resourceNode.Type}\nSource Address: 0x{resourceNode.SourceAddress:X16}");
|
||||
$"Resource Type: {resourceNode.Type}\nObject Address: 0x{resourceNode.ObjectAddress:X16}\nResource Handle: 0x{resourceNode.ResourceHandle:X16}\nLength: 0x{resourceNode.Length:X}");
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue