diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs new file mode 100644 index 00000000..e3d06198 --- /dev/null +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -0,0 +1,140 @@ +using System; +using System.IO; +using Lumina.Data.Files; +using Lumina.Extensions; +using OtterTex; + +namespace Penumbra.Import.Textures; + +public static class TexFileParser +{ + public static ScratchImage Parse( Stream data ) + { + using var r = new BinaryReader( data ); + var header = r.ReadStructure< TexFile.TexHeader >(); + + var meta = header.ToTexMeta(); + if( meta.Format == DXGIFormat.Unknown ) + { + throw new Exception( $"Could not convert format {header.Format} to DXGI Format." ); + } + + if( meta.Dimension == TexDimension.Unknown ) + { + throw new Exception( $"Could not obtain dimensionality from {header.Type}." ); + } + + var scratch = ScratchImage.Initialize( meta ); + CopyData( scratch, r ); + + return scratch; + } + + private static unsafe void CopyData( ScratchImage image, BinaryReader r ) + { + fixed( byte* ptr = image.Pixels ) + { + var span = new Span< byte >( ptr, image.Pixels.Length ); + var readBytes = r.Read( span ); + if( readBytes < image.Pixels.Length ) + { + throw new Exception( $"Invalid data length {readBytes} < {image.Pixels.Length}." ); + } + } + } + + public static TexFile.TexHeader ToTexHeader( this TexMeta meta ) + { + var ret = new TexFile.TexHeader() + { + Height = ( ushort )meta.Height, + Width = ( ushort )meta.Width, + Depth = ( ushort )Math.Max( meta.Depth, 1 ), + MipLevels = ( ushort )Math.Min(meta.MipLevels, 13), + Format = meta.Format.ToTexFormat(), + Type = meta.Dimension switch + { + _ when meta.IsCubeMap => TexFile.Attribute.TextureTypeCube, + TexDimension.Tex1D => TexFile.Attribute.TextureType1D, + TexDimension.Tex2D => TexFile.Attribute.TextureType2D, + TexDimension.Tex3D => TexFile.Attribute.TextureType3D, + _ => 0, + }, + }; + return ret; + } + + public static TexMeta ToTexMeta( this TexFile.TexHeader header ) + => new() + { + Height = header.Height, + Width = header.Width, + Depth = Math.Max( header.Depth, ( ushort )1 ), + MipLevels = header.MipLevels, + ArraySize = 1, + Format = header.Format.ToDXGI(), + Dimension = header.Type.ToDimension(), + MiscFlags = header.Type.HasFlag( TexFile.Attribute.TextureTypeCube ) ? D3DResourceMiscFlags.TextureCube : 0, + MiscFlags2 = 0, + }; + + private static TexDimension ToDimension( this TexFile.Attribute attribute ) + => ( attribute & TexFile.Attribute.TextureTypeMask ) switch + { + TexFile.Attribute.TextureType1D => TexDimension.Tex1D, + TexFile.Attribute.TextureType2D => TexDimension.Tex2D, + TexFile.Attribute.TextureType3D => TexDimension.Tex3D, + _ => TexDimension.Unknown, + }; + + public static TexFile.TextureFormat ToTexFormat( this DXGIFormat format ) + => format switch + { + DXGIFormat.R8UNorm => TexFile.TextureFormat.L8, + DXGIFormat.A8UNorm => TexFile.TextureFormat.A8, + DXGIFormat.B4G4R4A4UNorm => TexFile.TextureFormat.B4G4R4A4, + DXGIFormat.B5G5R5A1UNorm => TexFile.TextureFormat.B5G5R5A1, + DXGIFormat.B8G8R8A8UNorm => TexFile.TextureFormat.B8G8R8A8, + DXGIFormat.B8G8R8X8UNorm => TexFile.TextureFormat.B8G8R8X8, + DXGIFormat.R32Float => TexFile.TextureFormat.R32F, + DXGIFormat.R16G16Float => TexFile.TextureFormat.R16G16F, + DXGIFormat.R32G32Float => TexFile.TextureFormat.R32G32F, + DXGIFormat.R16G16B16A16Float => TexFile.TextureFormat.R16G16B16A16F, + DXGIFormat.R32G32B32A32Float => TexFile.TextureFormat.R32G32B32A32F, + DXGIFormat.BC1UNorm => TexFile.TextureFormat.BC1, + DXGIFormat.BC2UNorm => TexFile.TextureFormat.BC2, + DXGIFormat.BC3UNorm => TexFile.TextureFormat.BC3, + DXGIFormat.BC5UNorm => TexFile.TextureFormat.BC5, + DXGIFormat.BC7UNorm => TexFile.TextureFormat.BC7, + DXGIFormat.R16G16B16A16Typeless => TexFile.TextureFormat.D16, + DXGIFormat.R24G8Typeless => TexFile.TextureFormat.D24S8, + DXGIFormat.R16Typeless => TexFile.TextureFormat.Shadow16, + _ => TexFile.TextureFormat.Unknown, + }; + + public static DXGIFormat ToDXGI( this TexFile.TextureFormat format ) + => format switch + { + TexFile.TextureFormat.L8 => DXGIFormat.R8UNorm, + TexFile.TextureFormat.A8 => DXGIFormat.A8UNorm, + TexFile.TextureFormat.B4G4R4A4 => DXGIFormat.B4G4R4A4UNorm, + TexFile.TextureFormat.B5G5R5A1 => DXGIFormat.B5G5R5A1UNorm, + TexFile.TextureFormat.B8G8R8A8 => DXGIFormat.B8G8R8A8UNorm, + TexFile.TextureFormat.B8G8R8X8 => DXGIFormat.B8G8R8X8UNorm, + TexFile.TextureFormat.R32F => DXGIFormat.R32Float, + TexFile.TextureFormat.R16G16F => DXGIFormat.R16G16Float, + TexFile.TextureFormat.R32G32F => DXGIFormat.R32G32Float, + TexFile.TextureFormat.R16G16B16A16F => DXGIFormat.R16G16B16A16Float, + TexFile.TextureFormat.R32G32B32A32F => DXGIFormat.R32G32B32A32Float, + TexFile.TextureFormat.BC1 => DXGIFormat.BC1UNorm, + TexFile.TextureFormat.BC2 => DXGIFormat.BC2UNorm, + TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm, + TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm, + TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm, + TexFile.TextureFormat.D16 => DXGIFormat.R16G16B16A16Typeless, + TexFile.TextureFormat.D24S8 => DXGIFormat.R24G8Typeless, + TexFile.TextureFormat.Shadow16 => DXGIFormat.R16Typeless, + TexFile.TextureFormat.Shadow24 => DXGIFormat.R24G8Typeless, + _ => DXGIFormat.Unknown, + }; +} \ No newline at end of file diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index c553ed9f..5be06adb 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -4,7 +4,6 @@ using System.IO; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Utility; using ImGuiNET; using ImGuiScene; using Lumina.Data.Files; @@ -18,6 +17,66 @@ using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; +//public static class ScratchImageExtensions +//{ +// public static Exception? SaveAsTex( this ScratchImage image, string path ) +// { +// try +// { +// using var fileStream = File.OpenWrite( path ); +// using var bw = new BinaryWriter( fileStream ); +// +// bw.Write( ( uint )image.Meta.GetAttribute() ); +// bw.Write( ( uint )image.Meta.GetFormat() ); +// bw.Write( ( ushort )image.Meta.Width ); +// bw.Write( ( ushort )image.Meta.Height ); +// bw.Write( ( ushort )image.Meta.Depth ); +// bw.Write( ( ushort )image.Meta.MipLevels ); +// } +// catch( Exception e ) +// { +// return e; +// } +// +// return null; +// } +// +// public static unsafe TexFile.TexHeader ToTexHeader( this ScratchImage image ) +// { +// var ret = new TexFile.TexHeader() +// { +// Type = image.Meta.GetAttribute(), +// Format = image.Meta.GetFormat(), +// Width = ( ushort )image.Meta.Width, +// Height = ( ushort )image.Meta.Height, +// Depth = ( ushort )image.Meta.Depth, +// }; +// ret.LodOffset[0] = 0; +// ret.LodOffset[1] = 1; +// ret.LodOffset[2] = 2; +// //foreach(var surface in image.Images) +// // ret.OffsetToSurface[ 0 ] = 80 + (image.P); +// return ret; +// } +// +// // Get all known flags for the TexFile.Attribute from the scratch image. +// private static TexFile.Attribute GetAttribute( this TexMeta meta ) +// { +// var ret = meta.Dimension switch +// { +// TexDimension.Tex1D => TexFile.Attribute.TextureType1D, +// TexDimension.Tex2D => TexFile.Attribute.TextureType2D, +// TexDimension.Tex3D => TexFile.Attribute.TextureType3D, +// _ => ( TexFile.Attribute )0, +// }; +// if( meta.IsCubeMap ) +// ret |= TexFile.Attribute.TextureTypeCube; +// if( meta.Format.IsDepthStencil() ) +// ret |= TexFile.Attribute.TextureDepthStencil; +// return ret; +// } +//} + public sealed class Texture : IDisposable { public enum FileType @@ -101,7 +160,7 @@ public sealed class Texture : IDisposable ImGuiUtil.DrawTableColumn( "Format" ); ImGuiUtil.DrawTableColumn( t.Header.Format.ToString() ); ImGuiUtil.DrawTableColumn( "Mip Levels" ); - ImGuiUtil.DrawTableColumn( t.Header.MipLevels.ToString()) ; + ImGuiUtil.DrawTableColumn( t.Header.MipLevels.ToString() ); ImGuiUtil.DrawTableColumn( "Data Size" ); ImGuiUtil.DrawTableColumn( $"{Functions.HumanReadableSize( t.ImageData.Length )} ({t.ImageData.Length} Bytes)" ); break; @@ -178,25 +237,36 @@ public sealed class Texture : IDisposable private bool LoadTex() { Type = FileType.Tex; - var tex = System.IO.Path.IsPathRooted( Path ) - ? Dalamud.GameData.GameData.GetFileFromDisk< TexFile >( Path ) - : Dalamud.GameData.GetFile< TexFile >( Path ); - BaseImage = tex ?? throw new Exception( "Could not read .tex file." ); - RGBAPixels = tex.GetRgbaImageData(); - CreateTextureWrap( tex.Header.Width, tex.Header.Height ); + using var stream = OpenTexStream(); + var scratch = TexFileParser.Parse( stream ); + BaseImage = scratch; + var rgba = scratch.GetRGBA( out var f ).ThrowIfError( f ); + RGBAPixels = rgba.Pixels[ ..( f.Meta.Width * f.Meta.Height * f.Meta.Format.BitsPerPixel() / 8 ) ].ToArray(); + CreateTextureWrap( scratch.Meta.Width, scratch.Meta.Height ); return true; } + private Stream OpenTexStream() + { + if( System.IO.Path.IsPathRooted( Path ) ) + { + return File.OpenRead( Path ); + } + + var file = Dalamud.GameData.GetFile( Path ); + return file != null ? new MemoryStream( file.Data ) : throw new Exception( $"Unable to obtain \"{Path}\" from game files." ); + } + private void CreateTextureWrap( int width, int height ) => TextureWrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( RGBAPixels, width, height, 4 ); private string? _tmpPath; - public void PathSelectBox( string label, string tooltip, IEnumerable paths ) + public void PathSelectBox( string label, string tooltip, IEnumerable< string > paths ) { ImGui.SetNextItemWidth( -0.0001f ); - var startPath = Path.Length > 0 ? Path : "Choose a modded texture here..."; - using var combo = ImRaii.Combo( label, startPath ); + var startPath = Path.Length > 0 ? Path : "Choose a modded texture here..."; + using var combo = ImRaii.Combo( label, startPath ); if( combo ) { foreach( var (path, idx) in paths.WithIndex() ) @@ -208,13 +278,15 @@ public sealed class Texture : IDisposable } } } + ImGuiUtil.HoverTooltip( tooltip ); } public void PathInputBox( string label, string hint, string tooltip, string startPath, FileDialogManager manager ) { _tmpPath ??= Path; - using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); + using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, + new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); ImGui.SetNextItemWidth( -ImGui.GetFrameHeight() - 3 * ImGuiHelpers.GlobalScale ); ImGui.InputTextWithHint( label, hint, ref _tmpPath, Utf8GamePath.MaxGamePathLength ); if( ImGui.IsItemDeactivatedAfterEdit() ) @@ -245,72 +317,4 @@ public sealed class Texture : IDisposable manager.OpenFileDialog( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath ); } } -} - -public static class ScratchImageExtensions -{ - public static Exception? SaveAsTex( this ScratchImage image, string path ) - { - try - { - using var fileStream = File.OpenWrite( path ); - using var bw = new BinaryWriter( fileStream ); - - bw.Write( (uint) image.Meta.GetAttribute() ); - bw.Write( (uint) image.Meta.GetFormat() ); - bw.Write( (ushort) image.Meta.Width ); - bw.Write( (ushort) image.Meta.Height ); - bw.Write( (ushort) image.Meta.Depth ); - bw.Write( (ushort) image.Meta.MipLevels ); - } - catch( Exception e ) - { - return e; - } - - return null; - } - - public static unsafe TexFile.TexHeader ToTexHeader( this ScratchImage image ) - { - var ret = new TexFile.TexHeader() - { - Type = image.Meta.GetAttribute(), - Format = image.Meta.GetFormat(), - Width = ( ushort )image.Meta.Width, - Height = ( ushort )image.Meta.Height, - Depth = ( ushort )image.Meta.Depth, - }; - ret.LodOffset[ 0 ] = 0; - ret.LodOffset[ 1 ] = 1; - ret.LodOffset[ 2 ] = 2; - //foreach(var surface in image.Images) - // ret.OffsetToSurface[ 0 ] = 80 + (image.P); - return ret; - } - - // Get all known flags for the TexFile.Attribute from the scratch image. - private static TexFile.Attribute GetAttribute( this TexMeta meta ) - { - var ret = meta.Dimension switch - { - TexDimension.Tex1D => TexFile.Attribute.TextureType1D, - TexDimension.Tex2D => TexFile.Attribute.TextureType2D, - TexDimension.Tex3D => TexFile.Attribute.TextureType3D, - _ => (TexFile.Attribute) 0, - }; - if( meta.IsCubeMap ) - ret |= TexFile.Attribute.TextureTypeCube; - if( meta.Format.IsDepthStencil() ) - ret |= TexFile.Attribute.TextureDepthStencil; - return ret; - } - - private static TexFile.TextureFormat GetFormat( this TexMeta meta ) - { - return meta.Format switch - { - _ => TexFile.TextureFormat.Unknown, - }; - } } \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index 28cb2906..00f246a4 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -75,7 +75,7 @@ public partial class Mod private List< FileRegistry > _availableFiles = null!; private List< FileRegistry > _mtrlFiles = null!; private List< FileRegistry > _mdlFiles = null!; - private List _texFiles = null!; + private List< FileRegistry > _texFiles = null!; private readonly HashSet< Utf8GamePath > _usedPaths = new(); // All paths that are used in @@ -90,7 +90,7 @@ public partial class Mod public IReadOnlyList< FileRegistry > MdlFiles => _mdlFiles; - public IReadOnlyList TexFiles + public IReadOnlyList< FileRegistry > TexFiles => _texFiles; // Remove all path redirections where the pointed-to file does not exist.