├── Contributing ├── 01_How to contribute.md ├── 02_Supporting the project.md └── 03_Contributors.md ├── Crowds └── 01_Remove clip from agents.md ├── Extra Things └── 01_Session.md ├── Fun └── 01_Custom Network Titles.md ├── HDAs ├── 01_Reading parameters.md ├── 02_Multiparm Block Creation.md ├── 03_String Parameter Menu.md ├── 04_Data Encapsulation.md └── 04_Python Module.md ├── LOPs ├── 01_Control Lights in Solaris With Python.md └── 02_Custom Metadata.md ├── Menu-Customization └── 01_Parm Menu.md ├── Node-Graph-Hooks ├── 01_Basic Setup.md └── 02_Intercepting Mouse Events.md ├── Personal-Pipeline ├── 00_Basics.md ├── 01_External Code Editor.md ├── 02_Personal Package.md ├── 03_Saving Things.md ├── 04_Life Hacks.md ├── 05_Utility Scripts.md └── 06_Houdini Types.md ├── Python-Panels ├── 01_Getting Started.md └── 02_Layout Building.md ├── Python-Sop └── 01_Performance.md ├── README.md ├── ROPs ├── 01_Render Scripts.md └── 02_Auto Generate MP4 Preview.md ├── SOPs ├── 01_Bake Object Level Transforms.md └── 02_Verbs.md ├── Shelf-Scripts ├── 01_Shelf Script Basics.md ├── 02_The Hou Module.md ├── 03_Using Kwargs.md ├── 04_Wedge from Parameter.md └── 05_Creating Nodes.md ├── TOPs ├── 00_Quick Notes.md ├── 01_Fast Resize with command line.md └── 02_Hbatch and Hython.md └── WIP └── 01_Sticky Notes.md /Contributing/01_How to contribute.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How to contribute" 3 | author: [ 4 | { name: "Luke Van", 5 | link: "https://github.com/lukevanlukevan"} 6 | ] 7 | --- 8 | 9 | # How to contribute 10 | 11 | This project is powered by you! You can contribute anything you feel is worth knowing or sharing. 12 | 13 | ## Method 1: 14 | 15 | Go to [the GitHub](https://github.com/lukevanlukevan/h-python-wiki) and make a pull request. 16 | 17 | Add your page under it's respective folder (or create a new one), and bundle your images in the folder if you need them. 18 | 19 | If you want to be really fancy, you can help me out by adding FrontMatter to your markdown file, at the top of the .md file like this: 20 | 21 | ```markdown 22 | --- 23 | title: "How to contribute" 24 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 25 | --- 26 | ``` 27 | 28 | ## Method 2: 29 | 30 | Construct a folder with your .md file and the images if needed. As above, FrontMatter is greatly appreciated as it saves a lot of time. 31 | 32 | Then, click through to the [upload page](/upload) and fill in the appropriate fields, upload your folder as an archive of your choice, and submit! 33 | 34 | -------------------------------------------------------------------------------- /Contributing/02_Supporting the project.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Supporting the project 3 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 4 | --- 5 | 6 | # Supporting the project 7 | 8 | As with any free project, and support of any type is always appreciated. Be it a donation, a submission or even some kind words on the Discord server, everything is a huge help to me! 9 | 10 | You can [buy me a coffee](https://bmc.link/lukevan), or you [contribute to the server](/Contributing#how-to-contribute) if you feel like you want to give something back. 11 | 12 | If you got this far, thank you! 13 | 14 | -------------------------------------------------------------------------------- /Contributing/03_Contributors.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contributors 3 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 4 | --- 5 | 6 | # Contributors 7 | 8 | Thank you very much to: 9 | 10 | Luke Van for 23 pages 11 | 12 | WaffleBoyTom for 9 pages 13 | 14 | -------------------------------------------------------------------------------- /Crowds/01_Remove clip from agents.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Remove clip from agents 3 | author: [{ name: "WaffleBoyTom", link: "https://www.github.com/WaffleBoyTom" }] 4 | --- 5 | 6 | # Remove clip from agents 7 | 8 | So the other day, Antini asked on Discord if there was a way to delete a clip from an agent's definition. I've been getting into crowds lately so I thought I'd take a look. It seems there's no sop that does that and while vex has utility functions for crowds, there's no function to delete clips, so python it is. 9 | 10 | Here's some docs to get you going: 11 | 12 | [What is an Agent Primitive](https://www.sidefx.com/docs/houdini/crowds/agents.html#agentdefinition) 13 | 14 | [hou.AgentDefinition() class](https://www.sidefx.com/docs/houdini/hom/hou/AgentDefinition.html) 15 | 16 | [hou.Agent() class](https://www.sidefx.com/docs/houdini/hom/hou/Agent.html) 17 | 18 | This is my setup: 19 | 20 | ![](/img/RemoveClips/1.png) 21 | 22 | A basic agent (mocapbiped3) with 3 different clips 23 | 24 | The code is pretty short so here's the whole thing. 25 | 26 | ```python 27 | node = hou.pwd() 28 | geo = node.geometry() 29 | 30 | for prim in geo.prims(): 31 | 32 | adef = prim.definition() 33 | fadef = adef.freeze() 34 | fadef.removeClip("walk_turn") 35 | prim.setDefinition(fadef) 36 | ``` 37 | 38 | An agent is a packed primitive so it's just a _hou.Prim()_, the loop iterate through every agent (in this case we could do `prim = geo.prim(0)` to get the agent as there's only one prim). 39 | 40 | By default, an agent's definition is read only so we need to create a read-write version with `hou.AgentDefinition().freeze()`. 41 | 42 | We can then remove a clip from that definition, in this case 'walk_turn'. 43 | 44 | Then we replace our agent definition with the new one. 45 | 46 | -------------------------------------------------------------------------------- /Extra Things/01_Session.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Session 3 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 4 | --- 5 | 6 | # Session 7 | 8 | Quick one that can be useful. If you ever need to store some form of variable in a way that it persists over multiple scripts or executes of a script, you can use `hou.session`. Open the python shell and write `hou.session.foo = "Hello!"`. 9 | 10 | Now you can call `hou.session.foo` anywhere and your variable will persist! 11 | 12 | If you open the Python Source Editor, you can also add functions and variables there. Annoyingly, variables created outside of the Source Editor don't show up when editing it, but they do persist. 13 | -------------------------------------------------------------------------------- /Fun/01_Custom Network Titles.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom Network Titles 3 | author: [{ name: "WaffleBoyTom", link: "https://www.github.com/WaffleBoyTom" }] 4 | --- 5 | 6 | # Custom Network Titles 7 | 8 | Shout out to Remi Pierre and Matt Estela for telling me about this. 9 | 10 | (this quick tip assumes you're using a version of Houdini that uses py3.9) 11 | 12 | You can customize the top left and top right titles of your network editor. 13 | 14 | Here's an example: 15 | 16 | ![](/img/CustomNetworkTitles/1.png) 17 | 18 | First off, copy this file `$HFS/houdini/python3.9libs/nodegraphtitle.py`. 19 | 20 | To find out where $HFS is, you can either : 21 | 22 | `~$ echo $HFS` with the Houdini env initialized or 23 | `~$ hconfig` to print the values of common houdini variables 24 | or inside Houdini : Help>About Houdini> Show Details and scroll down until you find _$HFS_. 25 | 26 | Now go to $HOUDINI*USER_PREF_DIR and in there, create a directory called \_python3.9libs* if there isn't one. 27 | 28 | Dive in and paste the .py in there. If you're on Linux, the file will be read-only so open the terminal inside the directory and do : 29 | 30 | `~$ chmod +rwx nodegraphtitle.py` 31 | 32 | This will give you read, write and execute permissions. 33 | 34 | The final path should look like this : `$HOUDINI_USER_PREF_DIR/python3.9libs/nodegraphtitle.py` 35 | which is : 36 | `$HOME/houdiniXX.X/python3.9libs/nodegraphtitle.py` 37 | 38 | Now you can edit the file however you'd like. 39 | 40 | In the screengrab, all I did was : 41 | 42 | ```python 43 | ... 44 | ... 45 | ... 46 | def networkEditorTitleLeft(editor): 47 | try: 48 | title = "I'm so custom" 49 | pwd = editor.pwd() 50 | ... 51 | ... 52 | ... 53 | ... 54 | ``` 55 | 56 | I don't know how often that script executes, so do tread carefully. 57 | 58 | -------------------------------------------------------------------------------- /HDAs/01_Reading parameters.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Reading Parameters" 3 | author: [ 4 | { name: "Luke Van", 5 | link: "https://github.com/lukevanlukevan"} 6 | ] 7 | --- 8 | 9 | # Reading Parameters 10 | 11 | When working with a custom HDA, one might find themselves wanting to add more functionality to the parameters, especially if you are handing off this asset to a team who arent interested in digging around, and just need easy access as quick as possible. 12 | This is an example of how to utilize the Scripts portion of an HDA. 13 | 14 | In my example, I have built an empty HDA with 2 parameters, one file input and a button with an icon and no label. 15 | 16 | ![](/img/ReadingParameters/1.png) 17 | 18 | We can right click on the node and click on 'Type Properties' and go the 'Scripts' tab. At the bottom left, we can click the 'Event Handler' dropdown and select 'Python Module,' this is the event handler that deals with actions on the HDA. 19 | 20 | In the script box on the right, add a function like this: `def OpenDir(kwargs):` 21 | 22 | If you've read the other pages, you will know that we can use `kwargs` to get all the extra information from the node. 23 | 24 | so if we `print(kwargs)`, we get this beautiful mess: 25 | 26 | ```python 27 | {'node': , 'parm': 28 | , 'script_multiparm_index': '-1', 'script_v 29 | alue0': '0', 'script_value': '0', 'parm_name': 'openBtn', 'script_multiparm_nesting' 30 | : '0', 'script_parm': 'openBtn'} 31 | ``` 32 | 33 | If we fish through this, we can see a few bits of use, namely 'node'. So in our script, we can now use the node to get any parm on the node. From here, we have a basic hou.node case here, where the [docs](https://www.sidefx.com/docs/houdini/hom/hou/Node.html) can guide us. 34 | 35 | So now, our updated function looks like this: 36 | 37 | ```python 38 | def OpenDir(kwargs): 39 | node = kwargs['node'] 40 | input = node.parm('file_input').eval() 41 | ``` 42 | 43 | > Note the use of `eval()` here, otherise we are just getting a reference to the parm. We specifically want to get the value of it, that is why we use `eval()`. 44 | 45 | Our initial goal was to open the path in our native explorer, so let's add that last line into our function. 46 | 47 | ```python 48 | def OpenDir(kwargs): 49 | node = kwargs['node'] 50 | input = node.parm('file_input').eval() 51 | hou.ui.showInFileBrowser(input) 52 | ``` 53 | 54 | The last step is to add a callback function to our buttom, to run the script when we press it. 55 | 56 | We use the function `hou.phm()` which is a shorthand for `hou.pwd().hdaModule()`. So with that, we can set the callback function to: 57 | 58 | ```python 59 | hou.phm().OpenDir(kwargs) 60 | ``` 61 | 62 | So now our button will open the location selected in the file input parameter. 63 | 64 | -------------------------------------------------------------------------------- /HDAs/02_Multiparm Block Creation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Multiparm Block Creation" 3 | author: 4 | [ 5 | { name: "WaffleBoyTom", link: "https://github.com/WaffleBoyTom" }, 6 | { name: "Luke Van", link: "https://github.com/lukevanlukevan" }, 7 | ] 8 | --- 9 | 10 | # Multiparm Block Creation 11 | 12 | How to create multiparm blocks, set and access them through the Python API/HOM. 13 | 14 | To create a multiparm block, you actually need to grab the "Folder" parameter. Then, set the "Folder Type" to one of the 'Multiparm Block' types. 15 | 16 | ![1](/img/MultiparmBlockCreation/1.png) 17 | 18 | All 3 types are essentially the same, only the UI differs. I tend to use the 'List' type most often but that's personal preference. 19 | 20 | You can now create a parameter of your choosing inside the block. In this example, I created a string parameter. Each time you press the '+' button on the block, it will create an instance of your parameter(s). In my case, it creates a string parameter. Note the '#' character in my parm name `parm_#`. This refers to the instance of the parameter. If you look at at the image below, you can see my parms are labelled: Parm 1, Parm 2, Parm 3. 21 | 22 | ![1](/img/MultiparmBlockCreation/2.png) 23 | 24 | ## Creating a Multiparm block and setting its instances with Python 25 | 26 | To create a parameter on a node, you need `node.addSpareParmTuple()` which requires a `hou.ParmTemplate`. Now that you know that a multiparm block is a folder, we can use the `hou.FolderParmTemplate` class to create our multiparm block. 27 | 28 | We need to initialize our FolderParmTemplate to have the `folder_type` argument as one of the MultiparmBlock types. In this case we will use the hou.folderType.MultiparmBlock. 29 | 30 | We will now create a parameter inside the multiparm. 31 | 32 | First we create the parm template, choosing for this example the `StringParmTemplate` class. 33 | 34 | We can now call `addParmTemplate()` on the multiparm template to add the `StringParmTemplate` to it. 35 | 36 | Then we use `addSpareParmTuple(template)` on the target node to create the multiparm block. 37 | 38 | ![1](/img/MultiparmBlockCreation/7.png) 39 | 40 | Say you want to set a float parameter to 5.25; you would use the `set()` method on your parameter object, like so: `node.parm('my_float_parm').set(5.25)` 41 | 42 | Multiparms work in a similar fashion. The `set()` method will accept an integer as an argument. For example: 43 | ![1](/img/MultiparmBlockCreation/3.png) 44 | 45 | So, let's now call `parm.set(15)`. This will create 15 instances of the string parameter we added to the block. 46 | 47 | Great! You've set your multiparm to the desired number of instances. If you want to clear it, simply set it to 0: `parm.set(0)` 48 | 49 | We've now created a multiparm block that holds a string parameter, and created 15 instances of said string parameter. Let's see what that snippet looks like: 50 | 51 | ```python 52 | node = hou.node('/obj/geo1/ME') #this is just a null inside of sops 53 | template = hou.FolderParmTemplate("my_block","My Block",folder_type = hou.folderType.MultiparmBlock) 54 | stemplate = hou.StringParmTemplate("my_instance_#","My Instance #",1) 55 | template.addParmTemplate(stemplate) 56 | parmtuple = node.addSpareParmTuple(template) 57 | node.parm('my_block').set(15) 58 | ``` 59 | 60 | ## Accessing the multiparm instances 61 | 62 | You can use a for loop `range(multiparm.evalAsInt()` to iterate through all items. 63 | 64 | Here's the catch: when creating a multiparm, there's an option to decide how the instances are numbered, the "First Instance" option. 65 | 66 | ![1](/img/MultiparmBlockCreation/4.png) 67 | 68 | As you can see, our parms start at 1. That's because we have 'First Instance' set to 1. This means that when looping through the parms, we'll have to add 1 to the iterator. 69 | 70 | ![1](/img/MultiparmBlockCreation/5.png) 71 | 72 | To get the Parm object, we're using `node.parm('parm_'+str(i+1))`, that's because 'First Instance' is set to 1 but iterators in loops start at 0 (you probably already know this). 73 | 74 | This is fine but it's something you have to keep in mind. 75 | 76 | Don't want to have to deal with that? Set 'First Instance' to 0, then you won't have to add 1 to your iterator. However that will also influence the look of your UI : the first parm is now labelled 'Parm 0'. 77 | 78 | ![1](/img/MultiparmBlockCreation/6.png) 79 | 80 | Earlier we created a multiparm block with Python. What if you want to set 'First Instance' using Python too? 81 | 82 | When creating the parm template, you can specify a 'tags' argument that will let you set that 'First Instance' to whatever your heart desires ( as long as it is an unsigned int, aka x >=0.). Well I say uint but you then have to convert it to a string. You'll most likely only ever set it to '0' or '1'. 83 | 84 | Let's take our previous snippet and specify the 'multistartoffset' tag to set the 'First Instance' option through Python. 85 | 86 | ```python 87 | node = hou.node('/obj/geo1/ME') #this is just a null inside of sops 88 | tags = dict() 89 | tags['multistartoffset'] = '0' #note that 0 is a string 90 | template = hou.FolderParmTemplate("my_block","My Block",folder_type = hou.folderType.MultiparmBlock,tags=tags) 91 | stemplate = hou.StringParmTemplate("my_instance_#","My Instance #",1) 92 | template.addParmTemplate(stemplate) 93 | parmtuple = node.addSpareParmTuple(template) 94 | node.parm('my_block').set(15) 95 | ``` 96 | 97 | Say you've rebuilt this setup, let's provide you with an example snippet that sets your multiparm block to 15 instances and sets each of them to some random gibberish. 98 | 99 | In the snippet below, `index == str(i)`. This works because we had set 'First Instance' to 0! Don't forget that by default 'First Instance' is equal to 1 which means you would have to set index to be `str(i+1)`. 100 | 101 | Here's what the code looks like: 102 | 103 | ```python 104 | import random 105 | import string 106 | import hou #depending on where you write this you might not need this import 107 | 108 | def create_rand_string(length): 109 | characters = string.ascii_letters + string.digits + string.punctuation 110 | rstring = ''.join(random.choice(characters) for i in range(length)) 111 | return rstring 112 | 113 | node = hou.node('/obj/geo1/ME') 114 | parm = node.parm('my_block') 115 | 116 | instances = 15 117 | 118 | parm.set(15) 119 | 120 | for i in range(instances): 121 | 122 | index = str(i) 123 | 124 | parm = node.parm('my_instance_'+index) 125 | 126 | randint = random.randint(i+1,20) 127 | 128 | parm.set(create_rand_string(randint)) 129 | ``` 130 | 131 | That's it! Set all the instances within a multiparm block using python. 132 | 133 | -------------------------------------------------------------------------------- /HDAs/03_String Parameter Menu.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "String Parameter Menu" 3 | author: [{ name: "WaffleBoyTom", link: "https://github.com/WaffleBoyTom" }] 4 | --- 5 | 6 | # String Parameter Menu 7 | 8 | When using nodes in Houdini you'll often come across these handy menus that usually come with string parameters. 9 | 10 | ![](/img/StringParameter/1.png) 11 | 12 | Let's look at how we can DIY that. 13 | 14 | Let's create a new string parameter and head to the menu tab of the parameter description. 15 | 16 | ![](/img/StringParameter/2.png) 17 | 18 | We'll tick _Use Menu_ and set it to _Replace (Field + Selection Menu)_. You can experiment with other options but this is usually the one you want. 19 | 20 | Now you can write your own menu by setting a token then a label. That's cool and all but this is Houdini, we'd like something procedural. Let's use the menu script tab to write some python instead ! 21 | 22 | The script is pretty much a callback that runs every time you click the button. The callback needs to return a list that's built exactly like a regular menu: 23 | _(token1,value1,token2,value2,token3,value3)_ 24 | 25 | In this example we'll get a list of directories. Let's say my hip is _/home/what/projects/pythonstuff.hip_. I want to get all the directories that live in _/home/what/projects_, my hip's parent directory. 26 | 27 | Let's look at our code ![](/img/StringParameter/4.png) 28 | 29 | ```python 30 | import os 31 | from pathlib import Path 32 | 33 | basedir = hou.expandString("$HIP") 34 | 35 | parent = Path(basedir).parent 36 | 37 | menuitems = list() 38 | 39 | dirs = os.listdir(parent) 40 | 41 | for dir in dirs: 42 | 43 | menuitems.append(dir) 44 | menuitems.append(dir) 45 | 46 | return menuitems 47 | ``` 48 | 49 | We're getting the path to the HIP with `hou.expandString('$HIP')` . We could do the same with `hou.getenv('HIP')`. Then we're getting the parent directory with `Path.parent`. We could also use `os.path.split()[-1]`. Many ways to skin a cat ; this example focuses on how you can create the menu, not so much on which methods/packages you should use. We're then creating a empty list. `os.listdir()` gives us a list of the directories contained within _parent_. We then iterate through the list and add the item to the menu. Why twice ? A menu needs a token and a label. In this case, we want them to have the same value. Let's illustrate this by reworking the loop. 50 | 51 | ```python 52 | for index,dir in enumerate(dirs): 53 | 54 | menuitems.append(str(index)) 55 | menuitems.append(dir) 56 | ``` 57 | 58 | In this example, the token would be : 0,1,2,... and the value would be each directory. So clicking on the menu item would return :0,1,2. 59 | 60 | ![](/img/StringParameter/5.png) 61 | 62 | This might be something you want but most of the time you want the values to match. 63 | 64 | If you want to check how more menus are implemented, the Labs or SideFx HDAs are a good place to start. 65 | 66 | -------------------------------------------------------------------------------- /HDAs/04_Data Encapsulation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Data Encapsulation" 3 | author: [{ name: "Milan Lampert", link: "https://github.com/lampmilan" }] 4 | --- 5 | 6 | # Data Encapsulation 7 | 8 | ## Encapsulate data in a HDA 9 | 10 | Sometimes, we need to create data that is unique to a specific node and cannot be modified by other nodes of the same type. 11 | In the Python module, most data and variables are shared among every node of the same type. 12 | 13 | ```python 14 | import hou 15 | 16 | # GLOBAL VAR EXAMPLE 17 | previous_value = 0 18 | 19 | def get_difference_from_global(current_value): 20 | global previous_value 21 | 22 | difference = current_value - previous_value 23 | print("DIFFERENCE: {}".format(difference)) 24 | 25 | previous_value = current_value 26 | ``` 27 | 28 | In this example, we manipulate the `previous_value` variable using the `global` keyword. You might expect the global keyword affect only that node, but in reality, that information is the same for every instance of the node. 29 | ![global var example](img/DataEncapsulation/1.gif) 30 | 31 | To avoid conflicts between node information, we need to encapsulate those pieces of information. For this, I'll demonstrate four different approaches to achieve this. All of them have some drawbacks, so the best choice often depends on the situation. 32 | For demonstration, I will use the same problem for all four approaches: calculating the difference between the previous and current value when a parameter is changed. 33 | 34 | ## Hidden parameter 35 | 36 | The most obvious solution is to use a hidden parameter and utilize it to store the value. There are many advantages to this approach: it's simple and fast to implement, it saves data between sessions, and this is the most "Houdini-like" approche. However, some data types don't have parameter types (such as lists, sets, and tuples). In such cases, you need to rely on type conversion. Additionally, over time as our HDA grows and we add more invisible parameters, it might become challenging to keep track of which parameters connect where and which ones get updated. 37 | 38 | I've created two parameters: one that the user can interact with and another one that will serve as our hidden parameter. 39 | ![hidden parmameters](img/DataEncapsulation/2.png) 40 | 41 | ```python 42 | # HIDDEN PARM EXAMPLE 43 | def get_difference_from_invisible_parm(current_value): 44 | previous_value = hou.pwd().parm('prev_value').eval() 45 | 46 | difference = current_value - previous_value 47 | print("DIFFERENCE: {}".format(difference)) 48 | 49 | hou.pwd().parm('prev_value').set(current_value) 50 | ``` 51 | 52 | ## UserData 53 | 54 | By default, every node comes with two dictionaries: "userData" and "cachedUserData". If we take a closer look at the Houdini documentation, they even refer to these two dictionaries as the ["Per-node user-defined data"](https://www.sidefx.com/docs/houdini/hom/nodeuserdata.html), making them the closest thing to the de facto encapsulation method. First, let's examine userData. It's a handy solution for storing string variables between sessions. Compared to parameters, userData can be created and accessed where we actually need it, reducing clutter in our parameter list. However, the problem is that it can only store strings, so we either heavily rely on type conversion or implement workarounds. In this case, I go with the last one and simply convert the UserData entry to an integer, which is then converted back to a string after we do the calculation. 55 | 56 | I define an entry in the `OnCreate` module, so there's no chance it's not available when I call it: 57 | 58 | ```python 59 | node = kwargs['node'] 60 | node.setUserData('previous_value', '0') 61 | ``` 62 | 63 | And this is the `PythonModule` section: 64 | 65 | ```python 66 | # USER DATA EXAMPLE 67 | def get_difference_from_user_data(current_value): 68 | previous_value_str = hou.pwd().userData('previous_value') 69 | previous_value = int(previous_value_str) 70 | 71 | difference = current_value - previous_value 72 | print("DIFFERENCE: {}".format(difference)) 73 | 74 | hou.pwd().setUserData('previous_value', str(current_value)) 75 | ``` 76 | 77 | ## CachedUserData 78 | 79 | Compared to userData, cachedUserData can store any type of data, such as integers, lists, dictionaries, and even objects. However, the main disadvantage is that it only stores data for the current session. 80 | 81 | Like previously I create the entry in the `OnCreate` module: 82 | 83 | ```python 84 | node = kwargs['node'] 85 | node.setCachedUserData('previous_value_cache', 0) 86 | ``` 87 | 88 | Python module: 89 | 90 | ```python 91 | # USER CACHE EXAMPLE 92 | def get_difference_from_user_cache(current_value): 93 | previous_value = hou.pwd().cachedUserData('previous_value_cache') 94 | 95 | difference = current_value - previous_value 96 | print("DIFFERENCE: {}".format(difference)) 97 | 98 | hou.pwd().setCachedUserData('previous_value_cache', current_value) 99 | ``` 100 | 101 | ## Singleton pattern 102 | 103 | Basically, it's like `cachedUserData`, but it's overcomplicated. It can store any data type for the current session. One minor advantage is that it gives you full access to every value while still encapsulating them. Additionally, it's open to extension, unlike the UserData classes (so we can add new methods if needed, such as `destroy_all` or `return_every_instance`). 104 | 105 | There are two main issues: 106 | - Singleton is a [controversial topic among Python developers](https://python-patterns.guide/gang-of-four/singleton/) because Python modules work exactly like a singleton. Here we use singleton because it's a more stable solution compared to HDA modules. (One issue I found out recently is that sometimes returning an object from another module actually gets converted to a string). 107 | - The implementation is rather robust, and we need to handle instance removal as well, otherwise, we'll face with some nice "hou.ObjectWasDeleted" errors. 108 | 109 | ```python 110 | # SINGLETON 111 | class PreviousValues: 112 | _instances = {} 113 | 114 | @classmethod 115 | def get_instance(cls, node_instance): 116 | if node_instance not in cls._instances: 117 | cls._instances[node_instance] = 0 118 | return cls._instances[node_instance] 119 | 120 | @classmethod 121 | def set_value(cls, node_instance, value): 122 | cls._instances[node_instance] = value 123 | 124 | @classmethod 125 | def delete_instance(cls, node_instance): 126 | cls._instances.pop(node_instance) 127 | 128 | 129 | 130 | def get_difference_from_singleton(current_value): 131 | previous_value = PreviousValues.get_instance(hou.pwd()) 132 | 133 | difference = current_value - previous_value 134 | print("DIFFERENCE: {}".format(difference)) 135 | 136 | PreviousValues.set_value(hou.pwd(), current_value) 137 | ``` 138 | 139 | The `OnDelete` module handle the remove of the instance: 140 | 141 | ```python 142 | type = kwargs['type'] 143 | node = kwargs['node'] 144 | 145 | type.hdaModule().PreviousValues.delete_instance(node) 146 | ``` 147 | -------------------------------------------------------------------------------- /HDAs/04_Python Module.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Python Module 3 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 4 | --- 5 | 6 | # Python Module 7 | 8 | You can do a lot of scripting right in an HDA. If you create a new HDA, in the type properties you can can click to tab for "Scripts". On the bottom left, there is an "Event Handler" dropdown. You can select any of these and bind some python to those events. The most basic on is the "Python Module" one, that is basically just a little environment for functions and logic that you can call from other places in the script. 9 | 10 | In the Python Module script, create a function like so: 11 | 12 | ```python 13 | def hello_world(**kwargs): 14 | print("Hello World") 15 | ``` 16 | 17 | Now you can use the script in the callback script of any parm. For example, make a button parm and on the callback script, set the language to Python and in the box, type `hou.phm().hello_world()` 18 | 19 | Now when you click the button, you run the function. 20 | -------------------------------------------------------------------------------- /LOPs/01_Control Lights in Solaris With Python.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Control Solaris Lights" 3 | author: [{ name: "WaffleBoyTom", link: "https://www.github.com/WaffleBoyTom" }] 4 | --- 5 | 6 | # Control Solaris Lights 7 | 8 | I'm starting to like USD more and more so let's write some code in Solaris. We'll look at setting attributes on our lights using the USD API and HOM. Then we'll finish it off by creating flickering lights. 9 | 10 | _This tutorial assumes basic knowledge of USD._ 11 | 12 | I've set up this simple scene in Solaris: a box, a XZ grid , four lights and a camera. ![](/img/ControlLights/1.png) 13 | Let's drop down a python lop and change the intensity of our lights to be 0, essentially turning them off. 14 | 15 | For now, we're only going to select one light and turn it off, and we'll see later how we can select all of them. 16 | 17 | We'll get our light prim with `stage.GetPrimAtPath('/lights/light1')`. Note the upper case _G_ in _Get_, that's sort of a gotcha as all the Houdini methods start with a lowercase letter, but this is Pixar's API. It takes some time getting used to. 18 | 19 | Let's print that: `>>> Usd.Prim()`. We now have a Usd.Prim object. Let's set its intensity to 0. 20 | 21 | First we need to get the attribute object: `intensity = light.GetAttribute('inputs:intensity')` 22 | 23 | ### Tip: 24 | 25 | Not sure what your attributes are? Look at the Solaris Spreadsheet aka the ScreneGraphDetails. ![](/img/ControlLights/2.png) 26 | Or you can print em all with `print(prim.GetAttributes())`. 27 | Let's now set that attribute to 0: `intensity.Set(0)`. 28 | 29 | The whole snippet looks like this: 30 | 31 | ```python 32 | node = hou.pwd() 33 | 34 | stage = node.editableStage() 35 | 36 | light = stage.GetPrimAtPath('/lights/light1') 37 | 38 | intensity = light.GetAttribute('inputs:intensity') 39 | 40 | intensity.Set(0) 41 | ``` 42 | 43 | Now that we've done this, we can look at SideFx's preset for this! ![](/img/ControlLights/3.png) 44 | 45 | It essentially does what we've just done, except it uses `hou.LopSelectionRule()` to avoid having to hardcode paths into your code. The LopSelectionRule Class means you can also create a parameter on your node that you could use to get the path. 46 | 47 | ```python 48 | ls = hou.LopSelectionRule() 49 | ls.setPathPattern('%type:Light') 50 | paths = ls.expandedPaths(node.inputs()[0]) 51 | ``` 52 | 53 | If you're wondering what _%type:Light_ refers to, it's what USD calls a primitive matching pattern. These patterns let you select primitives based on some condition. [Here's the docs page:](https://www.sidefx.com/docs/houdini/solaris/pattern.html). 54 | 55 | Let's now look at animating our lights. We'll turn them on and off again to create some kind of flickering. 56 | 57 | We'll turn them all off to start with, then we'll drop another python lop to implement the flickering logic in. All I've done here is used the Preset but replaced _0.5_ with _0.0_, making the intensity 0.0 on all those lights. In the next python lop we'll use the preset again but delete the line where it sets the intensity. 58 | 59 | First things first: let's set a random intensity per light. 60 | 61 | There are a million ways to generate random values and it's slightly outside the scope of Houdini Python so I'll share my snippet here and we'll move on to actually animating the values. 62 | 63 | ```python 64 | import secrets 65 | import numpy as np 66 | 67 | node = hou.pwd() 68 | 69 | ls = hou.LopSelectionRule() 70 | ls.setPathPattern('%type:Light') 71 | paths = ls.expandedPaths(node.inputs()[0]) 72 | 73 | stage = node.editableStage() 74 | 75 | for index,path in enumerate(paths): 76 | 77 | prim = stage.GetPrimAtPath(path) 78 | 79 | intensity = prim.GetAttribute('inputs:intensity') 80 | 81 | seed = secrets.randbits(86+index+35) 82 | 83 | rng = np.random.default_rng(seed+index+362) 84 | 85 | rand_float = rng.uniform(0.5,2.5) 86 | 87 | intensity.Set(rand_float) 88 | ``` 89 | 90 | In this snippet I'm importing _secrets_ to generate a random seed, then using _numpy_ to generate a random float between 0.5 and 2.5. Do know that using _secrets_ means your code will evaluate to something different each time so be careful ! For more predictable results you can do this instead: `rng = np.random.default_rng(index)`. Then delete these two lines: `import secrets` and `seed = secrets.randbits(86+index+35)`. 91 | 92 | Let's look at only setting that intensity on random frames. What I'm going for here is: one light blinks every like 2 frames then another one blinks every 3 or 4 frames, etc... 93 | 94 | So I'm gonna generate a random integer that I'll then use with modulo. If frame%modulo == 0 returns true, then I'll activate the light. 95 | 96 | To animate our _inputs:intensity_, we'll have to author its time samples. 97 | 98 | The Usd Prim Attribute methods `Set()`and `Get()` actually sample the value at the given time sample. If left blank, you're sampling at the default time sample, aka Usd.timeCode.Default(). 99 | Note that if you want to explicitly use it, you'll need to import Usd: `from pxr import Usd`. 100 | Let's use the overloaded function to author our time samples: `primattrib.Set(value,time)`. 101 | 102 | ```python 103 | import secrets 104 | import numpy as np 105 | import random 106 | 107 | 108 | node = hou.pwd() 109 | 110 | ls = hou.LopSelectionRule() 111 | ls.setPathPattern('%type:Light') 112 | paths = ls.expandedPaths(node.inputs()[0]) 113 | 114 | stage = node.editableStage() 115 | 116 | framerange = hou.playbar.frameRange() 117 | start = int(framerange.x()) 118 | end = int(framerange.y()) 119 | 120 | for index,path in enumerate(paths): 121 | 122 | prim = stage.GetPrimAtPath(path) 123 | 124 | intensity = prim.GetAttribute('inputs:intensity') 125 | 126 | #create random float 127 | 128 | seed = secrets.randbits(86+index+35) 129 | 130 | rng = np.random.default_rng(seed + index+362) 131 | 132 | rand_float = rng.uniform(0.5,2.5) 133 | 134 | #create random int 135 | 136 | random.seed(seed+index+45) 137 | 138 | randint = random.randint(2,6) 139 | 140 | for i in range(start,end+1): 141 | 142 | intensity.Set(rand_float,i) 143 | 144 | ``` 145 | 146 | Our code now looks like this. We're iterating through every frame of the timeline and setting the random value every frame with `primattrib.Set(value,timesample)`. If you need to get a value at a specific time sample then use `primattrib.Get(timesample)`. 147 | 148 | Almost there ! Now we want to add a condition to only set the intensity to that random float if frame%randint == 0. No if blocks necessary, let's be clever and multiply rand_float by `(i%randint == 0)`. 149 | 150 | ```python 151 | for i in range(start,end+1): 152 | 153 | intensity.Set(rand_float * (i%randint==0) ,i) 154 | ``` 155 | 156 | We can now inspect our time samples with the SceneGraphLayers. ![](/img/ControlLights/4.png) 157 | 158 | Let's have a look at our animation. Warning: flashing lights ![](/img/ControlLights/1.mp4) 159 | 160 | We can randomize it further by generating a random int every time sample. 161 | 162 | ```python 163 | for i in range(start,end+1): 164 | 165 | random.seed(seed+index+545+i) 166 | 167 | randint = random.randint(2,6) 168 | 169 | intensity.Set(rand_float * (i%randint==0) ,i) 170 | ``` 171 | 172 | Note that you could do this in vex as well and it might be easier. Here's the code: 173 | 174 | ```python 175 | float frame = f@Frame; 176 | 177 | float rand = fit01(rand(i@elemnum+6230),0.5,2.5); 178 | 179 | int randint = int(fit01(rand(i@elemnum+frame+365241),2,8)); 180 | 181 | f@inputs:intensity = rand * (int(frame) % randint == 0); 182 | 183 | ``` 184 | 185 | While this works, it's less performant than Python. The performance hit isn't that huge though so vex is still a good option inside of LOPs, if you'd rather go that route. If performance is crucial then Python is much better suited for the task. 186 | 187 | Let's have a final look at our code: 188 | 189 | ```python 190 | import secrets 191 | import numpy as np 192 | import random 193 | 194 | 195 | node = hou.pwd() 196 | 197 | ls = hou.LopSelectionRule() 198 | ls.setPathPattern('%type:Light') 199 | paths = ls.expandedPaths(node.inputs()[0]) 200 | 201 | stage = node.editableStage() 202 | 203 | framerange = hou.playbar.frameRange() 204 | start = int(framerange.x()) 205 | end = int(framerange.y()) 206 | 207 | for index,path in enumerate(paths): 208 | 209 | prim = stage.GetPrimAtPath(path) 210 | 211 | intensity = prim.GetAttribute('inputs:intensity') 212 | 213 | #create random float 214 | 215 | seed = secrets.randbits(86+index+35) 216 | 217 | rng = np.random.default_rng(seed + index+362) 218 | 219 | rand_float = rng.uniform(0.5,2.5) 220 | 221 | for i in range(start,end+1): 222 | 223 | random.seed(seed+index+545+i) 224 | 225 | randint = random.randint(2,6) 226 | 227 | intensity.Set(rand_float * (i%randint==0) ,i) 228 | ``` 229 | 230 | Don't forget to get rid of _secrets_ if you want more predictable results. 231 | 232 | -------------------------------------------------------------------------------- /LOPs/02_Custom Metadata.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom Metadata 3 | author: [{ name: "WaffleBoyTom", link: "https://www.github.com/WaffleBoyTom" }] 4 | --- 5 | 6 | # Custom Metadata 7 | 8 | Here's how you can write custom data to USD prims and fetch them inside parameters. 9 | 10 | In a python lop we can do : 11 | 12 | ```python 13 | node = hou.pwd() 14 | stage = node.editableStage() 15 | prim = stage.GetPrimAtPath('/geo/myCube') 16 | prim.SetCustomDataByKey("myCustomData","/geo/sphere") 17 | ``` 18 | 19 | You know have custom data in your prim's metadata. 20 | ![](/img/CustomMetadata/1.png) 21 | 22 | You can now use a pretty long python expression to get that value inside a parameter (don't forget to tell Houdini it's a python expression and not a string value or a hscript expression) 23 | 24 | ```python 25 | hou.node('.').input(0).stage().GetPrimAtPath('/geo/myCube').GetCustomDataByKey('myCustomData') 26 | ``` 27 | 28 | You could always create a primvar instead. 29 | 30 | ```python 31 | from pxr import Sdf 32 | node = hou.pwd() 33 | stage = node.editableStage() 34 | prim = stage.GetPrimAtPath('/geo/myCube') 35 | customattrib = prim.CreateAttribute("myCustomAttrib",Sdf.ValueTypeNames.String) 36 | customattrib.Set("/geo/sphere") 37 | ``` 38 | 39 | Then to get it, use 40 | 41 | ```python 42 | hou.node('.').input(0).stage().GetPrimAtPath('/geo/myCube').GetAttribute('myCustomAttrib').Get() 43 | ``` 44 | 45 | ![](/img/CustomMetadata/2.png) 46 | 47 | -------------------------------------------------------------------------------- /Menu-Customization/01_Parm Menu.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Parm Menu" 3 | author: [ 4 | { name: "Luke Van", 5 | link: "https://github.com/lukevanlukevan"} 6 | ] 7 | --- 8 | 9 | # Parm Menu 10 | 11 | As usual, I always suggest starting with running through the [docs](https://www.sidefx.com/docs/houdini/basics/config_menus.html) to get used the the methods needed to get your ideas into working features. 12 | 13 | If you have been interested so far, you probably have some form of personal package set up by now. If not, I suggest you create one so you have the freedom to make changes (and break things). 14 | 15 | So, if we create a new file in the root our package folder called `PARMmenu.xml`. This is the name of the file that augments the paramter right click menu. 16 | 17 | In this file, paste this code: 18 | 19 | ```xml 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ``` 29 | 30 | When we go back and forth editing the menu, Houdini wont automatically reload the menu. So create a shelf script as shown: 31 | 32 | ```python 33 | hscript(menurefresh) 34 | ``` 35 | 36 | This runs the hscript command `menurefresh` which reloads the menu xml files. 37 | 38 | Now there are 2 directions you can go. The first is the ideal, where you have a python script in your package at `scripts/python/scriptName.py`. This allows you to easily edit individual files rather than the mess of all of the code inside the `PARMmenu.xml` file. 39 | 40 | Now we can do something in our script, let's start of with a basic function: 41 | 42 | ```python 43 | def getParmName(): 44 | // script here 45 | ``` 46 | 47 | the reason we created a function rather than just running raw code in here, is so that we can bundle multiple functions into one script if they are related. 48 | 49 | If we add a `print('hello')` into our function, we can link that function in our `PARMmenu.xml` file: 50 | 51 | ```xml 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | 69 | 70 | ``` 71 | 72 | You may have noticed the weird indentation in the XML file, this is sadly just how the file needs it. It doesnt play nicely with the indentation level from the XML stuff, and the python stuff needs to start at indent 0. 73 | 74 | What we did here is is create a script item with a unique ID. We added a label (what is shown in the menu), and added a `` tag that contains a small python script that imports our script ('scriptName') and then runs the function we created ('getParmName'). 75 | 76 | Right now, there is no way for our script file to know any information about the parameter we right clicked on. We can now pass that into our function in the XML file and our scipt. 77 | 78 | ```python 79 | // before 80 | scriptName.getParmName() 81 | 82 | //after 83 | scriptName.getParmName(kwargs) 84 | ``` 85 | 86 | We also need to add this to our script so that we can use the information: 87 | 88 | ```python 89 | def getParmName(kwargs): 90 | print(kwargs) 91 | ``` 92 | 93 | Now when we right click, and run our script, we can see all of the information that we can get from the parameter. 94 | 95 | A little issue with constantly going back and forth between Houdini and editing the code, is that you may find the script is updating on the Houdini side. A quick fix is adapt the XML script code to have this at the start: 96 | 97 | ```python 98 | from imp import reload 99 | import scriptName 100 | reload(scriptName) 101 | 102 | scriptName.getParmName(kwargs) 103 | ``` 104 | 105 | This uses the reload module to reload the script when the paramter script is run. Ideally you should remove this when shipping to customers or users. 106 | 107 | Now when we run the script on the parameter, we get a console popup with all the information. The first bit is named 'parms' and we can access the first (and only) parameter with square brackets. In this case, we want to be getting the parameter name, so first adjust your script as so: 108 | 109 | ```python 110 | def getParmName(kwargs): 111 | p = kwargs['parms'][0] 112 | ``` 113 | 114 | Now we have the parameter as a variable, and we can go to the docs for [hou.Parm](https://www.sidefx.com/docs/houdini/hom/hou/Parm.html) and treat it exactly how we would any other script. 115 | 116 | Last thing, we want to get the name, obviously! 117 | 118 | ```python 119 | def getParmName(kwargs): 120 | p = kwargs['parms'][0] 121 | name = p.name() 122 | print(name) 123 | ``` 124 | 125 | And the console says: "scale" (I clicked on the scale parameter). Success! 126 | 127 | The last bit I want to share is how to filter based on the parameter type. If you have a script that you only want to show up when clicked on ramps, for example. 128 | 129 | in our XML file, we can use the `` tag to filter based any rules: 130 | 131 | ```xml 132 | 133 | parm = kwargs["parms"] 134 | if parm is None: 135 | return False 136 | 137 | return parm[0].parmTemplate().type().name() == 'Ramp' 138 | 139 | ``` 140 | 141 | The menu item will only be displayed if the expression returns true. 142 | 143 | -------------------------------------------------------------------------------- /Node-Graph-Hooks/01_Basic Setup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic Setup 3 | author: [{name: 'Luke Van', link: 'https://github.com/lukevanlukevan'},] 4 | --- 5 | # Basic Setup 6 | 7 | Provided we have a [Personal Package](https://www.houpywiki.com/Personal-Pipeline#personal-package) set up, you can create a python3.9libs folder, and inside that, a nodegraphhooks.py file. 8 | 9 | There is a [writeup from SideFX](https://www.sidefx.com/docs/houdini/hom/network.html) on some basics, but I found the docs to be a little thin when searching for info on this part. 10 | 11 | Back to `nodegraphhooks.py`, you need to setup up and event handler that gets passed an event from Houdini, which accepts a uievent and pending actions as its arguments. 12 | 13 | ```python 14 | import hou 15 | from canvaseventtypes import * 16 | 17 | def createEventHandler(uievent, pending_actions): 18 | return None, False 19 | 20 | ``` 21 | 22 | This is one of those great Houdini things that needs a restart every time, so for live devving, I suggest setting it up as follows: 23 | 24 | ```python 25 | import hou 26 | from canvaseventtypes import * 27 | import nodegraphdisplay as display 28 | import nodegraphview as view 29 | 30 | import extra 31 | from importlib import reload 32 | 33 | 34 | def createEventHandler(uievent, pending_actions): 35 | reload(extra) 36 | if extra.do_stuff(uievent): 37 | return None, True 38 | else: 39 | return None, False 40 | 41 | ``` 42 | 43 | Because of the reload function, we can now create an `extra.py` next to the `nodegraphhooks.py` and do all our work there and have it update. 44 | 45 | Inside of `extra.py` I have set up the `do_stuff` function and now we can get started. 46 | 47 | ```python 48 | import hou 49 | from canvaseventtypes import * 50 | import nodegraphdisplay as display 51 | import nodegraphview as view 52 | from datetime import datetime 53 | 54 | 55 | def do_stuff(uievent): 56 | if isinstance(uievent, MouseEvent): 57 | print("there is a mouse event") 58 | return False 59 | ``` 60 | 61 | If we load up Houdini now and move out mouse around in the network view, you will see lines being printed in the console. Now that we have connected ourselves, we can experiment with what info we get. One thing to note is that the mouse moved event is fired very often, so I will just set up a condition so we miss that bit. 62 | 63 | ```python 64 | import hou 65 | from canvaseventtypes import * 66 | import nodegraphdisplay as display 67 | import nodegraphview as view 68 | from datetime import datetime 69 | 70 | 71 | def do_stuff(uievent): 72 | if not uievent.eventtype == 'mousemove': 73 | print(uievent) 74 | 75 | return False 76 | ``` 77 | 78 | If we click once and press a key on the keyboard, we can get these two to analyze: 79 | 80 | ``` 81 | MouseEvent(editor=, eventtype='mouseenter', selected=NetworkComponent 82 | (item=None, name='invalid', index=0), located=NetworkComponent(item=None, name='invalid', index=0 83 | ), mousepos=, mousestartpos=, mousestate=MouseSta 84 | te(lmb=0, mmb=0, rmb=0), dragging=0, wheelvalue=0, modifierstate=ModifierState(alt=0, ctrl=0, shi 85 | ft=0), time=183989.407) 86 | 87 | KeyboardEvent(editor=, eventtype='keyhit', located=N 88 | etworkComponent(item=None, name='invalid', index=0), mousepos=, mousestate=MouseState(lmb=0, mmb=0, rmb=0), key='C', modifierstate=Modif 90 | ierState(alt=0, ctrl=0, shift=0), time=183986.282) 91 | MouseEvent(editor=, eventtype='mouseexit', selected= 92 | NetworkComponent(item=None, name='invalid', index=0), located=NetworkComponent(i 93 | tem=None, name='invalid', index=0), mousepos=, mousestart 94 | pos=, mousestate=MouseState(lmb=0, mmb=0, rmb=0), draggin 95 | g=0, wheelvalue=0, modifierstate=ModifierState(alt=0, ctrl=0, shift=0), time=183 96 | 987.416) 97 | ``` 98 | 99 | Lots of info to digest in those, and in [Intercepting Mouse Events](#intercepting-mouse-events) we will look further at these. -------------------------------------------------------------------------------- /Node-Graph-Hooks/02_Intercepting Mouse Events.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Intercepting Mouse Events 3 | author: [{name: 'Luke Van', link: 'https://github.com/lukevanlukevan'},] 4 | --- 5 | # Intercepting Mouse Events 6 | 7 | Now that we are set up, we can get down into how to leverage more from this system. If you scroll up and look at how I set up `nodegraphhooks.py` you will see that I have an if statement, that returns either `None, True` or `None, False`. If we look into the definition for the `createEventHandler` function, we see the 2 args are passed off down the line to some mystical Houdini event pipeline. The bottom line is, if we reurn the second arg as True, we are essentially saying "don't use the native event handler, we are doind our own stuff here". I set the if statement up so we dont need to keep adjusting the intial file and restarting Houdini. Now in our `extra.py` we can return True or False wherever we break out, and control what is hijacked and not. 8 | 9 | For example: 10 | 11 | ```python 12 | def do_stuff(uievent): 13 | if isinstance(uievent, MouseEvent): 14 | print("Nobody move! Im hijacking this mouse event") 15 | return True 16 | else: 17 | return False 18 | ``` 19 | 20 | Now if we drop down a node, by default it will be selected, and if we try click off it to deselect it... we can't! That is by design, above we returned True if our event is a mouse event. Now we are pretty much just writing up if statements to refine until we only wrap the case we want, otherwise we block half of the native functionality. 21 | 22 | Let's say for example that we want to be able to shift + ctrl click a node and create a Null connected to it. 23 | 24 | ```python 25 | import hou 26 | from canvaseventtypes import * 27 | import nodegraphdisplay as display 28 | import nodegraphview as view 29 | from datetime import datetime 30 | 31 | 32 | def do_stuff(uievent): 33 | if isinstance(uievent, MouseEvent): 34 | if (uievent.eventtype == 'mousedown'): 35 | ctrl = uievent.modifierstate.ctrl 36 | shift = uievent.modifierstate.shift 37 | if (ctrl and shift): 38 | print("we made it") 39 | else: 40 | return False 41 | ``` 42 | 43 | Here we see we can we checked the `modifierstate` from our `uievent` to check which modifiers were down when the event fired. 44 | 45 | Let's add that Null functionality: 46 | 47 | ```python 48 | import hou 49 | from canvaseventtypes import * 50 | import nodegraphdisplay as display 51 | import nodegraphview as view 52 | from datetime import datetime 53 | 54 | 55 | def do_stuff(uievent): 56 | if isinstance(uievent, MouseEvent): 57 | if (uievent.eventtype == 'mousedown'): 58 | ctrl = uievent.modifierstate.ctrl 59 | shift = uievent.modifierstate.shift 60 | if (ctrl and shift): 61 | try: 62 | pos = uievent.mousepos 63 | editor = uievent.editor 64 | items = editor.networkItemsInBox(pos, pos, for_select=True) 65 | node = items[-1][0] 66 | 67 | name = node.name() 68 | 69 | name = "OUT_" + name 70 | 71 | null = node.createOutputNode('null', name) 72 | null.moveToGoodPosition() 73 | return True 74 | except: 75 | return False 76 | else: 77 | return False 78 | ``` 79 | 80 | The fun part of this setup is that it doesnt use the selected node, it grabs the one under your cursor. Nice and snappy! 81 | 82 | ![](/img/InterceptingMouseEvents/01.mp4) -------------------------------------------------------------------------------- /Personal-Pipeline/00_Basics.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Basics" 3 | author: [ 4 | { name: "Luke Van", 5 | link: "https://github.com/lukevanlukevan"} 6 | ] 7 | --- 8 | 9 | # Basics 10 | 11 | Once you get further into messing with all of the menus and scripts, you may find just working in the shelf tool editor to be a bit cumbersome, as well as in some cases, it can even end up corrupting your shelf tool and losing a few or all scripts. I've opted for setting up a personal package that allows me to organize and control all of the things. It also allows you to share your package with others for personal purposes, or even if you are working as a team. 12 | 13 | Here are all the things I would suggest setting up: 14 | 15 | - [Set up your external code editor](#external-code-editor) 16 | - [Create a personal package](#personal-package) 17 | -------------------------------------------------------------------------------- /Personal-Pipeline/01_External Code Editor.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "External Code Editor" 3 | author: [ 4 | { name: "Luke Van", 5 | link: "https://github.com/lukevanlukevan"} 6 | ] 7 | --- 8 | 9 | # External Code Editor 10 | 11 | My IDE of choice is VS Code. You know the drill here, pick the one you like the most, google things that don't line up! 12 | 13 | Install the Pylance Exstention, and then open up your `settings.json` file and add this. 14 | 15 | ```json 16 | // paths depend on your Houdini installation 17 | "python.analysis.extraPaths": [ 18 | "C:\\Program Files\\Side Effects Software\\Houdini 19.5.303\\houdini\\python3.9libs", 19 | "C:\\Program Files\\Side Effects Software\\Houdini 19.5.303\\python39\\lib\\site-packages-forced" 20 | ] 21 | ``` 22 | 23 | Now you can edit python and have access to the `hou` autocompletes which is helpful. 24 | -------------------------------------------------------------------------------- /Personal-Pipeline/02_Personal Package.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Personal Package" 3 | author: [ 4 | { name: "Luke Van", 5 | link: "https://github.com/lukevanlukevan"} 6 | ] 7 | --- 8 | 9 | # Personal Package 10 | 11 | So, when you get deeper and start creating HDAs and scripts, you will notice the default saving location is always relative to some default Houdini location. With the package we are creating, the goal is to have a mirrored skeleton directory of the native Houdini folder, where you can build (and break) things without affeting the default stuff. This also makes it very easy to share and keep track of the chaos you have made. 12 | 13 | To start off, create a folder somewhere that suits you. Maybe it's on the work NAS, or on Dropbox. The key point here is that it is not in your Houdini folder. In my case, I will call it `LVTools`. 14 | 15 | Then you can jump to your Houdini preferences folder and go into the packages folder and create a new .json file. For organization purposes, I would suggest naming it the same as your package folder. In my case, I now have `LVTools.json`. Inside it, its pretty basic for now: 16 | 17 | ```json 18 | { 19 | "env": [ 20 | { 21 | "LV": "Z:/Assets/LVTools" 22 | } 23 | ], 24 | "path": "$LV" 25 | } 26 | ``` 27 | 28 | Now you have 2 things. Houdini will load this folder at startup, and you have a path link that you can access with `$LV` in any path field. When saving things, now its pretty easy to link the relative to this new variable. 29 | 30 | If you're really committed, you would initialize a git repo here too and commit and push your changes. 31 | -------------------------------------------------------------------------------- /Personal-Pipeline/03_Saving Things.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Saving Things" 3 | author: [ 4 | { name: "Luke Van", 5 | link: "https://github.com/lukevanlukevan"} 6 | ] 7 | --- 8 | 9 | # Saving Things 10 | 11 | Now that you have you package set up and your variable working, you can start saving things to this package. 12 | 13 | When working with HDAs, I normally set the path to Hip directory, then to custom. Then it's fast to adjust to your vraible. This is the least amount of work required to get to your file saved in the right spot. 14 | 15 | ![](/img/SavingThings/01.gif) 16 | -------------------------------------------------------------------------------- /Personal-Pipeline/04_Life Hacks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Life Hacks" 3 | author: [ 4 | { name: "Luke Van", 5 | link: "https://github.com/lukevanlukevan"} 6 | ] 7 | --- 8 | 9 | # Life Hacks 10 | 11 | Pulling a [Paul Esteves](https://www.instagram.com/paulesteves28/) and calling this a hack. :P 12 | 13 | Sadly certain files are not "package-able" and need to be edited in the Houdini prefs folder. One of those is `jump.pref`, which we can use in this instance to add our new package variable to this file. 14 | 15 | ```text 16 | $LV 17 | ``` 18 | 19 | That's literally it, now when you open up the file explorer in Houdini, you can see the unexpanded variable on the sidebar. 20 | 21 | ![](/img/LifeHacks/01.png) 22 | 23 | This is a good timesave if you are often jumping around saving stuff. Certain menus (like saving the shelves) will benefit from this. 24 | -------------------------------------------------------------------------------- /Personal-Pipeline/05_Utility Scripts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Utility Scripts 3 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 4 | --- 5 | 6 | # Utility Scripts 7 | 8 | Often you ay find youself building little shelf utility scripts. I have a messy shelf with all the ideas I have, and once they become useful enough, I port them over to a main utility file, in my case, `scripts/python/LVUtils.py`. 9 | 10 | With this file, there is the benefit of being able to create a bunch of helper functions I may use in multiple places and keep everything nice and neat. For example, if you want to always have a clean console when things go wrong, you could create a function like: 11 | 12 | ```python 13 | def lv_error(f, message): 14 | 15 | print("------------------------") 16 | print(f"Error running: {f}") 17 | print("Logging error") 18 | print(message) 19 | print("------------------------") 20 | ``` 21 | 22 | Here the function takes two arguments, the function name and the message. In each tool you could have a simple try... except statement and get a clean log for each of them. 23 | 24 | ```python 25 | def lv_error(f, message): 26 | 27 | print("------------------------") 28 | print(f"Error running: {f}") 29 | print(message) 30 | print("------------------------") 31 | 32 | 33 | def print_selected_nodes(): 34 | try: 35 | nodes = hou.selectedNodes() 36 | print(nodes) 37 | except Error as e: 38 | lv_error("Print Selected Nodes", e) 39 | ``` 40 | 41 | Now if the function ever goes wrong, you get a clean log for the error. 42 | -------------------------------------------------------------------------------- /Personal-Pipeline/06_Houdini Types.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Houdini Types 3 | author: 4 | [ 5 | { name: "Igor Elovikov", link: "https://github.com/igor-elovikov" }, 6 | { name: "Luke Van", link: "https://github.com/lukevanlukevan" }, 7 | ] 8 | --- 9 | 10 | # Houdini Types 11 | 12 | When working with an [External Code Editor](#external-code-editor), you will see I often use `import hou` to get autocomplete suggestions. The only issue is natively the hou module errors out as needing `self` as the first argument for each function. This is easy to get around, as we can install [Houdini Types](https://pypi.org/project/types-houdini/). 13 | 14 | To install these, its useful to put them in a location you can link to and also that is somewhat "linked" to Houdini. 15 | 16 | To install these, open the Houdini shell by going to Windows -> Shell, then copy and paste this line: 17 | 18 | `python -m pip install types-houdini --target %HOUDINI_USER_PREF_DIR%/python3.9libs` 19 | 20 | It's important to do this in the Houdini shell, otherwise it just gets dropped into a location called "%HOUDINI_USER_PREF_DIR%". 21 | 22 | Once that's installed, open up VSCode and install Pylance if not already installed. Open your user settings JSON and find the `"python.analysis.extraPaths"` line, or create it if missing. 23 | 24 | Then, add 2 paths here: 25 | 26 | ```json 27 | "C:\\Users\\YOUR_USERNAME\\Documents\\houdini19.5\\python3.9libs", 28 | "C:\\Program Files\\Side Effects Software\\Houdini 19.5.493\\python39\\lib\\site-packages-forced", 29 | 30 | ``` 31 | 32 | These 2 lines are easily got using the Houdini shell by using `echo %HOUDINI_USER_PREF_DIR%/python3.9libs` and `echo %HFS/python39/lib/site-packages-forced`. 33 | -------------------------------------------------------------------------------- /Python-Panels/01_Getting Started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Getting Started" 3 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 4 | --- 5 | 6 | # Getting Started 7 | 8 | This is going to be a biggie, so strap in. 9 | 10 | ## Quick note: 11 | 12 | There are 2 ways to do anything that uses python scripts/files. The first is to script in the file, like with shelf tools, but I prefer the next method, which is just having all the external files, and importing them and calling them from Houdini. 13 | 14 | Quick rundown of the 3 key bits: 15 | 16 | yourpanel.pypanel (in your $PKG/python_panels) 17 | 18 | yourpanel.py (in your $PKG/scripts/python/PANEL_NAME) 19 | 20 | yourpanel.ui (in your $PKG/scripts/python/PANEL_NAME) 21 | 22 | ## My First PyPanel 23 | 24 | Go to Window -> Python Panel Editor. Open this and create a "new interface" in the top right. The first thing you want to do is change the path your package folder. Drop it in `$PKG/python_panels` and give it an appropriate name. In my case, this was `LVButton.pypanel`. Now you can change the name and label as normal. 25 | 26 | You will see the default code with a big comment block and this at the bottom: 27 | 28 | ```python 29 | from hutil.qt import QtWidgets 30 | 31 | def onCreateInterface(): 32 | widget = QtWidgets.QLabel('Hello World!') 33 | return widget 34 | ``` 35 | 36 | Before you even think about what's goin on, lets create the files I mentioned above in our package/python folder. 37 | 38 | I will make a folder called LVButton and inside it, create LVButton.py for now, we will get to LVButton.ui later. Copy and paste the code from the Python Panel Editor into your new file. 39 | 40 | Back in Houdini, go to Windows -> New Floating Window and then right click the panel name, and go to Misc -> Python Panel. Later on we can add this panel as a normal pane, this just allows us to hard refresh the panel which is useful in dev stage. 41 | 42 | There is a dropdown in the top left with some of the other python panels, and yours most likely won't be there. Go to your Python Panel Editor window and click the tab in the top left called "Toolbar Menu". Find your newly created Panel and move it over the right side and click Apply/Accept. Now you can see it in the dropdown. 43 | 44 | At this point, you should have something like this: 45 | 46 | ![](/img/PyPanel/01.png) 47 | 48 | > Not very interesting, but it's something. 49 | 50 | What we want to do now is start using the external file we created (LVButton.py) and use that rather than the code in the Houdini Editor. 51 | 52 | For that, in the Python Panel Editor, we are going to change the code to this: 53 | 54 | ```python 55 | from imp import reload 56 | from LVButton import LVButton 57 | reload(LVButton) 58 | 59 | def OnCreateInterface(): 60 | return LVButton.LVButton() 61 | ``` 62 | 63 | > Quick note on using reload from imp, this is just a habit to make sure that your external file gets reloaded while you edit it. Once you're done, its safe to remove the reload. 64 | 65 | Now we can close the Python Panel Editor window. That's it for now, now we can just use our IDE and refresh the script in Houdini. 66 | 67 | Let's get to breaking down the code now. The very first line is `from hutil.Qt import QtWidgets`. QtWidgets is the UI library that is used for python panels. You can also import them from PySide2. That's what I use, as they are cross compatible. There are some fancy Houdini only ones that are in the hutil one, so use your discretion. 68 | 69 | Now, because we importing the code, we need to adjust the way it gets built. The most important thing is to create our UI as a class. 70 | 71 | ```python 72 | from PySide2 import QtWidgets 73 | 74 | class LVButton(QtWidgets.QWidget): 75 | def __init__(self): 76 | super().__init__() 77 | widget = QtWidgets.QLabel("Hello World!") 78 | ``` 79 | 80 | Cool, let's save this and refresh the widget 81 | Oh, a bunch of nothing. Why is that? Well, you can see we created the class and in brackets we set it to `QtWidgets.QWidget`. So what we have is essentially a QWidget that we are working with. We need to do this as the panel needs to have a single widget as the root of it. 82 | 83 | Another quick side note. If you have ever worked with HTML, you may know how the nesting of elements works. This is no different, with the annoying exception that the flow in most cases is a widget, that has a child Layout, that can have one or more child widgets that can have a child layout, etc. 84 | 85 | Right now, the reason we dont have anything showing is that we are just showing the native QWidget, and haven't added the QLabel we created yet. 86 | 87 | We can now create a layout, and add the widget to that layout, and then set the layout out of hero widget to that layout. The order is important, just like nesting dolls. You should add the lowest level to the parent and then go up a level. 88 | 89 | ```python 90 | from PySide2 import QtWidgets 91 | 92 | class LVButton(QtWidgets.QtWidget): 93 | def __init__(self): 94 | super().__init__() 95 | widget = QtWidgets.QLabel("Hello World!") 96 | root_layout = QtWidgets.QVBoxLayout() 97 | root_layout.addWidget(widget) 98 | self.setLayout(root_layout) 99 | ``` 100 | 101 | Now we can refresh, and we can see our label! 102 | 103 | This is called LVButton, so we have to add a button somewhere here. With our current layout, we have a simple vertical layout, called `QVBoxLayout`, but it is good to refer to [the docs](https://doc.qt.io/qtforpython-5/modules.html) to see which layout may be best for your case. 104 | 105 | Back to our button, let's change the QLabel to a QPushButton 106 | 107 | ```python 108 | from PySide2 import QtWidgets 109 | 110 | class LVButton(QtWidgets.QtWidget): 111 | def __init__(self): 112 | super().__init__() 113 | widget = QtWidgets.QButton("Hello World!") 114 | root_layout = QtWidgets.QVBoxLayout() 115 | root_layout.addWidget(widget) 116 | self.setLayout(root_layout) 117 | ``` 118 | 119 | Button in, let's work on the clicking. QPushButton inherits from QAbstractButton, and if we check the docs for that, we can see it emits a signal when clicked. With QtWidgets, we have to connect that signal to a function. 120 | 121 | ```python 122 | from PySide2 import QtWidgets 123 | 124 | class LVButton(QtWidgets.QtWidget): 125 | def __init__(self): 126 | super().__init__() 127 | widget = QtWidgets.QButton("Hello World!") 128 | widget.clicked.connect(self.message_log) 129 | root_layout = QtWidgets.QVBoxLayout() 130 | root_layout.addWidget(widget) 131 | self.setLayout(root_layout) 132 | 133 | def message_log(self): 134 | print("You clicked the button") 135 | ``` 136 | 137 | You may have noticed we needed to call our function with `self.message_log` rather than just `message_log`. This is because we are inside the class still, and therefore we need to specificy that we are calling it's own function rather that a random function that may be floating around in our code. 138 | 139 | Inside the `message_log` function, we can basically do anything we want there now, standard Houdini stuff. You have essentially created a button that you can dock and run anything, create nodes, or drive any other tools you have created. 140 | 141 | During this, you may have noticed that the addWidget, setLayout dance is really annoying, so hop over to [Layout Building](#layout-building) to see how it can get easier! 142 | -------------------------------------------------------------------------------- /Python-Panels/02_Layout Building.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Layout Building" 3 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 4 | --- 5 | 6 | # Layout Building 7 | 8 | Remember that myseterious `mypanel.ui` file I spoke about above? Let's demystify that. 9 | 10 | The first thing you want to do is [download Qt Designer](https://build-system.fman.io/qt-designer-download) 11 | 12 | Once you open it up, the "New Form" window should pop up, and I always just click the "Widget" one. Save this file to your `$PKG/scripts/python/PANEL_NAME` folder. 13 | 14 | You should be staring at a blank widget. The next step is to set the layout of the root widget. You would think you can drag a layout from the left side, but this software is so strange, that you do it a bit backwards. Drag a button from the left and then right click in the background of the widget and Set Layout to "Lay Out Vertically" 15 | 16 | ![](/img/LayoutBuilding/2.mp4) 17 | 18 | Save again and swap back to your code editor. Let's dive back into the example above, and make some changes at the start. 19 | 20 | ```python 21 | import hou 22 | from PySide2 import QtWidgets, QtUiTools 23 | 24 | class LVButton(QtWidgets.QtWidget): 25 | def __init__(self): 26 | super().__init__() 27 | self.folderpath = hou.getenv("PKG") + "/scripts/python/PANEL_NAME" 28 | ui_file_path = self.folderpath + "/LVButton.ui" 29 | 30 | loader = QtUiTools.QUiLoad() 31 | self.ui = loader.load(ui_file_path) 32 | 33 | widget = QtWidgets.QButton("Hello World!") 34 | widget.clicked.connect(self.message_log) 35 | root_layout = QtWidgets.QVBoxLayout() 36 | root_layout.addWidget(widget) 37 | self.setLayout(root_layout) 38 | 39 | def message_log(self): 40 | print("You clicked the button") 41 | ``` 42 | 43 | > Note: PKG is swapped out to whatever your package name is, specifically without the $ in this case. PANEL_NAME is also swapped out to whatever your panel name is. 44 | 45 | We create a variable that stores the where to find the .ui file and we use the QtUiTools.QUiLoader to load the ui file. 46 | 47 | Now that we have this ui as a variable, it isn't actually in our panel yet. We need to find an actual widget in the ui, which is our root widget. I like to go back to Qt Designer and rename the top level element to "root". 48 | 49 | Then in our code, we can add the imported UI to the main layout we already created. 50 | 51 | ```python 52 | import hou 53 | from PySide2 import QtWidgets, QtUiTools 54 | 55 | class LVButton(QtWidgets.QtWidget): 56 | def __init__(self): 57 | super().__init__() 58 | self.folderpath = hou.getenv("PKG") + "/scripts/python/PANEL_NAME" 59 | ui_file_path = self.folderpath + "/LVButton.ui" 60 | 61 | loader = QtUiTools.QUiLoad() 62 | self.ui = loader.load(ui_file_path) 63 | 64 | root_layout = QtWidgets.QVBoxLayout() 65 | root_layout.addWidget(self.ui) 66 | # widget.clicked.connect(self.message_log) 67 | 68 | self.setLayout(root_layout) 69 | 70 | def message_log(self): 71 | print("You clicked the button") 72 | ``` 73 | 74 | Now that we have our imported layout working, it becomes a lot easier to build out a good layout in Qt Designer and go back and forth until it suits your needs. 75 | 76 | Above, the `widget.clicked` line was commented out as we ditched that widget. We can connect our new button to this again, but we need to get a reference to the button. For that, we can use the `findChild()` function, that takes 2 arguments, the type of widget and the name. This is why its crucial to name all widgets in Qt Designer (or at least the important ones). Go back and rename the QPushButton to "btn". 77 | 78 | ```python 79 | import hou 80 | from PySide2 import QtWidgets, QtUiTools 81 | 82 | class LVButton(QtWidgets.QtWidget): 83 | def __init__(self): 84 | super().__init__() 85 | self.folderpath = hou.getenv("PKG") + "/scripts/python/PANEL_NAME" 86 | ui_file_path = self.folderpath + "/LVButton.ui" 87 | 88 | loader = QtUiTools.QUiLoad() 89 | self.ui = loader.load(ui_file_path) 90 | 91 | root_layout = QtWidgets.QVBoxLayout() 92 | root_layout.addWidget(self.ui) 93 | 94 | self.btn = self.ui.findChild(QPushButton, "btn") 95 | self.btn.clicked.connect(self.message_log) 96 | 97 | self.setLayout(root_layout) 98 | 99 | def message_log(self): 100 | print("You clicked the button") 101 | ``` 102 | 103 | Your code editor may have picked up a squiggly line under QPushButton. This is because we called it directly rather than using `QtWidgets.QPushButton`. This is fixable by adding QPushButton to our import in the top. 104 | 105 | ```python 106 | import hou 107 | from PySide2 import QtWidgets, QtUiTools 108 | from PySide2.QtWidgets import QPushButton 109 | 110 | ... 111 | ``` 112 | 113 | So now we can connect the whole thing together, and we have restored our original functionality with a far easier layout building method. 114 | 115 | ```python 116 | import hou 117 | from PySide2 import QtWidgets, QtUiTools 118 | 119 | class LVButton(QtWidgets.QtWidget): 120 | def __init__(self): 121 | super().__init__() 122 | self.folderpath = hou.getenv("PKG") + "/scripts/python/PANEL_NAME" 123 | ui_file_path = self.folderpath + "/LVButton.ui" 124 | 125 | loader = QtUiTools.QUiLoad() 126 | self.ui = loader.load(ui_file_path) 127 | 128 | root_layout = QtWidgets.QVBoxLayout() 129 | root_layout.addWidget(self.ui) 130 | 131 | self.btn = self.ui.findChild(QPushButton, "btn") 132 | self.btn.clicked.connect(self.message_log) 133 | 134 | self.setLayout(root_layout) 135 | 136 | def message_log(self): 137 | print("You clicked the button") 138 | ``` 139 | 140 | While it's still a bit of a mystery to me where `self` is needed, I normally opt for creating variables for anything that is an interface item with self. 141 | 142 | ## Happy Building! 🔨 143 | 144 | -------------------------------------------------------------------------------- /Python-Sop/01_Performance.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Performance" 3 | author: [{ name: "WaffleBoyTom", link: "https://github.com/WaffleBoyTom" }] 4 | --- 5 | 6 | # Performance 7 | 8 | So you've been dabbling with Python and now you're wondering if maybe you should use the Python SOP to do geometry operations. Well, wonder no longer, you shouldn't. 9 | 10 | VEX is absolutely perfect for manipulating geometry; that's what it was built for! Python is great for a ton of things but this isn't one of them. So unless you're calling an API or doing something really specific, prefer VEX over Python for geometry manipulation (unless you hate being efficient). 11 | 12 | Now that you've been warned, let's wrangle some points with Python. 13 | 14 | If you try to modify SOP geometry from outside of a Python SOP, Houdini will raise a `hou.GeometryPermissionError()` so we'll use a Python SOP to read and write to our geometry object. 15 | 16 | We'll recreate what I made in VEX here: pushing points along their normal, randomize the amplitude and animate them back and forth using `cos()`. 17 | 18 | ![](/img/Performance/1.png) 19 | 20 | If you drop down a Python SOP and look at the code presets, you'll find something that does almost what we want. ![](/img/Performance/2.png) 21 | We'll use that as a launching pad for our effect. 22 | 23 | Let's first import the math module so we can use `math.cos()`. We're also going to import the random module because we want to randomize our amplitude. 24 | 25 | Now we're going to create a few channels that we can access from within our code. Unfortunately, the python sop doesn't have a handy button like the wrangle so we're going to have to create them ourselves. 26 | 27 | For the time being, I'm only going to create a frequency channel, we can add more as we progress. 28 | 29 | We can easily read that channel using `node.parm('freq').eval()`. 30 | 31 | Let's now push our points along their normal, then we'll look into randomizing and animating. 32 | 33 | Inside the for-each loop, I can read the normal attribute using `point.attribValue('N')`. That returns a tuple. Our `point.position()` is a `hou.Vector3()` object so we'll have to convert our tuple to that class to add them: `pos += hou.Vector3(dir)`. 34 | 35 | Let's look at our code so far: 36 | 37 | ```python 38 | import math 39 | import random 40 | 41 | node = hou.pwd() 42 | geo = node.geometry() 43 | 44 | frequency = node.parm('freq').eval() 45 | 46 | for point in geo.points(): 47 | 48 | pos = point.position() 49 | 50 | dir = point.attribValue('N') 51 | 52 | pos += hou.Vector3(dir) 53 | 54 | point.setPosition(pos) 55 | 56 | ``` 57 | 58 | Let's now add some random amplitude to make it a bit more interesting. 59 | 60 | To get a random seed per point we could use `point.number()` but we're just going to use the loop's iterator. To achieve that, let's rewrite our loop to use enumerate: `for index,point in enumerate(geo.points()):`. 61 | 62 | Next step is to create a Random() object, then set its seed to our iterator -- to which we can add some random value that we could potentially read from a channel but I'm just going to hard-code one -- . Then we can use `random.uniform(start,end)` to generate a random number. 63 | 64 | The snippet now looks something like this : 65 | 66 | ```python 67 | import math 68 | import random 69 | 70 | node = hou.pwd() 71 | geo = node.geometry() 72 | 73 | frequency = node.parm('freq').eval() 74 | 75 | for index,point in enumerate(geo.points()): 76 | 77 | rng = random.Random() 78 | 79 | rng.seed(index+689) 80 | 81 | randamp = rng.uniform(1,2) 82 | 83 | pos = point.position() 84 | 85 | dir = point.attribValue('N') 86 | 87 | pos += (hou.Vector3(dir) * randamp ) 88 | 89 | point.setPosition(pos) 90 | ``` 91 | 92 | Our result now looks like this: ![](/img/Performance/3.png) 93 | 94 | We want to animate our `cos()` so let's access the current frame with `hou.frame()`. 95 | 96 | We'll use that, multiplied with frequency, as an argument for `math.cos()`. 97 | 98 | We now want to offset each point, let's use the iterator and the Random() object again 99 | 100 | Here's what the final snippet looks like: 101 | 102 | ```python 103 | import math 104 | import random 105 | 106 | node = hou.pwd() 107 | geo = node.geometry() 108 | 109 | frequency = node.parm('freq').eval() 110 | 111 | frame = hou.frame() 112 | 113 | for index,point in enumerate(geo.points()): 114 | 115 | rng = random.Random() 116 | 117 | rng.seed(index+689) 118 | 119 | randamp = rng.uniform(1,2) 120 | 121 | rng.seed(index+235) 122 | 123 | randoffset = rng.uniform(1,5) 124 | 125 | offset = randoffset * 35 126 | 127 | cos = math.cos((frame+offset) * frequency) * 0.1 128 | 129 | pos = point.position() 130 | 131 | dir = point.attribValue('N') 132 | 133 | pos += (hou.Vector3(dir) * randamp * cos ) 134 | 135 | point.setPosition(pos) 136 | ``` 137 | 138 | Feel free to replace all the literals with channels so you can play around with sliders. 139 | 140 | Let's now do some performance monitoring! 141 | 142 | With 50K primitives, 25 002 points , the wrangle runs at _>120 fps_. The Python sop? _1.5_... 143 | 144 | Calling `point.number()` inside the loop instead of using `enumerate()` seems to slow it down even more but it's barely noticeable, it's excruciatingly slow regardless... 145 | 146 | With 2K prims and 1002 points, Python reaches around _34 fps_. 147 | 148 | Now you know this isn't something you should do with Python! 149 | 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HouPy Wiki Pages 2 | 3 | The content of this repo is all of the page content that is propogated onto [HouPy Wiki](https://houpywiki.com). 4 | 5 | [Join the discord](https://discord.gg/5j9s6hVADs). 6 | 7 | -------------------------------------------------------------------------------- /ROPs/01_Render Scripts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Render Scripts 3 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 4 | --- 5 | 6 | # Render Scripts 7 | 8 | Almost all ROPs will have a section for scripts: 9 | 10 | - Pre-Render: Before the whole sequence. 11 | - Pre-Frame: Before each frame. 12 | - Post-Frame: After each frame. 13 | - Post-Render: After the whole sequence. 14 | 15 | These are pretty self explanatory. In the box you can type some Hscript (visit www.houhscriptwiki.com... jk) or some Python 😎. Make sure to change the right hand dropdown to Python for the fun stuff. 16 | 17 | These boxes can run their own scripts written straight into them, or you can alt+E and get the big editor box and write some multiline stuff. 18 | 19 | In the post render box, add: 20 | 21 | ```python 22 | import LVRender 23 | from imp import reload 24 | reload(LVRender) 25 | 26 | LVRender.post_render() 27 | ``` 28 | 29 | Standard reloading stuff here, then we call a function in the file we loaded. 30 | 31 | Now that we have conquered that, we we can get into the next bit below. 32 | -------------------------------------------------------------------------------- /ROPs/02_Auto Generate MP4 Preview.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Auto Generate MP4 Preview 3 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 4 | --- 5 | 6 | # Auto Generate MP4 Preview 7 | 8 | In almost every shot, you will be comping your work in some way, but sometimes it can save time and have a video that you can use for quick preview or dropping into Slack, etc. 9 | 10 | Let's create a new script eg. `LVRender.py` and inside it, we can add a function. 11 | 12 | ```python 13 | import hou 14 | 15 | def pos_render(node=hou.pwd()): 16 | print(node) 17 | ``` 18 | 19 | We pass in an optional argument of `hou.pwd()`. This allows up to run the script elsewhere, not just from the post render. If we run it, we get our ROP the script was called from. 20 | 21 | Now we can get a few values that we will need. 22 | 23 | ```python 24 | import hou 25 | 26 | def post_render(node=hou.pwd()): 27 | start = node.parm("f1").eval() # type: ignore 28 | parm = node.parm("RS_outputFileNamePrefix") 29 | wide = parm.unexpandedString() # type: ignore 30 | wide = wide.replace("$OS", node.name()) 31 | 32 | if "$F4" in wide: 33 | wide = wide.replace("$F4", "%04d") 34 | elif "$F3" in wide: 35 | wide = wide.replace("$F3", "%03d") 36 | elif "$F2" in wide: 37 | wide = wide.replace("$F2", "%02d") 38 | 39 | full = hou.expandString(wide) 40 | print("full: ", full) 41 | 42 | video = "".join(full.split(".")[:-1]) + ".mp4" 43 | 44 | video = video.replace("%04d", "") 45 | video = video.replace("%03d", "") 46 | video = video.replace("%02d", "") 47 | ``` 48 | 49 | A bunch of string manipulations here to get our unexpanded output path, and replace the `$F4` with the `%04d` that ffmpeg uses to glob all our output images. 50 | 51 | At the end we replace that with nothing to get a clean no digit version of the path for our video 52 | 53 | Now we need to run ffmpeg and create our video, for this I chose `subprocess` as its pretty simple to construct a string and pass it in. 54 | 55 | To build our string: 56 | 57 | ```python 58 | command = f'ffmpeg -framerate 25 -y -start_number {start} -i "{full}" -c:v libx264 -crf 23 -pix_fmt yuv420p "{video}"' 59 | 60 | ``` 61 | 62 | Then we can finally call the full command with `subprocess.Popen()`: 63 | 64 | ```python 65 | def post_render(node=hou.pwd()): 66 | start = node.parm("f1").eval() # type: ignore 67 | parm = node.parm("RS_outputFileNamePrefix") 68 | wide = parm.unexpandedString() # type: ignore 69 | wide = wide.replace("$OS", node.name()) 70 | 71 | if "$F4" in wide: 72 | wide = wide.replace("$F4", "%04d") 73 | elif "$F3" in wide: 74 | wide = wide.replace("$F3", "%03d") 75 | elif "$F2" in wide: 76 | wide = wide.replace("$F2", "%02d") 77 | 78 | full = hou.expandString(wide) 79 | print("full: ", full) 80 | 81 | video = "".join(full.split(".")[:-1]) + ".mp4" 82 | 83 | video = video.replace("%04d", "") 84 | video = video.replace("%03d", "") 85 | video = video.replace("%02d", "") 86 | 87 | command = f'ffmpeg -framerate 25 -y -start_number {start} -i "{full}" -c:v libx264 -crf 23 -pix_fmt yuv420p "{video}"' 88 | 89 | subprocess.Popen(command) 90 | 91 | hou.ui.showInFileBrowser(video) 92 | ``` 93 | 94 | Cherry on top is that it opens and explorer window with our file selected. 95 | 96 | If you were gonna be really snazzy, you could break this into 2 functions, one that renders the video, and a preprocess that you can use all over Houdini to create videos. 97 | -------------------------------------------------------------------------------- /SOPs/01_Bake Object Level Transforms.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Bake Object Level Transforms" 3 | author: [{ name: "WaffleBoyTom", link: "https://github.com/WaffleBoyTom" }] 4 | --- 5 | 6 | # Bake Object Level Transforms 7 | 8 | This might seem a bit niche but I discovered this while helping someone with their HDA and thought it might be interesting to look at. 9 | 10 | Say you have this ![sm](/img/BakeObjLevelTransforms/1.png) 11 | How would you bake that transform so you just have a camera with a bunch of keyframes instead ? ![](/img/BakeObjLevelTransforms/2.png) 12 | 13 | My scene, as shown earlier, is simple : a camera ('/obj/cam1') and a null ('/obj/null1'). 14 | 15 | The rotation along the y axis of the null is animated with an expression so the camera orbits around the null, very basic stuff. 16 | 17 | The final code is fairly awkward to try and run in a shell, so I prototyped in the source editor instead. ![](/img/BakeObjLevelTransforms/3.png) 18 | 19 | First step, grab the original camera, copy it, then offset it along the x axis in the network so it isn't sitting right on top of our original camera, then disconnect it from the null. 20 | I've hardcoded the original camera here but depending on what you want to do you might want to use hou.selectedNodes() or something else. 21 | 22 | ```python 23 | import hou 24 | 25 | origcam = hou.node('/obj/cam1') 26 | 27 | cam2 = hou.copyNodesTo((origcam,),origcam.parent())[0] 28 | cam2.setPosition(cam2.position() + hou.Vector2((2.0,0.0))) 29 | cam2.setInput(0,None) 30 | ``` 31 | 32 | In Houdini, object nodes have local and world transforms that you can access with Python, [refer to this docs page for more information.](https://www.sidefx.com/docs/houdini/hom/hou/ObjNode.html) 33 | 34 | If I get my original camera's world transform and set that to my new camera , they will be at the same position, using objNode.worldTransform() and objNode.setWorldTransform(). 35 | The limitation here is that you can access a worldTransform matrix at a certain point in time(objNode.worldTransformAtTime()) but can't set it at a certain point in time (~~objNode.setWorldTransformAtTime()~~). Bummer, right ? It does make sense though, because otherwise you would have something animated but no indication that it actually is. 36 | 37 | The obvious workaround is to set keyframes. 38 | Luckily, objNodes come with a setParmTransform() method that sets that world transform using the parameters, which we can set keyframes on. 39 | 40 | Even more luckily, SideFx shows us how the method is implemented, so we can tweak it to fit our needs. 41 | 42 | This is SideFx's code, which you can find at the linked posted above. 43 | 44 | ```python 45 | def setParmTransform(self, matrix): 46 | parm_values = matrix.explode( 47 | transform_order=self.parm('xOrd').evalAsString(), rotate_order=self.parm('rOrd').evalAsString(), pivot=hou.Vector3(self.evalParmTuple('p'))) 48 | for parm_name, key in ('t', 'translate'), ('r', 'rotate'), ('s', 'scale'): 49 | self.parmTuple(parm_name).set(parm_values[key]) 50 | ``` 51 | 52 | The main issue is that we can't call parmTuple.setKeyframe(). We have to call parm.setKeyframe(). We'll be adding a nested for loop to iterate through the tuple. 53 | 54 | ```python 55 | for index,parm in enumerate(node.parmTuple(parm_name)): 56 | val = parm_values[key] 57 | val = val[index] 58 | parm.setKeyframe(hou.Keyframe(val,time)) 59 | ``` 60 | 61 | This way we're iterating through every parameter of the tuple and setting a keyframe at the given time. 62 | 63 | One thing to know is that methods that ask for a _time_ argument require a _time_ argument, which means it can't be frames. SideFx provides a hou.frameToTime() function and again the way it's implemented [which is gonna be useful for us.](https://www.sidefx.com/docs/houdini/hom/hou/frameToTime.html) 64 | 65 | What we'll do is iterate through the timeline by getting the start and end frame, convert that to a _time_ with the formula mentioned in the docs and then we can run our function. 66 | 67 | Let's convert setParmTransform() to be a function instead of a method, integrate our nested loop, then run the function in a loop that runs for every frame of the timeline. 68 | 69 | Here's our final snippet : 70 | 71 | ```python 72 | import hou 73 | 74 | origcam = hou.node('/obj/cam1') 75 | 76 | def customSetParmTransform(node, matrix,time=0): 77 | 78 | parm_values = matrix.explode( 79 | transform_order=node.parm('xOrd').evalAsString(), 80 | rotate_order=node.parm('rOrd').evalAsString(), 81 | pivot=hou.Vector3(node.evalParmTuple('p'))) 82 | 83 | for parm_name, key in ('t', 'translate'), ('r', 'rotate'), ('s', 'scale'): 84 | 85 | for index,parm in enumerate(node.parmTuple(parm_name)): 86 | 87 | val = parm_values[key] 88 | val = val[index] 89 | parm.setKeyframe(hou.Keyframe(val,time)) 90 | 91 | 92 | cam2 = hou.copyNodesTo((origcam,),origcam.parent())[0] 93 | cam2.setPosition(cam2.position() + hou.Vector2((2.0,0.0))) 94 | cam2.setInput(0,None) 95 | 96 | frange = hou.playbar.frameRange() 97 | start = int(frange[0]) 98 | end = int(frange[1]) 99 | 100 | 101 | for i in range(start,end+1): 102 | 103 | ftt = (float(i)-1.0) / hou.fps() 104 | 105 | customSetParmTransform(cam2,origcam.worldTransformAtTime(ftt),ftt) 106 | ``` 107 | 108 | This runs a bit slowly admittedly. SideFx recommends using parm.setKeyframes((keyframeTuple)) for faster performance but I couldn't think of a clever way to integrate this, so it's brute force for now. Let us know if you find a better way. 109 | 110 | Right, so I got frustrated and decided to rewrite it using parm.setKeyframes(). 111 | After some wrangling, this is what I ended up with 112 | 113 | ```python 114 | import hou 115 | 116 | origcam = hou.node('/obj/cam1') 117 | 118 | def customSetParmTransform(node, matrix,time,it,kflist): 119 | 120 | parm_values = matrix.explode( 121 | transform_order=node.parm('xOrd').evalAsString(), 122 | rotate_order=node.parm('rOrd').evalAsString(), 123 | pivot=hou.Vector3(node.evalParmTuple('p'))) 124 | 125 | 126 | for parm_name, key in ('t', 'translate'), ('r', 'rotate'), ('s', 'scale'): 127 | 128 | letters = ('x','y','z') 129 | 130 | for index,parm in enumerate(node.parmTuple(parm_name)): 131 | 132 | val = parm_values[key] 133 | val = val[index] 134 | keyf = hou.Keyframe(val,time) 135 | 136 | klist = keydict[parm_name+letters[index]] 137 | klist[it] = keyf 138 | 139 | cam2 = hou.copyNodesTo((origcam,),origcam.parent())[0] 140 | cam2.setPosition(cam2.position() + hou.Vector2((2.0,0.0))) 141 | cam2.setInput(0,None) 142 | 143 | parms = ('tx','ty','tz','rx','ry','rz','sx','sy','sz') 144 | 145 | frange = hou.playbar.frameRange() 146 | start = int(frange[0]) 147 | end = int(frange[1]) 148 | 149 | total = end-(start-1) 150 | 151 | keydict = dict() 152 | 153 | for parm in parms: 154 | 155 | keydict[parm] = [0]*total 156 | 157 | 158 | nindex = 0 159 | 160 | for i in range(start,end+1): 161 | 162 | ftt = (float(i)-1.0) / hou.fps() 163 | 164 | customSetParmTransform(cam2,origcam.worldTransformAtTime(ftt),ftt,nindex,keydict) 165 | 166 | nindex+=1 167 | 168 | 169 | 170 | for index, parm in enumerate(parms): 171 | 172 | kftuple = tuple(keydict[parm]) 173 | 174 | cam2.parm(parm).setKeyframes(kftuple) 175 | ``` 176 | 177 | This runs so much quicker. 178 | 179 | -------------------------------------------------------------------------------- /SOPs/02_Verbs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Verbs 3 | author: [{ name: "WaffleBoyTom", link: "https://www.github.com/WaffleBoyTom" }] 4 | --- 5 | 6 | # Verbs 7 | 8 | If you're not familiar with verbs, make sure you watch this Jeff Lait Masterclass: 9 | https://vimeo.com/222881605 10 | 11 | You can skip to around 50:00 for the verb part, but I recommend you watch the whole thing, otherwise verbs won't really make sense. If you're already familiar with compiling/compiled blocks then feel free to skip to the verbs part. 12 | 13 | Here's a couple example of using verbs: 14 | 15 | ```python 16 | def runVerb(geo,name,input=[], parms=None): 17 | 18 | verb = hou.sopNodeTypeCategory().nodeVerb(name) 19 | if parms: 20 | verb.setParms(parms) 21 | 22 | verb.execute(geo,input) 23 | 24 | node = hou.pwd() 25 | geo = hou.Geometry() 26 | 27 | runVerb(geo,"box") 28 | 29 | runVerb(geo,"vdbfrompolygons",[geo]) 30 | 31 | runVerb(geo,"vdbsmoothsdf",[geo]) 32 | 33 | parms = {"conversion" : 2} 34 | 35 | runVerb(geo,"convertvdb",[geo],parms) 36 | 37 | node.geometry().clear() 38 | node.geometry().merge(geo) 39 | ``` 40 | 41 | runVerb() is a wrapper to help execute a verb. When executing a verb you need what you would need if you were using a node instead: 42 | 43 | - a name : "box", "vdbfrompolygons",... 44 | - a geo to execute the verb on. If you're generating a box then you can start from an empty hou.Geometry() object but if you want to run a smooth verb on a box for example, you'll need that box's geometry object. 45 | - an input / multiple inputs : if you try to execute a ray verb or a boolean verb you'll need multiple inputs. The input argument is a list of hou.Geometry() objects. 46 | - parameters : Parms is a dictionnary parameter. If left blank, values will be set to default. Any parameter included in the dictionary will override the defaults. 47 | 48 | Remember that SideFx provides a couple examples here ![](/img/Verbs/1.png) 49 | 50 | Here's another snippet that generates boxes on boxes on boxes: 51 | 52 | ```python 53 | def box(geo,height): 54 | 55 | verb = hou.sopNodeTypeCategory().nodeVerb('box') 56 | parms = dict() 57 | parms["t"] = (0.0,height,0.0) 58 | verb.setParms(parms) 59 | verb.execute(geo,[]) 60 | 61 | 62 | node = hou.pwd() 63 | 64 | startheight = 0.5 65 | 66 | geo = hou.Geometry() 67 | 68 | numboxes = 15 # node.parm('numboxes').evalAsInt() 69 | 70 | for i in range(numboxes): 71 | 72 | box(geo,startheight+i) 73 | node.geometry().merge(geo) 74 | ``` 75 | 76 | ![](/img/Verbs/2.mp4) 77 | 78 | -------------------------------------------------------------------------------- /Shelf-Scripts/01_Shelf Script Basics.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Shelf Script Basics 3 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 4 | --- 5 | 6 | # Shelf Script Basics 7 | 8 | From a very simple standpoint, a shelf script is a single use script that can be clicked on and bound to a hotkey. 9 | You may be familiar with "shelf tools", which, under the hood, are a python script that places a bunch of nodes and sets their parameters to desired values. 10 | 11 | To start off, lets make a script that will print to the console. 12 | 13 | Create a new shelf tab, and then right click in the tab to create a "New tool." 14 | 15 | Inside, we type: 16 | 17 | ```python 18 | print('we created a new script') 19 | ``` 20 | 21 | From this, we get our message in the console. Very exciting! 22 | 23 | -------------------------------------------------------------------------------- /Shelf-Scripts/02_The Hou Module.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: The Hou Module 3 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 4 | --- 5 | 6 | # The Hou Module 7 | 8 | In the previous example, we used `print()`. This is one of the 'native' python functions. 9 | In most cases, you will be doing some python work where you will be using functions from the source program. In Houdini, all of the functions you could used are available in the `hou` module, which is available already, no need to import it. 10 | 11 | If we create a new shelf script and type: 12 | 13 | ```python 14 | node = hou.selectedNodes() 15 | print(node) 16 | ``` 17 | 18 | If we select a node and run that script, the console pops up and says `(,)`. 19 | 20 | Woo hoo. Now is a good time to open up the [Houdini docs](https://www.sidefx.com/docs/houdini/hom/hou/index.html) for the `hou` module. (Get used to looking at this alot). 21 | 22 | Now let's make something useful. A shelf script to open the project directory: 23 | 24 | ```python 25 | hou.ui.showInFileBrowser(hou.hipFile.path()) 26 | ``` 27 | 28 | This script runs a _function_ called `showInFileBrowser`. It's a part of the `hou.ui` module. This module, according to docs is [Module containing user interface related functions](https://www.sidefx.com/docs/houdini/hom/hou/ui.html). 29 | This function takes an argument `(file_path)` which we need to find out dynamically. That can be done with `hou.hipFile` ie. the file we are in, and then we use `.path()` to get the... path. 30 | Note that the last part has an empty bracket pair. This is because it is a function, but it take no arguments. 31 | 32 | So combined, the script gets the path of the project file, and shows it to us in the explorer. 33 | 34 | -------------------------------------------------------------------------------- /Shelf-Scripts/03_Using Kwargs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using Kwargs 3 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 4 | --- 5 | 6 | # Using kwargs 7 | 8 | In a shelf script, it is also possible to access some extra information related to the running of the command. The docs say: 9 | "When Houdini calls your script, it adds a dictionary variable named kwargs to the script’s context." You can read more [here](https://www.sidefx.com/docs/houdini/hom/tool_script.html#:~:text=Arguments-,When,-Houdini%20calls%20your). 10 | 11 | The link above has the listed variables you can access, but for this example, I'm going to be checking if the shift key is pressed. The key name is `shiftclick` and the type is `bool`. 12 | 13 | So, new shelf script, and we type: 14 | 15 | ```python 16 | if kwargs['shiftclick']: 17 | print('Shifted') 18 | else: 19 | print('unshifted') 20 | ``` 21 | 22 | The name and type are fairly self explanatory, and you can do everything you could do with any other variable. 23 | -------------------------------------------------------------------------------- /Shelf-Scripts/04_Wedge from Parameter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Wedge from Parameter 3 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 4 | --- 5 | 6 | # Wedge from parameter 7 | 8 | Someone asked about a way to copy a parameter, and have a hotkey to create a wedge node for that parm. So here is that. 9 | 10 | We start off with the usual jump through hoops to get the pane under cursor, as we want to be able to press the hotkey and place a node. 11 | 12 | ```python 13 | d = hou.ui.curDesktop() // our current desktop 14 | ctx = d.paneTabUnderCursor() // pane under cursor 15 | 16 | con = ctx.pwd() 17 | t = con.type().name() // get the name of the type of our current pane tab. ie 'network editor' 18 | ``` 19 | 20 | ```python 21 | d = hou.ui.curDesktop() 22 | ctx = d.paneTabUnderCursor() 23 | 24 | p = hou.parmClipboardContents()[0]['path'] 25 | 26 | con = ctx.pwd() 27 | t = con.type().name() 28 | 29 | if t == 'topnet' or t == 'topnetmgr': 30 | 31 | try: 32 | sel = hou.selectedNodes()[-1] 33 | if sel.type().name() == 'wedge': 34 | w = sel 35 | count = w.parm('wedgeattributes').eval() 36 | w.parm('wedgeattributes').set(count+1) 37 | w.parm(f'exportchannel{count+1}').set(1) 38 | w.parm(f'channel{count+1}').set(p) 39 | w.parm(f'name{count+1}').set(p.split("/")[-1]) 40 | w.parm('previewselection').set(1) 41 | 42 | except: 43 | w = con.createNode('wedge') 44 | w.setPosition(ctx.cursorPosition()) 45 | w.parm('wedgeattributes').set(1) 46 | w.parm('exportchannel1').set(1) 47 | w.parm('channel1').set(p) 48 | w.parm('name1').set(p.split("/")[-1]) 49 | w.parm('previewselection').set(1) 50 | ``` 51 | 52 | -------------------------------------------------------------------------------- /Shelf-Scripts/05_Creating Nodes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Creating Nodes 3 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 4 | --- 5 | 6 | # Creating Nodes 7 | 8 | One of the simplest quality of life boosts is speeding up your common tasks with automation. 9 | 10 | Let's create a script we can add to a radial menu that will let us create a new empty 'geo' node and name it in one go. We will also add this to the radial menu that you can use in the network editor. 11 | 12 | > At this point, the ideal way to be working with all these python setups is to create a personal package and have it linked into your Houdini like other packages. If you're confused by this, you can read more here. This tutorial uses those methods, so it's good to be familiar. 13 | 14 | Open up your IDE of choice, and create a new file in your `package/scripts/python` folder. In my case, I will bundle a bunch of scripts together, so one single Utils file will work. I called mine `LVUtils.py`. 15 | 16 | The first thing I do is add `import hou` at the top of the file. This ensure that the code completion registers our functions. 17 | Back to the scripting. Because we are going to use a radial menu, we have access to a bunch of glorius kwargs that tell us info about where we called it from. 18 | 19 | ```python 20 | import hou 21 | 22 | def create_named_node(): 23 | geo_name = hou.ui.readInput("Container name:")[1] 24 | ``` 25 | 26 | Super basic here: we use the [hou.ui](https://www.sidefx.com/docs/houdini/hom/hou/ui.html) function from the 'Scripted UI' section called `readInput()` 27 | 28 | This stores the user input as the variable `geo_name`. 29 | 30 | > Note the `[1]` we use here, it returns a dict and we want the second index, for me the first was empty. 31 | 32 | ```python 33 | import hou 34 | 35 | def createNamedGeo(kwargs): 36 | geo_name = hou.ui.readInput("Container name:")[1] 37 | 38 | path = kwargs['pane'].pwd() 39 | ``` 40 | 41 | > We changed the function to take kwargs as an argument. 42 | 43 | Nowe we have a path variable that is reading `pane` from kwargs and using [pwd](https://www.sidefx.com/docs/houdini/hom/hou/pwd.html) which gives us the path to our current location we called the script from. If you `print` it you can see it changes as we move around nodes. 44 | 45 | Full script: 46 | 47 | ```python 48 | import hou 49 | 50 | def createNamedGeo(kwargs): 51 | geo_name = hou.ui.readInput("Container name:")[1] 52 | 53 | path = kwargs['pane'].pwd() 54 | 55 | new_geo = path.createNode("geo", geo_name) 56 | 57 | net_editor = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor) 58 | 59 | new_geo.setPosition(net_editor.cursorPosition()) 60 | ``` 61 | 62 | We use the `net_editor` bit to get our network editor and get the cursor positon to move the created node to it. 63 | 64 | Now we can go to the 'Radial Menus' menu and click 'Network Editor'. 65 | 66 | On the left we can add a new Menu and call it what we like, set the hotkey, and now add your scrript from the shelf tool you created. 67 | -------------------------------------------------------------------------------- /TOPs/00_Quick Notes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Fast Resize with Generic Generator" 3 | author: [ 4 | { name: "Luke Van", 5 | link: "https://github.com/lukevanlukevan"}, 6 | ] 7 | simple: 'true' 8 | --- 9 | 10 | There is some information overlap. So also check out [wedge from parameter](/Shelf-Scripts#wedge-from-parameter) for some shelf script powered TOP stuff. 11 | 12 | -------------------------------------------------------------------------------- /TOPs/01_Fast Resize with command line.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Fast Resize with Generic Generator" 3 | author: [ 4 | { name: "Luke Van", 5 | link: "https://github.com/lukevanlukevan"}, 6 | ] 7 | --- 8 | 9 | # Fast Resize with Generic Generator 10 | 11 | If you've ever tried to resize images with the dreaded ROP Composite TOP, you will know the pain of waiting 8 seconds for a single crop from 1:1 to 16:9. 12 | 13 | We can use ImageMagick commandline tools to resize the image far faster. 14 | 15 | We can start with a file pattern TOP that is reading all images from our directory, like this `$HIP/images/*.png`. 16 | 17 | One caveat is that the images don't get passed perfectly to the resize setup, so just enabe the `Output Attribute` parm on the file pattern TOP. 18 | 19 | From here, we can drop down an ImageMagick node, set the input source to custom file path and pass our output attriute from above in to this box. 20 | 21 | We set the "Operation" to "Convert" and then go down to the extra arguments section. 22 | 23 | Set the "Argument Name" to `resize` and set the "Argument Source" to Custom Value. Then we can set the "Arguement Value" to `1920x1080` or whatever values you need. I have this wrapped up in an HDA with a slider for size that lets me quickly chop down images for ImageMagick montages, etc. This is more of an excercise into using custom ImageMagick commands in the extra arguments section. Read more commands [here.](https://imagemagick.org/script/command-line-options.php) 24 | -------------------------------------------------------------------------------- /TOPs/02_Hbatch and Hython.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hbatch and Hython 3 | author: [{ name: "WaffleBoyTom", link: "https://www.github.com/WaffleBoyTom" }] 4 | --- 5 | 6 | # Hbatch and Hython 7 | 8 | Let's run TOPnets from the terminal using hbatch and hython 9 | 10 | I have a simple TOPnet that creates directories and subdirectories, kind of like what the `Set Project`option does in Houdini : `$HIP/project_{wedgeindex}/tex|flips|geo|render` 11 | 12 | `hbatch` is hscript, we'll use the python equivalent `hython` later. 13 | 14 | when running `hbatch $myhipfile` or `hython $myhipfile` , it will open a textport or a python shell where you can input commands interactively. From there you can use whatever hscript or python commands to cook your topnet. Let's be lazy and run everything with a single command. 15 | 16 | `hbatch` lets you use the `-c` flag to specify a `.cmd` file 17 | 18 | `test.cmd` 19 | 20 | ```c 21 | echo "Cooking Topnet" 22 | topcook /obj/mytopnet 23 | echo "Done" 24 | quit 25 | ``` 26 | 27 | Then you can run: 28 | 29 | `hbatch -v -c test.cmd $myfile`. 30 | 31 | `-v` is the verbose flag. 32 | 33 | "but this is a python wiki, I don't wanna hear about hscript". 34 | 35 | Let's take a look at `hython`: 36 | 37 | `$HHP/pdgjob/topcook.py` is a python utility script provided by SideFx that lets you run a topnet from the command line. 38 | 39 | ```python 40 | hython $HHP/pdgjob/topcook.py --hip $myfile --toppath /obj/create_directories_and_sub_directories. 41 | ``` 42 | 43 | `--hip` expects a hipfile. 44 | `--toppath` expects a topnet. 45 | 46 | -------------------------------------------------------------------------------- /WIP/01_Sticky Notes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sticky Notes 3 | author: [{ name: "Luke Van", link: "https://github.com/lukevanlukevan" }] 4 | --- 5 | 6 | # Sticky Notes 7 | 8 | A good sticky note is can really help organize a file, just look at [any of Rich Lord's project files](https://www.richlord.com/getmyfiles). There aren't great controls for changing things on the sticky note other than the basic stuff in the right click menu, but there are a good few python functions that can add a lot of value. 9 | 10 | To start of, let's simply get out sticky note and print out the text size: 11 | 12 | --------------------------------------------------------------------------------