Restructure Live Preview.

This commit is contained in:
Ottermandias 2023-08-31 18:25:29 +02:00
parent e5e555b981
commit a768b039a8
6 changed files with 478 additions and 518 deletions

View file

@ -0,0 +1,131 @@
using System;
using System.Threading;
using Dalamud.Game;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using Penumbra.GameData.Files;
namespace Penumbra.Interop.MaterialPreview;
public sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase
{
public const int TextureWidth = 4;
public const int TextureHeight = MtrlFile.ColorSet.RowArray.NumRows;
public const int TextureLength = TextureWidth * TextureHeight * 4;
private readonly Framework _framework;
private readonly Texture** _colorSetTexture;
private readonly Texture* _originalColorSetTexture;
private Half[] _colorSet;
private bool _updatePending;
public Half[] ColorSet
=> _colorSet;
public LiveColorSetPreviewer(IObjectTable objects, Framework framework, MaterialInfo materialInfo)
: base(objects, materialInfo)
{
_framework = framework;
var mtrlHandle = Material->MaterialResourceHandle;
if (mtrlHandle == null)
throw new InvalidOperationException("Material doesn't have a resource handle");
var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures;
if (colorSetTextures == null)
throw new InvalidOperationException("Draw object doesn't have color set textures");
_colorSetTexture = colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot);
_originalColorSetTexture = *_colorSetTexture;
if (_originalColorSetTexture == null)
throw new InvalidOperationException("Material doesn't have a color set");
Structs.TextureUtility.IncRef(_originalColorSetTexture);
_colorSet = new Half[TextureLength];
_updatePending = true;
framework.Update += OnFrameworkUpdate;
}
protected override void Clear(bool disposing, bool reset)
{
_framework.Update -= OnFrameworkUpdate;
base.Clear(disposing, reset);
if (reset)
{
var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture);
if (oldTexture != null)
Structs.TextureUtility.DecRef(oldTexture);
}
else
{
Structs.TextureUtility.DecRef(_originalColorSetTexture);
}
}
public void ScheduleUpdate()
{
_updatePending = true;
}
private void OnFrameworkUpdate(Framework _)
{
if (!_updatePending)
return;
_updatePending = false;
if (!CheckValidity())
return;
var textureSize = stackalloc int[2];
textureSize[0] = TextureWidth;
textureSize[1] = TextureHeight;
var newTexture = Structs.TextureUtility.Create2D(Device.Instance(), textureSize, 1, 0x2460, 0x80000804, 7);
if (newTexture == null)
return;
bool success;
lock (_colorSet)
{
fixed (Half* colorSet = _colorSet)
{
success = Structs.TextureUtility.InitializeContents(newTexture, colorSet);
}
}
if (success)
{
var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)newTexture);
if (oldTexture != null)
Structs.TextureUtility.DecRef(oldTexture);
}
else
{
Structs.TextureUtility.DecRef(newTexture);
}
}
protected override bool IsStillValid()
{
if (!base.IsStillValid())
return false;
var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures;
if (colorSetTextures == null)
return false;
if (_colorSetTexture != colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot))
return false;
return true;
}
}

View file

