Rendering Patching SOHO

Overview

SOHO is a Python framework Houdini uses to drive various rendering programs. The open scripting interface allows you to see exactly how Houdini uses SOHO modules to create renderer-specific scene description files. It also allows you to customize the rendering pipeline completely or in part.

Because SOHO looks for its scripts on the Houdini path, you can replace a SOHO module and customize its actions to meet your needs. For example, one effects studio needed to do shader swapping, which is fairly trivial with SOHO. They copied one of the SOHO files from the Houdini install location to an earlier point on the Houdini path and then edited the copied file.

The major shortcoming of this “replace the module” approach is as new versions of Houdini are released, the SOHO modules have new features added (and bugs fixed), so you need to re-copy the file and re-apply your changes to it, for each new version. It gets even more complicated if you need to run multiple versions of Houdini, meaning you need to maintain multiple patched files.

One approach to making patching easier might be “delegation”, similar to SOHO Filtering: allow the user to register callback functions that SOHO calls at various points in the pipeline (for example, when a geometry object is processed) to allow customization. The problem with this approach is granularity. Should the callback be called once per object, or multiple times for each feature block (shaders, transforms, etc.). In the case of shader replacement, the callbacks would have to be very fine-grained (on a per-shader basis). Also, the code paths of the SOHO modules for the various renderers are different, making it difficult (or impossible) to provide a consistent delegation interface for all renderers. For these reasons, it’s unlikely this approach will ever be implemented.

A better, though not perfect, solution is monkey-patching the SOHO modules.

Monkey-patching

Monkey-patching (see the Wikipedia entry) is a dynamic programming technique you can use to replace individual functions/methods in a Python library without changing the library’s source code.

For example:

import math

def my_sin(x):
    return x-x**3/6.0+x**5/120.0  # Taylor series

math.sin = my_sin

del my_sin  # Optional: the my_sin name is no longer needed

This code loads the math module (or simply assigns it to math if it’s already loaded). It defines a new function my_sin which uses the first few terms of the Taylor series to compute the result, and then replaces the math module’s sin function with my_sin.

The changes the global in-memory copy of math shared by all code in the current interpreter. Any other code in this interpreter that imports (or has imported) the math module and calls math.sin will now call the my_sin code instead of the standard sin.

(Note that deleting the my_sin variable will remove that name from the namespace, but the code is still referenced by math.sin.)

It should be quite obvious that this can potentially be a dangerous thing to do in general (especially in terms of your sanity when you're debugging). However, it does provide a flexible approach to patching SOHO.

Monkey-patching SOHO

This is an example of monkey patching the LightSource method in RIBapi.py, replacing it with a wrapper.

#
# Monkey Patch of RIB.py
#
import hou, os, os.path, RIBapi

# Grab a reference to the "real" LightSource.
old_light_source = RIBapi.LightSource

def should_patch():
    """
    Checks an environment variable to see whether
    to apply the patch
    """

    # Grab an environment variable from hscript
    monkey = hou.hscript('echo $MONKEY')[0]

    return (not monkey == '\n')

def LightSource(shader, handle, parmlist):
    """
    A wrapper around the LightSource function
    """

    handle = old_light_source(shader, handle, parmlist)

    # Do something with the handle...


if should_patch():
    print 'Patching LightSource'
    RIBapi.LightSource = LightSource

# Now, execute the RIB.py from the $HFS version of SOHO
execfile(os.path.expandvars('$HFS/houdini/soho/RIB.py'))

# ...and finally restore the LightSource command
RIBapi.LightSource = old_light_source

The should_patch() function checks an environment variable to decide whether to patch the RIB code. This lets you control the patching by setting an environment variable within the Houdini session instead of having to change the source code. The code in the LightSource replacement can use any method defined in the hou module as well as the SOHO API.