Skip to content

Instantly share code, notes, and snippets.

@noisecrime
Last active August 15, 2024 08:28
Show Gist options
  • Save noisecrime/dfff33bac68b66a022a41d5f53c750da to your computer and use it in GitHub Desktop.
Save noisecrime/dfff33bac68b66a022a41d5f53c750da to your computer and use it in GitHub Desktop.
Unity Editor script that allows you to double-click a fbx file in the project browser and it will open and import into Blender.
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;
/// <summary>
/// NoiseCrimeStudio OneShots are single scripts that provide specific funactionality.
/// </summary>
namespace NoiseCrimeStudios.OneShot.Editor
{
/// <summary>
/// Double click a FBX file in the Project Browser for it to be imported into Blender.
/// </summary>
/// <remarks>
/// FBX2BlenderImporter
/// https://gist.github.com/noisecrime/dfff33bac68b66a022a41d5f53c750da
/// 2024.08.12 Version 2.0
/// (c) 2024 NoiseCrime
///
/// Public domain, do with whatever you like, commercial or not.
/// This comes with no warranty, use at your own risk!
///
/// INSTALLATION
/// Place this script into an EDITOR folder in your Unity Project Assets.
///
/// INSTRUCTIONS
/// Double-click a FBX file in the Unity Project Browser to open/import it directly into Blender.
/// If you have another default program assigned to FBX file types then you can hold Shift key down
/// when double clicking to open using it instead.
///
/// NOTES
/// Blender version is found via the Windows Registry program association for '.blend' files.
/// If no version is found it will ask you to locate one.
/// If you need to use a specific version use the 'Windows/NoiseCrimeStudios/Locate Blender Application' menu.
/// </remarks>
public static class FBX2BlenderImporter
{
// Unity MenuItem path strings
private const string MenuItemWindow = "Window/NoiseCrimeStudios/Locate Blender Application";
// Blender expected default location
private static readonly string s_blenderDefaultLocation = @"C:\Program Files\Blender Foundation\Blender\blender.exe";
// Blender cached location in Unity EditorPrefs
private static readonly string s_userBlenderLocationKey = "com.noisecrimestudios.FBX2Blender.location";
// Filters for OpenFilePanelWithFilters
private static readonly string[] s_filters = new string[] { "Executable", "exe" };
[MenuItem(MenuItemWindow, false, 426)]
private static void UserBlenderLocation()
{
LocateBlenderDialog();
}
/// <summary>
/// Use double-click callback on asset to trigger import of the FBX to Blender.
/// </summary>
/// <remarks>
/// Important: This will intercept ALL double clicks - in Project Browser and Console!
/// </remarks>
[UnityEditor.Callbacks.OnOpenAssetAttribute(1)]
static bool OnOpenedAsset(int instanceID, int line)
{
// Hold down shift when double clicking to allow Unity to handle the asset natively.
if ( Event.current.shift )
return false;
// Check Extension
string assetPath = AssetDatabase.GetAssetPath(instanceID);
string extension = Path.GetExtension(assetPath).ToUpperInvariant();
// Returning false will allow Unity to handle the asset natively.
if ( !extension.Equals(".FBX") )
return false;
SendFBX2Blender(assetPath, RegistryBlenderLocation());
return true;
}
private static void SendFBX2Blender(string assetPath, string blenderLocation = "")
{
try
{
// If location is invalid check User Preferences for Blender location.
if ( string.IsNullOrEmpty(blenderLocation) )
blenderLocation = EditorPrefs.GetString(FBX2BlenderImporter.s_userBlenderLocationKey, FBX2BlenderImporter.s_blenderDefaultLocation);
// If location is invalid ask user to locate Blender and update our preferences.
if ( !File.Exists(blenderLocation) )
blenderLocation = LocateBlenderDialog();
// Get absolute path to FBX file and replace backslash with forwardslash for impoprting into Blender.
assetPath = Path.GetFullPath(assetPath).Replace(@"\", @"/");
// Validate Exists
if ( !File.Exists(assetPath) )
throw new FileNotFoundException($"FBX2BlenderImporter: FBX file not found.\n{assetPath}");
// Validate FBX
if ( !Path.GetExtension(assetPath).Equals(".fbx", System.StringComparison.InvariantCultureIgnoreCase) )
throw new InvalidOperationException($"FBX2BlenderImporter: File is not FBX\n{assetPath}");
// Construct arguments for commandline
string arguments = $"--python-expr \"import bpy;bpy.ops.import_scene.fbx( filepath = '{assetPath}' )\"";
// Debug.Log($"{arguments}\n{assetPrelativePathath}\n{absolutePath}\n{blenderLocation}");
// Create Process to open Blender.exe and pass in commandline arguments
Process process = new Process();
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.FileName = blenderLocation;
startInfo.Arguments = arguments;
process.StartInfo = startInfo;
process.Start();
}
catch ( Exception e )
{
Debug.LogWarning($"FBX2BlenderImporter: Failed. See Console for details.\n{e}");
}
}
/// <summary>
/// Allow user to locate the Blender application via file dialog.
/// </summary>
private static string LocateBlenderDialog()
{
string location = EditorUtility.OpenFilePanelWithFilters("Locate Blender.exe", @"C:\Program Files\", FBX2BlenderImporter.s_filters);
if ( string.IsNullOrEmpty(location) || !File.Exists(location) )
throw new FileNotFoundException($"FBX2BlenderImporter: Blender.exe not assigned.\nPath: [{location}]");
// Cache User Preferences for Blender location.
EditorPrefs.SetString(FBX2BlenderImporter.s_userBlenderLocationKey, location);
return location;
}
/// <summary>
/// Retreieve Blender application via Windows Registry.
/// </summary>
/// <returns></returns>
private static string RegistryBlenderLocation()
{
string blenderLocation = "";
try
{
blenderLocation = WindowsShellApi.AssociationQueryString(WindowsShellApi.ASSOCSTR.EXECUTABLE, ".blend");
// Windows tries to be helpful asking you to launch 'OpenWith.exe' but that would just be confusing here!
if ( blenderLocation.Equals(@"C:\WINDOWS\system32\OpenWith.exe") )
{
blenderLocation = "";
throw new InvalidOperationException("No default application assigned to blend files.");
}
}
catch ( InvalidOperationException e )
{
Debug.LogWarning($"FBX2BlenderImporter:AssociationQueryString: Failed. Trying alternative methods.\n{e}");
}
return blenderLocation;
}
}
/// <summary>
/// Access Windows registry to discover file/protocol associations.
/// </summary>
/// <remarks>
/// https://learn.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-assocquerystringa
/// </remarks>
public static class WindowsShellApi
{
#region Window Enum Types
/// <summary>
/// The flags that can be used to control the search.
/// It can be any combination of ASSOCF values, except that only one ASSOCF_INIT value can be included
/// </summary>
[Flags]
public enum ASSOCF
{
NONE = 0x00000000,
INIT_NOREMAPCLSID = 0x00000001,
INIT_BYEXENAME = 0x00000002,
OPEN_BYEXENAME = 0x00000002,
INIT_DEFAULTTOSTAR = 0x00000004,
INIT_DEFAULTTOFOLDER = 0x00000008,
NOUSERSETTINGS = 0x00000010,
NOTRUNCATE = 0x00000020,
VERIFY = 0x00000040,
REMAPRUNDLL = 0x00000080,
NOFIXUPS = 0x00000100,
IGNOREBASECLASS = 0x00000200,
INIT_IGNOREUNKNOWN = 0x00000400,
INIT_FIXED_PROGID = 0x00000800,
IS_PROTOCOL = 0x00001000,
INIT_FOR_FILE = 0x00002000
}
/// <summary>
/// The ASSOCSTR value that specifies the type of string that is to be returned.
/// </summary>
public enum ASSOCSTR
{
COMMAND = 1,
EXECUTABLE,
FRIENDLYDOCNAME,
FRIENDLYAPPNAME,
NOOPEN,
SHELLNEWVALUE,
DDECOMMAND,
DDEIFEXEC,
DDEAPPLICATION,
DDETOPIC,
INFOTIP,
QUICKTIP,
TILEINFO,
CONTENTTYPE,
DEFAULTICON,
SHELLEXTENSION,
DROPTARGET,
DELEGATEEXECUTE,
SUPPORTED_URI_PROTOCOLS,
PROGID,
APPID,
APPPUBLISHER,
APPICONREFERENCE,
MAX
}
#endregion
[DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)]
private static extern uint AssocQueryString(
ASSOCF flags,
ASSOCSTR str,
string pszAssoc,
string pszExtra,
[Out] StringBuilder pszOut,
ref uint pcchOut
);
/// <summary>
/// Perform a AssociationQuery on Registry
/// </summary>
/// <param name="association">The ASSOCSTR value that specifies the type of string that is to be returned.</param>
/// <param name="extension">A pointer to a null-terminated string that is used to determine the root key.</param>
/// <returns></returns>
/// <remarks>
/// A pointer to a null-terminated string that is used to determine the root key.
/// The following four types of strings can be used.
/// File name extension: A file name extension, such as .txt.
/// CLSID: A CLSID GUID in the standard "{GUID}" format.
/// ProgID: An application's ProgID, such as Word.Document.8.
/// Executable name: The name of an application's .exe file.
/// The ASSOCF_OPEN_BYEXENAME flag must be set in flags.
/// </remarks>
public static string AssociationQueryString(ASSOCSTR association, string extension)
{
const int S_OK = 0;
const int S_FALSE = 1;
uint length = 0;
// First call to find length
uint ret = AssocQueryString(ASSOCF.NONE, association, extension, null, null, ref length);
if ( ret != S_FALSE )
throw new InvalidOperationException("Could not determine associated string");
// Second call get the data
var sb = new StringBuilder(( int )length); // (length-1) will probably work too as the marshaller adds null termination
ret = AssocQueryString(ASSOCF.NONE, association, extension, null, sb, ref length);
if ( ret != S_OK )
throw new InvalidOperationException("Could not determine associated string");
return sb.ToString();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment