Last active
August 15, 2024 08:28
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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