sjt.is

Pipeline. Period.

Textures, it’s always the textures

‘What to do with the textures?’ is something that constantly pops up at work, that is where should we store them?

When I started back at Caoz after my stint at SPI I was scrambling to set up a somewhat working pipeline for our TV production that was starting up. We wanted to use Tactic since we had been lacking any sort of project tracking up until that point. (For some reason we were using Alienbrain which we have thankfully pretty much removed from our workflow) The problem is that modeling/shading had started and I was more worried about getting a functioning animatic > animation > lighting workflow going. The unfortunate side-effect of this was that the textures for the assets were now in 2+ locations, some of the textures were textures that were shared between assets and someone was clever enought to put them somewhere like

R:/project/assets/textures

But some textures had been made after we started moving to working on a server (instead of alienbrains local->remote syncing shenanigans) so they ended up in a proper location like

//server/share/project/asset/Pictures/textures/

Another thing we wanted to achieve was to have the textures files locally on each machine rendering. All of this texture crap led me to using something I dubbed the texture pit. This is just a vast dump of all the textures located on a common path on the server, e.g.

//server/share/project/common_asset/texture_pit

And at lookdev publish time all the textures are copied over there (if they don’t already exist) as well as a .tx file is created for our dear Arnold renderer. Then we replace the link inside Softimage to point to the environment variable $CZ_TEXTURE_PIT which for all gui instances of Softimage point to the server side texture pit. Now we turn to rendering. At the start of every render job the pit is synced to the local hard drive of the render client and then it is just a matter of redefining $CZ_TEXTURE_PIT within XSIBATCH to point to the synced texture pit on the local machine. Tada, all textures in one place and locally on the render machines.

This setup requires us also to define a texture root for SItoA as well as re-export all our environment standins and set the export to use relative texture paths.

Pros

  • It’s comfortable to know that all the textures are in that one folder on the server.
  • If the same texture file is used in multiple assets (same filename) then the one single texture in the pit already got it covered.
  • The textures are on the render clients local hard drives meaning faster renders and less network load.
  • We have hardly seen missing textures errors since we started using the pit, and if we encounter them, they are easily resolved with dumping the missing texture into the pit.

Cons

  • An evergrowing vast collection of textures being synced around the network many of maybe are never used because the texture was used for a prop which was used in episode no. 2 but never again, still that texture is being synced and stored on all our clients.
  • Wasted space on the server, keeeping the textures within the asset folder and the pit == twice the storage.
  • Finding a texture to use on a new asset requires the texture file names to be very descriptive so you can find them easily.
  • Does not enforce a structured workflow for texture artists, they can pretty much save their textures where they want while they work and then just at publish time sync their textures to the pit. So getting back to their work files can be tricky.
  • No version tracking whatsoever (this is just our setup, version tracking within the pit is quite possible)

I think I have written enough about our texture handling (or lack thereof) at Caoz.

scntocEditor

I just put on github a small side-project which should make it a tad easier for me to handle scntoc files at work. I thought that since I would do a special module to do this I might as well share it. So I present to you scntocEditor

It still has no documentation of any sort and the GUI could use like a 1000 things to make it really nice. But let’s take it one step at a time.

update?

Well it’s been more than a year since I wrote anything here and I’d like to do more of it so I am trying to ditch wordpress for something much minimalistic. After I saw Doug Hellmann’s post I decided to try tinkerer and it looks quite super. Lets see if that gets me somewhat motivated to write something clever.

I moved all the old posts (including my most popular post, ‘Replacing layers in AfterEffects’) over to restructured text and dumped into tinker (if you are curious you can check out my mega-hacky script here)

I did not try moving all the comments from wp to disqus. I haven’t even googled if that is possible.

Super or super() ?

I’ve seen two ways of calling superclass methods.

class Subclass(Superclass):
    def __init__(self):
        Superclass.__init__(self)

or

class Subclass(Superclass):
    def __init__(self):
        super(Subclass, self).__init__(self)

and I started wondering about the difference between the two. The short simple answer is “none”. The slightly longer answer (Here is the long answer) is that the first method is the only way to go for classic classes, but new-style classes also support the super() builtin (in python 3.0 you can simply call super() without the type and object arguments). You gain a couple of things with the super() builtin. First, you can change the name of the superclass without having to go into all the overloaded methods to change Superclass to DifferentSuperclass. I have seen code where the same functionality was achieved by simply doing this:

