Editor/FabAssetData.cs
using System;
using System.Text.Json.Serialization;
namespace FabBridge;
/// <summary>
/// Represents the root JSON structure received from Fab/Quixel Bridge
/// </summary>
public class FabExportData
{
[JsonPropertyName( "assets" )]
public List<FabAsset> Assets { get; set; } = new();
}
/// <summary>
/// Represents a single asset exported from Fab
/// The actual Fab JSON format from Epic Games Launcher
/// </summary>
public class FabAsset
{
[JsonPropertyName( "id" )]
public string Id { get; set; }
[JsonPropertyName( "name" )]
public string Name { get; set; }
[JsonPropertyName( "type" )]
public string Type { get; set; }
[JsonPropertyName( "category" )]
public string Category { get; set; }
[JsonPropertyName( "path" )]
public string Path { get; set; }
// Fab uses "meshes" not "meshList"
[JsonPropertyName( "meshes" )]
public List<FabMesh> Meshes { get; set; } = new();
// Fab uses "materials" array containing texture info
[JsonPropertyName( "materials" )]
public List<FabMaterial> Materials { get; set; } = new();
// Components array (Quixel Bridge format) - textures with type/path/format
[JsonPropertyName( "components" )]
public List<FabComponent> Components { get; set; } = new();
// Additional textures (list of file paths)
[JsonPropertyName( "additional_textures" )]
public List<string> AdditionalTextures { get; set; } = new();
// Native files
[JsonPropertyName( "native_files" )]
public List<FabNativeFile> NativeFiles { get; set; } = new();
// Metadata (nested object with launcher, megascans, fab sections)
[JsonPropertyName( "metadata" )]
public FabMetadata Metadata { get; set; }
// Legacy formats for compatibility
[JsonPropertyName( "meshList" )]
public List<FabMesh> MeshList { get; set; } = new();
[JsonPropertyName( "textureSets" )]
public List<FabTextureSet> TextureSets { get; set; } = new();
[JsonPropertyName( "lodList" )]
public List<FabLod> LodList { get; set; } = new();
[JsonPropertyName( "meta" )]
public List<FabMeta> Meta { get; set; } = new();
/// <summary>
/// Gets all meshes from either Meshes or MeshList
/// </summary>
public List<FabMesh> GetAllMeshes()
{
var result = new List<FabMesh>();
if ( Meshes?.Count > 0 ) result.AddRange( Meshes );
if ( MeshList?.Count > 0 ) result.AddRange( MeshList );
return result;
}
/// <summary>
/// Gets all textures from Components, Materials, and AdditionalTextures
/// </summary>
public List<FabTexture> GetAllTextures()
{
var result = new List<FabTexture>();
// Add from Components (Quixel Bridge format)
if ( Components != null )
{
foreach ( var comp in Components )
{
if ( IsTextureFile( comp.Path ) )
{
result.Add( new FabTexture
{
Path = comp.Path,
Name = comp.Name,
Type = comp.Type,
Format = comp.Format
} );
}
}
}
// Add from Materials (Fab format - textures is now a Dictionary<string, string>)
if ( Materials != null )
{
foreach ( var mat in Materials )
{
if ( mat.Textures != null )
{
foreach ( var kvp in mat.Textures )
{
var textureType = kvp.Key; // e.g., "albedo", "normal", "roughness"
var texturePath = kvp.Value; // e.g., "C:/path/to/texture.jpg"
if ( !string.IsNullOrEmpty( texturePath ) && IsTextureFile( texturePath ) )
{
result.Add( new FabTexture
{
Path = texturePath,
Type = textureType,
Name = System.IO.Path.GetFileNameWithoutExtension( texturePath )
} );
}
}
}
}
}
// Add from AdditionalTextures (list of file paths as strings)
if ( AdditionalTextures != null )
{
foreach ( var texturePath in AdditionalTextures )
{
if ( !string.IsNullOrEmpty( texturePath ) && IsTextureFile( texturePath ) )
{
// Try to infer type from filename
var filename = System.IO.Path.GetFileNameWithoutExtension( texturePath ).ToLowerInvariant();
var type = InferTextureType( filename );
result.Add( new FabTexture
{
Path = texturePath,
Type = type,
Name = System.IO.Path.GetFileNameWithoutExtension( texturePath )
} );
}
}
}
// Add from TextureSets (legacy format)
if ( TextureSets != null )
{
foreach ( var set in TextureSets )
{
if ( set.Textures != null )
{
result.AddRange( set.Textures );
}
}
}
return result;
}
/// <summary>
/// Infers texture type from filename
/// </summary>
private static string InferTextureType( string filename )
{
if ( filename.Contains( "basecolor" ) || filename.Contains( "albedo" ) || filename.Contains( "diffuse" ) )
return "albedo";
if ( filename.Contains( "normal" ) )
return "normal";
if ( filename.Contains( "roughness" ) )
return "roughness";
if ( filename.Contains( "metallic" ) || filename.Contains( "metalness" ) )
return "metalness";
if ( filename.Contains( "ao" ) || filename.Contains( "ambient" ) )
return "ao";
if ( filename.Contains( "displacement" ) || filename.Contains( "height" ) )
return "displacement";
if ( filename.Contains( "cavity" ) )
return "cavity";
if ( filename.Contains( "gloss" ) )
return "gloss";
if ( filename.Contains( "specular" ) )
return "specular";
if ( filename.Contains( "bump" ) )
return "bump";
// Translucency takes priority over opacity - it's the correct format for s&box
if ( filename.Contains( "translucency" ) )
return "translucency";
if ( filename.Contains( "opacity" ) || filename.Contains( "alpha" ) )
return "opacity";
if ( filename.Contains( "emissive" ) )
return "emissive";
// Check occlusion last since "occlusion" could match "ambientocclusion"
if ( filename.Contains( "occlusion" ) )
return "ao";
return "unknown";
}
private static bool IsTextureFile( string path )
{
if ( string.IsNullOrEmpty( path ) )
return false;
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";
}
/// <summary>
/// Gets the display name for this asset
/// </summary>
public string GetDisplayName()
{
// Try to get name from various sources
// 1. Direct Name property
if ( !string.IsNullOrEmpty( Name ) )
return Name;
// 2. Megascans metadata name
if ( !string.IsNullOrEmpty( Metadata?.Megascans?.Name ) )
return Metadata.Megascans.Name;
// 3. Fab listing title
if ( !string.IsNullOrEmpty( Metadata?.Fab?.Listing?.Title ) )
return Metadata.Fab.Listing.Title;
// 4. Try to extract from path
if ( !string.IsNullOrEmpty( Path ) )
{
var dirName = System.IO.Path.GetFileName( Path );
if ( !string.IsNullOrEmpty( dirName ) )
return dirName;
}
return Id ?? "Unknown";
}
/// <summary>
/// Gets the display name for this asset (sanitized for file names)
/// </summary>
public string GetSafeFileName()
{
var name = GetDisplayName();
// Remove invalid characters
foreach ( var c in System.IO.Path.GetInvalidFileNameChars() )
{
name = name.Replace( c, '_' );
}
return name.ToLowerInvariant().Replace( " ", "_" );
}
}
/// <summary>
/// Represents a mesh/model file in the export (Fab format)
/// </summary>
public class FabMesh
{
// Fab uses "file" for the path
[JsonPropertyName( "file" )]
public string File { get; set; }
// Legacy "path" property
[JsonPropertyName( "path" )]
public string Path { get; set; }
[JsonPropertyName( "name" )]
public string Name { get; set; }
[JsonPropertyName( "type" )]
public string Type { get; set; }
[JsonPropertyName( "format" )]
public string Format { get; set; }
[JsonPropertyName( "material_index" )]
public int MaterialIndex { get; set; }
[JsonPropertyName( "lods" )]
public List<FabLod> Lods { get; set; } = new();
/// <summary>
/// Gets the actual file path (from File or Path property)
/// </summary>
public string GetFilePath() => !string.IsNullOrEmpty( File ) ? File : Path;
/// <summary>
/// Build an ordered list of LOD source file paths for this mesh.
/// Index 0 is the primary (LOD0) mesh; higher indices are progressively lower-detail LODs.
/// </summary>
/// <param name="fallbackLodList">Asset-level LodList to use when this mesh has no per-mesh Lods.</param>
public List<string> GetLodFilePaths( List<FabLod> fallbackLodList = null )
{
var paths = new List<string>();
var mainPath = GetFilePath();
if ( !string.IsNullOrEmpty( mainPath ) )
paths.Add( mainPath );
var lodSource = Lods != null && Lods.Count > 0 ? Lods : fallbackLodList;
if ( lodSource == null )
return paths;
// Sort by Lod index so LOD1 < LOD2 < LOD3 ...
foreach ( var lod in lodSource.OrderBy( l => l.Lod ) )
{
var p = lod.GetFilePath();
if ( string.IsNullOrEmpty( p ) )
continue;
// Skip the entry that points to the same file as the main mesh (avoids duplicate LOD0).
if ( !string.IsNullOrEmpty( mainPath ) && string.Equals( p, mainPath, StringComparison.OrdinalIgnoreCase ) )
continue;
paths.Add( p );
}
return paths;
}
}
/// <summary>
/// Represents a material definition from Fab (contains texture paths)
/// </summary>
public class FabMaterial
{
[JsonPropertyName( "name" )]
public string Name { get; set; }
[JsonPropertyName( "file" )]
public string File { get; set; }
[JsonPropertyName( "flipnmapgreenchannel" )]
public bool FlipNormalY { get; set; }
// Textures dictionary: maps texture type (albedo, normal, roughness, etc.) to file path
[JsonPropertyName( "textures" )]
public Dictionary<string, string> Textures { get; set; } = new();
/// <summary>
/// Gets texture path by type name
/// </summary>
public string GetTexturePath( string type )
{
if ( Textures != null && Textures.TryGetValue( type.ToLowerInvariant(), out var path ) )
return path;
return null;
}
}
/// <summary>
/// Represents a set of PBR textures (legacy format)
/// </summary>
public class FabTextureSet
{
[JsonPropertyName( "name" )]
public string Name { get; set; }
[JsonPropertyName( "textures" )]
public List<FabTexture> Textures { get; set; } = new();
}
/// <summary>
/// Represents a single texture map
/// </summary>
public class FabTexture
{
[JsonPropertyName( "path" )]
public string Path { get; set; }
[JsonPropertyName( "name" )]
public string Name { get; set; }
[JsonPropertyName( "type" )]
public string Type { get; set; }
[JsonPropertyName( "resolution" )]
public string Resolution { get; set; }
[JsonPropertyName( "format" )]
public string Format { get; set; }
/// <summary>
/// Maps Fab texture types to s&box naming conventions
/// </summary>
public string GetSboxSuffix()
{
return Type?.ToLowerInvariant() switch
{
"albedo" => "_color",
"diffuse" => "_color",
"basecolor" => "_color",
"normal" => "_normal",
"roughness" => "_rough",
"metalness" => "_metal",
"metallic" => "_metal",
"ao" => "_ao",
"occlusion" => "_occlusion",
"ambientocclusion" => "_ao",
"displacement" => "_height",
"height" => "_height",
"translucency" => "_translucency",
"opacity" => "_opacity",
"emissive" => "_selfillum",
"mask" => "_mask",
"bump" => "_bump",
"gloss" => "_gloss",
"glossiness" => "_gloss",
"specular" => "_specular",
"cavity" => "_cavity",
_ => $"_{Type?.ToLowerInvariant() ?? "unknown"}"
};
}
/// <summary>
/// Determines if this texture type requires linear color space
/// </summary>
public bool IsLinearColorSpace()
{
var type = Type?.ToLowerInvariant();
// Normal, roughness, metalness, AO, displacement are linear data
return type == "normal" || type == "roughness" || type == "metalness" ||
type == "metallic" || type == "ao" || type == "ambientocclusion" ||
type == "displacement" || type == "height" || type == "mask";
}
}
/// <summary>
/// Represents a component/variation of an asset (Quixel Bridge format)
/// Used for textures in Quixel Bridge: has type, path, format
/// </summary>
public class FabComponent
{
[JsonPropertyName( "path" )]
public string Path { get; set; }
[JsonPropertyName( "name" )]
public string Name { get; set; }
[JsonPropertyName( "type" )]
public string Type { get; set; }
[JsonPropertyName( "format" )]
public string Format { get; set; }
}
/// <summary>
/// Represents a native file reference
/// </summary>
public class FabNativeFile
{
[JsonPropertyName( "path" )]
public string Path { get; set; }
[JsonPropertyName( "type" )]
public string Type { get; set; }
}
/// <summary>
/// Represents LOD information
/// </summary>
public class FabLod
{
[JsonPropertyName( "path" )]
public string Path { get; set; }
[JsonPropertyName( "file" )]
public string File { get; set; }
[JsonPropertyName( "lod" )]
public int Lod { get; set; }
[JsonPropertyName( "name" )]
public string Name { get; set; }
public string GetFilePath() => !string.IsNullOrEmpty( File ) ? File : Path;
}
/// <summary>
/// Represents metadata about the asset (legacy format)
/// </summary>
public class FabMeta
{
[JsonPropertyName( "key" )]
public string Key { get; set; }
[JsonPropertyName( "name" )]
public string Name { get; set; }
// Value can be string, number, or boolean in the JSON
[JsonPropertyName( "value" )]
public object Value { get; set; }
/// <summary>
/// Gets the value as a string regardless of original type
/// </summary>
public string GetValueAsString() => Value?.ToString();
}
/// <summary>
/// Root metadata object containing launcher, megascans, and fab sections
/// </summary>
public class FabMetadata
{
[JsonPropertyName( "launcher" )]
public FabLauncherMetadata Launcher { get; set; }
[JsonPropertyName( "megascans" )]
public FabMegascansMetadata Megascans { get; set; }
[JsonPropertyName( "fab" )]
public FabFabMetadata Fab { get; set; }
}
/// <summary>
/// Launcher-specific metadata
/// </summary>
public class FabLauncherMetadata
{
[JsonPropertyName( "version" )]
public string Version { get; set; }
[JsonPropertyName( "listening_port" )]
public int ListeningPort { get; set; }
}
/// <summary>
/// Megascans-specific metadata (contains asset name, categories, maps, etc.)
/// </summary>
public class FabMegascansMetadata
{
[JsonPropertyName( "name" )]
public string Name { get; set; }
[JsonPropertyName( "id" )]
public string Id { get; set; }
[JsonPropertyName( "categories" )]
public List<string> Categories { get; set; } = new();
[JsonPropertyName( "tags" )]
public List<string> Tags { get; set; } = new();
[JsonPropertyName( "physicalSize" )]
public string PhysicalSize { get; set; }
[JsonPropertyName( "highest_available_res" )]
public int HighestAvailableRes { get; set; }
[JsonPropertyName( "meta" )]
public List<FabMeta> Meta { get; set; } = new();
}
/// <summary>
/// Fab marketplace metadata
/// </summary>
public class FabFabMetadata
{
[JsonPropertyName( "listing" )]
public FabListing Listing { get; set; }
[JsonPropertyName( "target" )]
public string Target { get; set; }
[JsonPropertyName( "quality" )]
public string Quality { get; set; }
[JsonPropertyName( "format" )]
public string Format { get; set; }
[JsonPropertyName( "isQuixel" )]
public bool IsQuixel { get; set; }
}
/// <summary>
/// Fab listing/product information
/// </summary>
public class FabListing
{
[JsonPropertyName( "title" )]
public string Title { get; set; }
[JsonPropertyName( "uid" )]
public string Uid { get; set; }
[JsonPropertyName( "description" )]
public string Description { get; set; }
[JsonPropertyName( "thumbnail" )]
public string Thumbnail { get; set; }
[JsonPropertyName( "listingType" )]
public string ListingType { get; set; }
}