Mostrando entradas con la etiqueta Maya. Mostrar todas las entradas
Mostrando entradas con la etiqueta Maya. Mostrar todas las entradas

jueves, 2 de marzo de 2017

Simple VFX animation Rig

Some day back in my old days, in the beginning of my new 3d life i was exposed to help riggers and VFX department on how to rig physical properties like vector forces etc with control curves. And i was surprised what an easy task this was and how much of a problem this meant for some people. Although i understand it could have been ages for them since they left school (yes, this is not even university level maths!) you probably didnt get a scientific path in high school. Anyways, im far from being a math nerd myself, and if you are an artist not very familiar with vectors and matrices you will probably discover how surprisingly easy this is.

What we want is basically to control the direction of a vector, for example nucleus' gravity by means of the rotation controlled by a handle/curve control.

So basically this corresponds to rotating a vector by a rotation matrix!

vr = [M].vo

being "vo" the original vector direction and "vr" the rotated vector.


Basically you perform this operation with the vector product node and hook its output in this case right into the axis unit vector of a vortex field.

In the outliner you have this marvellous, beautiful arrow that serves as the possible curve control of a hipothetically more complex part of a rig, which indicates the initial direction of the vector.

And the results are here in a demo video using nParticles and field forces!!
God! that was quick!! i think this is the shortest blog entry ive done so far!!!! and in the middle of a working week!!!!

Hope you enjoyed!

 

sábado, 11 de febrero de 2017

PyQt Agnostic Launcher II

As part of the improvements i have been carrying on to the VFX pipeline we are developing i wanted to dig deeper into the problem of executing a PySide Maya tool outside the DCC, this is, as standalone, as well as being able to execute it inside Maya without doing any changes to the code. I already came out with a first version of the launcher which you can see in http://jiceq.blogspot.com.es/2016/08/pyqt-agnostic-tool-launcher.html  . This basically detects whether there isn´t a Qt host application running and if not, we assume it is Maya running.


 @contextlib.contextmanager  
 def application():    
   if not QtGui.qApp:  
     app = QtGui.QApplication(sys.argv)  
     parent = None  
     yield parent  
     app.exec_()  
   else:  
     parent = get_maya_main_window()  
     yield parent  


This works fine for the beginnings of a VFX pipeline, mostly based in Maya. But as soon as you face the need to integrate other heterogeneous packages (that ship with any version of Python and PyQt, which is becoming a standard in the industry. see: http://www.vfxplatform.com/  ) you will probably want to be able to, at least, run the same GUI embedded in different packages as well as standalone. So the need to distinguish between host apps arises and this first solution falls short.

One poor solution is to query the Operating System whether the maya.exe/maya.bin or nuke.exe/nuke.bin processes were running. In the following fashion, for example:

  
def tdfx_is_maya_process_running():    
   return tdfx_is_process_running('maya')
def tdfx_is_nuke_process_running():    
   return tdfx_is_process_running('nuke')
def tdfx_is_process_running(process_name):
   if os.platform() == 'windows':
      ''' specific os code here '''
      return is_running    
   elif os.platform() == 'linux':
      ''' specific os code here '''
      return is_running
   return False

And use this instead in the previous if-statement to retrieve the corresponding QApplication main window instance.

This is a very poor solution, if we can call it a solution. It doesnt work well: you may have an instance of Maya or Nuke running, but you may want to run in standalone mode your custom script from your preferred IDE. The above functions will both return True, first problem. Second, it will depend on order of evaluation, so if you are testing first "tdfx_is_maya_process_running()" then your launcher will attempt to get the Maya main window instance. And third and most important, your launcher wont work because internally it is detecting Maya, so it is reporting the presence of a QApplication.qApp pointer, when you are in standalone mode and there is no qApp pointer actually!

So basically, this approach is not valid. What we really want to query is not the processes running, but more specifically if my current script is running embedded in a qt host application or not, and if so, i want to be able to know which one is.

I googled a little bit and was surprised that some people had faced this problem and meanly resolved it their own -not so great and elegant- way. I just thought there must be some way in Qt to query the host application. I just cant acknowledge something so basic wasnt taken into account in the framework. After some looking into the documentation..eureka, i found this line:


QtWidgets.QApplication.applicationName()

which returns the name of the host application. In standalone Qt apps, it is a parameter that must be set by the programmer.


def tdfx_qthostapp_is_maya():
    return tdfx_qthostapp_is('Maya-2017')

def tdfx_qthostapp_is_nuke():
    return tdfx_qthostapp_is('Nuke')

def tdfx_qthostapp_is(dcc_name):
    from PySide2 import QtWidgets
    hostappname = QtWidgets.QApplication.applicationName()
    if hostappname == dcc_name:
        return True
    return False


Consequently my new contextmanager version takes the following form:

 @contextlib.contextmanager  
 def application():    
   if tdfx_qthostapp_is_none():  
     app = QtGui.QApplication(sys.argv)  
     parent = None  
     yield parent  
     app.exec_()  
   elif tdfx_qthostapp_is_maya():  
     parent = get_maya_main_window()  
     yield parent
   elif tdfx_qthostapp_is_nuke():
     parent = get_nuke_main_window()
     yield parent  

This is a step improvement towards easing the integration of other PyQt-API-based DCCs in a VFX pipeline and easing the task of the programmer, thus avoiding to produce GUI application-specific code. Nonetheless, there is still some work to do that i will deal with when i have more time. This is, making the GUI code fully portable between PySide2 and PySide (or Qt4 and Qt5). There are already some solutions out there like the "Qt.py module" that intends to abstract the GUI from the Qt4 to Qt5 big jump in recent Maya 2017 Python API.


viernes, 25 de noviembre de 2016

TACTIC Python API Tweak: Hack To Report Copied Byte Amount To Qt Widget

During the development of some Maya Tools that used the Southpaw Tactic Python API I bumped into the following, at first simple, problem: I wanted to give a visual report of the uploading progress process. Each artist had to check-in their work to the asset management system via internet.

The first version of the tool only gave report of the progress by means of a progress bar that visually was enough to notify when the upload had finished. This worked ideally for multiple tiny files. But soon Groom & Hair artists, as well as VFX artist where generating a lot of huge simulating data that needed to be uploaded.

We were working remotely and uploading the artist's work could easily take a couple of hours. The first approach was to use HTTP protocol to transfer those huge amounts of files. There we found a bug in the Python API of Tactic v4.4.04 that limited the file size to 10 MB (10*1024*104 bytes) that forced us to look in the documentation and upgrade to a newer version of Tactic that had this bug fixed. But that's another story.

What interests me here is that the Python Tactic API upload functions dont give any report of the number of bytes uploaded. It only gives a report of when an entire file has been checked-in, this is, by doing a Piecewise Check-In.

So we changed the upload method to use Tactic's handoff dir which consists basically on replacing the HTTP protocol by a protocol like CIFS or NFS where you just perform a copy from your local to the server's directory just like you would between two directories on your local filesystem.

That was the first step.

Now once, definitely using the most powerful transfer method. I only needed to have a look at the API. The "tactic_client_stub.py" module and the "TacticServerStub" class. The Piecewise Check-in works as explained here.



You can see that the API uses the "shutil.copy" and "shutil.move" methods to upload. I cannot tweak the "shutil" module, since it's a built-in one that comes by default with the Maya Python Interpreter. But i can build my own :))!!

My goal is to be able to report the amount of bytes transferred using a Qt Widget so basically i have to simulate a Signal/Slot behaviour from the copy/move methods. It would be nice if i could add a callback inside that method that triggered a Qt Signal, isnt it?!


A LEAST INTRUSIVE SOLUTION



The shutil module uses a lot of different methods to copy files considering the metadata, creation and last modification time, user owner and group owner and the permissions bits, etc. It is explained here.

All of them at last, call the "copyfileobj" method. That's the method i want to tweak.

Now, what kind of function can trigger a Qt Signal?? what are its requisites??

I remembered all Qt Classes inherit from the QObject Class.. A quick look at the PyQt Documentation explains it.


"The central feature in this model is a very powerful mechanism for seamless object communication called signals and slots"

So basically, the only thing i need is to define a class that inherits from QObject, define a custom signal and have the callback method to emit the signal!!. The following is not production code, it is just an example of how it would work.


All that is left is to catch the signal in the proper QWidget, with this information you can compute the time left for the upload to finish and hence give an estimate based on internet speed.

This solution is simple, straightforward and doesnt imply rewriting the TacticServerStub Class. Maybe if i find myself in the need of tweaking again i would consider writing my own TacticServerStub class.

Comments & Critics Welcome!!

viernes, 18 de noviembre de 2016

Animatable Pivot - Rolling Cube Demo

INTRODUCTION

During the production of "Deep" the movie, the rigging department had to design the rigs of ice cubes that some characters were pushing. In order to achieve this rolling cube the rig needed to dynamically change the rotation pivot.

I didnt have time to look further into it so i couldnt be of much help at the time. But since one of my interests is rigging i decided to dig deeper when i had enough time.

If you do a google search the problem of rolling cubes is something most Riggers and Character TDs have faced anytime. One of the most interesting articles on how it can be done is this one. But i wanted to do it my own way and in different ways. One using the Node Editor, Matrix Multiplication and Geometric Transformations and the other, by using constraints. I'll show both here.

The first approach is simple: animate the cube setting keys in the Rotate Pivot X,Y,Z  attributes. If you do that, you will notice that it doesnt work. Just when you change pivot, the cube suffers a translation due to the fact that the rotation is applied again but with the new pivot. This is, it doesnt remember the rotation you performed with the previous pivot.

So the solution is to calculate the translation difference between pivots and apply it to the top controller.


I started with the outliner configuration you can see above. This configuration is generic. It works for all kinds of meshes and any number of pivots, The cube pCube1 can be substitued by whatever mesh you want. Here to illustrate better, i have used a NURB Surface and positioned one in each corner of the cube.

The main transform group has an enum attribute to select the pivot. Once chosen, we calculate each pivots world position from the hierarchy and its local rotate pivot position. This is important because if you just simply use the pivots world rotate position you will cause a cycle in the transformations as it changes every time you rotate. The local rotate pivot position doesnt. So it becomes necessary to compute the world position traversing the hierarchy bottom-up. 

Here is the Node Graph.



Another way of doing it is, instead of using matrix multiplication, using the tools maya provides, this is by using constraints. We constrain from for example a locator to all the NURBS pivots. And instead of  using the Node Editor we manipulate the weights of the constraint of each of the pivots.

Both alternatives make use of scriptJobs. They are both here provided here.
The first one calculates the pivots difference in an accum buffer.



The second one modifies the contrain weights and applies the calculation to the Rotate Pivot of the transform group.



FURTHER DEVELOPMENT

I just wanted to play a bit with those concepts and figure out how i would tackle with the problem myself. Needless to say it still needs to be organised as most rigs are, which means being able to set keys in a Curve Control. This is not a big change though.

As I previously said, this configuration is generic. it would work for any kind of mesh and any number and distribution of pivots.

Here is the final video file.

viernes, 30 de septiembre de 2016

Python Multithreaded Asset Downloader

We are getting towards the end of the production of El Viaje Imposible 's teaser. A mixed 3d/real image project i ve been working on for the last few months along with other fantastic and experienced co-workers.

For the time we ve been developing the pipeline and artists using it, everyday there were some kind of issue and lately the problem was due to the http protocol we used to do the transfers. This was set from the very beginning hoping to review the different uploading methods our pipe allows in the future where the need to send massive amounts of files aroused.

Well, this time has come, Cloth and Hair Artists are already working and in order to pass on their work to the lighters they need to export caches files. Taking into account that our Hair plugin generates  a cache file per frame (even though one can choose to do inter frame caching also, i.e. to avoid flickering), that there may be a couple of plugin nodes that read/export cache multiplied by the number of characters in a shot this makes hundreds of files if not thousands of files to be sent to the server. Hence the need of a good bulletproof protocol.

Forgot to say, a lot of artists are working remotely! With all the inconvenientes this implies, you see.

This week we have improved a lot our checkin/checkout pipeline. We dont use anymore HTTP but have relied now on Samba as our audiovisual project management system allows this.

From my part, one of the improvements i ve done this week is to "parallelize" the assets downloader tool. The first release was running a unique thread in the background and downloaded each pipeline task assets sequentially. 

This was unbearable when we got deeper in the production as more advanced tasks depended upon all the previous  tasks. This means in order to perform a task, an artist should wait until near more than a hundred tasks were checked taking as long as 10 min sitting just with crossed arms.

IMPLEMENTATION

The goal was to substitute the sequential background thread with a configurable number of independent threads each in charge of checking the assets of a unique task. For this, we identify a class Job that is responsible for holding its own connection through Tactic API and all the metadata needed to tell to Tactic what are the assets it is looking for.

Then we define our Worker Class that will be sharing a thread-safe Queue. This Worker class will ask for the current job indefinitely while there are still jobs in the queue. Actually this is a variant of the Producer/Consumer problem where we fulfill the queue with jobs from the beginning, so there is no need for a producer thread.


class Worker(QtCore.QThread):
        
        '''
        define different signals to emit
        '''
        def __init__(self, queue, report):
            QtCore.QThread.__init__(self)
            self.queue = queue
            self.abort = False
            '''
            rest of variables
            '''        
        def run(self):

            while not self.abort and not self.queue.empty():
                job = self.queue.get()
                
                try:
                                
                    response = job.execute()
                    
                except Exception,e:
                    
                    process(e)

                self.queue.task_done()          


One of the problems left then is how to shutdown all the threads when closing the QDialog. I had quite a hard time figuring out the best way of doing it. 
Googleing a bit, people asked the same questions when your thread is running a while True sort of loop. Most people tend to confirm that the most elegant way is to put a "semaphor" also called "sentinel" which no any other thing that a boolean that is checked within every iteration. This allows to set this boolean from outside the thread, so next time it iterates it will jump out of the loop.

Another possibility is to put a Job None Object in the queue, so that immediately after retrieving it from the queue the thread checks its value and exits accordingly. This would work for a single thread, if we spawn 10 threads we should put 10 None Job Objects in the queue.

This leaves the question..¿how to terminate a specific thread? It's not needed here but rather something to think of later...

I resorted to the first elegant solution, that's the reason of the self.abort. So here is the code that overrides the closeEvent()


    def closeEvent(self, *args, **kwargs):
        
        for t in self.threads:
            if t.isRunning():
                t.abort = True
        import time
        time.sleep(2)
        for t in self.td.threads:
            t.terminate()
       
        return QtGui.QDialog.closeEvent(self, *args, **kwargs)


As you can see, before closing, we set the semaphore of each thread to True. The interesting thing about this code is that if you inmediately after try to terminate the thread (im not gonna discuss here the correctness of terminating/killing a thread) the window gets hung. Not sure why this is happening. All we need to do is sleep() a sufficient amount of time to give all the threads the chance to get out of the job.execute() and check for the semaphore.

My only concern with this solution is: what happens if one of the threads is downloading say 1 GB of data? would 2 seconds like in the example be enough time for it to get to the semaphore checking and then exit ? 

That's why i would really want to tweak the Tactic API and get low level for each downloaded chunk of data for example 10 MB. In 10MB slices this problem would disappear... but i'm stuck with the API for now and its interface.


IS IT REALLY PARALLEL?

Well not really. This same code in C or C++ would work totally parallelized but we are bumping into the GIL here, the Global Interpreter Lock of the MayaPy and CPython interpreters. You can have a look at all the posts regarding this in google. Basically, the GIL is a mechanism that forbids the python code to run more than one thread at the same time. This is to prevent the Interpreter's memory gets corrupted.

if we want full parallelization, we should go into multiprocessing which differs from multithreading in that each spawned process has its own memory space. Ideal when you dont need to share objects between processes for example or the need is little. Apart from the fact that, a lot of benchmarks that some people have done, come to the conclusion that in Python, multithreading tends to take more time in CPU-bound tasks that the same code running in single thread.

So if your task is CPU expensive, then try to go Multiprocessing rather than multithreading. But, if your tasks are I/O, networking ,etc (like it is the case here) i find multithreading more suitable. 

Nevertheless, i would like to give it another spin to the code a run benchmarks this time using the multiprocessing module.








domingo, 7 de agosto de 2016

PyQt Agnostic Tool Launcher

After some weeks of resting and a new challenge at Drakhar Studio where i am developing the nuts & bolts of a pipeline software that communicates with TACTIC, i have now some spare time to talk about one of my recent discoveries concerning Python programming.

I'm always looking on how to improve my code (in general, no matter what the programming language is), although it's certainly true Python is one in a million because of many reasons, one of them being that a bunch of design patterns are an intrinsic part of the language, such as decorators and context managers.

More specifically, i was looking for a way to run my tools regardless or whether it was standalone (this is, running its own QtApplication, or embedded into Maya's). In the past, i didnt have much time to dig into this and consequently, i used two separate launchers.

Last week this was solved by the use of a context manager. I realised i could make good use of it, since in both cases (running standalone or in a host app) i had to make the same two calls:

 tool_window = Gui_class(parent)  
 tool_window.show()  

Where Gui_class() is the main GUI PyQt Class. The difference is the parent argument where in one case it must be None and in the other it must be a pointer to the Maya GUI Main Window.

This difference in the parent argument could be handled just the same way as for example the open() context manager works:

 with open(filepath, 'wb') as f:  
    src_lines = f.readlines()  

In this case, the advantage is the user doesnt have to remember to close the file stream and the open() method yields the stream as 'f'.


LAUNCHER IMPLEMENTATION


 @contextlib.contextmanager  
 def application():    
   if not QtGui.qApp:  
     app = QtGui.QApplication(sys.argv)  
     parent = None  
     yield parent  
     app.exec_()  
   else:  
     parent = get_maya_main_window()  
     yield parent  


This is my implementation of my custom context manager, it is basically an if statement that checks whether the tool is being lauched from a host application or in his own QApplication. This function could be already the main launcher, the only need is to put the previous two lines showed where the yield statement goes. But this goes against one of the OOP principles, Dont Repeat Yourself (DRY).

So if we push a little bit further we end up with:

 def launcher(cls):      
   with application() as e:  
     window = cls(parent=e)  
     window.show()  

Where cls is the name of the main GUI class. This way, we have an agnostic launcher prepared to work as standalone and within a host app like maya.

Needless to say that some pieces of the tool only will work inside Maya but this way at least we can launch the tool for GUI refinement and development.

No more separate launchers with duplicate parts of the code!!









martes, 1 de marzo de 2016

Plugin Loading And Threads

Back to coding: TTL_Cameratronic
 
After some time solving issues regarding the publishing process of the shots i m finally back to coding. This time it's a tool whose purpose is to be able to ease the task of the crowd department. They need to work faster and cook simulations faster so the way the tool will work is: they will load the layout of a sequence of shots, with the corresponding cameras obviously, perform the cache of all the sequence and then cut where necessary.

This will require the tool first to iterate through the file system searching for camera files which come as alembics, read from the last published version the duration of the shot and then reference all the cameras setting them with the corresponding frame offset.

Ideally, the tool should also create a master camera which will switch between all the cameras referenced thus composing the sequence's final camera behaviour. I will do this by "parentconstraining" the master camera to all the shot cameras and then keyframing the weights to 0 or 1. Seems easy to do this way.

The tool also will show list of the camera names (which will have to be renamed after the shot number they represent), the start and end frames and finally the alembic file to be loaded (since there may be several different versions we will take always by default the last one).




Those are images of the look & feel of the tool still in development but so far functional: with splash screen and the final result in a list. It needs some tweaks more like a "delete cameras button" for example and offsetting the start frame. Also it lacks the master camera functionality. But i think all left to do won't be much problematic.


GUI & Threads

Since the search process in the filesystem can take a while i will have to deal with threads. Recall that the main thread is responsible for all the GUI painting so any process i want to do has to be pushed into the background in a secondary thread.

Also there is a progress bar which means in this case  a third thread responsible for actually referencing all the camera.

Both secondary threads are used sequentially, so there is no harm and trouble in things like data sharing or concurrency.

The bigger problem I faced, so to speak, and the reason of this post is that loading the alembic plugin caused me some pain at first. The tool has to check whether the plugin is loaded, and if not, proceed to the load.

Now, in the beginning i tried to do this in the same thread responsible for the alembics referencing just before. The result was Maya crashing....

Then, intuitively ( i hadnt read the documentation at that moment) i decided to move that method to the main thread. Now it wasnt crashing but the cameras werent loaded. But i noticed something: loading a maya plugin seems to take some time, a time where Maya is busy presumably registering all the plugins and furthermore seemingly also updates the GUI. This made me think of the "evalDeferred()" method and its updated, non-deprecated equivalent "executeDeferred()" which according to documentation:


maya.utils

The maya.utils package is where utility routines that are not specific to either the API or Commands are stored. This module will likely expand in future versions.
Currently, the maya.utils package contains three routines relevant to threading (see the previous section for details on executeInMainThreadWithResult).
There are two other routines in maya.utils:
  • maya.utils.processIdleEvents(). It is mostly useful for testing: it forces the processing of any queued up idle events.
  • maya.utils.executeDeferred().
    (Similar to maya.utils.executeInMainThreadWithResult() except that it does not wait for the return value.) It delays the execution of the given script or function until Maya is idle. This function runs code using the idle event loop. This means that the main thread must become idle before this Python code is executed.
    There are two different ways to call this function. The first is to supply a single string argument which contains the Python code to execute. In that case the code is interpreted. The second way to call this routine is to pass it a callable object. When that is the case, then the remaining regular arguments and keyword arguments are passed to the callable object.

 As said "It delays the execution of the given script or function until Maya is idle."

All i had to do is put the plugin loading method in the main thread and execute the code inside my thread with Maya.utils.executeDeferred().


Additional Comment

Another solution which havent been tested but i believe should work according to the documentation is if you really want the code of loading the plugin in the thread you should use executeInMainThreadWithResult().

Despite restrictions, there are many potential uses for threading in Python within the context of Maya; for example, spawning a thread to watch a socket for input. To make the use of Python threads more practical, we have provided a way for other threads to execute code in the main thread and wait upon the result.
The maya.utils.executeInMainThreadWithResult() function takes either a string containing Python code or a Python callable object such as a function. In the latter case, executeInMainThreadWithResult() also accepts both regular and keyword arguments that are passed on to the callable object when it is run.
 

 
 

miércoles, 23 de diciembre de 2015

Python Idiosyncrasies (I)

I want to post here some Python language characteristics i find different and interesting for anyone coming from more traditional ones such as C. So far i've used them every once and a while in the time i've been coding tools here in Madrid and i intend to do several related posts in the future as i learn new features.

I have to say that this only relates to Python 2.7 which is the version Mayapy 2015 comes with.

Composite conditions

Here we find the two key words: "any" and "all"

Both can receive as input a list of expressions that return a boolean and in turn the result is a boolean

"any" equivalent: "if sentence1 or sentence2 or sentence3.... or sentenceN"

"all" equivalent: "if sentence1 and sentence2 and sentence3... and sentenceN"

This way those complex expressions in the conditional can be reduced in combination with a list comprehension. For example this snippet of code shows how to check if some letters appear in a string:


1:  if any(letter in "somestring" for letter in ['a','b','c','s']):  


would return True whereas:


1:  if all(letter in "somestring" for letter in ['a','b','c','s'])  


would return False. 


Iterating multiple lists (I)

Use here the zip() command as stated here:


1:  for elementA, elementB in zip(listA,listB):  


It is worth noting that the zip command iterates until the last element of the shortest of the lists. Though many times both lists have same length, it might not always be the case. Furthermore, you might encounter the need to iterate until the longest list, returning a predefined value for empty element. There is another command for this from the itertools module:


1:  import itertools  
2:  list1 = ['a1','b1']  
3:  list2 = ['a2','b2','c2']  
4:  for element1, elemen2 in itertools.izip_longest(list1,list2)   


By default empty indices' value are set to 'None'.

What happpens if you want this behaviour but also be able to use the element's index?

There is the "enumerate" command that returns a list's current index and element.


1:  for index,element in enumerate(list1):  


And even more: we can combine iterating through multiple lists with the index of the element. In this case we will have one index and two variables holding each current element like so:


 for same_index, elementA, elementB in enumerate(zip(listA,listB)):  
 


 Iterating multiple lists (II)

What we have seen previously is okay, but there is a better way to do the same things in terms of execution time and memory consumption and it relies again on the itertools module.

Using a context manager i have measured the execution time of both functions hereby:


 def slow():  
          for i, (x,y) in enumerate(zip(range(10000000),range(10000000))):  
              pass  
 def quick():  
          for i, x,y in itertools.izip(itertools.count(), range(10000000),range(10000000)):  
              pass  
 measure(slow)  
 measure(quick)  


 Slow() function took 3.32 seconds whereas quick() took 1.30!! A good reason to take into account the itertools module.

Joining strings

Difference between os.path.join() and str.join() both are similar in behaviour.

os.path.join('path','to','directory')

takes a variable number of string arguments to assemble them all into a string with os.sep (operating system path separator). Note that it won't put an os.sep character at the beginning of the string neither at the end. But, if it's an absolute path you can always set as first argument the os.sep character.

if you have the arguments in a list instead of comma separated you can expand the list into it using the * operator thus this would also work:

os.path.join(*['path','to','directory'] 


 Now the str.join method does take a tuple or list as argument and the 'str' string or character will be the separator thus this:

'-'.join(['a','b','c'])

yields the following string : 'a-b-c'. Note again that there is no '-' at the beginining nor at the end.   

domingo, 13 de diciembre de 2015

Driving A Vector's Direction By Euler With Maya Node Editor

Introduction

Past thursday one my fellow mates at the office in charge of refining the shots and animating dynamics asked me to help him with a problem he was facing: he needed to map the euler rotation angles from a CTRL transform to the nucleus' gravity direction vector. In short: control with angles the direction of a vector.

He was trying with the "angleBetween" node looking for some way to solve his problem. I have to say that at first he wouldnt explain  to me correctly what he needed but that's because he was striving for it himself.

The next day, this is last friday, while at the bus in the morning to the office i remembered the conversation he had a couple of days earlier asking for this to my fellow riggers. They seemed to fill the expectations of my comrade in need with their answers. It wasnt clear for me but i was too busy with something else and didnt want to intercede.... Until now, where this was something that was puzzling him for quite a few days now.

So i started to suppose what was the problem and started to think how i would solve it with nodes.

Controlling Vectors through Rotation

In fact, once aware of the problem the solution is just applying some simple math. We have a curve/CTRL's transform that represents rotations with Euler angles.

Well, we need to apply the rotation of the control to the vector. In terms of maths, we need to get the matrix representation of the rotation. Maya's node editor has a ComposeMatrix node that generates a matrix with information on translation, rotation, scale, shear depending on what the input is.

Next step is to multiply this matrix by the vector we want to transform. Again, Maya has VectorProduct node for this that can do different operations such as Dot, Cross, VectorMatrix and PointMatrix.

v' = M * v

All that is left is just attach the output of the operation to the nucleus's gravity vector....and voilà!!!

Voilà????? Hold on a second, there is a little problem.

One little problem

The way i approached for the first time the node network i was convinced it had to work from the beginning since these are simple maths. But i was committing one error that stems from the fact that i was a little unaware of how nodes work.

I was using the current gravity vector direction to feed the operation and then the result attached back to the nucleus as input..... And i tried to test with known trigonometric values and i was getting a strange behaviour: the numbers just didn't correspond to a valid result.

My network suffered from a "cycle" that never stopped: i'm feeding the input of the attribute with the output of the same attribute after doing some calculation....

But i found one workaround for this.



Workaround

After trying inefficiently to break that cycle using an intermediate transform where i would store the result and pass it on again to nucleus i decided to write a post in a Maya forum convinced of the fact that this can be made without using a transform or any other supplemental entities.

But i kept thinking after the post, enbraved by the fact that i wasnt getting a quick question and i was stuck. Well, if all i need is the starting value of the nucleus how about putting that same initial value into a transform and use it instead of the nucleus to feed the calculation? That way  i can attach directly the result to the nucleus gravity vector. All i have to make sure is that both vector values: nucleus initial value and transform's value are the same!! It doesnt matter really which channels of the transfrom i use provided they represent the three vector componentes X, Y,Z. I decided to use the translate.






Additional Comments

It's worth noting that since the initial value of the vector is such that the module is 1.0, the results after the matrix multiplication must be also of module 1.0. Since a rotation matrix doesnt change the module of a vector. 

This was helpful to rapidly notice wrong numbers and therefore there was a problem. Also, testing with cosine and sine values of most known angles such as 0,30,45,60,90 degrees.... One is used to see numbers like 0.707 or 0.5 and 0.866 etc....

Also, note that because we are using the rotation the translation of the control/curve doesnt matter.

jueves, 17 de septiembre de 2015

Nested References in Maya

I've found myself in the need at work to be able to get all the references a shot had. What you can get with file(q = True, r = True) are only the top level references leaving behind the nested ones.





I've read somewhere that is a good practice trying to avoid such levels of depth, but in this case we were in a closet.

So after a while searching the web and the official Maya python documentation i hit with referenceQuery and the "children" flag.

Finally i developed this simple method that fills a python list with reference filenames:


    def add_nested_references(self,parent_ref_list):
       
        for parent_ref in parent_ref_list:
            child_list = mc.referenceQuery(parent_ref, f = True, ch = True)
            if child_list != None:
                   self.add_nested_references(child_list)
                   parent_ref_list.extend(child_list)




domingo, 2 de agosto de 2015

Advanced Multithreading GUI Programming with PySide

One of the tasks i was in charge of was to look for a video player in linux that met several requirements from the art director. Some of them were:

- available for linux
- free
- frame by frame backwards and forward playing
- hotkeys for the above, not only buttons
- able to play several clips in a row
- no blank frame between clips
- able to play the audio of the video as well
- autoloop feature
- plays .mov, .mp4, .avi, compatible with the most possible codecs out there.

The autodesk RV Player is the best one according to some experienced animators but we couldn't afford licenceses for each individual.

After testing the obvious ones such as VLC (with jump in time extension), Openshot, MPlayer (with SMPlayer front-end)... All of them were lacking any of the listed features. Concretely i couldnt restrain my disappointment when i found that VLC's extension wasnt working as claimed on the web.

Several days passed by, and in the meantime i was busy doing other stuff and the animators had to deal with openshot which is not properly a video player but rather a video editor.

Anyways i found out that openshot was built upon the media lovin't toolkit video engine (MELT from now on)

So i decided to install it and give it a go.

Couldn't compile the last version 0.9.6 so i tried the 0.9.2 and yeah it worked!!

I was testing it and realized it met all the primary requisites only drawback: it had to be launched from the terminal and this was not as much user-friendly as it should for an animator.

So i was talking to my boss and he suggested coding a gui for maya that listed all the videos in the work directory and launch the player through Maya. That's when i got "hands on".

CODING THE TOOL

I was getting pretty advanced with the gui programming in PySide/Python time to do a test and launch the player came.

And here i discovered a big problem for me that took me some time to resolve. The problem was that the video player was correctly launched if done manually from an opened terminal but when done from the python script with:

import subprocess

command = ["gnome-terminal", "-x", "bash", "-c", "/usr/bin/melt <videofilefortest>"]
subprocess.call(command, shell = False, env = os.environ.copy())

it was giving an error regarding one of the dynamic link libraries (the famous "DLL" files in windows).

TWO WAYS TO SOLVE IT

Further investigation revealed that doing "ldconfig" in that terminal and launching again the player was a solution. "ldconfig" apparently rebuilds a cache file that specifies the name and path of each dynamic library so the system is able to find them. That opened a way to go. I just had to be able to login as root in the script, do a "ldconfig" and then launch MELT.

Another logical way was to mimic the same environment configuration between the two terminals setting and unsetting the ENV variables until both matched. It was clear that both terminals had different configuration. A "printenv > env_good.txt" and "printenv > env_bad.txt" helped to list the ENV variables of both terminals.

SOLUTION

I was jumping from one strategy to the other always testing. Had some difficulties trying to login as root in a script and also i wasn't confortable hardcoding the root password for obvious security reasons.

Finally i noticed that some important ENV variables werent set in the "good terminal" such as PYTHONHOME PYTHONPATH etc. So I decided to UNSET them prior to launching the player. With some help from a linux forum a user suggested also to UNSET not only those variables but also PATH, LD_LIBRARY_PATH. I agreed and finally it worked!!

EXPLANATION

Apparently, the way subprocess.call works is it executes the commands in a child process. Child processes inherit by default the environment from the parent process which in this case is Maya. But Maya itself is launched with specific configuration that doesnt necessarily match the default one.

It was the LD_LIBRARY_PATH what made it work. Makes sense since the concrete problem was the system wasnt finding where the libraries were.


ADVANCED GUI PROGRAMMING

That problem solved the rest was straightforward.

I noticed that the "search-the-filesystem-for-videos" was taking a bit too much time causing the tool to be apparently "frozen". So I decided to add something showing the tool was busy while searching. Something like a "living" waiting pattern.

This obviously led to the use of multithreading: one showing the waiting state while the other performs the search.

First i was wrong because i tried to run in a background process the GUI waiting icon. And i was wrong because a simple search in google showed that all the GUI processing has to be done in the main thread, and not in secondary ones. That is a good thing to know. I just had to switch the tasks and.. voilá.

I was really proud of it, not that it mattered too much since it was more like an aesthetic thing but i have to admit i haven't done much multithreading programming in all my years of programming.

domingo, 19 de julio de 2015

Selection Button In Maya MEL

It's been a while since my last post. The reason is that work's keeping me really busy and i'm learning a lot about Python and MEL. 

The last couple of weeks i 've left temporarily rigging the props and i 've got back to scripting.

It happens that some of the main characters' rigs cannot use FK because the dynamics don't work, so i was talking to one of the lead animators and he came up with an idea, and i was the one to code it and make it possible. I did several scripts that accomplish all the tasks the lead animator asked for the main and secondary characters. Still it has to be tested by the art director and the other lead animators but it feels like it's really useful since it mimics an FK chain with IK controls.

One of the scripts is a panel with selection buttons. Those buttons select each locator so they can animate the locators and then bake the animation.

In MEL we have the "select -r <whatever>" which selects the new by replacing the old. Also we have "select -tgl <whatever>" which adds the selected to the current selection.

The MEL code for a button is:

button -label $label -command $command

Where $command stands for some sort of  "select" as described before. The problem is none of them 
works as it should. I wanted to "select -r" replace but with the ability to Shift Select other locators, this is to add them to the selection if i use Shift just like Maya works.

The solution is to add the following line of code to the $command:

So that finally the button code results like this:

string $command = "string $object=\"object_name_to_select\"; int $mods = `getModifiers`; if (($mods)%2 == 1) select -add $object; else select $object;";

button -label $label -command $command

I'd like to post some rigs i've done but i'm afraid i cannot show the models due to confidentiality terms. 

domingo, 19 de abril de 2015

UV Texturing: A Problem and A Solution

These days there's been a lack of entries in my blog mainly cuz i'm spending my time developing my demo reel and mainly cuz i want to meet the final deadline of the CG Student Awards 2015.

My intention is to present a short cut of say 2 or 3 seconds but caring about all aspects of CG and after, show the breakdown of everything. It will be a pirate shooting a ship cannon, there will be particles, characters, hard surface modeling, compositing and.... texturing!

Yes the problem i faced i want to talk about now is making the uv's of my cannon. I mean, i could just have done automatic uv's with mudbox but i wanted to follow the same process as in videogames and what the hell , i wanted to learn how to make proper UVs!

For this, i had my cannon finished and i exported the base mesh without smoothing/subdividing it to an OBJ file in order to be imported in UVLAYOUT 2.06. The UVs are always made in the low base mesh resolution.

Well, i started working in the uvs, it was a lot of work since i handmade all the uvs of even the tiny nails. And when i got to the point where i wanted to export my model with the uvs if found what i thought was a big problem: Headus UVLayout 2.06 exported all in a single mesh, despite the fact that it can read and load an obj with groups of meshes.

So what a problem i thought, since i wanted to be able to maintain the groups of meshes to work easily when texture painting and giving highest resolutions to different parts of the cannon. I really though at first i had wasted a lot of time.

The first thing to clarify was if the problem was effectively from the Headus UVlayout 2.06 tool. Navigating through the forums of UVLayout i found exactly that from version 2.07 up until the current they added the functionality to export an obj with several groups of meshes!!. Well from now on i will work with Headus  UVLayout 2.08 if i can.

Now the problem was still there: how to transfer the uvs from the obj with single mesh to a multimesh object with no uvs.

The answer again was in the internet. In Maya there is a tool called "Transfer Attributes" which can do just that: it transfers the properties of a mesh to another, i was still a bit skeptical since one obj file was all packed in a single mesh and my Maya cannon original low res file was grouped into different meshes, so not quite sure it would work.

I opened my Maya file with the low res grouped into meshes ship cannon and i imported the obj file with the UVs. Selected both, did "Transfer Attributes" Options, checked "UV" and applied..... And it Worked!!

Now I have at last, my model separated and grouped in different meshes all with the same UVs i had worked!!!. Now I can proceed to Mudbox and start painting!!!

lunes, 23 de febrero de 2015

Maya Python/PyQt Alignment Plugin Rewritten and Extended

Unfortunately, a few days ago my computer decided to crash forcing me to reinstall the OS. I thought i had all my code files and stuff safe in the Data drive... well I proved to trust wrong because guess where the system got installed with the recovery partition? yes !! in my data drive!!. What pisses me off mostly is that i ve lost all the five programming assignments from the Coursera MOOC "Algorithms, part I", which i ve previously talked about in this blog. Other code I had was some python/pygame computational geometry stuff i did in my spare time.

And finally, my first Maya plugin also was lost. So far from being discouraged, i decided to recode again the plugin adding the feature someone suggested me in the spanish TD facebook group and also i decided to extend the plugin in order to be able to do 3 types of operations with the object's pivot.

Before i post the youtube video showing how it works i will talk a bit about some tips i didn't post previously that i had forgot and that i had to face again.

Qt Designer

I used Qt Designer to visually establish the layout of my window. Two things about this:

- first, i wanted to use two sets of exclusive radioButtons which led me to set two separate QButtonGroups. With the left button on each QButtonGroup i selected the radiobuttons to be members of it.

- Secondly. I wanted to put QGroupBoxes because it is a feature that enhances the visual look of the window as well as helping figure out visually which parameters are part of the same option. For this you have to firstly select the groupbox and drop it in the window layout, and after and only after, put inside the necessary widgets. You will notice if you have done it correctly because the widgets appear to be "parented" to the groupbox in the "Object Inspector" 

This is how it looks like:




UI xml file conversion to python file

The .ui file saved by Qt Designer is no more than an xml file that has to be converted to .py file with all the window layout so that we can instance it in our window class from our Python/PyQt main file.

For this we open the windows command line, we set the path to our .ui file and type:

pyuic4 align.ui > align.py


Our Maya Plugin

We declare our class that this time inherits from QWidget and add the WindowStayOnTopHint.

class AlignWindow(QtGui.QWidget):
    def __init__(self, parent=None):

        QtGui.QWidget.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint)
        self.ui = Ui_Alignment()
        self.ui.setupUi(self)


Since Maya is running its own QApplication thread this code causes Maya to stall:

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    mw = AlignWindow()
    mw.show()
    sys.exit(app.exec_())

But instead can make the window visible when running from Visual Studio. To make it work in Maya it suffices to write:

myWindow = AlignWindow()
myWindow.show()


Three different Pivot Alignment Techniques

- pivot in object(s). Aligns the pivot(s) within the bounding box(es) of each of the object(s). That was what my first plugin, yes the one i lost did.

- pivot(s) to object's pivot. Aligns a set of object(s) pivot(s) to the last selected object's pivot.

- object(s) to object. Aligns a set of object(s)'s pivot(s) to the last object selected but this time so that we translate the objects maintaining their relative position regarding their pivots.