Pyblish in unity

got pyblish to load in unity.
next step will be get QML working.

some experimenting getting pyblish to work in unity
will follow up sometime later

the python package for unity has come along and now has python 3 support, build in.
to install add this to the manifest:

"com.unity.scripting.python": "4.0.0-exp.5",

related thread: Sharplish - Porting Pyblish to C#
curious if this is still relevant / worked on.

lite is easy to get to work, struggling with QML. likely it cant find pyqt5

lite launched from unity, with unity as host
note it does freeze unity untill you close pyblish.
prob have to launch as a subprocess to remove the lock.

this doesn’t happen anymore, unsure why

note that i installed pyside2 in the unity folder, to get the included example to work.
go to project settings, python tab, then launch the console
image
when pip installing through here it will install to sitepackages in your unity project.

trying to get QML in unity but failing. nothing shows, nothing errors

import pyblish.api
import pyblish_qml
from pyblish_qml import api

pyblish.api.register_gui("pyblish_qml")
api.register_python_executable(r"C:\Projects\ML_test\ML_test\Library\PythonInstall\python.exe")
api.register_pyqt5(r"C:\Projects\ML_test\ML_test\Library\PythonInstall\Lib\site-packages") #PyQt5"
pyblish.api.register_host("unity")
result = api.show()

getting following output

Already installed, uninstalling..
Pyblish QML shutdown successful.
Using Python @ 'C:\Projects\ML_test\ML_test\Library\PythonInstall\python.exe'
Using PyQt5 @ 'C:\Projects\ML_test\ML_test\Library\PythonInstall\Lib\site-packages'
Targets: default

i’ve tried to install pyqt5 but no luck getting QML to work.

sample unity collect plugin. get’s all cameras from the scene

class CollectCameras(pyblish.api.ContextPlugin):
    """Get cameras"""
    label = "Collect unity cameras"
    order = pyblish.api.CollectorOrder
    hosts = ["unity"]
    families = ['cameras']

    def process(self, context):
        cameras = [x.name for x in UnityEngine.Camera.allCameras]
        if cameras:
            print(cameras) # outputs: ['Main Camera']
            instance_cameras = context.create_instance('cameras', icon="cubes", families=self.families, family=self.families[0]) 
            instance_cameras[:] = cameras 
            
pyblish.api.register_plugin(CollectCameras)

Great job getting Unity up to modern publishing practices! :slight_smile:

Frankly I’m surprised it doesn’t, as it really should. Qt is an event loop, like an infinite while loop. It is designed to be the main loop of any thread it runs in, and handle events within. Given Unity is not running within such a Qt loop, starting a Qt loop within Unity’s main loop should block all traffic.

If it doesn’t, then Qt wouldn’t be running its loop which technically should make the Qt GUI either unresponsive or extremely sluggish.

To make such a thing work, you would need to interleave the two loops; such as setting up a timer in Unity to call Qt every so often (e.g. at 60 FPS). There is material online for this so I won’t go through it here, it’s not terribly uncommon although it is not the common path.

Getting QML running however should not be a problem, as this is the usecase it was designed to handle. QML would run external to Unity, with a completely separate even loop, within its own Pyblish (sub)process.

When you run it, make sure it also has access to the PYTHONPATH environment variable. Here’s a complete example that should work in any DCC with support for Python.

i was using the unreal post as a guide actually :slight_smile:

since QML runs in a sep process i figured that would be better for unity
but since i don’t get an error or crash i’m a bit lost atm on how to continue.
I might have another go at it sometime.

is there any chance the QML is instantly closed/garbage collected since we don’t save a reference to it?
i’ve run into similar things before when running QT outside of maya, and i had to store the isntance in a non used var to keep it alive.

It’s possible Unity calls each Python command in a separate Python instance, in which case yes it would vanish and take QML with it. You could easily test this by e.g.

myvar = 5
print(myvar)

If this works, then commands are run in the same process. If not, they are independent, which would be a terrible Python integration.

If it does work, then modules would also be kept around, and the QML server is stored here.

You could try and inspect that via an interactive shell inside of Unity to see if it reveals any clues.

from what i remember every time i ran the python command in the editor, i had to reimport my libs.
they didnt stay around

tested and can confirm unity starts a new session every time:
sadness_unity

and i’d guess when doing a while loop it would freeze unity when python is executing
i wonder if we can somehow save the python instance in a c# editor class to keep it alive.

btw if you call both in the same execute it would work

myvar = 5
print(myvar)

but as soon as the execute finishes it wipes everything.
so the QML launches and instantly gets collected

i need to look at the pyside2 example they include with the package.
they do some thread voodoo in there and get a pyside2 window working that controls unity without freezing.

the example code from unity is great.
but nearly 200 lines of code to get 1 button in pyside, and select a camera from the scene.
in c# it’s like 5 lines? :sweat_smile:

it seems it might be trickier than i expected.

also leaving the lite code here to save myself (and anyone else) time later

import pyblish_lite
import pyblish.api
pyblish.api.register_gui("pyblish_lite")
window = pyblish_lite.show()

pyblish lite only freezes unity the first time you launch it.
when you close and relaunch it, it works fine.
Magic! (and this is consistant behaviour)

if you want it to work from first launch, you can do the following overcomplicated setup:

reusing the camera pyside2 sample code, we can launch lite first time without freezing unity.
(the proper non magic way)
(honestly way easier to just launch pyblish and close re-open. :laughing: )

reloading python modules needs restarting unity. :confounded:
not sure yet how to reload python modules in unity which really slows things down.

files: (most code is frankensteined from the unity example)

  • pyblish_unity.py
import logging
import os
import sys
import traceback
from unity_python.common.scheduling import exec_on_main_thread, exec_on_main_thread_async
# This is the C# System.dll not the Python sys module.
import System
import UnityEngine
import UnityEditor
from UnityEditor.Scripting.Python.Samples import PySideExample
import pyblish.api
import pyblish_qml
from pyblish_qml import api

# Get PySide2
try:
    from PySide2 import QtCore, QtUiTools, QtWidgets
    # from Qt import QtCore, QtWidgets # lite doesnt launch with Qt :(
except ModuleNotFoundError:
    UnityEngine.Debug.LogError("Please install PySide2 to use the PySideExample")
    raise

### Globals
_PYSIDE_UI = None
_qApp = None

### UI class
class PySidePyblishUI():
    # If we use slots we need to include weakref support
    __slots__ = [ '_dialog', '__weakref__' ]
    def __init__(self):
        self._dialog = None
        try:
            import pyblish_lite
            import pyblish.api
            pyblish.api.register_gui("pyblish_lite")
            window = pyblish_lite.show()
            self._dialog = window # lite returns a dialog, QML returns a server :P 
        except:
            log('Got an exception while creating the dialog.', logging.ERROR, traceback.format_exc())
            raise

# For logging, we don't need to wait for the log to occur before returning
# control: we can asynchronously execute it.
@exec_on_main_thread_async
def log(what, level=logging.INFO, traceback=None):
    """
    Short-hand method to log a message in Unity. At logging.DEBUG it prints
    into the Editor's log file (https://docs.unity3d.com/Manual/LogFiles.html)
    At level logging.INFO, logging.WARN and logging.ERROR it uses
    UnityEngine.Debug.Log, UnityEngine.Debug.LogWarning and
    UnityEngine.Debug.LogError, respectively.
    """
    message = "{}".format(what)
    if traceback:
        message += "\nStack:\n{}".format(traceback)
    if level == logging.DEBUG:
        System.Console.WriteLine(message)
    elif level == logging.INFO:
        UnityEngine.Debug.Log(message)
    elif level == logging.WARN:
        UnityEngine.Debug.LogWarning(message)
    else:
        UnityEngine.Debug.LogError(message)

