merge 2.0.0 into migration
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using LightlessSync.API.Dto.Chat;
|
||||
|
||||
namespace LightlessSync.Services.Chat;
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
using LightlessSync;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.Chat;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.WebAPI;
|
||||
|
||||
@@ -1,114 +1,254 @@
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.Gui.NamePlate;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.NativeWrapper;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
using static LightlessSync.UI.DtrEntry;
|
||||
using LSeStringBuilder = Lumina.Text.SeStringBuilder;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public class NameplateService : DisposableMediatorSubscriberBase
|
||||
/// <summary>
|
||||
/// NameplateService is used for coloring our nameplates based on the settings of the user.
|
||||
/// </summary>
|
||||
public unsafe class NameplateService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private delegate nint UpdateNameplateDelegate(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex);
|
||||
|
||||
// Glyceri, Thanks :bow:
|
||||
[Signature("40 53 55 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B 84 24", DetourName = nameof(UpdateNameplateDetour))]
|
||||
private readonly Hook<UpdateNameplateDelegate>? _nameplateHook = null;
|
||||
|
||||
private readonly ILogger<NameplateService> _logger;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly INamePlateGui _namePlateGui;
|
||||
private readonly IGameGui _gameGui;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly PairUiService _pairUiService;
|
||||
|
||||
public NameplateService(ILogger<NameplateService> logger,
|
||||
LightlessConfigService configService,
|
||||
INamePlateGui namePlateGui,
|
||||
IClientState clientState,
|
||||
PairUiService pairUiService,
|
||||
LightlessMediator lightlessMediator) : base(logger, lightlessMediator)
|
||||
IGameGui gameGui,
|
||||
IObjectTable objectTable,
|
||||
IGameInteropProvider interop,
|
||||
LightlessMediator lightlessMediator,
|
||||
PairUiService pairUiService) : base(logger, lightlessMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_namePlateGui = namePlateGui;
|
||||
_clientState = clientState;
|
||||
_gameGui = gameGui;
|
||||
_objectTable = objectTable;
|
||||
_pairUiService = pairUiService;
|
||||
|
||||
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
|
||||
_namePlateGui.RequestRedraw();
|
||||
Mediator.Subscribe<VisibilityChange>(this, (_) => _namePlateGui.RequestRedraw());
|
||||
interop.InitializeFromAttributes(this);
|
||||
_nameplateHook?.Enable();
|
||||
Refresh();
|
||||
|
||||
Mediator.Subscribe<VisibilityChange>(this, (_) => Refresh());
|
||||
}
|
||||
|
||||
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||
/// <summary>
|
||||
/// Detour for the game's internal nameplate update function.
|
||||
/// This will be called whenever the client updates any nameplate.
|
||||
///
|
||||
/// We hook into it to apply our own nameplate coloring logic via <see cref="SetNameplate"/>,
|
||||
/// </summary>
|
||||
private nint UpdateNameplateDetour(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
|
||||
{
|
||||
if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen))
|
||||
try
|
||||
{
|
||||
SetNameplate(namePlateInfo, battleChara);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error in NameplateService UpdateNameplateDetour");
|
||||
}
|
||||
|
||||
return _nameplateHook!.Original(raptureAtkModule, namePlateInfo, numArray, stringArray, battleChara, numArrayIndex, stringArrayIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the player should be colored based on conditions (isFriend, IsInParty)
|
||||
/// </summary>
|
||||
/// <param name="playerCharacter">Player character that will be checked</param>
|
||||
/// <param name="visibleUserIds">All visible users in the current object table</param>
|
||||
/// <returns>PLayer should or shouldnt be colored based on the result. True means colored</returns>
|
||||
private bool ShouldColorPlayer(IPlayerCharacter playerCharacter, HashSet<ulong> visibleUserIds)
|
||||
{
|
||||
if (!visibleUserIds.Contains(playerCharacter.GameObjectId))
|
||||
return false;
|
||||
|
||||
var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember);
|
||||
var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend);
|
||||
|
||||
bool partyColorAllowed = _configService.Current.overridePartyColor && isInParty;
|
||||
bool friendColorAllowed = _configService.Current.overrideFriendColor && isFriend;
|
||||
|
||||
if ((isInParty && !partyColorAllowed) || (isFriend && !friendColorAllowed))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setting up the nameplate of the user to be colored
|
||||
/// </summary>
|
||||
/// <param name="namePlateInfo">Information given from the Signature to be updated</param>
|
||||
/// <param name="battleChara">Character from FF</param>
|
||||
private void SetNameplate(RaptureAtkModule.NamePlateInfo* namePlateInfo, BattleChara* battleChara)
|
||||
{
|
||||
if (!_configService.Current.IsNameplateColorsEnabled || _clientState.IsPvPExcludingDen)
|
||||
return;
|
||||
if (namePlateInfo == null || battleChara == null)
|
||||
return;
|
||||
|
||||
var obj = _objectTable.FirstOrDefault(o => o.Address == (nint)battleChara);
|
||||
if (obj is not IPlayerCharacter player)
|
||||
return;
|
||||
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
var visibleUsersIds = snapshot.PairsByUid.Values
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(u => (ulong)u.PlayerCharacterId)
|
||||
.ToHashSet();
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(u => (ulong)u.PlayerCharacterId)
|
||||
.ToHashSet();
|
||||
|
||||
var colors = _configService.Current.NameplateColors;
|
||||
//Check if player should be colored
|
||||
if (!ShouldColorPlayer(player, visibleUsersIds))
|
||||
return;
|
||||
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
var playerCharacter = handler.PlayerCharacter;
|
||||
if (playerCharacter == null)
|
||||
continue;
|
||||
var originalName = player.Name.ToString();
|
||||
|
||||
var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember);
|
||||
var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend);
|
||||
bool partyColorAllowed = (_configService.Current.overridePartyColor && isInParty);
|
||||
bool friendColorAllowed = (_configService.Current.overrideFriendColor && isFriend);
|
||||
//Check if not null of the name
|
||||
if (string.IsNullOrEmpty(originalName))
|
||||
return;
|
||||
|
||||
if (visibleUsersIds.Contains(handler.GameObjectId) &&
|
||||
!(
|
||||
(isInParty && !partyColorAllowed) ||
|
||||
(isFriend && !friendColorAllowed)
|
||||
))
|
||||
{
|
||||
handler.NameParts.TextWrap = CreateTextWrap(colors);
|
||||
//Check if any characters/symbols are forbidden
|
||||
if (HasForbiddenSeStringChars(originalName))
|
||||
return;
|
||||
|
||||
if (_configService.Current.overrideFcTagColor)
|
||||
{
|
||||
bool hasActualFcTag = playerCharacter.CompanyTag.TextValue.Length > 0;
|
||||
bool isFromDifferentRealm = playerCharacter.HomeWorld.RowId != playerCharacter.CurrentWorld.RowId;
|
||||
bool shouldColorFcArea = hasActualFcTag || (!hasActualFcTag && isFromDifferentRealm);
|
||||
//Swap color channels as we store them in BGR format as FF loves that
|
||||
var cfgColors = SwapColorChannels(_configService.Current.NameplateColors);
|
||||
var coloredName = WrapStringInColor(originalName, cfgColors.Glow, cfgColors.Foreground);
|
||||
|
||||
if (shouldColorFcArea)
|
||||
{
|
||||
handler.FreeCompanyTagParts.OuterWrap = CreateTextWrap(colors);
|
||||
handler.FreeCompanyTagParts.TextWrap = CreateTextWrap(colors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//Replace string of nameplate with our colored one
|
||||
namePlateInfo->Name.SetString(coloredName.EncodeWithNullTerminator());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts Uint code to Vector4 as we store Colors in Uint in our config, needed for lumina
|
||||
/// </summary>
|
||||
/// <param name="rgb">Color code</param>
|
||||
/// <returns>Vector4 Color</returns>
|
||||
private static Vector4 RgbUintToVector4(uint rgb)
|
||||
{
|
||||
float r = ((rgb >> 16) & 0xFF) / 255f;
|
||||
float g = ((rgb >> 8) & 0xFF) / 255f;
|
||||
float b = (rgb & 0xFF) / 255f;
|
||||
return new Vector4(r, g, b, 1f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the string has any forbidden characters/symbols as the string builder wouldnt append.
|
||||
/// </summary>
|
||||
/// <param name="s">String that has to be checked</param>
|
||||
/// <returns>Contains forbidden characters/symbols or not</returns>
|
||||
private static bool HasForbiddenSeStringChars(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s))
|
||||
return false;
|
||||
|
||||
foreach (var ch in s)
|
||||
{
|
||||
if (ch == '\0' || ch == '\u0002')
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps the given string with the given edge and text color.
|
||||
/// </summary>
|
||||
/// <param name="text">String that has to be wrapped</param>
|
||||
/// <param name="edgeColor">Edge(border) color</param>
|
||||
/// <param name="textColor">Text color</param>
|
||||
/// <returns>Color wrapped SeString</returns>
|
||||
public static SeString WrapStringInColor(string text, uint? edgeColor = null, uint? textColor = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return SeString.Empty;
|
||||
|
||||
var builder = new LSeStringBuilder();
|
||||
|
||||
if (textColor is uint tc)
|
||||
builder.PushColorRgba(RgbUintToVector4(tc));
|
||||
|
||||
if (edgeColor is uint ec)
|
||||
builder.PushEdgeColorRgba(RgbUintToVector4(ec));
|
||||
|
||||
builder.Append(text);
|
||||
|
||||
if (edgeColor != null)
|
||||
builder.PopEdgeColor();
|
||||
|
||||
if (textColor != null)
|
||||
builder.PopColor();
|
||||
|
||||
return builder.ToReadOnlySeString().ToDalamudString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request redraw of nameplates
|
||||
/// </summary>
|
||||
public void RequestRedraw()
|
||||
{
|
||||
_namePlateGui.RequestRedraw();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private static (SeString, SeString) CreateTextWrap(DtrEntry.Colors color)
|
||||
/// <summary>
|
||||
/// Toggles the refresh of the Nameplate addon
|
||||
/// </summary>
|
||||
protected void Refresh()
|
||||
{
|
||||
var left = new Lumina.Text.SeStringBuilder();
|
||||
var right = new Lumina.Text.SeStringBuilder();
|
||||
AtkUnitBasePtr namePlateAddon = _gameGui.GetAddonByName("NamePlate");
|
||||
|
||||
left.PushColorRgba(color.Foreground);
|
||||
right.PopColor();
|
||||
if (namePlateAddon.IsNull)
|
||||
{
|
||||
_logger.LogInformation("NamePlate addon is null, cannot refresh nameplates.");
|
||||
return;
|
||||
}
|
||||
|
||||
left.PushEdgeColorRgba(color.Glow);
|
||||
right.PopEdgeColor();
|
||||
var addonNamePlate = (AddonNamePlate*)namePlateAddon.Address;
|
||||
|
||||
return (left.ToReadOnlySeString().ToDalamudString(), right.ToReadOnlySeString().ToDalamudString());
|
||||
if (addonNamePlate == null)
|
||||
{
|
||||
_logger.LogInformation("addonNamePlate addon is null, cannot refresh nameplates.");
|
||||
return;
|
||||
}
|
||||
|
||||
addonNamePlate->DoFullUpdate = 1;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (disposing)
|
||||
{
|
||||
_nameplateHook?.Dispose();
|
||||
}
|
||||
|
||||
_namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate;
|
||||
_namePlateGui.RequestRedraw();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
312
LightlessSync/Services/TextureCompression/IndexDownscaler.cs
Normal file
312
LightlessSync/Services/TextureCompression/IndexDownscaler.cs
Normal file
@@ -0,0 +1,312 @@
|
||||
using System.Numerics;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
/*
|
||||
* Index upscaler code (converted/reversed for downscaling purposes) provided by Ny
|
||||
* thank you!!
|
||||
*/
|
||||
|
||||
namespace LightlessSync.Services.TextureCompression;
|
||||
|
||||
internal static class IndexDownscaler
|
||||
{
|
||||
private static readonly Vector2[] SampleOffsets =
|
||||
{
|
||||
new(0.25f, 0.25f),
|
||||
new(0.75f, 0.25f),
|
||||
new(0.25f, 0.75f),
|
||||
new(0.75f, 0.75f),
|
||||
};
|
||||
|
||||
public static Image<Rgba32> Downscale(Image<Rgba32> source, int targetWidth, int targetHeight, int blockMultiple)
|
||||
{
|
||||
var current = source.Clone();
|
||||
|
||||
while (current.Width > targetWidth || current.Height > targetHeight)
|
||||
{
|
||||
var nextWidth = Math.Max(targetWidth, Math.Max(blockMultiple, current.Width / 2));
|
||||
var nextHeight = Math.Max(targetHeight, Math.Max(blockMultiple, current.Height / 2));
|
||||
var next = new Image<Rgba32>(nextWidth, nextHeight);
|
||||
|
||||
for (var y = 0; y < nextHeight; y++)
|
||||
{
|
||||
var srcY = Math.Min(current.Height - 1, y * 2);
|
||||
for (var x = 0; x < nextWidth; x++)
|
||||
{
|
||||
var srcX = Math.Min(current.Width - 1, x * 2);
|
||||
|
||||
var topLeft = current[srcX, srcY];
|
||||
var topRight = current[Math.Min(current.Width - 1, srcX + 1), srcY];
|
||||
var bottomLeft = current[srcX, Math.Min(current.Height - 1, srcY + 1)];
|
||||
var bottomRight = current[Math.Min(current.Width - 1, srcX + 1), Math.Min(current.Height - 1, srcY + 1)];
|
||||
|
||||
next[x, y] = DownscaleIndexBlock(topLeft, topRight, bottomLeft, bottomRight);
|
||||
}
|
||||
}
|
||||
|
||||
current.Dispose();
|
||||
current = next;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private static Rgba32 DownscaleIndexBlock(in Rgba32 topLeft, in Rgba32 topRight, in Rgba32 bottomLeft, in Rgba32 bottomRight)
|
||||
{
|
||||
Span<Rgba32> ordered = stackalloc Rgba32[4]
|
||||
{
|
||||
bottomLeft,
|
||||
bottomRight,
|
||||
topRight,
|
||||
topLeft
|
||||
};
|
||||
|
||||
Span<float> weights = stackalloc float[4];
|
||||
var hasContribution = false;
|
||||
|
||||
foreach (var sample in SampleOffsets)
|
||||
{
|
||||
if (TryAccumulateSampleWeights(ordered, sample, weights))
|
||||
{
|
||||
hasContribution = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasContribution)
|
||||
{
|
||||
var bestIndex = IndexOfMax(weights);
|
||||
if (bestIndex >= 0 && weights[bestIndex] > 0f)
|
||||
{
|
||||
return ordered[bestIndex];
|
||||
}
|
||||
}
|
||||
|
||||
Span<Rgba32> fallback = stackalloc Rgba32[4] { topLeft, topRight, bottomLeft, bottomRight };
|
||||
return PickMajorityColor(fallback);
|
||||
}
|
||||
|
||||
private static bool TryAccumulateSampleWeights(ReadOnlySpan<Rgba32> colors, in Vector2 sampleUv, Span<float> weights)
|
||||
{
|
||||
var red = new Vector4(
|
||||
colors[0].R / 255f,
|
||||
colors[1].R / 255f,
|
||||
colors[2].R / 255f,
|
||||
colors[3].R / 255f);
|
||||
|
||||
var symbols = QuantizeSymbols(red);
|
||||
var cellUv = ComputeShiftedUv(sampleUv);
|
||||
|
||||
Span<int> order = stackalloc int[4];
|
||||
order[0] = 0;
|
||||
order[1] = 1;
|
||||
order[2] = 2;
|
||||
order[3] = 3;
|
||||
|
||||
ApplySymmetry(ref symbols, ref cellUv, order);
|
||||
|
||||
var equality = BuildEquality(symbols, symbols.W);
|
||||
var selector = BuildSelector(equality, symbols, cellUv);
|
||||
|
||||
const uint lut = 0x00000C07u;
|
||||
|
||||
if (((lut >> (int)selector) & 1u) != 0u)
|
||||
{
|
||||
weights[order[3]] += 1f;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selector == 3u)
|
||||
{
|
||||
equality = BuildEquality(symbols, symbols.Z);
|
||||
}
|
||||
|
||||
var weight = ComputeWeight(equality, cellUv);
|
||||
if (weight <= 1e-6f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var factor = 1f / weight;
|
||||
|
||||
var wW = equality.W * (1f - cellUv.X) * (1f - cellUv.Y) * factor;
|
||||
var wX = equality.X * (1f - cellUv.X) * cellUv.Y * factor;
|
||||
var wZ = equality.Z * cellUv.X * (1f - cellUv.Y) * factor;
|
||||
var wY = equality.Y * cellUv.X * cellUv.Y * factor;
|
||||
|
||||
var contributed = false;
|
||||
|
||||
if (wW > 0f)
|
||||
{
|
||||
weights[order[3]] += wW;
|
||||
contributed = true;
|
||||
}
|
||||
|
||||
if (wX > 0f)
|
||||
{
|
||||
weights[order[0]] += wX;
|
||||
contributed = true;
|
||||
}
|
||||
|
||||
if (wZ > 0f)
|
||||
{
|
||||
weights[order[2]] += wZ;
|
||||
contributed = true;
|
||||
}
|
||||
|
||||
if (wY > 0f)
|
||||
{
|
||||
weights[order[1]] += wY;
|
||||
contributed = true;
|
||||
}
|
||||
|
||||
return contributed;
|
||||
}
|
||||
|
||||
private static Vector4 QuantizeSymbols(in Vector4 channel)
|
||||
=> new(
|
||||
Quantize(channel.X),
|
||||
Quantize(channel.Y),
|
||||
Quantize(channel.Z),
|
||||
Quantize(channel.W));
|
||||
|
||||
private static float Quantize(float value)
|
||||
{
|
||||
var clamped = Math.Clamp(value, 0f, 1f);
|
||||
return (MathF.Round(clamped * 16f) + 0.5f) / 16f;
|
||||
}
|
||||
|
||||
private static void ApplySymmetry(ref Vector4 symbols, ref Vector2 cellUv, Span<int> order)
|
||||
{
|
||||
if (cellUv.X >= 0.5f)
|
||||
{
|
||||
symbols = SwapYxwz(symbols, order);
|
||||
cellUv.X = 1f - cellUv.X;
|
||||
}
|
||||
|
||||
if (cellUv.Y >= 0.5f)
|
||||
{
|
||||
symbols = SwapWzyx(symbols, order);
|
||||
cellUv.Y = 1f - cellUv.Y;
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector4 BuildEquality(in Vector4 symbols, float reference)
|
||||
=> new(
|
||||
AreEqual(symbols.X, reference) ? 1f : 0f,
|
||||
AreEqual(symbols.Y, reference) ? 1f : 0f,
|
||||
AreEqual(symbols.Z, reference) ? 1f : 0f,
|
||||
AreEqual(symbols.W, reference) ? 1f : 0f);
|
||||
|
||||
private static uint BuildSelector(in Vector4 equality, in Vector4 symbols, in Vector2 cellUv)
|
||||
{
|
||||
uint selector = 0;
|
||||
if (equality.X > 0.5f) selector |= 4u;
|
||||
if (equality.Y > 0.5f) selector |= 8u;
|
||||
if (equality.Z > 0.5f) selector |= 16u;
|
||||
if (AreEqual(symbols.X, symbols.Z)) selector |= 2u;
|
||||
if (cellUv.X + cellUv.Y >= 0.5f) selector |= 1u;
|
||||
|
||||
return selector;
|
||||
}
|
||||
|
||||
private static float ComputeWeight(in Vector4 equality, in Vector2 cellUv)
|
||||
=> equality.W * (1f - cellUv.X) * (1f - cellUv.Y)
|
||||
+ equality.X * (1f - cellUv.X) * cellUv.Y
|
||||
+ equality.Z * cellUv.X * (1f - cellUv.Y)
|
||||
+ equality.Y * cellUv.X * cellUv.Y;
|
||||
|
||||
private static Vector2 ComputeShiftedUv(in Vector2 uv)
|
||||
{
|
||||
var shifted = new Vector2(
|
||||
uv.X - MathF.Floor(uv.X),
|
||||
uv.Y - MathF.Floor(uv.Y));
|
||||
|
||||
shifted.X -= 0.5f;
|
||||
if (shifted.X < 0f)
|
||||
{
|
||||
shifted.X += 1f;
|
||||
}
|
||||
|
||||
shifted.Y -= 0.5f;
|
||||
if (shifted.Y < 0f)
|
||||
{
|
||||
shifted.Y += 1f;
|
||||
}
|
||||
|
||||
return shifted;
|
||||
}
|
||||
|
||||
private static Vector4 SwapYxwz(in Vector4 v, Span<int> order)
|
||||
{
|
||||
var o0 = order[0];
|
||||
var o1 = order[1];
|
||||
var o2 = order[2];
|
||||
var o3 = order[3];
|
||||
|
||||
order[0] = o1;
|
||||
order[1] = o0;
|
||||
order[2] = o3;
|
||||
order[3] = o2;
|
||||
|
||||
return new Vector4(v.Y, v.X, v.W, v.Z);
|
||||
}
|
||||
|
||||
private static Vector4 SwapWzyx(in Vector4 v, Span<int> order)
|
||||
{
|
||||
var o0 = order[0];
|
||||
var o1 = order[1];
|
||||
var o2 = order[2];
|
||||
var o3 = order[3];
|
||||
|
||||
order[0] = o3;
|
||||
order[1] = o2;
|
||||
order[2] = o1;
|
||||
order[3] = o0;
|
||||
|
||||
return new Vector4(v.W, v.Z, v.Y, v.X);
|
||||
}
|
||||
|
||||
private static int IndexOfMax(ReadOnlySpan<float> values)
|
||||
{
|
||||
var bestIndex = -1;
|
||||
var bestValue = 0f;
|
||||
|
||||
for (var i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (values[i] > bestValue)
|
||||
{
|
||||
bestValue = values[i];
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
private static bool AreEqual(float a, float b) => MathF.Abs(a - b) <= 1e-5f;
|
||||
|
||||
private static Rgba32 PickMajorityColor(ReadOnlySpan<Rgba32> colors)
|
||||
{
|
||||
var counts = new Dictionary<Rgba32, int>(colors.Length);
|
||||
foreach (var color in colors)
|
||||
{
|
||||
if (counts.TryGetValue(color, out var count))
|
||||
{
|
||||
counts[color] = count + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
counts[color] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return counts
|
||||
.OrderByDescending(kvp => kvp.Value)
|
||||
.ThenByDescending(kvp => kvp.Key.A)
|
||||
.ThenByDescending(kvp => kvp.Key.R)
|
||||
.ThenByDescending(kvp => kvp.Key.G)
|
||||
.ThenByDescending(kvp => kvp.Key.B)
|
||||
.First().Key;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Lumina.Data.Files;
|
||||
using OtterTex;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Penumbra.Api.Enums;
|
||||
|
||||
namespace LightlessSync.Services.TextureCompression;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LightlessSync.Services.TextureCompression;
|
||||
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.FileCache;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -2,7 +2,6 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Buffers.Binary;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.IO;
|
||||
using OtterTex;
|
||||
using OtterImage = OtterTex.Image;
|
||||
@@ -15,7 +14,6 @@ using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
/*
|
||||
* Index upscaler code (converted/reversed for downscaling purposes) provided by Ny
|
||||
* OtterTex made by Ottermandias
|
||||
* thank you!!
|
||||
*/
|
||||
@@ -183,7 +181,7 @@ public sealed class TextureDownscaleService
|
||||
return;
|
||||
}
|
||||
|
||||
using var resized = ReduceIndexTexture(originalImage, targetSize.width, targetSize.height);
|
||||
using var resized = IndexDownscaler.Downscale(originalImage, targetSize.width, targetSize.height, BlockMultiple);
|
||||
|
||||
var resizedPixels = new byte[targetSize.width * targetSize.height * 4];
|
||||
resized.CopyPixelDataTo(resizedPixels);
|
||||
@@ -231,8 +229,7 @@ public sealed class TextureDownscaleService
|
||||
|
||||
private static bool IsIndexMap(TextureMapKind kind)
|
||||
=> kind is TextureMapKind.Mask
|
||||
or TextureMapKind.Index
|
||||
or TextureMapKind.Ui;
|
||||
or TextureMapKind.Index;
|
||||
|
||||
private Task<bool> TryDropTopMipAsync(
|
||||
string hash,
|
||||
@@ -423,39 +420,6 @@ public sealed class TextureDownscaleService
|
||||
private static int ReduceDimension(int value)
|
||||
=> value <= 1 ? 1 : Math.Max(1, value / 2);
|
||||
|
||||
private static Image<Rgba32> ReduceIndexTexture(Image<Rgba32> source, int targetWidth, int targetHeight)
|
||||
{
|
||||
var current = source.Clone();
|
||||
|
||||
while (current.Width > targetWidth || current.Height > targetHeight)
|
||||
{
|
||||
var nextWidth = Math.Max(targetWidth, Math.Max(BlockMultiple, current.Width / 2));
|
||||
var nextHeight = Math.Max(targetHeight, Math.Max(BlockMultiple, current.Height / 2));
|
||||
var next = new Image<Rgba32>(nextWidth, nextHeight);
|
||||
|
||||
for (int y = 0; y < nextHeight; y++)
|
||||
{
|
||||
var srcY = Math.Min(current.Height - 1, y * 2);
|
||||
for (int x = 0; x < nextWidth; x++)
|
||||
{
|
||||
var srcX = Math.Min(current.Width - 1, x * 2);
|
||||
|
||||
var topLeft = current[srcX, srcY];
|
||||
var topRight = current[Math.Min(current.Width - 1, srcX + 1), srcY];
|
||||
var bottomLeft = current[srcX, Math.Min(current.Height - 1, srcY + 1)];
|
||||
var bottomRight = current[Math.Min(current.Width - 1, srcX + 1), Math.Min(current.Height - 1, srcY + 1)];
|
||||
|
||||
next[x, y] = DownscaleIndexBlock(topLeft, topRight, bottomLeft, bottomRight);
|
||||
}
|
||||
}
|
||||
|
||||
current.Dispose();
|
||||
current = next;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private static Image<Rgba32> ReduceLinearTexture(Image<Rgba32> source, int targetWidth, int targetHeight)
|
||||
{
|
||||
var clone = source.Clone();
|
||||
@@ -470,271 +434,6 @@ public sealed class TextureDownscaleService
|
||||
return clone;
|
||||
}
|
||||
|
||||
private static Rgba32 DownscaleIndexBlock(in Rgba32 topLeft, in Rgba32 topRight, in Rgba32 bottomLeft, in Rgba32 bottomRight)
|
||||
{
|
||||
Span<Rgba32> ordered = stackalloc Rgba32[4]
|
||||
{
|
||||
bottomLeft,
|
||||
bottomRight,
|
||||
topRight,
|
||||
topLeft
|
||||
};
|
||||
|
||||
Span<float> weights = stackalloc float[4];
|
||||
var hasContribution = false;
|
||||
|
||||
foreach (var sample in SampleOffsets)
|
||||
{
|
||||
if (TryAccumulateSampleWeights(ordered, sample, weights))
|
||||
{
|
||||
hasContribution = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasContribution)
|
||||
{
|
||||
var bestIndex = IndexOfMax(weights);
|
||||
if (bestIndex >= 0 && weights[bestIndex] > 0f)
|
||||
{
|
||||
return ordered[bestIndex];
|
||||
}
|
||||
}
|
||||
|
||||
Span<Rgba32> fallback = stackalloc Rgba32[4] { topLeft, topRight, bottomLeft, bottomRight };
|
||||
return PickMajorityColor(fallback);
|
||||
}
|
||||
|
||||
private static readonly Vector2[] SampleOffsets =
|
||||
{
|
||||
new(0.25f, 0.25f),
|
||||
new(0.75f, 0.25f),
|
||||
new(0.25f, 0.75f),
|
||||
new(0.75f, 0.75f),
|
||||
};
|
||||
|
||||
private static bool TryAccumulateSampleWeights(ReadOnlySpan<Rgba32> colors, in Vector2 sampleUv, Span<float> weights)
|
||||
{
|
||||
var red = new Vector4(
|
||||
colors[0].R / 255f,
|
||||
colors[1].R / 255f,
|
||||
colors[2].R / 255f,
|
||||
colors[3].R / 255f);
|
||||
|
||||
var symbols = QuantizeSymbols(red);
|
||||
var cellUv = ComputeShiftedUv(sampleUv);
|
||||
|
||||
Span<int> order = stackalloc int[4];
|
||||
order[0] = 0;
|
||||
order[1] = 1;
|
||||
order[2] = 2;
|
||||
order[3] = 3;
|
||||
|
||||
ApplySymmetry(ref symbols, ref cellUv, order);
|
||||
|
||||
var equality = BuildEquality(symbols, symbols.W);
|
||||
var selector = BuildSelector(equality, symbols, cellUv);
|
||||
|
||||
const uint lut = 0x00000C07u;
|
||||
|
||||
if (((lut >> (int)selector) & 1u) != 0u)
|
||||
{
|
||||
weights[order[3]] += 1f;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selector == 3u)
|
||||
{
|
||||
equality = BuildEquality(symbols, symbols.Z);
|
||||
}
|
||||
|
||||
var weight = ComputeWeight(equality, cellUv);
|
||||
if (weight <= 1e-6f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var factor = 1f / weight;
|
||||
|
||||
var wW = equality.W * (1f - cellUv.X) * (1f - cellUv.Y) * factor;
|
||||
var wX = equality.X * (1f - cellUv.X) * cellUv.Y * factor;
|
||||
var wZ = equality.Z * cellUv.X * (1f - cellUv.Y) * factor;
|
||||
var wY = equality.Y * cellUv.X * cellUv.Y * factor;
|
||||
|
||||
var contributed = false;
|
||||
|
||||
if (wW > 0f)
|
||||
{
|
||||
weights[order[3]] += wW;
|
||||
contributed = true;
|
||||
}
|
||||
|
||||
if (wX > 0f)
|
||||
{
|
||||
weights[order[0]] += wX;
|
||||
contributed = true;
|
||||
}
|
||||
|
||||
if (wZ > 0f)
|
||||
{
|
||||
weights[order[2]] += wZ;
|
||||
contributed = true;
|
||||
}
|
||||
|
||||
if (wY > 0f)
|
||||
{
|
||||
weights[order[1]] += wY;
|
||||
contributed = true;
|
||||
}
|
||||
|
||||
return contributed;
|
||||
}
|
||||
|
||||
private static Vector4 QuantizeSymbols(in Vector4 channel)
|
||||
=> new(
|
||||
Quantize(channel.X),
|
||||
Quantize(channel.Y),
|
||||
Quantize(channel.Z),
|
||||
Quantize(channel.W));
|
||||
|
||||
private static float Quantize(float value)
|
||||
{
|
||||
var clamped = Math.Clamp(value, 0f, 1f);
|
||||
return (MathF.Round(clamped * 16f) + 0.5f) / 16f;
|
||||
}
|
||||
|
||||
private static void ApplySymmetry(ref Vector4 symbols, ref Vector2 cellUv, Span<int> order)
|
||||
{
|
||||
if (cellUv.X >= 0.5f)
|
||||
{
|
||||
symbols = SwapYxwz(symbols, order);
|
||||
cellUv.X = 1f - cellUv.X;
|
||||
}
|
||||
|
||||
if (cellUv.Y >= 0.5f)
|
||||
{
|
||||
symbols = SwapWzyx(symbols, order);
|
||||
cellUv.Y = 1f - cellUv.Y;
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector4 BuildEquality(in Vector4 symbols, float reference)
|
||||
=> new(
|
||||
AreEqual(symbols.X, reference) ? 1f : 0f,
|
||||
AreEqual(symbols.Y, reference) ? 1f : 0f,
|
||||
AreEqual(symbols.Z, reference) ? 1f : 0f,
|
||||
AreEqual(symbols.W, reference) ? 1f : 0f);
|
||||
|
||||
private static uint BuildSelector(in Vector4 equality, in Vector4 symbols, in Vector2 cellUv)
|
||||
{
|
||||
uint selector = 0;
|
||||
if (equality.X > 0.5f) selector |= 4u;
|
||||
if (equality.Y > 0.5f) selector |= 8u;
|
||||
if (equality.Z > 0.5f) selector |= 16u;
|
||||
if (AreEqual(symbols.X, symbols.Z)) selector |= 2u;
|
||||
if (cellUv.X + cellUv.Y >= 0.5f) selector |= 1u;
|
||||
|
||||
return selector;
|
||||
}
|
||||
|
||||
private static float ComputeWeight(in Vector4 equality, in Vector2 cellUv)
|
||||
=> equality.W * (1f - cellUv.X) * (1f - cellUv.Y)
|
||||
+ equality.X * (1f - cellUv.X) * cellUv.Y
|
||||
+ equality.Z * cellUv.X * (1f - cellUv.Y)
|
||||
+ equality.Y * cellUv.X * cellUv.Y;
|
||||
|
||||
private static Vector2 ComputeShiftedUv(in Vector2 uv)
|
||||
{
|
||||
var shifted = new Vector2(
|
||||
uv.X - MathF.Floor(uv.X),
|
||||
uv.Y - MathF.Floor(uv.Y));
|
||||
|
||||
shifted.X -= 0.5f;
|
||||
if (shifted.X < 0f)
|
||||
{
|
||||
shifted.X += 1f;
|
||||
}
|
||||
|
||||
shifted.Y -= 0.5f;
|
||||
if (shifted.Y < 0f)
|
||||
{
|
||||
shifted.Y += 1f;
|
||||
}
|
||||
|
||||
return shifted;
|
||||
}
|
||||
|
||||
private static Vector4 SwapYxwz(in Vector4 v, Span<int> order)
|
||||
{
|
||||
var o0 = order[0];
|
||||
var o1 = order[1];
|
||||
var o2 = order[2];
|
||||
var o3 = order[3];
|
||||
|
||||
order[0] = o1;
|
||||
order[1] = o0;
|
||||
order[2] = o3;
|
||||
order[3] = o2;
|
||||
|
||||
return new Vector4(v.Y, v.X, v.W, v.Z);
|
||||
}
|
||||
|
||||
private static Vector4 SwapWzyx(in Vector4 v, Span<int> order)
|
||||
{
|
||||
var o0 = order[0];
|
||||
var o1 = order[1];
|
||||
var o2 = order[2];
|
||||
var o3 = order[3];
|
||||
|
||||
order[0] = o3;
|
||||
order[1] = o2;
|
||||
order[2] = o1;
|
||||
order[3] = o0;
|
||||
|
||||
return new Vector4(v.W, v.Z, v.Y, v.X);
|
||||
}
|
||||
|
||||
private static int IndexOfMax(ReadOnlySpan<float> values)
|
||||
{
|
||||
var bestIndex = -1;
|
||||
var bestValue = 0f;
|
||||
|
||||
for (var i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (values[i] > bestValue)
|
||||
{
|
||||
bestValue = values[i];
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
private static bool AreEqual(float a, float b) => MathF.Abs(a - b) <= 1e-5f;
|
||||
|
||||
private static Rgba32 PickMajorityColor(ReadOnlySpan<Rgba32> colors)
|
||||
{
|
||||
var counts = new Dictionary<Rgba32, int>(colors.Length);
|
||||
foreach (var color in colors)
|
||||
{
|
||||
if (counts.TryGetValue(color, out var count))
|
||||
{
|
||||
counts[color] = count + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
counts[color] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return counts
|
||||
.OrderByDescending(kvp => kvp.Value)
|
||||
.ThenByDescending(kvp => kvp.Key.A)
|
||||
.ThenByDescending(kvp => kvp.Key.R)
|
||||
.ThenByDescending(kvp => kvp.Key.G)
|
||||
.ThenByDescending(kvp => kvp.Key.B)
|
||||
.First().Key;
|
||||
}
|
||||
|
||||
private static bool ShouldTrim(in TexMeta meta, int targetMaxDimension)
|
||||
{
|
||||
|
||||
@@ -7,7 +7,5 @@ public enum TextureMapKind
|
||||
Specular,
|
||||
Mask,
|
||||
Index,
|
||||
Emissive,
|
||||
Ui,
|
||||
Unknown
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.GameData.Files;
|
||||
|
||||
namespace LightlessSync.Services.TextureCompression;
|
||||
@@ -37,9 +32,9 @@ public sealed class TextureMetadataHelper
|
||||
|
||||
private static readonly (TextureUsageCategory Category, string Token)[] CategoryTokens =
|
||||
{
|
||||
(TextureUsageCategory.Ui, "/ui/"),
|
||||
(TextureUsageCategory.Ui, "/uld/"),
|
||||
(TextureUsageCategory.Ui, "/icon/"),
|
||||
(TextureUsageCategory.UI, "/ui/"),
|
||||
(TextureUsageCategory.UI, "/uld/"),
|
||||
(TextureUsageCategory.UI, "/icon/"),
|
||||
|
||||
(TextureUsageCategory.VisualEffect, "/vfx/"),
|
||||
|
||||
@@ -104,9 +99,6 @@ public sealed class TextureMetadataHelper
|
||||
(TextureMapKind.Specular, "_s"),
|
||||
(TextureMapKind.Specular, "_spec"),
|
||||
|
||||
(TextureMapKind.Emissive, "_em"),
|
||||
(TextureMapKind.Emissive, "_glow"),
|
||||
|
||||
(TextureMapKind.Index, "_id"),
|
||||
(TextureMapKind.Index, "_idx"),
|
||||
(TextureMapKind.Index, "_index"),
|
||||
@@ -133,10 +125,10 @@ public sealed class TextureMetadataHelper
|
||||
_dataManager = dataManager;
|
||||
}
|
||||
|
||||
public bool TryGetRecommendationInfo(TextureCompressionTarget target, out (string Title, string Description) info)
|
||||
public static bool TryGetRecommendationInfo(TextureCompressionTarget target, out (string Title, string Description) info)
|
||||
=> RecommendationCatalog.TryGetValue(target, out info);
|
||||
|
||||
public TextureUsageCategory DetermineCategory(string? gamePath)
|
||||
public static TextureUsageCategory DetermineCategory(string? gamePath)
|
||||
{
|
||||
var normalized = Normalize(gamePath);
|
||||
if (string.IsNullOrEmpty(normalized))
|
||||
@@ -193,7 +185,7 @@ public sealed class TextureMetadataHelper
|
||||
return TextureUsageCategory.Unknown;
|
||||
}
|
||||
|
||||
public string DetermineSlot(TextureUsageCategory category, string? gamePath)
|
||||
public static string DetermineSlot(TextureUsageCategory category, string? gamePath)
|
||||
{
|
||||
if (category == TextureUsageCategory.Customization)
|
||||
return GuessCustomizationSlot(gamePath);
|
||||
@@ -218,7 +210,7 @@ public sealed class TextureMetadataHelper
|
||||
TextureUsageCategory.Companion => "Companion",
|
||||
TextureUsageCategory.VisualEffect => "VFX",
|
||||
TextureUsageCategory.Housing => "Housing",
|
||||
TextureUsageCategory.Ui => "UI",
|
||||
TextureUsageCategory.UI => "UI",
|
||||
_ => "General"
|
||||
};
|
||||
}
|
||||
@@ -260,7 +252,7 @@ public sealed class TextureMetadataHelper
|
||||
return false;
|
||||
}
|
||||
|
||||
private void AddGameMaterialCandidates(string? gamePath, IList<MaterialCandidate> candidates)
|
||||
private static void AddGameMaterialCandidates(string? gamePath, IList<MaterialCandidate> candidates)
|
||||
{
|
||||
var normalized = Normalize(gamePath);
|
||||
if (string.IsNullOrEmpty(normalized))
|
||||
@@ -286,7 +278,7 @@ public sealed class TextureMetadataHelper
|
||||
}
|
||||
}
|
||||
|
||||
private void AddLocalMaterialCandidates(string? localTexturePath, IList<MaterialCandidate> candidates)
|
||||
private static void AddLocalMaterialCandidates(string? localTexturePath, IList<MaterialCandidate> candidates)
|
||||
{
|
||||
if (string.IsNullOrEmpty(localTexturePath))
|
||||
return;
|
||||
@@ -397,7 +389,7 @@ public sealed class TextureMetadataHelper
|
||||
return TextureMapKind.Unknown;
|
||||
}
|
||||
|
||||
public bool TryMapFormatToTarget(string? format, out TextureCompressionTarget target)
|
||||
public static bool TryMapFormatToTarget(string? format, out TextureCompressionTarget target)
|
||||
{
|
||||
var normalized = (format ?? string.Empty).ToUpperInvariant();
|
||||
if (normalized.Contains("BC1", StringComparison.Ordinal))
|
||||
@@ -434,7 +426,7 @@ public sealed class TextureMetadataHelper
|
||||
return false;
|
||||
}
|
||||
|
||||
public (TextureCompressionTarget Target, string Reason)? GetSuggestedTarget(string? format, TextureMapKind mapKind)
|
||||
public static (TextureCompressionTarget Target, string Reason)? GetSuggestedTarget(string? format, TextureMapKind mapKind)
|
||||
{
|
||||
TextureCompressionTarget? current = null;
|
||||
if (TryMapFormatToTarget(format, out var mapped))
|
||||
@@ -446,7 +438,6 @@ public sealed class TextureMetadataHelper
|
||||
TextureMapKind.Mask => TextureCompressionTarget.BC4,
|
||||
TextureMapKind.Index => TextureCompressionTarget.BC3,
|
||||
TextureMapKind.Specular => TextureCompressionTarget.BC4,
|
||||
TextureMapKind.Emissive => TextureCompressionTarget.BC3,
|
||||
TextureMapKind.Diffuse => TextureCompressionTarget.BC7,
|
||||
_ => TextureCompressionTarget.BC7
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ public enum TextureUsageCategory
|
||||
Companion,
|
||||
Monster,
|
||||
Housing,
|
||||
Ui,
|
||||
UI,
|
||||
VisualEffect,
|
||||
Unknown
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user