Monday, October 10, 2011

MotionBuilder Python & Window Focusing

I recently had to do some window handle trickery with Python and MotionBuilder. I have an external python tool that is communicating with MotionBuilder, 3dsmax, and in the near future Maya. For this post I will only be touching on the MotionBuilder implementation of the tool.   When I run the tool it will return a list of scene objects within a list view. If I select on a item in the list view it should select the object within MotionBuilder. While the selection in the scene navigator is updated, since the tool is not a child of the MotionBuilder application, MotionBuilder will not redraw the viewport. I'm quite new to MoBu but the only code I saw to potentially force redraw the scene was.

FBSystem().Scene.Evaluate()

It is likely that the Mobu viewport/rendering is on a separate thread from normal scene processing. Which would explain why the scene navigator updates and the viewport does not. The viewport only seems to update if the application is the focused application. With this in mind I decided to force change the application focus to get the results I wanted.

When the python tool makes a connection to the specific dcc app I will call the following:
import win32gui, win32con
 
def enum_callback(self, hwnd, results): 
 self.winlist.append((hwnd, win32gui.GetWindowText(hwnd)))
 
def get_dcc_hwnd(self, app_name): 
 
 self.toplist = [] 
 self.winlist = [] 
 
 win32gui.EnumWindows(self.enum_callback, self.toplist) 
 
 # look for the specific application window name 
 dcc = [(hwnd, title) for hwnd, title in self.winlist if app_name in title.lower()] 
 if dcc != None and dcc[0] != None: 
  dcc = dcc[0] 
  self.dcc_hwnd = dcc[0]
 
get_dcc_hwnd( 'motionbuilder')               
This will return self.dcc_hwnd and all that really is is a window process handle or long integer. In this example it is. DCC Handle: 3214530

Now that I have stored the window handle when I select an object in my listview it will call the following:
def set_dcc_focus(self):
 if self.dcc_hwnd != None:
 
  # store the current python tool window handle
  current_hwnd = win32gui.GetForegroundWindow()
 
  # use the window handle to set focus
  win32gui.SetForegroundWindow(self.dcc_hwnd)
 
  # important to sleep here as to allow Mobu time to draw
  time.sleep(0.03)

  try:
  # set the focus back to the python tool
   win32gui.SetForegroundWindow(current_hwnd)
  except:
   return
With this I can click on all the objects in my external tool and see the select update 'realtime' within the MotionBuilder viewport. The code will change the window focus momentarily and just long enough to redraw the MotionBuilder viewport then immediately return focus back to the python tool. With the sleep time I have set here it is fast enough that I can be pressing down on the keyboard to step between items in the listview without noticing the app focus switching. However, I have noticed that if Motionbuilder doesn't have enough time to redraw, some things have the possibility of not rendering when the viewport is redrawn. So the sleep here could become variable and dependent on the amount of objects within the viewport.

UPDATE:
In the comments below, Kevin pointed out that you can skip all of this and modify the application.txt by adding the following line under [Timing]
NoFastIdleOnDeactivate = No

Thanks Kevin!

11 comments:

  1. Posted this back on twitter, but though maybe people who read your blog might be interested as well: You can also add a line into the application.txt file:
    Timing]
    NoFastIdleOnDeactivate = No

    This will make the UI update (at least the viewer) without being in focus.

    - Kevin

    ReplyDelete
  2. Fantastic! It works almost as good as my hack! :)

    Thanks for stopping by and pointing me in the right direction. I couldn't find this anywhere.

    ReplyDelete
  3. Thanks guys for posting this - Just what the doctor ordered!

    ReplyDelete
  4. Hello, I’m new in Python and MoBu, can someone help me? I have simple script:

    from pyfbsdk import *
    from time import sleep

    def FindObject(name):
    cl = FBComponentList()
    FBFindObjectsByName( name, cl, True, False )
    return cl[0]

    pCube = FindObject("Cube")

    pCube.Rotation = FBVector3d(0, 0, 10)
    FBSystem().Scene.Evaluate()
    sleep(1)

    pCube.Rotation = FBVector3d(0, 0, 20)
    FBSystem().Scene.Evaluate()
    sleep(1)

    pCube.Rotation = FBVector3d(0, 0, 30)

    What I have to do to see real-time rotation in viewport? This is very important to me, to have real-time refresh of view. Currently refreshing is done only on end of script execution. Please don’t write that this script is written poorly, this is only example. Thank you very much for any ideas.

    ReplyDelete
  5. Sune, I wish I could say there is a working solution to realtime updating but unfortunately there does not seem to be. I will point you to two area forum posts that seem to state that fact. If Kevin has found any additional information that states otherwise I'm sure we'd both be happy to hear it.

    http://area.autodesk.com/forum/autodesk-motionbuilder/python/how-to-update-the-timeline-cursor-position-realtime-when-running-a-script/

    http://area.autodesk.com/forum/Autodesk-MotionBuilder/python/iterating-through-per-frame-functions-without-stepforward/

    ReplyDelete
  6. Hi Linka,

    I you are dead set on having your viewport update and if you are doing something fairly repetitive on every frame (like animation export or maybe a custom plot method), you can attach calls to your code to the UiIdle event. I've constructed code the will cycle between doing 2-3 different things every iteration and it's not too hairy.

    That said, this is a bit of a different way of constructing a "script" and might be slightly complicated, depending on where you are at

    To get a head start you should have a look at FBSystemEvents.py in the Python samples, as well as "impexpsample" in the Open Reality SDK (C++) section.

    Doing it this way will let you see every update, but your interface will also be "live", so you can actually navigate the viewport, delete objects while the thing is running..

    All this said, I have no idea if there is an easy way of doing what you want. Or if all you really need is a progress bar :-)

    Cheers,
    Sune

    ReplyDelete
  7. Thank you very much for answers.
    Sune, yours approach with UiIdle event is what I need. My script will be modifying objects on scene based on data collected from the serial port. This data will be put into serial port by external device. Script will be used for scene preparation (setup camera position and angle, move some objects, rotate and scale them on the scene), and if performance will be enough then additionally also for simple motion capture. That's why would be great if UI will be "live".

    I will look into suggested samples. I will write my result.

    Thank you.

    ReplyDelete
  8. Great, do let us know how that works out!

    Cheers :-)

    ReplyDelete
  9. Sorry, maybe my question has obvious answer, but how can I run example: “FBSystemEvents.py”?

    When I’m trying to run it from Python Editor I’m getting an error:

    line 50, in Register
    sys.OnConnectionNotify.Add(OnConnectionNotify)
    AttributeError: 'NoneType' object has no attribute 'Add'

    I’m working with MB 2011.
    Please, give me some hint.

    ReplyDelete
  10. I'm on 2011 and I don't get these errors.

    I know this might be obvious, but do you think there is a chance that you or someone changed something in the script by mistake? Might be worth reverting to the original one, if you have not already.

    ReplyDelete
  11. Thank you all for help.

    My script running :)

    I used OnUIIdle event, like Sune suggested. This event gives me about 20 refreshes per second. MoBu UI "live" all the time. It is great :D

    Error what I had, was from environment variable for Python.

    ReplyDelete