from module import Superclass as BaseClass

class Subclass(BaseClass):
    def __init__(self):
        BaseClass.__init__(self)

I find this very confusing having to scroll all the way to the import statements to see the classes superclass, but it has the same effect. Second, and the really cool thing, is that super() searches all of your classes ancestors to find the method you wan to call. So if you have a complex inheritance scheme you don’t have to know/remember/care which superclass implemented the method you want to call and Python uses it’s MRO (Method Resolution Order) to search the class tree until it finds your method. For the detailed answer and some examples I highly recommend Raymond Hettinger’s excellent post on the matter just be aware that his examples are using Python 3 syntax.

PyQt and drag’n’drop

Today I was trying to get drag and drop working in PyQt and I had the worst time getting it to work. I was trying to have a QListWidget accept file drops which would then add the path to the file to the list. To enable dropping on a widget you must do several things. First of all you must call:

setAcceptDrops(True)

Then you have to implement the following methods:

dragEnterEvent(self, event)
dropEvent(self, event)

What I forgot to do (when I read the docs carefully) is that you also need to implement

dragMoveEvent(self, event)

Which of course makes sense when you think about it. First the cursor enters the widget, then moves within it, then you drop. Simple, right? And since I couldn’t find an example of this behavior when I was googling (drop file on list, list adds file path) I threw together this example. This example can also reorder the list after you have dragged files on it.

import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *

class MyListWidget(QListWidget):
  def __init__(self, parent):
    super(MyListWidget, self).__init__(parent)
    self.setAcceptDrops(True)
    self.setDragDropMode(QAbstractItemView.InternalMove)

  def dragEnterEvent(self, event):
    if event.mimeData().hasUrls():
      event.acceptProposedAction()
    else:
      super(MyListWidget, self).dragEnterEvent(event)

  def dragMoveEvent(self, event):
    super(MyListWidget, self).dragMoveEvent(event)

  def dropEvent(self, event):
    if event.mimeData().hasUrls():
      for url in event.mimeData().urls():
        self.addItem(url.path())
      event.acceptProposedAction()
    else:
      super(MyListWidget,self).dropEvent(event)

class MyWindow(QWidget):
  def __init__(self):
    super(MyWindow,self).__init__()
    self.setGeometry(100,100,300,400)
    self.setWindowTitle("Filenames")

    self.list = MyListWidget(self)
    layout = QVBoxLayout(self)
    layout.addWidget(self.list)

    self.setLayout(layout)

if __name__ == '__main__':

  app = QApplication(sys.argv)
  app.setStyle("plastique")

  window = MyWindow()
  window.show()

  sys.exit(app.exec_())

coral

I have recently been looking at a quite cool program called coral. I recommend you take a look at it. Anyway I’ve put on github a repository of a possibly useful plugin (it’s the same plugin only one is in python and the other one is a compiled one) that can read pc2 caches. *NOTE* the python one requires some changes to the coral source that haven’t made it back to the repo on google code, but that will probably happen at some point. This is mostly there for me version-tracking my code, but you are free to prod at it or *gasp* use it. coral_plugins on github

Batch adventures (or: expanding variables in batch files)

I was looking into writing a fairly simple batch file to install some stuff and update environment variables. At some point I wanted to do something like this (constructing a string of ‘;’ separated paths):

set env_vars = \path\to\stuff
if x%user%==xsveinbjorn (
set env_vars = %env_vars%;\another\path
)
if x%hostname%==xgefjun (
set env_vars = %env_vars%;\whoah\this\is\radical
)
echo env_vars

When I would run this I would always get

\path\to\stuff;\whoah\this\is\radical

(Notice that the second path \another\path is missing from the result) Being very new to writing batch scripts I found this extremely confusing since this sort of stuff would work like a charm in python. So I started the google machine. The reason this happens is that variables enclosed in ‘%’ are expanded when the script is read, which is quite different from the way python handles this resolving stuff at the last possible time. So when the script is read it will actually look something like this:

set env_vars = \path\to\stuff
if xsveinbjorn==xsveinbjorn (
set env_vars = \path\to\stuff;\another\path
)
if xgefjun==xgefjun (
set env_vars = \path\to\stuff;\whoah\this\is\radical
)
echo env_vars