@ -0,0 +1,149 @@
using System;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
namespace Penumbra.Interop.MaterialPreview;
public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
{
private readonly ShaderPackage* _shaderPackage;
private readonly uint _originalShPkFlags;
private readonly float[] _originalMaterialParameter;
private readonly uint[] _originalSamplerFlags;
public LiveMaterialPreviewer(IObjectTable objects, MaterialInfo materialInfo)
: base(objects, materialInfo)
{
var mtrlHandle = Material->MaterialResourceHandle;
if (mtrlHandle == null)
throw new InvalidOperationException("Material doesn't have a resource handle");
var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle;
if (shpkHandle == null)
throw new InvalidOperationException("Material doesn't have a ShPk resource handle");
_shaderPackage = shpkHandle->ShaderPackage;
if (_shaderPackage == null)
throw new InvalidOperationException("Material doesn't have a shader package");
var material = (Structs.Material*)Material;
_originalShPkFlags = material->ShaderPackageFlags;
if (material->MaterialParameter->TryGetBuffer(out var materialParameter))
_originalMaterialParameter = materialParameter.ToArray();
else
_originalMaterialParameter = Array.Empty<float>();
_originalSamplerFlags = new uint[material->TextureCount];
for (var i = 0; i < _originalSamplerFlags.Length; ++i)
_originalSamplerFlags[i] = material->Textures[i].SamplerFlags;
}
protected override void Clear(bool disposing, bool reset)
{
base.Clear(disposing, reset);
if (reset)
{
var material = (Structs.Material*)Material;
material->ShaderPackageFlags = _originalShPkFlags;
if (material->MaterialParameter->TryGetBuffer(out var materialParameter))
_originalMaterialParameter.AsSpan().CopyTo(materialParameter);
for (var i = 0; i < _originalSamplerFlags.Length; ++i)
material->Textures[i].SamplerFlags = _originalSamplerFlags[i];
}
}
public void SetShaderPackageFlags(uint shPkFlags)
{
if (!CheckValidity())
return;
((Structs.Material*)Material)->ShaderPackageFlags = shPkFlags;
}
public void SetMaterialParameter(uint parameterCrc, Index offset, Span<float> value)
{
if (!CheckValidity())
return;
var constantBuffer = ((Structs.Material*)Material)->MaterialParameter;
if (constantBuffer == null)
return;
if (!constantBuffer->TryGetBuffer(out var buffer))
return;
for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i)
{
ref var parameter = ref _shaderPackage->MaterialElements[i];
if (parameter.CRC == parameterCrc)
{
if ((parameter.Offset & 0x3) != 0
|| (parameter.Size & 0x3) != 0
|| (parameter.Offset + parameter.Size) >> 2 > buffer.Length)
return;
value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]);
return;
}
}
}
public void SetSamplerFlags(uint samplerCrc, uint samplerFlags)
{
if (!CheckValidity())
return;
var id = 0u;
var found = false;
var samplers = (Structs.ShaderPackageUtility.Sampler*)_shaderPackage->Samplers;
for (var i = 0; i < _shaderPackage->SamplerCount; ++i)
{
if (samplers[i].Crc == samplerCrc)
{
id = samplers[i].Id;
found = true;
break;
}
}
if (!found)
return;
var material = (Structs.Material*)Material;
for (var i = 0; i < material->TextureCount; ++i)
{
if (material->Textures[i].Id == id)
{
material->Textures[i].SamplerFlags = (samplerFlags & 0xFFFFFDFF) | 0x000001C0;
break;
}
}
}
protected override bool IsStillValid()
{
if (!base.IsStillValid())
return false;
var mtrlHandle = Material->MaterialResourceHandle;
if (mtrlHandle == null)
return false;
var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle;
if (shpkHandle == null)
return false;
if (_shaderPackage != shpkHandle->ShaderPackage)
return false;
return true;
}
}

View file

@ -0,0 +1,70 @@
using System;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
namespace Penumbra.Interop.MaterialPreview;
public abstract unsafe class LiveMaterialPreviewerBase : IDisposable
{
private readonly IObjectTable _objects;
public readonly MaterialInfo MaterialInfo;
public readonly CharacterBase* DrawObject;
protected readonly Material* Material;
protected bool Valid;
public LiveMaterialPreviewerBase(IObjectTable objects, MaterialInfo materialInfo)
{
_objects = objects;
MaterialInfo = materialInfo;
var gameObject = MaterialInfo.GetCharacter(objects);
if (gameObject == nint.Zero)
throw new InvalidOperationException("Cannot retrieve game object.");
DrawObject = (CharacterBase*)MaterialInfo.GetDrawObject(gameObject);
if (DrawObject == null)
throw new InvalidOperationException("Cannot retrieve draw object.");
Material = MaterialInfo.GetDrawObjectMaterial(DrawObject);
if (Material == null)
throw new InvalidOperationException("Cannot retrieve material.");
Valid = true;
}
public void Dispose()
{
if (Valid)
Clear(true, IsStillValid());
}
public bool CheckValidity()
{
if (Valid && !IsStillValid())
Clear(false, false);
return Valid;
}
protected virtual void Clear(bool disposing, bool reset)
{
Valid = false;
}
protected virtual bool IsStillValid()
{
var gameObject = MaterialInfo.GetCharacter(_objects);
if (gameObject == nint.Zero)
return false;
if ((nint)DrawObject != MaterialInfo.GetDrawObject(gameObject))
return false;
if (Material != MaterialInfo.GetDrawObjectMaterial(DrawObject))
return false;
return true;
}
}

