Fix some stuff, add Saving.

This commit is contained in:
Ottermandias 2022-09-23 15:21:30 +02:00
parent 47f5e14972
commit 1f2d5246fe
5 changed files with 248 additions and 139 deletions

View file

@ -46,13 +46,9 @@ public partial class CombinedTexture
var left = DataLeft( offset ); var left = DataLeft( offset );
var right = DataRight( x, y ); var right = DataRight( x, y );
var alpha = right.W + left.W * ( 1 - right.W ); var alpha = right.W + left.W * ( 1 - right.W );
if( alpha == 0 ) var rgba = alpha == 0
{ ? new Rgba32()
return; : new Rgba32( ( ( right * right.W + left * left.W * ( 1 - right.W ) ) / alpha ) with { W = alpha } );
}
var sum = ( right * right.W + left * left.W * ( 1 - right.W ) ) / alpha;
var rgba = new Rgba32( sum with { W = alpha } );
_centerStorage.RGBAPixels[ offset ] = rgba.R; _centerStorage.RGBAPixels[ offset ] = rgba.R;
_centerStorage.RGBAPixels[ offset + 1 ] = rgba.G; _centerStorage.RGBAPixels[ offset + 1 ] = rgba.G;
_centerStorage.RGBAPixels[ offset + 2 ] = rgba.B; _centerStorage.RGBAPixels[ offset + 2 ] = rgba.B;

View file

@ -1,5 +1,7 @@
using System; using System;
using System.IO;
using System.Numerics; using System.Numerics;
using Lumina.Data.Files;
using OtterTex; using OtterTex;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
@ -10,6 +12,14 @@ namespace Penumbra.Import.Textures;
public partial class CombinedTexture : IDisposable public partial class CombinedTexture : IDisposable
{ {
public enum TextureSaveType
{
AsIs,
Bitmap,
BC5,
BC7,
}
private enum Mode private enum Mode
{ {
Empty, Empty,
@ -29,6 +39,8 @@ public partial class CombinedTexture : IDisposable
public bool IsLoaded public bool IsLoaded
=> _mode != Mode.Empty; => _mode != Mode.Empty;
public Exception? SaveException { get; private set; } = null;
public void Draw( Vector2 size ) public void Draw( Vector2 size )
{ {
if( _mode == Mode.Custom && !_centerStorage.IsLoaded ) if( _mode == Mode.Custom && !_centerStorage.IsLoaded )
@ -49,76 +61,118 @@ public partial class CombinedTexture : IDisposable
return; return;
} }
try
{
var image = Image.LoadPixelData< Rgba32 >( _current.RGBAPixels, _current.TextureWrap!.Width, var image = Image.LoadPixelData< Rgba32 >( _current.RGBAPixels, _current.TextureWrap!.Width,
_current.TextureWrap!.Height ); _current.TextureWrap!.Height );
image.Save( path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression } ); image.Save( path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression } );
SaveException = null;
}
catch( Exception e )
{
SaveException = e;
}
} }
public void SaveAsDDS( string path, DXGIFormat format, bool fast, float threshold = 0.5f ) private void SaveAs( string path, TextureSaveType type, bool mipMaps, bool writeTex )
{
if( _current == null || _mode == Mode.Empty )
{ {
if( _current == null )
return; return;
switch( _mode )
{
case Mode.Empty: return;
case Mode.LeftCopy:
case Mode.RightCopy:
if( _centerStorage.BaseImage is ScratchImage s )
{
if( format != s.Meta.Format )
{
s = s.Convert( format, threshold );
} }
s.SaveDDS( path ); try
{
if( _current.BaseImage is not ScratchImage s )
{
s = ScratchImage.FromRGBA( _current.RGBAPixels, _current.TextureWrap!.Width,
_current.TextureWrap!.Height, out var i ).ThrowIfError( i );
}
var tex = type switch
{
TextureSaveType.AsIs => _current.Type is Texture.FileType.Bitmap or Texture.FileType.Png ? CreateUncompressed(s, mipMaps ) : s,
TextureSaveType.Bitmap => CreateUncompressed( s, mipMaps ),
TextureSaveType.BC5 => CreateCompressed( s, mipMaps, false ),
TextureSaveType.BC7 => CreateCompressed( s, mipMaps, true ),
_ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ),
};
if( !writeTex )
{
tex.SaveDDS( path );
} }
else else
{ {
var image = ScratchImage.FromRGBA( _current.RGBAPixels, _current.TextureWrap!.Width, SaveTex( path, tex );
_current.TextureWrap!.Height, out var i ).ThrowIfError( i );
image.SaveDDS( path ).ThrowIfError();
} }
break; SaveException = null;
}
catch( Exception e )
{
SaveException = e;
} }
} }
//private void SaveAs( bool success, string path, int type ) private static void SaveTex( string path, ScratchImage input )
//{ {
// if( !success || _imageCenter == null || _wrapCenter == null ) var header = input.Meta.ToTexHeader();
// { if( header.Format == TexFile.TextureFormat.Unknown )
// return; {
// } throw new Exception( $"Could not save tex file with format {input.Meta.Format}, not convertible to a valid .tex formats." );
// }
// try
// { using var stream = File.OpenWrite( path );
// switch( type ) using var w = new BinaryWriter( stream );
// { header.Write( w );
// case 0: w.Write( input.Pixels );
// var img = Image.LoadPixelData< Rgba32 >( _imageCenter, _wrapCenter.Width, _wrapCenter.Height ); }
// img.Save( path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression } );
// break; private static ScratchImage AddMipMaps( ScratchImage input, bool mipMaps )
// case 1: => mipMaps ? input.GenerateMipMaps() : input;
// if( TextureImporter.RgbaBytesToTex( _imageCenter, _wrapCenter.Width, _wrapCenter.Height, out var tex ) )
// { private static ScratchImage CreateUncompressed( ScratchImage input, bool mipMaps )
// File.WriteAllBytes( path, tex ); {
// } if( input.Meta.Format == DXGIFormat.B8G8R8A8UNorm)
// return AddMipMaps(input, mipMaps);
// break;
// case 2: if( input.Meta.Format.IsCompressed() )
// //ScratchImage.LoadDDS( _imageCenter, ) {
// //if( TextureImporter.RgbaBytesToDds( _imageCenter, _wrapCenter.Width, _wrapCenter.Height, out var dds ) ) input = input.Decompress( DXGIFormat.B8G8R8A8UNorm );
////{ }
// // File.WriteAllBytes( path, dds ); else
////} {
// input = input.Convert( DXGIFormat.B8G8R8A8UNorm );
// break; }
// }
// } return AddMipMaps( input, mipMaps );
// catch( Exception e ) }
// {
// PluginLog.Error( $"Could not save image to {path}:\n{e}" ); private static ScratchImage CreateCompressed( ScratchImage input, bool mipMaps, bool bc7 )
// } {
var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC5UNorm;
if( input.Meta.Format == format)
{
return input;
}
if( input.Meta.Format.IsCompressed() )
{
input = input.Decompress( DXGIFormat.B8G8R8A8UNorm );
}
input = AddMipMaps( input, mipMaps );
return input.Compress( format, CompressFlags.BC7Quick | CompressFlags.Parallel );
}
public void SaveAsTex( string path, TextureSaveType type, bool mipMaps )
=> SaveAs( path, type, mipMaps, true );
public void SaveAsDds( string path, TextureSaveType type, bool mipMaps )
=> SaveAs( path, type, mipMaps, false );
public CombinedTexture( Texture left, Texture right ) public CombinedTexture( Texture left, Texture right )
{ {

View file

@ -43,6 +43,26 @@ public static class TexFileParser
} }
} }
public static void Write( this TexFile.TexHeader header, BinaryWriter w )
{
w.Write( ( uint )header.Type );
w.Write( ( uint )header.Format );
w.Write( header.Width );
w.Write( header.Height );
w.Write( header.Depth );
w.Write( header.MipLevels );
unsafe
{
w.Write( header.LodOffset[ 0 ] );
w.Write( header.LodOffset[ 1 ] );
w.Write( header.LodOffset[ 2 ] );
for( var i = 0; i < 13; ++i )
{
w.Write( header.OffsetToSurface[ i ] );
}
}
}
public static TexFile.TexHeader ToTexHeader( this TexMeta meta ) public static TexFile.TexHeader ToTexHeader( this TexMeta meta )
{ {
var ret = new TexFile.TexHeader() var ret = new TexFile.TexHeader()
@ -50,7 +70,7 @@ public static class TexFileParser
Height = ( ushort )meta.Height, Height = ( ushort )meta.Height,
Width = ( ushort )meta.Width, Width = ( ushort )meta.Width,
Depth = ( ushort )Math.Max( meta.Depth, 1 ), Depth = ( ushort )Math.Max( meta.Depth, 1 ),
MipLevels = ( ushort )Math.Min(meta.MipLevels, 13), MipLevels = ( ushort )Math.Min( meta.MipLevels, 12 ),
Format = meta.Format.ToTexFormat(), Format = meta.Format.ToTexFormat(),
Type = meta.Dimension switch Type = meta.Dimension switch
{ {
@ -61,6 +81,31 @@ public static class TexFileParser
_ => 0, _ => 0,
}, },
}; };
unsafe
{
ret.LodOffset[ 0 ] = 0;
ret.LodOffset[ 1 ] = 1;
ret.LodOffset[ 2 ] = 2;
ret.OffsetToSurface[ 0 ] = 80;
var size = meta.Format.BitsPerPixel() * meta.Width * meta.Height / 8;
for( var i = 1; i < meta.MipLevels; ++i )
{
ret.OffsetToSurface[ i ] = ( uint )( 80 + size );
size >>= 2;
if( size == 0 )
{
ret.MipLevels = ( ushort )i;
break;
}
}
for( var i = ret.MipLevels; i < 13; ++i )
{
ret.OffsetToSurface[ i ] = 0;
}
}
return ret; return ret;
} }