(I resolved the variables %user% to ‘sveinbjorn’ and %hostname% to ‘gefjun’) And we can clearly see why we got the result that we got. So what we have to do is tell the batch file to expand the variables as we go along (something close to how python does things) instead of this expand-it-when-you-read-it nonsense. To do this we simply have to do three things:

  1. Add this line to the top of the code

    setlocal EnableDelayedExpansion
  2. Add this to the end of it

    endlocal
    
  3. Replace the enclosing ‘%’ to ‘!’ for the variable you want to delay the expansion for.

    if x%user%==xsveinbjorn (
    set env_vars = !env_vars!;\another\path
    )

So my original example would look like this:

setlocal EnableDelayedExpansion
set env_vars = \path\to\stuff
if xsveinbjorn==xsveinbjorn (
set env_vars = \path\to\stuff;\another\path
)
if xgefjun==xgefjun (
set env_vars = \path\to\stuff;\whoah\this\is\radical
)
echo env_vars
endlocal

Now we get the result we were looking for:

\path\to\stuff;\another\path;\whoah\this\is\radical

This also applies to loops and other places where the variable might change between the time you start the batch script and when you actually want to expand it.

subprocess.Popen and the env argument

Today I was looking at launching programs with some custom environment variables set and if you don’t want to redefine a whole lot of them for this new environment I’d recommend is copying the os.environ dictionary and do your changes on the copy, like so:

new_env = os.environ.copy()
new_env['MEGAVARIABLE'] = 'MEGAVALUE'
subprocess.Popen('path', env=new_env)

Then you should have your program launched with all them fancy environment variables set.

XSICollections, python and you

Have you ever called a some scripting function in Softimage and gotten back an XSICollection and printed that return value only to get a ‘None’ value printed to the log? Don’t worry, you got some data there. Just remember that thats the way python in Softimage prints XSICollections if you would iterate over the collection you will find your values:

for object in returnedCollectin:
   # do something with object
   ...

I have seen some people who just started scripting in xsi run in to this little issue.

sjtCopySkinWeights

This is a simple plug-in command to copy skin weights from one object to another with some significant restrictions: Download sjtCopySkinWeights

  1. The meshes must be exactly mirrors of each other vertex-number wise. This plugin simply uses the vertex numbers of the mesh so each vertex of object A must have a corresponding vertex on object B.
  2. The joint names must be somehow side-differentiated e.g. L_joint and R_joint or left_side_arm_JNT and right_side_arm_JNT or you must somehow be able to map one side of joint names to the other.

This has mostly been useful when dealing with hands, you skin one and then you have to copy the weights over, but sometimes maya does a lousy job. This plug-in is pretty much like going in the component editor and copying each value for each vertex between meshes (which would be insane to do by hand). Note: this is not a very fast script (I was trying to learn the Maya API while writing this) so give it some time to work through dense meshes. If you have questions or comments just either send me an email or comment below.

Usage

  1. Select the object with the correct weights.
  2. Select the object with the incorrect weights.
  3. Run the command e.g. ‘sjtCopySkinWeights -s “L_” -r “R_”’

Arguments

This command accepts two arguments -search/-s and -replace/-r and each argument requires a string to follow it that tells the plugin how to map the right joint names to the left ones. Yes it works kind of backwards. The command first constructs a list of all the influences on the first object, then it goes over every vertex on the other object and tries to map it’s influences to the ones in the influence-list from the first object.

Example

On one side the joints are called L_....._JNT and the other R_......_JNT the proper arguments for this command to work would be (MEL syntax):

sjtCopySkinWeights -s "L_" -r "R_"

(if no arguments are givien, this is the default search pattern) This would also have worked (since the search strings can be regular expressions):

sjtCopySkinWeights -s "L_" -r "^[rR]_"

This would allow the right side joints to be named either R_...._JNT or r_...._JNT Just remember the regular expression should go with the -r flag, the -s flag is just used to replace the match from the -r flag with.

Example file

To test the command immediately you can load up th test file included in the zip download ‘copyWeights_example.ma’. First try scrubbing in the timeline to see the difference in weighting. The right side (pCube1) bends in the middle but the left side(pCube2) does not. To copy the weights from pCube1 to pCube2 first load the plugin, select pCube1 then pCube2 and type (MEL syntax):

sjtCopySkinWeights

into the command line (or script editor) and then the weights will have been copied from pCube1 to pCube2. Voila.