View file

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.Interop.ResourceTree;
using Penumbra.String;
namespace Penumbra.Interop.MaterialPreview;
public enum DrawObjectType
{
PlayerCharacter,
PlayerMainhand,
PlayerOffhand,
PlayerVfx,
MinionCharacter,
MinionUnk1,
MinionUnk2,
MinionUnk3,
};
public readonly record struct MaterialInfo(DrawObjectType Type, int ModelSlot, int MaterialSlot)
{
public nint GetCharacter(IObjectTable objects)
=> GetCharacter(Type, objects);
public static nint GetCharacter(DrawObjectType type, IObjectTable objects)
=> type switch
{
DrawObjectType.PlayerCharacter => objects.GetObjectAddress(0),
DrawObjectType.PlayerMainhand => objects.GetObjectAddress(0),
DrawObjectType.PlayerOffhand => objects.GetObjectAddress(0),
DrawObjectType.PlayerVfx => objects.GetObjectAddress(0),
DrawObjectType.MinionCharacter => objects.GetObjectAddress(1),
DrawObjectType.MinionUnk1 => objects.GetObjectAddress(1),
DrawObjectType.MinionUnk2 => objects.GetObjectAddress(1),
DrawObjectType.MinionUnk3 => objects.GetObjectAddress(1),
_ => nint.Zero,
};
public nint GetDrawObject(nint address)
=> GetDrawObject(Type, address);
public static nint GetDrawObject(DrawObjectType type, IObjectTable objects)
=> GetDrawObject(type, GetCharacter(type, objects));
public static unsafe nint GetDrawObject(DrawObjectType type, nint address)
{
var gameObject = (Character*)address;
if (gameObject == null)
return nint.Zero;
return type switch
{
DrawObjectType.PlayerCharacter => (nint)gameObject->GameObject.GetDrawObject(),
DrawObjectType.PlayerMainhand => *((nint*)&gameObject->DrawData.MainHand + 1),
DrawObjectType.PlayerOffhand => *((nint*)&gameObject->DrawData.OffHand + 1),
DrawObjectType.PlayerVfx => *((nint*)&gameObject->DrawData.UnkF0 + 1),
DrawObjectType.MinionCharacter => (nint)gameObject->GameObject.GetDrawObject(),
DrawObjectType.MinionUnk1 => *((nint*)&gameObject->DrawData.MainHand + 1),
DrawObjectType.MinionUnk2 => *((nint*)&gameObject->DrawData.OffHand + 1),
DrawObjectType.MinionUnk3 => *((nint*)&gameObject->DrawData.UnkF0 + 1),
_ => nint.Zero,
};
}
public unsafe Material* GetDrawObjectMaterial(CharacterBase* drawObject)
{
if (drawObject == null)
return null;
if (ModelSlot < 0 || ModelSlot >= drawObject->SlotCount)
return null;
var model = drawObject->Models[ModelSlot];
if (model == null)
return null;
if (MaterialSlot < 0 || MaterialSlot >= model->MaterialCount)
return null;
return model->Materials[MaterialSlot];
}
public static unsafe List<MaterialInfo> FindMaterials(IObjectTable objects, string materialPath)
{
var needle = ByteString.FromString(materialPath.Replace('/', '\\'), out var m, true) ? m : ByteString.Empty;
var result = new List<MaterialInfo>(Enum.GetValues<DrawObjectType>().Length);
foreach (var type in Enum.GetValues<DrawObjectType>())
{
var drawObject = (CharacterBase*)GetDrawObject(type, objects);
if (drawObject == null)
continue;
for (var i = 0; i < drawObject->SlotCount; ++i)
{
var model = drawObject->Models[i];
if (model == null)
continue;
for (var j = 0; j < model->MaterialCount; ++j)
{
var material = model->Materials[j];
if (material == null)
continue;
var mtrlHandle = material->MaterialResourceHandle;
var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle);
if (path == needle)
result.Add(new MaterialInfo(type, i, j));
}
}
}
return result;
}
}

