To give you an idea of what I am talking about here, here is a clip of the handles in action on a prototype of the tool:
See video attachment 1
Some quick intro things:
Since I want the tool to be fun to use, I wanted to avoid having a ton of sliders and such, which is why I am using a custom viewer state with guide geometry as handles. I knew from the start that I would have a lot of handles, so early on I wanted to create a system that makes it easy for me to hook up new handles as I am making the tool - specifically so that I don't have to go back into the code every time, or spend a lot of time doing repetitive tasks.
Before getting into the workflow, though, I should explain what type of parameters I need to control with this system: transforms and float1 sliders. The float1 sliders are mostly used to determine the round-/sharpness of certain parts of the car, which is why I sometimes refer to these as 'bevel' parameters/handles. The most important thing, though, is that all of the parameters on the tool function on a 0-1 basis. This means that a transform handle doesn't actually contain a direct position, but instead says where the position is within the given bounds. Here's a quick example:
If the 'minimum values' are (1, 5, 0.2), and the 'maximum values' are (2, 25, 0.8), and the 'parameter values' are (0.5, 0.25, 1), the position of the handle will be (1.5, 10, 0.8).
I could have used the position directly, but because the handles are often influenced by other handles around it, I decided to use the relative position. Otherwise, it would mean that the handle would behave unnaturally by not moving with the structure around it:
See video attachment 2
The workflow
The system has two parts: the python viewer state that is attached to the car creator, and a separate HDA that I use inside the car creator to create the handles.
The handle HDA:
The handle HDA makes it easy for me to place new handles, since I only need to fill in the parameter name and what it needs to be able to do. As stated, the tool at hand only needs transform handles and a handle that can act as a float-1 slider. So, when I create a new handle, I can check which axes the handle needs to move on, and/or if the handle has a float-1 slider associated with it. Optionally, I can also control the color of the handle, the size, and if the handle belongs to a specific group of handles (which I will explain in a second).
The HDA interface
As an input, the HDA needs to receive a location to put the handle on through a detail attribute. For transform handles, it needs the minimum and the maximum values, after which the location will be based on the current parameter value. If the handle only has the float-1 slider, the detail parameter only needs a single position to place the handle.
With these inputs, the HDA determines the look of the handle:
It creates shapes for each axis that the handle is able to move on, and (if needed) a shape that represents the bevel handle. The transform handles are simple pointy shapes to show in which direction the handle can be moved; the bevel handle is a shape in the middle of the handle, which morphs depending on the parameter value: if the parameter in on 'round', the shape will be a sphere, and if it's on 'pointy', it will be a sharp cube. This is to give the user some feedback about the state of the parameter. Once this is done, the handle geometry is packed to avoid having too many primitives to run over later.
Some handle examples
Then, the HDA stores some information on the handle in the form of prim attributes:
- The handle min and max positions (in the case of a transform handle)
- The handle offset, which is a visual offset in case the handle would otherwise
- intersect with geometry, for example
- Whether the handle has a bevel parameter
- Which axes the handle is allowed to move on
- The handle group, which can be utilized by the user to selectively show groups of handles, instead of always showing all handles.
- The name of the parameter that the handle controls.
Prim attributes on the handle geometry
This process gives me the handle visually, as well as the information that the viewer state needs to function, all by just filling in a name and checking some boxes. To make the process even easier though, I also added some automation to the node placement:
- Upon placement, the HDA creates a null node that contains the handle, as well as an 'object merge' node that refers to it, which is then wired into the null node that contains all of the guide geometry handles.
- The nodes are automatically renamed based on the name of the parameter, which makes it easier to find the nodes that belong to it.
- If I delete the handle, the null and reference nodes are also deleted. For the null node this is based on the node connection, and for the reference node this is based on the node name. This makes it not quite suitable to duplicate the HDA, but since I haven't needed to do that much, I prefer the automatic cleanup that this provides.
Node setup for the handle tool
The viewer state:
In short, the viewer state does the following:
- It detects what the user is clicking on.
- It finds the parameter that is associated with that handle.
- It reads the minimum and maximum values in case of a transform handle, or the current value in case of a bevel handle.
- It reads the new position when the user drags, and calculates the new value for the parameter.
This is what the viewer state is doing in a little more detail:
To begin with, the state checks a few things: it keeps track of whether or not the shift-key is being held; it keeps track of what the user is clicking on; and (when it needs to) it keeps track of some other information like the current mouse location.
If the user clicks on a handle, the state retrieves the prim attributes stored on that handle through a ray. This way, it can easily find what parameter should be changed. If the shift key is being held, '_bevel' will be appended to the parameter name to find the corresponding bevel parameter. From here, the state either goes down the 'bevel' route, or it goes down the 'transform' route.
The bevel route is pretty simple: the state checks the updated mouse location against the mouse location when the user clicked; divides that number to make the slider progression slower and feel more natural; and then adds that value to the parameter on the tool. To help with visualization, I also create a 'hou.TextDrawable' object at the click location while the dragging is active, which shows the updated parameter value (0-1). Of course, since the handle shape changes with the slider, there was already some visual feedback, but I found that this makes it easier for the user to understand when the min/max has been reached.
Example of a handle being used
See video attachment 3
The transform route is a little longer, but similar in complexity: once the user clicks on one of these handles, it first retrieves the current parameter value(s), which I use to determine the current handle position (Admittedly, I could have simply read the position with another attribute on the handle, but we live and we learn). The only reason I'm getting the current position is to determine the offset I need to account for between the click position and the handle position. Without adding that offset, the handle would snap if the user clicks off-center on the handle.
I then determine the best plane direction to ray against when the user is dragging based on the moveable axes, and use hou.hmath.intersectPlane() to get the updated position in 3D space. I apply the offset from the click to the position as well as the offset attribute from the handle, and then determine where the position falls relative to the min/max attributes of the handle. Once I have the percentage for each axis, I apply the values to the parameter, which updates both the position of the handle, as well as the result on the actual tool.
Handle filtering:
Lastly, there is one special action the user can perform: if they double click, this switches the active handle group based on what they clicked on (remember that this was one of the attributes on the handle as well). If a specific part of the car was clicked on, the handles for the corresponding section appear, while if no geometry was clicked on, the mode switches between all handles or only 'global' handles (meaning no 'detail' handles). It does this by reading a prim attribute off the car geometry and using that result for an ordered menu parameter on the tool. This functionality essentially acts as a filter so as not to overwhelm the user with too many handles.
The actual 'filtering' is done in the node network, where nodes with 'inactive' groups are deleted.
Handle filtering my double clicking
See video attachment 4
Filtering node setup
I hope this explains clearly enough how I am using guide geometry and viewer states for my current project, and that it might be useful to someone else
. Since I am relatively new to using python, I may not have done everything in the most optimal way, so any feedback is of course welcome, as well as any questions!
