mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Restructure Live Preview.
This commit is contained in:
parent
e5e555b981
commit
a768b039a8
6 changed files with 478 additions and 518 deletions
131
Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs
Normal file
131
Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
149
Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs
Normal file
149
Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
120
Penumbra/Interop/MaterialPreview/MaterialInfo.cs
Normal file
120
Penumbra/Interop/MaterialPreview/MaterialInfo.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -16,6 +16,7 @@ using Penumbra.GameData;
|
||||||
using Penumbra.GameData.Data;
|
using Penumbra.GameData.Data;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
using Penumbra.Interop.MaterialPreview;
|
||||||
using Penumbra.String;
|
using Penumbra.String;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
using static Penumbra.GameData.Files.ShpkFile;
|
using static Penumbra.GameData.Files.ShpkFile;
|
||||||
|
|
@ -460,46 +461,19 @@ public partial class ModEditWindow
|
||||||
{
|
{
|
||||||
UnbindFromMaterialInstances();
|
UnbindFromMaterialInstances();
|
||||||
|
|
||||||
var localPlayer = LocalPlayer(_edit._dalamud.Objects);
|
var instances = MaterialInfo.FindMaterials(_edit._dalamud.Objects, FilePath);
|
||||||
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 foundMaterials = new HashSet<nint>();
|
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))
|
if (foundMaterials.Contains((nint)material))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, subActorType, childObjectIndex, modelSlot,
|
MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, materialInfo));
|
||||||
materialSlot));
|
|
||||||
foundMaterials.Add((nint)material);
|
foundMaterials.Add((nint)material);
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException)
|
catch (InvalidOperationException)
|
||||||
|
|
@ -515,12 +489,11 @@ public partial class ModEditWindow
|
||||||
if (!colorSet.HasValue)
|
if (!colorSet.HasValue)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var (subActorType, childObjectIndex, modelSlot, materialSlot) in instances)
|
foreach (var materialInfo in instances)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, subActorType,
|
ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, materialInfo));
|
||||||
childObjectIndex, modelSlot, materialSlot));
|
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException)
|
catch (InvalidOperationException)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue