Editor/Converters/FabTextureConverter.cs
using System;
using System.IO;
using System.Text.Json;
using Editor.Inspectors;
using TextureFile = Editor.Inspectors.TextureInspector.TextureFile;
using TextureSequence = Editor.Inspectors.TextureInspector.TextureFile.TextureSequence;
using GammaType = Editor.Inspectors.TextureInspector.TextureFile.GammaType;
using ImageFormatType = Editor.Inspectors.TextureInspector.TextureFile.ImageFormatType;
using MipAlgorithm = Editor.Inspectors.TextureInspector.TextureFile.MipAlgorithm;
namespace FabBridge.Converters;
/// <summary>
/// Converts Fab texture files to s&box VTEX format
/// </summary>
public static class FabTextureConverter
{
/// <summary>
/// Result of a texture conversion
/// </summary>
public class ConversionResult
{
public bool Success { get; set; }
public string SourcePath { get; set; }
public string DestinationPath { get; set; }
public string VtexPath { get; set; }
public string RelativePath { get; set; }
public string Error { get; set; }
public Asset Asset { get; set; }
}
/// <summary>
/// Convert a Fab texture to s&box format
/// </summary>
/// <param name="fabTexture">The Fab texture info</param>
/// <param name="assetBaseName">Base name for the asset (e.g., material name)</param>
/// <param name="destinationFolder">Absolute path to destination folder</param>
/// <returns>Conversion result with paths and asset reference</returns>
public static ConversionResult Convert( FabTexture fabTexture, string assetBaseName, string destinationFolder )
{
var result = new ConversionResult
{
SourcePath = fabTexture.Path
};
try
{
// Validate source file exists
if ( !File.Exists( fabTexture.Path ) )
{
result.Error = $"Source file not found: {fabTexture.Path}";
return result;
}
// Ensure destination folder exists
Directory.CreateDirectory( destinationFolder );
// Build destination filename with proper suffix
var suffix = fabTexture.GetSboxSuffix();
var extension = Path.GetExtension( fabTexture.Path );
var destFileName = $"{assetBaseName}{suffix}{extension}";
var destPath = Path.Combine( destinationFolder, destFileName );
// Copy the texture file
File.Copy( fabTexture.Path, destPath, overwrite: true );
result.DestinationPath = destPath;
// Verify the file was actually copied
if ( !File.Exists( destPath ) )
{
result.Error = $"File copy succeeded but file not found at destination: {destPath}";
Log.Error( $"FabBridge: {result.Error}" );
return result;
}
var fileInfo = new FileInfo( destPath );
Log.Info( $"FabBridge: Copied texture to {destPath} (size: {fileInfo.Length} bytes)" );
// Register the texture as an asset now so it can be compiled before the vmat
// references it. Without this, s&box discovers the texture mid-vmat-compile and
// spams "on-demand recompile ... at least one out of date dependency".
result.Asset = AssetSystem.RegisterFile( destPath );
// Store the relative path to the IMAGE file (not vtex)
result.RelativePath = GetRelativePath( destPath );
result.Success = true;
Log.Info( $"FabBridge: Texture relative path: {result.RelativePath}" );
// Optionally create VTEX file for compilation (but material can reference jpg directly)
// Commenting out VTEX creation for now as it may be causing issues
// var vtexPath = Path.ChangeExtension( destPath, ".vtex" );
// CreateVtexFile( destPath, vtexPath, fabTexture );
// result.VtexPath = vtexPath;
}
catch ( Exception ex )
{
result.Error = ex.Message;
Log.Error( $"FabBridge: Texture conversion failed: {ex.Message}" );
}
return result;
}
/// <summary>
/// Convert multiple textures from a texture set
/// </summary>
public static List<ConversionResult> ConvertTextureSet( FabTextureSet textureSet, string assetBaseName, string destinationFolder )
{
var results = new List<ConversionResult>();
foreach ( var texture in textureSet.Textures )
{
var result = Convert( texture, assetBaseName, destinationFolder );
results.Add( result );
}
return results;
}
/// <summary>
/// Convert all textures from a Fab asset
/// </summary>
public static List<ConversionResult> ConvertAllTextures( FabAsset fabAsset, string destinationFolder )
{
var results = new List<ConversionResult>();
var baseName = fabAsset.GetSafeFileName();
// Use the new GetAllTextures method that handles all formats
var allTextures = fabAsset.GetAllTextures();
Log.Info( $"FabTextureConverter: Found {allTextures.Count} textures to convert" );
foreach ( var texture in allTextures )
{
Log.Info( $"FabTextureConverter: Converting texture - Type: {texture.Type}, Path: {texture.Path}" );
var result = Convert( texture, baseName, destinationFolder );
results.Add( result );
}
return results;
}
/// <summary>
/// Creates a VTEX file for the texture
/// </summary>
private static void CreateVtexFile( string imagePath, string vtexPath, FabTexture fabTexture )
{
// Get the relative path for the image (normalized to forward slashes)
var relativePath = GetRelativePath( imagePath );
// Determine if we need linear color space (for normal/roughness/etc)
var isLinear = fabTexture.IsLinearColorSpace();
// Create the texture file definition
var textureFile = new TextureFile
{
Sequences = new List<TextureSequence>
{
new TextureSequence
{
Source = relativePath,
IsLooping = true
}
},
InputColorSpace = isLinear ? GammaType.Linear : GammaType.SRGB,
OutputColorSpace = GammaType.Linear,
OutputFormat = ImageFormatType.BC7, // High quality compression
OutputMipAlgorithm = MipAlgorithm.Box,
OutputTypeString = "2D"
};
// Serialize and write
var json = Json.Serialize( textureFile );
File.WriteAllText( vtexPath, json );
Log.Info( $"FabBridge: Created VTEX file at {vtexPath}" );
}
/// <summary>
/// Gets the project-relative path for a texture (normalized to forward slashes)
/// </summary>
public static string GetRelativePath( string absolutePath )
{
var project = Project.Current;
if ( project == null )
{
Log.Warning( "FabBridge: GetRelativePath - No project, returning normalized absolute path" );
return NormalizePath( absolutePath );
}
var projectPath = project.GetAssetsPath();
// Normalize both paths for comparison
var normalizedAbsolute = absolutePath.Replace( '\\', '/' );
var normalizedProject = projectPath.Replace( '\\', '/' );
// Ensure project path ends with /
if ( !normalizedProject.EndsWith( "/" ) )
normalizedProject += "/";
Log.Info( $"FabBridge: GetRelativePath debug:" );
Log.Info( $" Input: {absolutePath}" );
Log.Info( $" Normalized absolute: {normalizedAbsolute}" );
Log.Info( $" Normalized project: {normalizedProject}" );
if ( normalizedAbsolute.StartsWith( normalizedProject, StringComparison.OrdinalIgnoreCase ) )
{
var relativePath = normalizedAbsolute.Substring( normalizedProject.Length );
Log.Info( $" Result: {relativePath}" );
return relativePath;
}
Log.Warning( $"FabBridge: Path not under project, returning normalized: {normalizedAbsolute}" );
return NormalizePath( absolutePath );
}
/// <summary>
/// Normalize path to use forward slashes (s&box convention)
/// </summary>
public static string NormalizePath( string path )
{
return path?.Replace( '\\', '/' );
}
}