domingo, 29 de enero de 2017

Thinking In Design

This week ive started to refactor our pipeline code. We are migrating to Maya 2017 among other things and i wasnt proud of how the development process was held during the last 7 months. To understand it a bit, 7 months ago we were facing a hurry in all aspects. We needed to produce a teaser in barely 4-5 months of strong, intense workload because we bet everything to reach to the AFM with something cool enough to raise some funds and produce the desired movie. The working conditions in terms of organization and qualified staff were a disadvantage. Something everyone of us had had to bear with. Nevertheless there were big pros, we all had passion and were totally committed to the project. I was going to say "Luckily the project worked out really well", but it was due to all our efforts and all the muscle we put into.

Anyways, from the point of view of the pipeline, which is what interests me here, besides the lack of organization we were dealing with a new Digital Asset Management tool where there is little documentation, so at the beginning we didnt have a precise idea of what where the capabilities, the pipeline was being developped at the same time the production started.... Briefly, i had no time to think properly about a good design. Don't misunderstand me, the code produced at that moment was completely functional and i have some testimonies claiming the tools were working well. But that step was necessary to explore the needs and can dos of the pipeline we were conceiving. Now we know how some things were done, we can improve them based on something that already works.

A REFACTORING EXAMPLE

As a little example, there is always the need to use a class that manages some common parameters with some common methods and functions. The way we did it first is just define a Singleton class, inherit from it and start to add parameters and their getters and setters, which in Python can be defined as @property . The manipulation of this data consists among other things, of storing their values , and loading them into memory, by means of some kind of persistence system. It could be a database or something as simple as a text file.

But this approach is really bad design because each time you define a new parameter, you need to change all the methods that input and output the parameters.... Really not very scalable!

Another constraint we didnt take into account is some of the parameters could be classified together. This is, they were related and could be interesting to group them. Some of them dont mean anything on their own if they are not accompanied with their corresponding mate.For example, a login consists of the username and the password. Having the username does not make sense if you havent defined also the password. Under the preceding approach every parameter is independent from the others. And there is no trace of those relationships.

Under those conditions i redesigned the system by making heavy use of inheritance. The related paraemeters could be grouped under a specific class wich derives from the Group Class called here "Section". This class is the atomic class responsible for managing a group of related parameters. So each time i want to expand with a new group of parameters, i only have to define a derived class that inherits from Section and define the parameter keys. Anyother functionality is already present in the base class.

Moreover, i can force from the base classe that the derived ones implement a PARAMS (param1,param2,etc) tuple which are automatically managed. This way i economize work as well as i ensure nobody misuses the class and understands how it is made. It is the same mechanism when we enforce the implementation of an abstract method in the class that inherits from the interface by raising a NotImplemented Exception.

The result is a much more easy to use and therefore extendable manager. Each time i want to create a new group of parameters i just need to define them in a new ConcreteSection class and no more worries than registering the section in the __init__ method. No any other changes to the manager!!

Enough talking, here is a UML class diagram exposing the generic final design.





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!!









sábado, 11 de junio de 2016

Simple Procedural Texture Generator and Visualizer

INTRODUCTION
I've been thinking about coding something related with perlin noise, just something that could be used as a justification. Normally i would have coded it in C++ with Qt but since ive been digging into the guts of python and PySide/PyQt for the last year, together with the fact that python GUI with PyQt is not that hard like in C++ (something it really does not have much interest once you get how the layouts, widgets, etc, work).

My only concern was performance because i wanted to do all the calculation and send the vertices data to the gpu each time you changed any of the parameter values governing the shape of the noise, the size, the visualization,..etc. I was willing to accept a little lag.

I wont explain deeply how Perlin noise works. For this you can have a look at the wikipedia or in a book i consider very useful: Texturing & Modeling: A Procedural Approach 

My approach basically consists of a function that generates values for a given octave. Then the final result will be a superposition of those octaves depending on the number specified.


INTERPOLATION

One of the options i wanted to explore was to obtain a more organic feel to the noise. With linear interpolation you can get some artifacts horizontally and vertically which really doesnt look well.

Here are the three interpolation methods:

1. linear
2. cosine
3. cubic 

All of the form "interpolate(x0,x1,t)"



 def Linear(a,b,t):  
   return a * (1 - t) + b * t  
 def Cosine(a,b,t):  
   t2 = (1 - math.cos(t * math.pi)) / 2.0  
   return (a * (1 - t2) + b * t2);  
 def Spline(x0,x1,t):  
   a = x0 - x1  
   b = -1.5 * x0 + 1.5 * x1  
   c = -0.5 * x0 + 0.5 * x1  
   d = x0  
   t2= t * t  
   return a * t2 * t + b * t2 + c * t + d  

The spline or cubic interpolation was used in a simplified manner. Normally the cubic interpolation formula uses information of 4 points: the two in the middle plus the rightmost and leftmost of them. For coding purposes, just to simplify, we assumed p0=p1 and p2=p3, hence the above code.

I will quote this page for the cubic interpolation just in case it disappears.

If the values of a function f(x) and its derivative are known at x=0 and x=1, then the function can be interpolated on the interval [0,1] using a third degree polynomial. This is called cubic interpolation. The formula of this polynomial can be easily derived.
A third degree polynomial and its derivative:
f(x) = ax^3 + bx^2 + cx + d
f'(x) = 3ax^2 + 2bx + c
plot

For the green curve:
a = -\tfrac{1}{2}\cdot2 + \tfrac{3}{2}\cdot4 - \tfrac{3}{2}\cdot2 + \tfrac{1}{2}\cdot3 = \tfrac{7}{2}
b = 2 - \tfrac{5}{2}\cdot4 + 2\cdot2 - \tfrac{1}{2}\cdot3 = -\tfrac{11}{2}
c = -\tfrac{1}{2}\cdot2 + \tfrac{1}{2}\cdot2 = 0
d = 4
f(x) = \tfrac{7}{2}(x-2)^3 - \tfrac{11}{2}(x-2)^2 + 4
The values of the polynomial and its derivative at x=0 and x=1:
f(0) = d
f(1) = a + b + c + d
f'(0) = c
f'(1) = 3a + 2b + c
The four equations above can be rewritten to this:
a = 2f(0) - 2f(1) + f'(0) + f'(1)
b = -3f(0) + 3f(1) - 2f'(0) - f'(1)
c = f'(0)
d = f(0)
And there we have our cubic interpolation formula.
Interpolation is often used to interpolate between a list of values. In that case we don't know the derivative of the function. We could simply use derivative 0 at every point, but we obtain smoother curves when we use the slope of a line between the previous and the next point as the derivative at a point. In that case the resulting polynomial is called a Catmull-Rom spline. Suppose you have the values p0, p1, p2 and p3 at respectively x=-1, x=0, x=1, and x=2. Then we can assign the values of f(0), f(1), f'(0) and f'(1) using the formulas below to interpolate between p1 and p2.
f(0) = p_1
f(1) = p_2
f'(0) = \dfrac{p_2 - p_0}{2}
f'(1) = \dfrac{p_3 - p_1}{2}
Combining the last four formulas and the preceding four, we get:

a = -\tfrac{1}{2}p_0 + \tfrac{3}{2}p_1 - \tfrac{3}{2}p_2 + \tfrac{1}{2}p_3

b = p_0 - \tfrac{5}{2}p_1 + 2p_2 - \tfrac{1}{2}p_3

c = -\tfrac{1}{2}p_0 + \tfrac{1}{2}p_2
d = p_1


OPENGL and PYTHON

One of the most time consuming aspects of dealing with PyOpenGL is that OpenGL is a C library and hence, if you code in C++ you share the same basic data types specially things like (void *) pointers, C arrays and the casting operation between types... But Python has its own data types!

1) I'll give you an example: Vertex Buffer Objects need to be passed a C array of GL_FLOAT values in order to specify vertex data. I was managing vertex data but in python lists. I discovered i had two options here: whether i used another dependency library such as Numpy with their immediate conversion between lists and arrays...or i could just use the "array" type. I finally chose this last option.


 from array import array  
 vertex_array = array('f', vertex_list)  
 index_array = array('i', index_list)  

where 'f' stands for float and 'i' for integer.

2) Another big problem i faced is how on earth i could update the vertex data sent to the buffer instead of deleting/creating/sending everything again as if i restarted the app.

 glBindBuffer(GL_ARRAY_BUFFER, self.vboId)  
 c_void_ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_READ_WRITE)  
 c_float_array_ptr = cast(c_void_ptr, POINTER(c_float))  
 # change vertex data    
 for i in range(len(vertex_list)):  
    c_float_array_ptr[i] = vertex_list[i]  
 glUnmapBuffer(GL_ARRAY_BUFFER)  


I discovered the buffer in video memory could be mapped to a chunk in RAM so that when changing one, it immediately applies to GPU. This is using "glMapBuffer/glUnmapBuffer".

But this function returns a "C void pointer" which in python terms is just an integer refering to some memory address.

We need a way to cast this void pointer to a float pointer (float array). That is the raison d'être of the next line. Needless to say i needed to import the ctypes module.

Then we can access finally the c_float_array_ptr as an iterable assigning float values from the python vertex_list!

Here is a video snippet of how the app works.











lunes, 18 de abril de 2016

FFMPEG and Multiprocessing

We are at the edge of the end of production here, very few stand still and with them very much of our daily joy because "there is no good or bad company", it's the people that conform and that you have a continuous treat with that count and make the working environment such a great place.

Anyways im gonna talk (as usual) about the last tool i ve had to code at work. Apparently, there s been a mismatching of shots between the two studios involved and from a production point of view they needed to have the whole movie in playblast sequentially so that they had the screen split in two, on top of it the last anim playblast and at the bottom the last refine so that they could compare and make sure the outer studio was getting the last version for lighting etc....

My first thought was to have a look at Adobe Premiere SDK and see if by any chance it had any python API i could play with. 

After a bit of research i found that there was no way to import an xml file with shots and duration and automatically convert it to a final video. Also the only thing you can do with premiere is plugin development with C++ at the "filters level" which means it is not as tweakable as Maya by any far. It was too much overkill for my  needs.

Then somehow i started to look for tools in linux and i bumped into ffmpeg. So surprised and amazed it was not my first choice! Now surely i would recommend it to anyone having to play with video compositing and mixing.

Now i can start to code!

TOOL SKELETON

First iterate through the anim and refine folders. As there is no conflict and no need to share data this could be easily parallelized. Each process would fill a dictionary where the key is like "ACT0X_SQ00XX_SH00XX" and the value the complete filepath of the most recent file.

       
def return_file_dict(root,queue):
    '''
    iterate through each filesystem branch and fill the dictionary, finally put it in the multiprocess safe queue
    '''
    queue.put(root)
    queue.put(file_dict)

def main():
    process_list = []
    queue = Manager().Queue()
    
    for root, dictionary in zip([DST_TMP_LAST_ANIM,DST_TMP_LAST_CROWD_OR_REFINE],[last_anim_dict,last_crowdrefine_dict]):
        p = Process(target=return_file_dict,args=(root,queue,))
        process_list.append(p)
        p.start()
    
    for p in process_list:
        p.join()
    
    '''
    Rescue both dictionaries and merge top/bottom with ffmpeg
    '''
       
 


After these all i needed was to filter both dictionaries and merge the two playblasts corresponding to a given shot/entry in the dict with ffmpeg.

Now these would open a gnome-terminal for each command. So the next thought was to pipe all the commands to a string which then would be executed in a single call to subprocess.call.

But there was a problem: there is a limit in the number of characters you can send as a command to subprocess.call. This was a good idea in the sense that it would only require a call and all would happen in the same terminal/linux process.

The next logical step was to say Ok i can't send all the commands as a string but i can dump the string to a shell script file and execute that shell script from within the subprocess call!!

       
    #
    # compound all the shell script commands into command_element_string
    #

    with open(FFMPEG_COMMANDS, 'w') as f:
        f.write(command_element_string)

    command = 'sh ' + FFMPEG_COMMANDS
    subprocess.call(['gnome-terminal','-x','bash','-c',command],shell=False,env=os.environ.copy())
    
 


FFMPEG SHELL SCRIPT CALLED FROM SUBPROCESS

       
command_element_string +='ffmpeg -y -i ' + last_anim_filepath + ' -i '+ last_crowdrefine_filepath + ' -filter_complex "[0:v]scale=w=999:h=540[v0];[1:v]scale=w=999:h=540[v1];[v0][v1]vstack=inputs=2[v]" -map "[v]" -map $RESULT:a -ac 2 -b:v 4M ' + output_filepath +';\n'
    
 


This would force the resolution to be w=999 h=540 of each of the videos we vertically stack. We Force it because if the resolutions dont match the conversion will fail.

Also another important comand here is the "$RESULT" value which in this case must be 0 or 1 depending on the audio track we choose to be embedded in the output file.

This can vary since there were playblasts from anim as well as from refine that were missing the audio. $RESULT is the result of doing and ffprobe test to each one of the files to ask for audio info.

The only unavoidable case left is when neither of the two files has an audio track, in which case the conversion fails. So far i could take care of this as well but i havent found yet this case so most probably not gonna treat it.

This is the embedded function in the shell script:

       
    command_element_string = 'function ttl_ffprobe()\n'
    command_element_string += '{\n'
    command_element_string += 'RESULT_ANIM="" ;\n'
    command_element_string += 'RESULT_REFINE="" ;\n'
    command_element_string += 'RESULT_ANIM=$(ffprobe -i $1 -show_streams -select_streams a -loglevel error) ;\n'
    command_element_string += 'RESULT_REFINE=$(ffprobe -i $2 -show_streams -select_streams a -loglevel error) ;\n'
    command_element_string += 'CHANNEL_SELECTION=0 ;\n'
    command_element_string += 'if [ -z "$RESULT_ANIM" ]\n'
    command_element_string += 'then\n'
    command_element_string += '\tCHANNEL_SELECTION=1\n'
    command_element_string += 'fi\n'
    command_element_string += 'return $CHANNEL_SELECTION\n'
    command_element_string += '}\n'
    
 

Once we have side by side all the playblasts anim and refine, all that is left is to merge all of them into the final sequence/movie which can be easily done with the "cat" command and properly chosen container. I refer you to the documentation: https://ffmpeg.org/ffmpeg.html