def create_or_reinitialize():
    # Create the QApplication if not already created
    global _qApp

    # added this so we dont rely on the poor global implimentation, 
    # avoid a lot of exceptions when anything goes wrong.
    app = QtWidgets.QApplication.instance()
    if app:
        _qApp = app

    if not _qApp:
        # Important: on mac, disable the native menu bar handling -- otherwise
        # the Unity menus will disappear and you risk a crash when Unity exits.
        QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_MacPluginApplication)
        _qApp = QtWidgets.QApplication([sys.executable])

    # Create the window if not already created; show it if it was
    # previously created but was hidden.
    global _PYSIDE_UI
    if not _PYSIDE_UI:
        _PYSIDE_UI = PySidePyblishUI()
    else:
        _PYSIDE_UI._dialog.show()

def on_update():
    QtWidgets.QApplication.processEvents()
  • PyblishUI.cs
using System.IO;
using UnityEditor;
using UnityEditor.Scripting.Python;
using UnityEngine;
using Python.Runtime;
namespace UnityEditor.Scripting.Python.Pyblish
{
    public class PyblishLauncher
    {
        const string kStateName = "com.unity.scripting.python.pyblish";
        /// <summary>
        /// Hack to get the current files directory
        /// </summary>
        /// <param name="fileName">Leave it blank to the current files directory</param>
        /// <returns></returns>
        private static string __DIR__([System.Runtime.CompilerServices.CallerFilePath] string fileName = "")
        {
            return Path.GetDirectoryName(fileName);
        }
        /// <summary>
        /// Menu to launch the client
        /// </summary>
        [MenuItem("Python/Examples/pyblish")]
        public static void OnMenuClick()
        {
            CreateOrReinitialize();
        }
       static void CreateOrReinitialize()
       {
            // You can manually add the sample directory to your sys.path in
            // the Python Settings under site-packages. Or you can do it
            // programmatically like so.
            string dir = __DIR__();
            PythonRunner.EnsureInitialized();
            using (Py.GIL())
            {
                dynamic sys = PythonEngine.ImportModule("sys");
                if ((int)sys.path.count(dir) == 0)
                {
                    sys.path.append(dir);
                }
            }
            // Now that weve set up the path correctly, we can import the
            // Python side of this example as a module:
            PythonRunner.RunString(@"
                    import pyblish_unity
                    # reload(pyblish_unity)
                    pyblish_unity.create_or_reinitialize()
                    # pyblish_unity.setup()
                    # pyblish_unity.show()
                    ");
            // A domain reload happens when you change C# code or when you
            // launch into play mode (unless you selected the option not to
            // reload then).
            //
            // When it happens, your C# state is entirely reinitialized. The
            // Python state, however, remains as it was.
            //
            // To store information about what happened in the previous domain,
            // Unity provides the SessionState. Alternately we could have
            // stored the data in a variable in Python.
            //
            SessionState.SetBool(kStateName, true);
        }
        /// <summary>
        /// Reconnect to the PySide UI upon a domain reload, if we created it
        /// in a previous domain.
        ///
        /// This is also called when Unity starts, in which case we wont have
        /// previously created the PySide UI.
        /// </summary>
        [InitializeOnLoadMethod]
        static void OnDomainLoad()
        {
            if (SessionState.GetBool(kStateName, false))
            {
                CreateOrReinitialize();
            }
        }
        static void OnUpdate()
        {
            // This is another way to call Python, handy if you want to mix and match
            // languages. Best practice: dont store references to objects from Python
            // longer than you need to -- let them be garbage collected.
            //
            // If you have unexplained crashes when running in this mode, often
            // its because you forgot to take the GIL.
            using (Py.GIL())
            {
                dynamic module = PythonEngine.ImportModule("pyblish_unity");
                module.on_update();
            }
        }
    }
}