View file

@ -1,483 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Dalamud.Game;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Files;
using Penumbra.Interop.ResourceTree;
using Penumbra.String;
using Structs = Penumbra.Interop.Structs;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private static unsafe Character* FindLocalPlayer(IObjectTable objects)
{
var localPlayer = objects[0];
if (localPlayer is not Dalamud.Game.ClientState.Objects.Types.Character)
return null;
return (Character*)localPlayer.Address;
}
private static unsafe Character* FindSubActor(Character* character, int subActorType)
{
if (character == null)
return null;
switch (subActorType)
{
case -1:
return character;
case 0:
return character->Mount.MountObject;
case 1:
var companion = character->Companion.CompanionObject;
if (companion == null)
return null;
return &companion->Character;
case 2:
var ornament = character->Ornament.OrnamentObject;
if (ornament == null)
return null;
return &ornament->Character;
default:
return null;
}
}
private static unsafe List<(int SubActorType, int ChildObjectIndex, int ModelSlot, int MaterialSlot)> FindMaterial(CharacterBase* drawObject, int subActorType, string materialPath)
{
static void CollectMaterials(List<(int, int, int, int)> result, int subActorType, int childObjectIndex, CharacterBase* drawObject, ByteString materialPath)
{
for (var i = 0; i < drawObject->SlotCount; ++i)
{
var model = drawObject->Models[i];
if (model == null)
continue;
for (var j = 0; j < model->MaterialCount; ++j)
{
var material = model->Materials[j];
if (material == null)
continue;
var mtrlHandle = material->MaterialResourceHandle;
var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle);
if (path == materialPath)
result.Add((subActorType, childObjectIndex, i, j));
}
}
}
var result = new List<(int, int, int, int)>();
if (drawObject == null)
return result;
var path = ByteString.FromString(materialPath.Replace('/', '\\'), out var m, true) ? m : ByteString.Empty;
CollectMaterials(result, subActorType, -1, drawObject, path);
var firstChildObject = (CharacterBase*)drawObject->DrawObject.Object.ChildObject;
if (firstChildObject != null)
{
var childObject = firstChildObject;
var childObjectIndex = 0;
do
{
CollectMaterials(result, subActorType, childObjectIndex, childObject, path);
childObject = (CharacterBase*)childObject->DrawObject.Object.NextSiblingObject;
++childObjectIndex;
}
while (childObject != null && childObject != firstChildObject);
}
return result;
}
private static unsafe CharacterBase* GetChildObject(CharacterBase* drawObject, int index)
{
if (drawObject == null)
return null;
if (index >= 0)
{
drawObject = (CharacterBase*)drawObject->DrawObject.Object.ChildObject;
if (drawObject == null)
return null;
}
var first = drawObject;
while (index-- > 0)
{
drawObject = (CharacterBase*)drawObject->DrawObject.Object.NextSiblingObject;
if (drawObject == null || drawObject == first)
return null;
}
return drawObject;
}
private static unsafe Material* GetDrawObjectMaterial(CharacterBase* drawObject, int modelSlot, int materialSlot)
{
if (drawObject == null)
return null;
if (modelSlot < 0 || modelSlot >= drawObject->SlotCount)
return null;
var model = drawObject->Models[modelSlot];
if (model == null)
return null;
if (materialSlot < 0 || materialSlot >= model->MaterialCount)
return null;
return model->Materials[materialSlot];
}
private abstract unsafe class LiveMaterialPreviewerBase : IDisposable
{
private readonly IObjectTable _objects;
protected readonly int SubActorType;
protected readonly int ChildObjectIndex;
protected readonly int ModelSlot;
protected readonly int MaterialSlot;
public readonly CharacterBase* DrawObject;
protected readonly Material* Material;
protected bool Valid;
public LiveMaterialPreviewerBase(IObjectTable objects, int subActorType, int childObjectIndex, int modelSlot, int materialSlot)
{
_objects = objects;
SubActorType = subActorType;
ChildObjectIndex = childObjectIndex;
ModelSlot = modelSlot;
MaterialSlot = materialSlot;
var localPlayer = FindLocalPlayer(objects);
if (localPlayer == null)
throw new InvalidOperationException("Cannot retrieve local player object");
var subActor = FindSubActor(localPlayer, subActorType);
if (subActor == null)
throw new InvalidOperationException("Cannot retrieve sub-actor (mount, companion or ornament)");
DrawObject = GetChildObject((CharacterBase*)subActor->GameObject.GetDrawObject(), childObjectIndex);
if (DrawObject == null)
throw new InvalidOperationException("Cannot retrieve draw object");
Material = GetDrawObjectMaterial(DrawObject, modelSlot, materialSlot);
if (Material == null)
throw new InvalidOperationException("Cannot retrieve material");
Valid = true;
}
~LiveMaterialPreviewerBase()
{
if (Valid)
Dispose(false, IsStillValid());
}
public void Dispose()
{
if (Valid)
Dispose(true, IsStillValid());
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing, bool reset)
{
Valid = false;
}
public bool CheckValidity()
{
if (Valid && !IsStillValid())
Dispose(false, false);
return Valid;
}
protected virtual bool IsStillValid()
{
var localPlayer = FindLocalPlayer(_objects);
if (localPlayer == null)
return false;
var subActor = FindSubActor(localPlayer, SubActorType);
if (subActor == null)
return false;
if (DrawObject != GetChildObject((CharacterBase*)subActor->GameObject.GetDrawObject(), ChildObjectIndex))
return false;
if (Material != GetDrawObjectMaterial(DrawObject, ModelSlot, MaterialSlot))
return false;
return true;
}
}
private sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase
{
private readonly ShaderPackage* _shaderPackage;
private readonly uint _originalShPkFlags;
private readonly float[] _originalMaterialParameter;
private readonly uint[] _originalSamplerFlags;
public LiveMaterialPreviewer(IObjectTable objects, int subActorType, int childObjectIndex, int modelSlot, int materialSlot) : base(objects, subActorType, childObjectIndex, modelSlot, materialSlot)
{
var mtrlHandle = Material->MaterialResourceHandle;
if (mtrlHandle == null)
throw new InvalidOperationException("Material doesn't have a resource handle");
var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle;
if (shpkHandle == null)
throw new InvalidOperationException("Material doesn't have a ShPk resource handle");
_shaderPackage = shpkHandle->ShaderPackage;
if (_shaderPackage == null)
throw new InvalidOperationException("Material doesn't have a shader package");
var material = (Structs.Material*)Material;
_originalShPkFlags = material->ShaderPackageFlags;
if (material->MaterialParameter->TryGetBuffer(out var materialParameter))
_originalMaterialParameter = materialParameter.ToArray();
else
_originalMaterialParameter = Array.Empty<float>();
_originalSamplerFlags = new uint[material->TextureCount];
for (var i = 0; i < _originalSamplerFlags.Length; ++i)
_originalSamplerFlags[i] = material->Textures[i].SamplerFlags;
}
protected override void Dispose(bool disposing, bool reset)
{
base.Dispose(disposing, reset);
if (reset)
{
var material = (Structs.Material*)Material;
material->ShaderPackageFlags = _originalShPkFlags;
if (material->MaterialParameter->TryGetBuffer(out var materialParameter))
_originalMaterialParameter.AsSpan().CopyTo(materialParameter);
for (var i = 0; i < _originalSamplerFlags.Length; ++i)
material->Textures[i].SamplerFlags = _originalSamplerFlags[i];
}
}
public void SetShaderPackageFlags(uint shPkFlags)
{
if (!CheckValidity())
return;
((Structs.Material*)Material)->ShaderPackageFlags = shPkFlags;
}
public void SetMaterialParameter(uint parameterCrc, Index offset, Span<float> value)
{
if (!CheckValidity())
return;
var cbuffer = ((Structs.Material*)Material)->MaterialParameter;
if (cbuffer == null)
return;
if (!cbuffer->TryGetBuffer(out var buffer))
return;
for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i)
{
ref var parameter = ref _shaderPackage->MaterialElements[i];
if (parameter.CRC == parameterCrc)
{
if ((parameter.Offset & 0x3) != 0 || (parameter.Size & 0x3) != 0 || (parameter.Offset + parameter.Size) >> 2 > buffer.Length)
return;
value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]);
return;
}
}
}
public void SetSamplerFlags(uint samplerCrc, uint samplerFlags)
{
if (!CheckValidity())
return;
var id = 0u;
var found = false;
var samplers = (Structs.ShaderPackageUtility.Sampler*)_shaderPackage->Samplers;
for (var i = 0; i < _shaderPackage->SamplerCount; ++i)
{
if (samplers[i].Crc == samplerCrc)
{
id = samplers[i].Id;
found = true;
break;
}
}
if (!found)
return;
var material = (Structs.Material*)Material;
for (var i = 0; i < material->TextureCount; ++i)
{
if (material->Textures[i].Id == id)
{
material->Textures[i].SamplerFlags = (samplerFlags & 0xFFFFFDFF) | 0x000001C0;
break;
}
}
}
protected override bool IsStillValid()
{
if (!base.IsStillValid())
return false;
var mtrlHandle = Material->MaterialResourceHandle;
if (mtrlHandle == null)
return false;
var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle;
if (shpkHandle == null)
return false;
if (_shaderPackage != shpkHandle->ShaderPackage)
return false;
return true;
}
}
private sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase
{
public const int TextureWidth = 4;
public const int TextureHeight = MtrlFile.ColorSet.RowArray.NumRows;
public const int TextureLength = TextureWidth * TextureHeight * 4;
private readonly Framework _framework;
private readonly Texture** _colorSetTexture;
private readonly Texture* _originalColorSetTexture;
private Half[] _colorSet;
private bool _updatePending;
public Half[] ColorSet => _colorSet;
public LiveColorSetPreviewer(IObjectTable objects, Framework framework, int subActorType, int childObjectIndex, int modelSlot, int materialSlot) : base(objects, subActorType, childObjectIndex, modelSlot, materialSlot)
{
_framework = framework;
var mtrlHandle = Material->MaterialResourceHandle;
if (mtrlHandle == null)
throw new InvalidOperationException("Material doesn't have a resource handle");
var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures;
if (colorSetTextures == null)
throw new InvalidOperationException("Draw object doesn't have color set textures");
_colorSetTexture = colorSetTextures + (modelSlot * 4 + materialSlot);
_originalColorSetTexture = *_colorSetTexture;
if (_originalColorSetTexture == null)
throw new InvalidOperationException("Material doesn't have a color set");
Structs.TextureUtility.IncRef(_originalColorSetTexture);
_colorSet = new Half[TextureLength];
_updatePending = true;
framework.Update += OnFrameworkUpdate;
}
protected override void Dispose(bool disposing, bool reset)
{
_framework.Update -= OnFrameworkUpdate;
base.Dispose(disposing, reset);
if (reset)
{
var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture);
if (oldTexture != null)
Structs.TextureUtility.DecRef(oldTexture);
}
else
Structs.TextureUtility.DecRef(_originalColorSetTexture);
}
public void ScheduleUpdate()
{
_updatePending = true;
}
private void OnFrameworkUpdate(Framework _)
{
if (!_updatePending)
return;
_updatePending = false;
if (!CheckValidity())
return;
var textureSize = stackalloc int[2];
textureSize[0] = TextureWidth;
textureSize[1] = TextureHeight;
var newTexture = Structs.TextureUtility.Create2D(Device.Instance(), textureSize, 1, 0x2460, 0x80000804, 7);
if (newTexture == null)
return;
bool success;
lock (_colorSet)
fixed (Half* colorSet = _colorSet)
success = Structs.TextureUtility.InitializeContents(newTexture, colorSet);
if (success)
{
var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)newTexture);
if (oldTexture != null)
Structs.TextureUtility.DecRef(oldTexture);
}
else
Structs.TextureUtility.DecRef(newTexture);
}
protected override bool IsStillValid()
{
if (!base.IsStillValid())
return false;
var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures;
if (colorSetTextures == null)
return false;
if (_colorSetTexture != colorSetTextures + (ModelSlot * 4 + MaterialSlot))
return false;
return true;
}
}
}

