Editor/FabJsonProtocol.cs
using System;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace FabBridge;
/// <summary>
/// Handles parsing of JSON data from Fab/Quixel Bridge
/// </summary>
public static class FabJsonProtocol
{
/// <summary>
/// Whether to save incoming JSON to files for debugging
/// </summary>
public static bool SaveJsonToFile { get; set; } = false;
/// <summary>
/// Parse the JSON export data from Fab
/// </summary>
public static FabExportData Parse( string json )
{
if ( string.IsNullOrWhiteSpace( json ) )
return null;
try
{
// Save full JSON to file for debugging
if ( SaveJsonToFile )
{
SaveJsonForDebugging( json );
}
// Log first 500 chars of JSON for debugging
var preview = json.Length > 500 ? json.Substring( 0, 500 ) + "..." : json;
Log.Info( $"FabJsonProtocol: Parsing JSON preview: {preview}" );
// Try direct deserialization first
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
// Check if it's wrapped in an outer object or is an array
var trimmed = json.Trim();
Log.Info( $"FabJsonProtocol: JSON starts with: {trimmed.Substring( 0, Math.Min( 50, trimmed.Length ) )}" );
if ( trimmed.StartsWith( "[" ) )
{
Log.Info( "FabJsonProtocol: Detected array format" );
// It's an array of assets
var assets = JsonSerializer.Deserialize<List<FabAsset>>( json, options );
return new FabExportData { Assets = assets ?? new() };
}
// Try parsing as a generic object to inspect structure
var node = JsonNode.Parse( json );
if ( node == null )
return null;
// Log top-level keys
if ( node is JsonObject jsonObj )
{
var keys = string.Join( ", ", jsonObj.Select( x => x.Key ) );
Log.Info( $"FabJsonProtocol: Top-level keys: {keys}" );
}
// Check common wrapper formats
// Format 1: { "assets": [...] }
if ( node["assets"] is JsonArray assetsArray )
{
Log.Info( "FabJsonProtocol: Found 'assets' array wrapper" );
var assets = assetsArray.Deserialize<List<FabAsset>>( options );
return new FabExportData { Assets = assets ?? new() };
}
// Format 2: Single asset object (Fab direct export format)
// Check for "id" key which is present in Fab exports
if ( node["id"] != null || node["meshes"] != null || node["materials"] != null )
{
Log.Info( "FabJsonProtocol: Detected single asset object format (Fab direct export)" );
try
{
var asset = node.Deserialize<FabAsset>( options );
if ( asset != null )
{
Log.Info( $"FabJsonProtocol: Successfully parsed asset - ID: {asset.Id}, Name: {asset.GetDisplayName()}" );
Log.Info( $"FabJsonProtocol: Meshes: {asset.Meshes?.Count ?? 0}, Materials: {asset.Materials?.Count ?? 0}" );
return new FabExportData { Assets = new List<FabAsset> { asset } };
}
}
catch ( Exception parseEx )
{
Log.Error( $"FabJsonProtocol: Failed to parse single asset: {parseEx.Message}" );
// Continue to try other formats
}
}
// Format 3: Quixel Bridge format with numbered indices or other wrappers
var exportData = new FabExportData();
foreach ( var prop in node.AsObject() )
{
// Try to parse each property as an asset
try
{
var asset = prop.Value.Deserialize<FabAsset>( options );
if ( asset != null && (!string.IsNullOrEmpty( asset.Id ) || !string.IsNullOrEmpty( asset.Name )) )
{
exportData.Assets.Add( asset );
}
}
catch
{
// Not an asset, skip
}
}
if ( exportData.Assets.Count > 0 )
return exportData;
Log.Warning( $"FabJsonProtocol: Could not parse JSON structure" );
return null;
}
catch ( Exception ex )
{
Log.Error( $"FabJsonProtocol: Parse error: {ex.Message}" );
return null;
}
}
/// <summary>
/// Try to extract texture information from various JSON formats
/// </summary>
public static List<FabTexture> ExtractTextures( JsonNode node )
{
var textures = new List<FabTexture>();
if ( node == null )
return textures;
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
// Check for textureSets array
if ( node["textureSets"] is JsonArray textureSets )
{
foreach ( var set in textureSets )
{
if ( set?["textures"] is JsonArray setTextures )
{
foreach ( var tex in setTextures )
{
var texture = tex.Deserialize<FabTexture>( options );
if ( texture != null )
textures.Add( texture );
}
}
}
}
// Check for components with textures
if ( node["components"] is JsonArray components )
{
foreach ( var comp in components )
{
var path = comp?["path"]?.GetValue<string>();
var type = comp?["type"]?.GetValue<string>();
if ( !string.IsNullOrEmpty( path ) && IsTextureFile( path ) )
{
textures.Add( new FabTexture
{
Path = path,
Type = type ?? GuessTextureType( path ),
Name = System.IO.Path.GetFileNameWithoutExtension( path )
} );
}
}
}
// Check for direct texture properties (Megascans format)
var textureTypes = new[] { "albedo", "normal", "roughness", "metalness", "ao", "displacement", "opacity" };
foreach ( var type in textureTypes )
{
var texNode = node[type] ?? node[type.ToUpperInvariant()];
if ( texNode is JsonArray texArray )
{
foreach ( var tex in texArray )
{
var path = tex?["path"]?.GetValue<string>() ?? tex?.GetValue<string>();
if ( !string.IsNullOrEmpty( path ) )
{
textures.Add( new FabTexture
{
Path = path,
Type = type,
Name = System.IO.Path.GetFileNameWithoutExtension( path )
} );
}
}
}
else if ( texNode != null )
{
var path = texNode["path"]?.GetValue<string>() ?? texNode.GetValue<string>();
if ( !string.IsNullOrEmpty( path ) )
{
textures.Add( new FabTexture
{
Path = path,
Type = type,
Name = System.IO.Path.GetFileNameWithoutExtension( path )
} );
}
}
}
return textures;
}
private static bool IsTextureFile( string path )
{
var ext = System.IO.Path.GetExtension( path )?.ToLowerInvariant();
return ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".tga" ||
ext == ".exr" || ext == ".tif" || ext == ".tiff" || ext == ".bmp";
}
private static string GuessTextureType( string path )
{
var name = System.IO.Path.GetFileNameWithoutExtension( path )?.ToLowerInvariant() ?? "";
if ( name.Contains( "albedo" ) || name.Contains( "diffuse" ) || name.Contains( "color" ) || name.Contains( "basecolor" ) )
return "albedo";
if ( name.Contains( "normal" ) || name.Contains( "nrm" ) )
return "normal";
if ( name.Contains( "rough" ) )
return "roughness";
if ( name.Contains( "metal" ) )
return "metalness";
if ( name.Contains( "ao" ) || name.Contains( "occlusion" ) )
return "ao";
if ( name.Contains( "height" ) || name.Contains( "disp" ) )
return "displacement";
if ( name.Contains( "opacity" ) || name.Contains( "alpha" ) )
return "opacity";
if ( name.Contains( "emissive" ) || name.Contains( "glow" ) )
return "emissive";
return "unknown";
}
/// <summary>
/// Saves the raw JSON to a file for debugging purposes
/// </summary>
private static void SaveJsonForDebugging( string json )
{
try
{
var project = Project.Current;
if ( project == null )
return;
var assetsPath = project.GetAssetsPath();
var debugFolder = System.IO.Path.Combine( assetsPath, "fab_imports", "_debug" );
System.IO.Directory.CreateDirectory( debugFolder );
var timestamp = DateTime.Now.ToString( "yyyy-MM-dd_HH-mm-ss" );
var filename = $"fab_export_{timestamp}.json";
var filepath = System.IO.Path.Combine( debugFolder, filename );
// Pretty print the JSON for readability
try
{
var parsed = JsonNode.Parse( json );
var prettyJson = parsed?.ToJsonString( new JsonSerializerOptions { WriteIndented = true } ) ?? json;
System.IO.File.WriteAllText( filepath, prettyJson );
}
catch
{
// If pretty printing fails, just save the raw JSON
System.IO.File.WriteAllText( filepath, json );
}
Log.Info( $"FabJsonProtocol: Saved JSON to {filepath}" );
}
catch ( Exception ex )
{
Log.Warning( $"FabJsonProtocol: Failed to save JSON for debugging: {ex.Message}" );
}
}
}