View file

@ -17,66 +17,6 @@ using Image = SixLabors.ImageSharp.Image;
namespace Penumbra.Import.Textures; 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 sealed class Texture : IDisposable
{ {
public enum FileType public enum FileType
@ -112,9 +52,6 @@ public sealed class Texture : IDisposable
public bool IsLoaded public bool IsLoaded
=> TextureWrap != null; => TextureWrap != null;
public Texture()
{ }
public void Draw( Vector2 size ) public void Draw( Vector2 size )
{ {
if( TextureWrap != null ) if( TextureWrap != null )
@ -195,12 +132,15 @@ public sealed class Texture : IDisposable
Clean(); Clean();
try try
{ {
if( !File.Exists( path ) )
throw new FileNotFoundException();
var _ = System.IO.Path.GetExtension( Path ) switch var _ = System.IO.Path.GetExtension( Path ) switch
{ {
".dds" => LoadDds(), ".dds" => LoadDds(),
".png" => LoadPng(), ".png" => LoadPng(),
".tex" => LoadTex(), ".tex" => LoadTex(),
_ => true, _ => throw new Exception($"Extension {System.IO.Path.GetExtension( Path )} unknown."),
}; };
Loaded?.Invoke( true ); Loaded?.Invoke( true );
} }

View file

@ -2,6 +2,7 @@ using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.ImGuiFileDialog;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
@ -19,7 +20,20 @@ public partial class ModEditWindow
private readonly FileDialogManager _dialogManager = ConfigWindow.SetupFileManager(); private readonly FileDialogManager _dialogManager = ConfigWindow.SetupFileManager();
private bool _overlayCollapsed = true; private bool _overlayCollapsed = true;
private DXGIFormat _currentFormat = DXGIFormat.R8G8B8A8UNorm;
private bool _addMipMaps = true;
private int _currentSaveAs = 0;
private static readonly (string, string)[] SaveAsStrings =
{
( "As Is", "Save the current texture with its own format without additional conversion or compression, if possible." ),
( "RGBA (Uncompressed)",
"Save the current texture as an uncompressed BGRA bitmap. This requires the most space but technically offers the best quality." ),
( "BC3 (Simple Compression)",
"Save the current texture compressed via BC3/DXT5 compression. This offers a 4:1 compression ratio and is quick with acceptable quality." ),
( "BC7 (Complex Compression)",
"Save the current texture compressed via BC7 compression. This offers a 4:1 compression ratio and has almost indistinguishable quality, but may take a while." ),
};
private void DrawInputChild( string label, Texture tex, Vector2 size, Vector2 imageSize ) private void DrawInputChild( string label, Texture tex, Vector2 size, Vector2 imageSize )
{ {
@ -37,7 +51,8 @@ public partial class ModEditWindow
_dialogManager ); _dialogManager );
var files = _editor!.TexFiles.Select( f => f.File.FullName ) var files = _editor!.TexFiles.Select( f => f.File.FullName )
.Concat( _editor.TexFiles.SelectMany( f => f.SubModUsage.Select( p => p.Item2.ToString() ) ) ); .Concat( _editor.TexFiles.SelectMany( f => f.SubModUsage.Select( p => p.Item2.ToString() ) ) );
tex.PathSelectBox( "##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", files); tex.PathSelectBox( "##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.",
files );
if( tex == _left ) if( tex == _left )
{ {
@ -52,6 +67,35 @@ public partial class ModEditWindow
tex.Draw( imageSize ); tex.Draw( imageSize );
} }
private void SaveAsCombo()
{
var (text, desc) = SaveAsStrings[ _currentSaveAs ];
ImGui.SetNextItemWidth( -ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X );
using var combo = ImRaii.Combo( "##format", text );
ImGuiUtil.HoverTooltip( desc );
if( !combo )
{
return;
}
foreach( var ((newText, newDesc), idx) in SaveAsStrings.WithIndex() )
{
if( ImGui.Selectable( newText, idx == _currentSaveAs ) )
{
_currentSaveAs = idx;
}
ImGuiUtil.HoverTooltip( newDesc );
}
}
private void MipMapInput()
{
ImGui.Checkbox( "##mipMaps", ref _addMipMaps );
ImGuiUtil.HoverTooltip(
"Add the appropriate number of MipMaps to the file." );
}
private void DrawOutputChild( Vector2 size, Vector2 imageSize ) private void DrawOutputChild( Vector2 size, Vector2 imageSize )
{ {
using var child = ImRaii.Child( "Output", size, true ); using var child = ImRaii.Child( "Output", size, true );
@ -62,27 +106,57 @@ public partial class ModEditWindow
if( _center.IsLoaded ) if( _center.IsLoaded )
{ {
SaveAsCombo();
ImGui.SameLine();
MipMapInput();
if( ImGui.Button( "Save as TEX", -Vector2.UnitX ) ) if( ImGui.Button( "Save as TEX", -Vector2.UnitX ) )
{ {
var fileName = Path.GetFileNameWithoutExtension( _left.Path.Length > 0 ? _left.Path : _right.Path ); var fileName = Path.GetFileNameWithoutExtension( _left.Path.Length > 0 ? _left.Path : _right.Path );
_dialogManager.SaveFileDialog( "Save Texture as TEX...", ".tex", fileName, ".tex", ( a, b ) => { }, _mod!.ModPath.FullName ); _dialogManager.SaveFileDialog( "Save Texture as TEX...", ".tex", fileName, ".tex", ( a, b ) =>
{
if( a )
{
_center.SaveAsTex( b, ( CombinedTexture.TextureSaveType )_currentSaveAs, _addMipMaps );
}
}, _mod!.ModPath.FullName );
} }
if( ImGui.Button( "Save as DDS", -Vector2.UnitX ) ) if( ImGui.Button( "Save as DDS", -Vector2.UnitX ) )
{ {
var fileName = Path.GetFileNameWithoutExtension( _right.Path.Length > 0 ? _right.Path : _left.Path ); var fileName = Path.GetFileNameWithoutExtension( _right.Path.Length > 0 ? _right.Path : _left.Path );
_dialogManager.SaveFileDialog( "Save Texture as DDS...", ".dds", fileName, ".dds", ( a, b ) => { if( a ) _center.SaveAsDDS( b, _currentFormat, false ); }, _mod!.ModPath.FullName ); _dialogManager.SaveFileDialog( "Save Texture as DDS...", ".dds", fileName, ".dds", ( a, b ) =>
{
if( a )
{
_center.SaveAsDds( b, ( CombinedTexture.TextureSaveType )_currentSaveAs, _addMipMaps );
} }
}, _mod!.ModPath.FullName );
}
ImGui.NewLine();
if( ImGui.Button( "Save as PNG", -Vector2.UnitX ) ) if( ImGui.Button( "Save as PNG", -Vector2.UnitX ) )
{ {
var fileName = Path.GetFileNameWithoutExtension( _right.Path.Length > 0 ? _right.Path : _left.Path ); var fileName = Path.GetFileNameWithoutExtension( _right.Path.Length > 0 ? _right.Path : _left.Path );
_dialogManager.SaveFileDialog( "Save Texture as PNG...", ".png", fileName, ".png", ( a, b ) => { if (a) _center.SaveAsPng( b ); }, _mod!.ModPath.FullName ); _dialogManager.SaveFileDialog( "Save Texture as PNG...", ".png", fileName, ".png", ( a, b ) =>
{
if( a )
{
_center.SaveAsPng( b );
}
}, _mod!.ModPath.FullName );
} }
ImGui.NewLine(); ImGui.NewLine();
} }
if( _center.SaveException != null )
{
ImGui.TextUnformatted( "Could not save file:" );
using var color = ImRaii.PushColor( ImGuiCol.Text, 0xFF0000FF );
ImGuiUtil.TextWrapped( _center.SaveException.ToString() );
}
_center.Draw( imageSize ); _center.Draw( imageSize );
} }