View file

@ -16,6 +16,7 @@ using Penumbra.GameData;
using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
using Penumbra.GameData.Structs;
using Penumbra.Interop.MaterialPreview;
using Penumbra.String;
using Penumbra.String.Classes;
using static Penumbra.GameData.Files.ShpkFile;
@ -460,46 +461,19 @@ public partial class ModEditWindow
{
UnbindFromMaterialInstances();
var localPlayer = LocalPlayer(_edit._dalamud.Objects);
if (null == localPlayer)
return;
var drawObject = (CharacterBase*)localPlayer->GameObject.GetDrawObject();
if (null == drawObject)
return;
var instances = FindMaterial(drawObject, -1, FilePath);
var drawObjects = stackalloc CharacterBase*[4];
drawObjects[0] = drawObject;
drawObjects[1] = *((CharacterBase**)&localPlayer->DrawData.MainHand + 1);
drawObjects[2] = *((CharacterBase**)&localPlayer->DrawData.OffHand + 1);
drawObjects[3] = *((CharacterBase**)&localPlayer->DrawData.UnkF0 + 1);
for (var i = 0; i < 3; ++i)
{
var subActor = FindSubActor(localPlayer, i);
if (null == subActor)
continue;
var subDrawObject = (CharacterBase*)subActor->GameObject.GetDrawObject();
if (null == subDrawObject)
continue;
instances.AddRange(FindMaterial(subDrawObject, i, FilePath));
drawObjects[i + 1] = subDrawObject;
}
var instances = MaterialInfo.FindMaterials(_edit._dalamud.Objects, FilePath);
var foundMaterials = new HashSet<nint>();
foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances)
foreach (var materialInfo in instances)
{
var material = GetDrawObjectMaterial(drawObjects[subActorType + 1], modelSlot, materialSlot);
var drawObject = (CharacterBase*)MaterialInfo.GetDrawObject(materialInfo.Type, _edit._dalamud.Objects);
var material = materialInfo.GetDrawObjectMaterial(drawObject);
if (foundMaterials.Contains((nint)material))
continue;
try
{
MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, subActorType, childObjectIndex, modelSlot,
materialSlot));
MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, materialInfo));
foundMaterials.Add((nint)material);
}
catch (InvalidOperationException)
@ -515,12 +489,11 @@ public partial class ModEditWindow
if (!colorSet.HasValue)
return;
foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances)
foreach (var materialInfo in instances)
{
try
{
ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, subActorType,
childObjectIndex, modelSlot, materialSlot));
ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, materialInfo));
}
catch (InvalidOperationException)
{