So you’ve made a killer component that you love using, but you suddenly find yourself wondering how best to re-use it in future projects. You could make a killer control panel for it, or create a more generalized method for passing in values with in CHOPs or DATs. You could just resign yourself to some more complex scripting – reaching deep into your component to set parameters one at a time. You could hard code it, you’ll probably be making some job specific changes to your custom component anyway, so what’s a little more hard coding? The 50000 series now features custom parameters, or you could use variables, or storage. Any one of these options might be right for your component, or maybe they’re just not quite right. Maybe what you really need is a little better reach with Python, but without as much head scratching specificity. If find yourself feeling this way, than extensions are about to make your TouchDesigner programming life so, so much better.
Using extensions requires a bit of leg work on your part as a programmer, it also means that you’ll want to do this for components that you’ll find yourself reusing time and again – after all, if you’re going to take some time to really think about how you want a reusable piece of code to work in a larger system it only makes sense to do this with something you know will be useful again. That is to say, this approach isn’t right for every circumstance, but the circumstances it is right for will really make a difference for you. We’re going to look at a rather impractical example to give us a lay of the land when it comes to using extensions – that’s okay. In fact, as you’re learning how to apply this approach to your workflow it’s worth practicing several times to make sure you have a handle on the ins and outs of the process.
Before we get too much further, what exactly is this extension business? If you’re to the point with TouchDesigner where you’re comfortable using Python to manipulate your networks, you’ll no doubt have come to rely on a number of methods – anything with a . followed by a call. For example:
- op(‘moviefilein1’).width – returns the width of the file
- op(‘moviefilein1’).height– returns the heightof the file
- op(‘table1’).numRows – returns the number of Rows
- op(‘table1’).numCols – returns the number of Columns
In each of these examples, it’s the .operation that extends how you might think of working with an operator. Custom extensions, means that you, the programmer, are now free to create your own set of extensions for a given component. The classic example for this kind of custom component and custom extension set for TouchDesigner would be a movie player. Once you build a movie player that cross fades between two videos, wouldn’t it be lovely to use something like op(‘videoPlayer’).PlayNext() or op(‘videoPlayer’).Loop(). The big idea here is that you should be free to do just that when working with a component, and custom extensions are a big part of that puzzle. This keeps us from reaching deep into the component to change parameters, and instead allows us to write modular code with layers of abstraction. If you’re still not convinced that this is a powerful feature, consider that when you start a car you’re not in the business of specifying how precisely the starter motor sequences each electrical signal to help the car turn over, or which spark plugs fire in which order – you issue a command car.start() with the expectation that the start() function holds all of the necessary information about how the vehicle actually starts. While this example might be reductive, it helps to illustrate the importance of abstraction. It’s impractical for you, the driver, to always be caught up in starting sequences in order to drive a car (one might make an argument against this, but for now let’s roll with the fact that many drivers don’t understand the magic behind a car starting when they turn the key), this embedded function allows the driver to focus on higher order operations – navigation, manipulation, etc. At the end of the day, that’s really what we’re after here – how do add a layer of abstraction that simplifies how we manipulate a given component.
That’s all well and good, but let’s get to the practical application of these concepts. In this case, before we actually start to see this in action, we need to have a working component to start working with. We are going to imagine that we want to build a generative component that’s got faceted torus that we use in lots of live sets. We want to be able to change a number of different elements for this Torus – its texture, background, rotation, deformation, to name a few. Let’s begin by putting together a simple render network to build this component, and then we can look at how extensions complement the work we’ve already done.
First let’s add an empty Base COMP to our network.
Inside of our new base let’s add a Camera, Geo, and Light COMP, as well as a Render TOP connected to an Out TOP. We’re building a simple rendering network, which should feel like old hat.
Let’s add a movie file in TOP, and a Composite TOP to our network. We’ll composite our render over our movie file in, so we have a background. In the image below only the changed parameters for the Composite TOP are shown.
Next let’s look inside of our geo COMP, and make a few changes. First let’s change our geo to be a polygon rather than a mesh. We’ll also turn off the display and render flags for the torus (don’t worry, we’ll turn them on further down the chain.
Next we’ll add a noise SOP.
Next we’ll add a facet SOP, turning on unique points and compute normals.
Finally, let’s add a null SOP. On the null, let’s turn on the display and render flags. When it’s all said and done we should have something that looks like this.
Let’s move up one layer out of our geo, back into the base we started in. Here let’s add a phong Material and apply it to our geo. Let’s also add a movie file in TOP connected to a null TOP, and set it as the color map for our phong.
While we’re still thinking about our material, lets make a few changes. Let’s set our diffuse color to white, our specular color to a light gray, and turn up our shininess to 255.
Let’s also make a few changes to our light COMP. I’m after a kind of shiny faceted torus, so let’s change our light to a cone light, place it overhead and to the right of our geometry, and set it to look at our geo.
I’ve gone ahead and changed file in my movie file in TOP to a different default image so I can see the whole torus. In the end you should have a network that looks something like this.
Thinking ahead, I know that I’m going to want to have the freedom of changing a few parameters for this texture. I’d like to be able to control if it’s monochrome, as well as a few of the levels of the image. Let’s add a monochrome TOP and a level TOP between the movie file in and the null TOP.
We’re almost ready to start thinking about extensions, but first we need to build a control network to operate our component. Let’s start by adding a constant CHOP and calling it attrAssign. Here I’m going to start thinking about what I want to control in this component. I want to drive the rotation of the x y and z axis for our geo, I want to control the amplitude of the noise, the saturation of our image, the black level, brightness, and opacity. I’m going to think of those parameters as:
I’ll start out my constant CHOP with those channel names, and some starting values.
For this particular component, I want to be able to set values, and have it smartly figure out transitions rather than needing it constantly feed it a set of smoothly changing values. There are a couple of different ways we might set this up, but I’m going to take a rout of using a speed CHOP for one set of operations, and a filter CHOP to smooth everything out. First I want to isolate the rx ry and rz channels, we can do that with a select CHOP. Next we’ll connect that to a speed CHOP. We can merge this changed set of channels back into the stream with a replace CHOP – replacing our static rx ry rz channels with the dynamic ones.
Finally, we can smooth out our data with a Filter CHOP, and end our chain of operations in a null CHOP.
Our last step here is to export or reference to each of our control parameters. Our rotation channels should be referenced by our Geo1 for rx, ry, and rz. The Noise SOP in Geo1 should be connected to the channel noiseAmp, and our image controls should be connected to their respective parameters – Monochrome, Black Level, Brightness, and Opacity. In the end, you should end up with a complete network that looks something like this.
Alright, we now finally have a basic generative component set up, and we’re ready to start thinking about how we want our extensions to work with this bad boy. Let’s start with the simplest ideas here, and work our way up to something more complex. For starters we need to add a text DAT to our network. I’m going to call mine genGeoClass.
Let’s add our first class to our genGeoClass text DAT. Our class is going to contain all of our functions that we want to use with our component. There are a few things we need to keep in mind for this process. First, white space is going to be important – tabs matter, and this is a great place to really learn that the hard way. Namespace also matters. We’re eventually going to promote our extensions (more on that later on down), and in order for that to work correctly our functions need to begin with capital letters. That will make more sense as we see that in practice, but for now it’s important to have that tucked away in your mind.
Let’s begin by adding a simple print command. First we define our class, and place our functions inside of the class. When we’re writing a class in Python we need to explicitly place self in our definitions. There are a number of very good reasons for this, and I’d encourage you to read up on the topic if you’re curious:
For our purposes, let’s get our class started with the following;
class GenGeo: def SimplePrint( self ): print( 'Hello World' ) return
Before we can see this in action, we need to set up our base COMP to use extensions. Let’s back out of our base, and take a look at our component parameters.
Here I’ve set the module reference to be the operator called genGeoClass inside of base1. We can also see that we’re specifcally referencing the GenGeo() class that we just wrote. I’ve also gone ahead and turned on promote extensions. Make sure you click “Re-Init” Extensions at the bottom of the parameters page, and then we can see our extension in action.
Next let’s add a text DAT to the same directory as our base1. Here we’ll use the following piece of code to call the SimplePrint() function we just defined:
op( 'base1' ).SimplePrint()
Let’s open our text port, and run our text DAT.
That should feel like a little slice of magic. If you choose not to promote your extensions, the syntax for calling a function looks something like this:
op( 'base1' ).ext.GenGeo.SimplePrint()
Okay, this has been a lot of work so far to only print out “Hello World.” How can we make this a little more interesting? I’m so glad you asked. Here’s a set of functions that I’ve already written. We can copy and paste these into our genGeoClass text DAT, and now we suddenly have a whole new host of functions we can call that perform some meta operations for us.
class GenGeo: def SimplePrint( self ): print( 'Hello World' ) return def TorusPar( self , rows , columns ): op('geo1/torus1').par.rows = rows op('geo1/torus1').par.cols = columns return def TorusParReset( self ): op('geo1/torus1').par.rows = 10 op('geo1/torus1').par.cols = 20 return def Texture( self , file ): op('moviefilein1').par.file = file return def TextureReset( self ): op('moviefilein1').par.file = app.samplesFolder + '/Map/TestPattern.jpg' return def Rot( self , rx , ry , rz ): attr = op('attrAssign') attr.par.value0 = rx attr.par.value1 = ry attr.par.value2 = rz return def RotReset( self ): attr = op('attrAssign') speed = op('speed1') filterCHOP = op('filter1') attr.par.value0 = 0 attr.par.value1 = 0 attr.par.value2 = 0 speed.par.resetpulse.pulse() filterCHOP.par.resetpulse.pulse() return def TorusNoise( self , noiseAmp ): op( 'attrAssign' ).par.value3 = noiseAmp return def Mono( self , monoVal ): op( 'attrAssign' ).par.value4 = monoVal return def Levels( self , blkLvl , bright , opacity ): attr = op('attrAssign') attr.par.value5 = blkLvl attr.par.value6 = bright attr.par.value7 = opacity return def PostProcessReset( self ): attr = op('attrAssign') attr.par.value4 = 0 attr.par.value5 = 0 attr.par.value6 = 1 attr.par.value7 = 1 return def Background( self , onOff ): op('comp1').bypass = onOff return
To better understand what all of these do let’s look at a quick cheat sheet that I made:
# Test Print Statement op( 'base1' ).SimplePrint() # Set Rows and Columns op( 'base1' ).TorusPar( 20 , 20 ) # Reset Rwos and Columns to 10 x 20 op( 'base1' ).TorusParReset() # Set the texture of a movie file in TOP op( 'base1' ).Texture( 'https://farm4.staticflickr.com/3696/10353390565_1fa6dbf704_o.jpg' ) # Reset the Texture of movie file in TOP op( 'base1' ).TextureReset() # Set the Rotation Speed for the x y and / or z axis op( 'base1' ).Rot( 10 , 15 , 20 ) # Reset the Rotation speed to 0, and the rotation values to 0 op( 'base1' ).RotReset() # Set the Amplitude paramater of the Noise SOP for the Torus op( 'base1' ).TorusNoise( 0.8 ) # Make the texture Monochrome op( 'base1' ).Mono( 1.0 ) # Control the Black Leve, Brightness, and Opacity of the Texture # that's applied to the Torus op( 'base1' ).Levels( 0.25 , 1.5 , 0.8 ) # Reset all post process effects op( 'base1' ).PostProcessReset() # Turn off Background Image - 0 will turn the Background back on op( 'base1' ).Background( 1 )
This is wonderful, but there’s one last thing for us to consider. Wouldn’t it be great if we had some initialization values in here, so at start-up or when we made a new instance of this comp we defaulted to a reliable base state? That would be lovely, and we can set that with an __init__ definition. Let’s add the following to our class:
def __init__( self ): print( 'Gen Init' ) attr = op('attrAssign') op('moviefilein1').par.file = app.samplesFolder + '/Map/TestPattern.jpg' attr.par.value4 = 0 attr.par.value5 = 0 attr.par.value6 = 1 attr.par.value7 = 1 return
That means our whole class should now look like this:
class GenGeo: def __init__( self ): print( 'Gen Init' ) attr = op('attrAssign') op('moviefilein1').par.file = app.samplesFolder + '/Map/TestPattern.jpg' attr.par.value4 = 0 attr.par.value5 = 0 attr.par.value6 = 1 attr.par.value7 = 1 return def SimplePrint( self ): print( 'Hello World' ) return def TorusPar( self , rows , columns ): op('geo1/torus1').par.rows = rows op('geo1/torus1').par.cols = columns return def TorusParReset( self ): op('geo1/torus1').par.rows = 10 op('geo1/torus1').par.cols = 20 return def Texture( self , file ): op('moviefilein1').par.file = file return def TextureReset( self ): op('moviefilein1').par.file = app.samplesFolder + '/Map/TestPattern.jpg' return def Rot( self , rx , ry , rz ): attr = op('attrAssign') attr.par.value0 = rx attr.par.value1 = ry attr.par.value2 = rz return def RotReset( self ): attr = op('attrAssign') speed = op('speed1') filterCHOP = op('filter1') attr.par.value0 = 0 attr.par.value1 = 0 attr.par.value2 = 0 speed.par.resetpulse.pulse() filterCHOP.par.resetpulse.pulse() return def TorusNoise( self , noiseAmp ): op( 'attrAssign' ).par.value3 = noiseAmp return def Mono( self , monoVal ): op( 'attrAssign' ).par.value4 = monoVal return def Levels( self , blkLvl , bright , opacity ): attr = op('attrAssign') attr.par.value5 = blkLvl attr.par.value6 = bright attr.par.value7 = opacity return def PostProcessReset( self ): attr = op('attrAssign') attr.par.value4 = 0 attr.par.value5 = 0 attr.par.value6 = 1 attr.par.value7 = 1 return def Background( self , onOff ): op('comp1').bypass = onOff return
Alright, so why do we care? Well, this application of extensions frees us to think differently about this component. Let’s say that I want to make a few changes to this component’s behavior. First I want to set a new image to be the texture for the torus, next I want to change the rotation speed on the x and y axis, and finally I want to turn up the noise SOP. Previously, I might think about this by writing a series of scripts that looked something like:
op( 'base1/attrAssign' ).par.value0 = 20 op( 'base1/attrAssign' ).par.value1 = 30 op( 'base1/attrAssign' ).par.value3 = 0.8 op( 'base1/moviefilein1' ).par.file = 'https://farm4.staticflickr.com/3696/10353390565_1fa6dbf704_o.jpg'
Instead, I can now write that like this:
op( 'base1' ).Texture( 'https://farm4.staticflickr.com/3696/10353390565_1fa6dbf704_o.jpg' ) op( 'base1' ).Rot( 20 , 30 , 0 ) op( 'base1' ).TorusNoise( 0.8 )
That might not seem like a huge difference here in our example network, but as we build larger and more complex components, this suddenly becomes hugely powerful as a working approach.
Check out the example file on GitHub if you get stuck along the way, or want to see exactly how I made this work.