Houdini 19.0 Solaris

Custom Layout Brushes

How to create layout brush digital assets you can use to customize the behavior of the Layout LOP.

On this page

Overview

The Layout LOP node has a brushing mode that lets you interactively populate the scene with point instanced models (for example, trees, bushes, rocks, buildings). How the node translates mouse events into creating instances is controlled by a brush asset. You can create your own brush asset that provides custom behavior defined by a SOP network.

Since the brush’s behavior is defined by a compiled SOP block, you can use the full power of (compile-able) SOPs, including curve and polygon manipulation nodes, scatter nodes, and VEX code in wrangles.

You can give the brush asset a parameter interface. This interface is displayed in the Layout UI when your brush is active. This lets you give your uses controls to customize your brush’s behavior.

Before you implement a brush, you should probably be familiar with the following subjects:

  • Compiled blocks. The implementation of the brush is inside a compiled block (so it can be invoked by the parent Layout node with high performance). There are limitations to what SOP nodes you can use inside a compiled block, as well as limitations on expressions that cause nodes to cook.

  • Wrangle nodes and VEX snippets. Often the easiest way to implement some behavior is to use an Attribute Wrangle to read attributes, compute values, and write new attributes. However, this requires some knowledge of VEX snippets and the built-in VEX functions.

Terms

Stroke

The user presses down a mouse button, moves the mouse, and releases the mouse button.

Invocation

One run of the compiled block in the brush SOP network. The compiled block is invoked many times during a stroke.

Tracking points

Points generated by projecting from the pointer down onto the stage plane/surface during a stroke.

Model

In this documentation, we’ll refer to the USD models you can instance onto points as models. This is an attempt to avoid confusion over the word asset, which will usually refer to the brush digital asset node.

Working set

Brushes are essentially plugins for the Layout node, to customize how mouse clicks, drags, and so on are interpreted in the Layout state. As part of the Layout interface, the user builds a “working set” of the USD models they want to be instancing.

In the brush network, the working set is represented by a set of points in the sixth (Working Set) input, where the point number (and the id point attribute) is the number you use to instance that model onto points, and other attributes on the points encode basic information about the models such as their bounding boxes.

It’s up to the brush to decide how/whether to instance the various models in the working set (see using the working set below).

Making a new custom brush

To...Do this

Start a new brush asset

In the Layout node UI, click the New Brush button in the Brushes section.

This opens the dialog for creating a new digital asset. You can specify the asset namespace, name, and save location. See creating a new digital asset for more information.

When you create a new brush asset using this button, it populates the asset with boilerplate nodes, sticky notes, and boilerplate Python code to make it easier to implement the brush. It also sets the new asset up so it’s automatically discovered as a new brush by the Layout node.

Edit a brush asset

  • To edit the network inside a brush, right-click the brush in the Layout node UI and choose Edit Network.

  • To edit the type properties of a brush (including the Python module), right-click the brush in the Layout node UI and choose Edit Type Properties.

Tip

The Layout node looks for assets with a section named layout_brush to build the list of available brushes. If for some reason you are creating a brush asset from scratch instead of copying the template, you must remember to add this section in the Extra Files tab of your asset.

Inside the SOP network

  • A brush is a SOP asset with six inputs and two outputs.

  • Within the network is a compiled block that is invoked over and over as the user moves the mouse.

  • The compiled block has many inputs containing information from the parent Layout node about the current state and what the user has done. The network inside the compiled block should use the geometry and attributes from the inputs to output new points to instance onto, and/or modify or delete existing points.

  • The compiled block is invoked even when the user is just moving the mouse around, before they begin a stroke. This allows you to display guides and possibly highlight existing models as the user moves the mouse over the scene.

    You should use Switch If nodes to make sure you only edit points when the user is making a stroke (that is, dragging the mouse).

  • The first input contains new points you're creating with the current stroke. The second input contains “existing” points that have already been created in the parent Layout node. The fifth input contains a polyline representing the current stroke (where the last point is the current position of the mouse). The first output is the points you want to be the new list of “new points”.

    How the brush handles the “new points” controls the interaction style. For example:

    • If, in the compiled block network, you keep piping the first (new points) input to the output, while adding a new point at the mouse position (the last point from the “stroke” input), the stroke will leave behind a trail of points.

    • If you throw away the “new points” input and just create a point at the mouse position (the last point in the “stroke” input) and send only that point to the output, the brush will appear to be dragging a single thing around with the mouse.

    See inputs below for more information.

  • To create a new point, use the Add SOP, then use an Attribute Wrangle to copy/compute the attributes you want on the point.

    Other than the first two inputs (“New Points” and “Existing Points”), don’t try to use re-use points from the other inputs directly (for example, deleting all points in the stroke except the last, and then sending that to the first output). Those points have tons of extra attributes.

    Having many unused point attributes harms performance.

    Tip

    The Layout node does not automatically remove any point attributes from the points you send to the output. If your points have attributes that aren’t meaningful outside the brush network, you should delete them with Attribute Delete before outputting the points.

  • To instance a model from the working set onto a point, do the following:

    • Set the point’s asset_id attribute to the point number of the point in the sixth (Working Set) input corresponding to the model you want to instance.

  • To modify a point (from either the “new” or “existing” inputs), do the following:

    • To set the position of a point, set the P vector3 point attribute.

    • To set the orientation and scale of the instanced model on a point, you have to encode the rotations and scales into a matrix3 point attribute called transform.

      You can easily set/edit a transform attribute using a Transform SOP. Add the Transofrm node to the network, set the Rotate and Scale parameters on the node to the values you want, and set the Attributes parameter to transform to write the transformations into the attribute.

    • Send any modified points to the the first output.

      If you modify an existing point, it will be deleted and an identical new point will be added to the first (New Points) input on the next invocation. So any existing points you edit with a stroke become part of the stroke’s “new” points.

    Note

    To scale/rotate instances, you must encode them in a transform matrix3 attribute. Do not use any other traditional instancing attributes such as pscale or orient.

  • If you haven’t modified existing points, you shouldn’t send them to the output (the output always represents the new set of “new points” created by the current stroke). If your brush doesn’t affect existing points, you don’t need to wire the second input into anything at all.

  • To delete an existing point, create a delete point attribute on the point and set it to 1. Then merge the point into the first output.

    To delete a point from the current stroke (the first (New Points) input), just don’t send it down to the output.

Inputs

Label

Description

1. New Points

All the points added by earlier invocations during the current stroke.

If you want to keep any of these points (that is, instantiate models onto them), wire the points into the first output. Any points from the first input that don’t make it down to the output are thrown away.

2. Existing Points

All points created in previous operations by any brush. These are the point instances currently committed into the

You can modify these points by changing their attributes in the SOP network. If the network modifies the attributes on a point in the second (Existing Points) input, the parent Layout node actually deletes the point from the “Existing” list and adds the point (with the modifications) to the first (New Points) input.

To delete existing points, set an integer point attribute named delete to 1 on the points you want to delete and merge them into the first output.

3. Static Geometry

This is “collision” geometry wired into the Layout node by the user. This is often “ground” geometry the user wants to place assets on.

You can use this when creating extra points. For example, as the input to Scatter to scatter new points over the ground geometry. You can merge this with the next input to scatter across both the static geometry and existing assets.

4. Asset Geometry

The geometry of all the instances created by this node (that is, the output of instancing models onto the existing points).

You can use this when creating extra points. For example, as the input to Scatter to scatter new points over the existing assets. You can merge this with the previous input to scatter across both the static geometry and existing assets.

5. Stroke

Contains a polyline primitive, where the points represent projected intersection points along the current stroke.

There are also global (detail) attributes in the input that encode information about the current instant, for example whether the user is holding the ⇧ Shift key. You can use these to allow the user to vary or customize the brush behavior with modifier keys.

  • The stroke is automatically projected onto/intersected with the Layout node’s input geometry, the existing instances, and the ground plane (the user can choose whether to intersect with static geometry, instances, or both (the default) when using the Layout node).

  • The parent Layout node only appends to the stroke when the user is clicking or dragging. When the user is simply moving the mouse around, the attributes on the last point of the stroke are updated instead of adding a new point.

  • If the user is dragging but the ray from the mouse pointer didn’t intersect anything, the parent Layout node appends a copy of the most recent valid point to the stroke.

See stroke attributes below for a list of attributes available on the stroke.

6. Working Set

Encodes each item in the Layout node’s working set (the models to be instantiated) as a disconnected point.

To instance a model, set a new point’s asset_index attribute to the point number of one of these points.

See the working set attributes below for a list of other attributes available on these points.

Outputs

Label

Description

1. Output Points

For each invocation, this contains the set of points you want to instance onto. Any points you output here are added to the first (New Points) input in the next invocation.

2. Guides

Guide geometry to draw over the instanced output. This lets you give the user visual feedback. For example, if your brush scatters models within a certain radius of the mouse pointer, you can draw a ring guide showing the limit of the scatter area.

In the brush asset’s Python module you can implement the onDrawGuides(draw_handle, guide_geo) function. The node calls this with a viewer handle and a hou.Geometry object representing the guide geometry from the second output. You can then use the hou.Drawable and hou.AdvancedDrawable objects to draw guides using styles that aren’t available to regular geometry (for example, dashed lines, line thickness, and glow).

If the onDrawGuides function does not exist in the asset’s Python module, the geometry in the second output is displayed directly as guide geometry.

Stroke input attributes

Global (detail) attributes

Nmae

Type

Description

mouse_drag

int

The mouse button is being held down (1 if True, 0 if False).

mouse_start

int

This is the start of a drag (1 if True, 0 if False).

mouse_locate

int

The user is moving the mouse without dragging (1 if True, 0 if False).

mouse_pick

int

The user clicked (1 if True, 0 if False).

mouse_changed

int

This is the end of a drag (1 if True, 0 if False).

is_left_button_down

int

LMB is being pressed (1 if True, 0 if False).

is_middle_button_down

int

MMB is being pressed (1 if True, 0 if False).

is_right_button_down

int

RMB is being pressed (1 if True, 0 if False).

is_caps_lock

int

The Caps Lock key is being pressed (1 if True, 0 if False).

is_ctrl_key

int

The ⌃ Ctrl key (or command key on Mac) is being pressed (1 if True, 0 if False).

is_shift_key

int

The ⇧ Shift key is being pressed (1 if True, 0 if False).

is_alt_key

int

The Alt key (option key on Mac) is being pressed,(1 if True, 0 if False).

seed

int

Contains a number which changes for each click/stroke, but stays the same across the entire stroke. This is useful to use as a seed value for random values that you want to be consistent across the stroke.

screen_to_world_xform

matrix

A transformation matrix to map from screen space to world space. This is useful when implementing a brush that works in screen space.

Point attributes on the stroke polyline

A polyline in the 5th input represents the user’s interactive stroke. Each point on the polyline represents a sample taken along the stroke. Each point has several attributes storing data about the sampled intersection.

As the user “draws” in the viewport, the node fires rays “into” the screen to find what the pointer is over in 3D space.

By default, the Layout node finds the nearest intersection with existing instances, the static (often “ground”) geometry, and the XZ ground plane. (Note that the user can set an option when using the Layout node to not intersect static geometry or not intersect existing instances.)

If a ray doesn’t intersect anything, the network is still invoked, but the new point in the Stroke input is simply a copy of the previous valid point.

Name

Type

Description

P

vector3

The (intersected) position in 3D space.

N

vector3

The normal at the intersected point.

ray_orig

vector3

The origin of the intersecting ray in 3D space (derived from the screen coordinates of the mouse pointer). This is useful to make a brush that works in screen space.

ray_dir

vector3

The direction of the intersecting ray in 3D space (from the mouse pointer “forward” into the scene, following the camera projection).

mouse_x

int

Horizontal position of the mouse pointer, in screen pixels.

mouse_y

int

Vertical position of the mouse pointer, in screen pixels.

hit_instance

int

If the mouse pointer was over an instanced model, this value is the point number of the instance, from the second (Existing points) input.

For example, you could make a brush that deletes any instances you drag over, by checking the contents of this attribute and preventing the point from being output.

Or, you could make a brush that pushes/nudges existing instances. If a stroke starts on an instance, you could then use subsequent points in the stroke to translate that instance’s point.

tablet_angle

float

Angle of the tablet pen, if the stroke was created using a tablet or similar device.

tablet_pressure

int

Pressure of the tablet pen, if the stroke was created using a tablet or similar device.

Working Set input point attributes

Each point in the sixth (Working Set) input corresponds to an item currently in the Layout node’s working set.

id

int

The same as the point number. This is the value you should set the asset_id attribute to on a point in the first output, to instance the corresponding model onto the point.

bbox_dim

vector

The size of the bounding box of the model corresponding to this point.

bbox_center

vector

The center of the bounding box.

The bounding box attributes can be useful to space out instances to prevent intersections when scattering, and/or to show footprint or bounding box guides.

See using the working set for more information.

Multi-stroke brush

  • Normally, on the mouse up event at the end of a stroke, the point output of the brush’s compiled block is “committed” and integrated into the Layout node’s point instance data.

  • If necessary, you can implement a brush that allows the user to make multiple strokes as part of one operation.

    For example, you could implement a fence-drawing brush where each click/drag makes a new corner in the fence until the user closes the fence by connecting back to the start (or until the user presses Enter to leave the fence open).

    You could also use a multi-stroke interaction to let the user draw rectangles, where one stroke defines the length of one side, and then a second drag pulls out the length of the adjecent side.

  • To indicate to the parent Layout node that the current operation should continue after the current stroke, add a end_stroke detail attribute to the first output, set to 0.

    The Layout node sees that the attribute exists and keeps the current operation active. Subsequent strokes will keep appending to the same list of “existing points”.

  • Then, to indicate to the parent Layout that the current operation is finished, set the end_stroke detail attribute on the first output to 1.

    In the fence making example, you would use an Attribute Create node to create the end_stroke integer detail attribute, and connect it to the first output. You could then use an expression or Attribute Wrangle to set the attribute to 1 if the end of the current stroke is near the starting point or the first stroke, or 0 otherwise.

    The layout node sees end_stroke exists but is set to 1, and commits the output.

  • The user can always press Enter to finish and commit the current operation.

  • Implementing a multi-stroke brush is more complex, but there are a couple of techniques that can help you

    • Use a series of Switch If nodes to switch to individual branches that handle each of the different event types.

    • If you need to pass information between invocations and/or strokes, you can set your own custom detail attributes on the first output. The detail attributes from the first output will be copied back into the first (New Points) input, until the operation is committed.

      In the example of drawing a rectangle, you could use a detail attribute to keep track of whether the current stroke is the first or second stroke of the operation.

Operation commit

When the user finishes an operation:

  • The Layout node clears any detail attributes added to the output geometry.

    Detail attributes you create on the output geometry become available as detail attributes on the first (New Points) input on the next iteration. If you use these detail attributes on the to communicate information between iterations (or strokes in a multi-stroke brush), the information will not carry over between operations.

Parameter interface

You can design a parameter interface for the brush, and in the compiled block, use a ch expression to read the value of a parameter and use it to change how the brush works.

For example, you could have a Density parameter (internal name density) on the brush asset to control how many points the brush scatters as the user drags. Inside the compile block, for example in the Force Total Count parameter of a Scatter SOP, you can use the expression ch("../density") to copy the parameter value.

When a brush is active in the Layout LOP, the layout interface shows the brush parameters, the same as they would appear in a parameter editor.

Scroll wheel

There are two ways to allow the user to use the scroll wheel to change how your brush behaves.

  • If your brush’s parameter interface has a parameter whose internal name starts with scroll, that parameter will be incremented/decremented when the user scrolls the mouse wheel. (If more than one parameter starts with scroll, the node will change them all.)

  • If you implement an onMouseWheelEvent(kwargs) function in the asset’s Python module, the Layout state will call it when the user scrolls the mouse wheel.

Using the working set

As a brush author, you can decide how to use the working set of models chosen by the user. You can choose to give significance to the ordering of the items in the working set, or not.

  • A common interpretation will be to randomly choose items from the working set, for example when scattering items across the scene.

  • Your brush can use the ordering of the working set. For example, you might make a brush that drops the first item in the working set into the scene, and scatters instances of the second item in a rough ring around it, and a ring of the third item around that, and so on (this might be useful for scattering trees with a small ecosystem of rocks, dead branches, etc. around each tree).

    As another example, in a brush that scatters items randomly, you could use the ordering of the items to influence how likely each item is to be chosen, with earlier items in the set being more likely than later items.

  • Currently there is no automatic way to indicate to the brush user whether the brush treats the ordering of the working set as significant. It should be part of the brush’s documentation, or be controllable in the brush’s parameter interface.

Python callbacks

A digital asset contains a section you can use to hold Python source code. This is often useful for storing shared utility functions for use by the asset’s various callbacks, as well as where the asset’s interactive state is implemented, if any.

For brush assets, the parent Layout node will check for a couple of functions in the

To...Do this

Edit the asset’s Python module

  1. Right click an instance of the brush asset and choose Edit Type Properties.

  2. In the Type Properties window, click the Scripts tab.

  3. On left side, click PythonModule if it’s not already selected.

  4. On the right side, you can edit the contents of the Python module.

The Layout state currently uses the following functions, if they exist in the brush asset’s Python module.

registerGadgets()

Called by the layout state to generate the necessary hou.GeometryDrawable objects for guides.

You should return a list of tuples containing the name and hou.drawableGeometryType.

def registerGadgets():
    return [("line_gadget", hou.drawableGeometryType.Line), ("face_gadget", hou.drawableGeometryType.Face)]

onDrawGuides(kwargs)

This is called when the layout state is asked to redraw. Some important kwargs are:

gadgets

A map of strings to hou.GeometryDrawable gadgets registered in registerGadgets.

draw_handle

A view handle. Pass this to the draw() method of the hou.GeometryDrawable gadgets in the gadgets argument.

guide_geo

A hou.Geometry object containing the geometry sent to the GUIDE_OUT output of the brush SOP network.

Note that this argument may be None. Your function should check for this and handle it gracefully.

This object can be used to pass on information from the brush to the guides. For example you could use a text drawable to display a detail attribute that your brush tacked on.

def onDrawGuides(kwargs):
    draw_handle = kwargs['draw_handle']
    guide_geo = kwargs['guide_geo']
    line_gadget = kwargs['gadgets']['line_gadget']
    face_gadget = kwargs['gadgets']['face_gadget']

    line_gadget.setGeometry(guide_geo)
    line_gadget.show(True)
    line_gadget.draw(draw_handle)

    face_gadget.setGeometry(guide_geo)
    face_gadget.show(True)
    face_gadget.draw(draw_handle)

onMouseWheelEvent(kwargs)

Called when the user scrolls the mouse wheel.

def onMouseWheelEvent(self, kwargs):
    # Get the UI device object
    device = kwargs["ui_event"].device()
    scroll_amount = device.mouseWheel()

This API does not currently support scroll axes other than vertical.

See the help for hou.UIEventDevice.mouseWheel() for information about the number returned.

hudTemplate()

This lets you specify HUD info template rows specific to this brush. The Layout node will insert these rows into its own HUD info panel.

The function should return a list of template row objects (not a top-level template dictionary). See HUD info panels for information about the template format.

def hudTemplate():
    return [
        {"key": "G", "label": "Change brush shape"},
        {"key": "mousewheel", "label": "Change brush radius"},
        {"key": "LMB", "label": "Scatter instances"},
        {"key": "Shift + LMB", "label": "Smooth instance orientations"},
    ]

hudUpdate()

This function is called when the layout state requires a HUD update due to the brush changing or due to an update being triggered via the layout.utils.requestHUDUpdate function.

It is useful for updating values in the HUD when something like a mode is changed. The Fill brush uses this to display different information depending on which fill mode is active.

def hudTemplate():
    return [{
        'id': 'info',
        'label': ''
    }]

def hudUpdate():
    # This function might be triggered by a parameter callback script
    # calling layout.utils.requestHUDUpdate(). We evaluate what mode is
    # active and update the 'info' label accordingly.
    import layout.utils
    node = layout.utils.currentBrushNode()
    mode = node.evalParm("mode")

    if mode == 0:
        label = 'Define grid'
    elif mode == 1:
        label = 'Define disk radius'
    elif mode == 2:
        label = 'Draw scatter region'
    else:
        raise ValueError("Invalid brush mode.")

    return  {'info': {'label': label, 'key': 'LMB'}}

radialMenu(kwargs)

This lets you specify the contents of brush-specific radial menu.

The function should return the kind of dictionary specified in generating a radial menu with a script. See that page for information on the structure and available keys.

The Layout node passes a dictionary containing one key: node, whose value is the instance of your brush asset. You can use this set parameter values in the menu’s script actions.

def radialMenu(kwargs):
    node = kwargs["node"]

    entries = {}
    entries['n'] = {
        'type':'script_action',
        'label':'Reset Radius',
        'script': lambda: node.setParm("radius", 1.0)
    }
    entries['e'] = {
        'type':'script_action',
        'label':'Embiggen Instances',
        'script': lambda: node.setParm("instance_scale", node.evalParm("instance_scale") + 1.0)
    }
    entries['s'] = {
        'type':'script_action',
        'label':'Disembiggen Instances',
        'script': lambda: node.setParm("instance_scale", node.evalParm("instance_scale") - 1.0)
    }
    return entries

Making a brush that works in screen space

To help make a brush that works in screen space, we provide the screen_to_world_xform detail attribute on the fifth (Stroke) input. This transform matrix corresponds to: viewport.windowToViewportTransform() viewport.viewportToNDCTransform() viewport.ndcToCameraTransform() viewport.cameraToModelTransform() viewport.modelToGeometryTransform()

In english what this does is maps a point in world space to screen space. To get the proper screen space position however you need to divide the resulting vector by its 'w' component to dehomogenize it. Once dehomogenized the x and y components represent the screen coordinates and the z component represents the depth from -1 to 1 NDC coordinates. -1 Being the near clipping plane and 1 being the far clipping plane.

Tips and notes

  • To handle asset assignment from a lworking set with more than one item, wire the working set into a Connectivity SOP with the Piece attribute name set to asset_index, then wire that into an Attrib From Pieces SOP's second input (wire the points you are instancing to into the first input).

  • To get the number of points in an input to a wrangle, you can use npoints(input_num) in the VEX snippet. This works in a wrangle since it cooks all its inputs but generally in compiled SOPs you need to use a spare input and call npoints on the spare input, for example npoints(-1). See compiled SOPs.

  • You can create nodes outside the compiled block to do “set up”, then add another Compile Block Start node to the compile block as an “extra argument” and wire the “outside” nodes into it. These nodes will be cooked once, and the static result will be available in the block from the block input.

    For example, if you want to set up some complex guide geometry, where only the transform might change during the invocation, not the geometry itself, you could create the the geometry outside the compile block and wire it in, instead of recreating it on each invocation.

Solaris

USD

Geometry

  • Importing SOP geometry into USD

    Details of how Houdini converts SOP geometry to USD, and how you can control the process.

  • Component Builder

    The Component Builder tool puts down a network snippet for creating a USD model from SOPs, with support for materials, variants, payloads, and layering.

Layout

  • Edit node

    Interactively transforms prims in the viewer. Can use physics collisions to position props realistically.

  • Layout node

    Provides tools for populating a scene with instanced USD assets. You can place individual components, paint/scatter components in different ways using customizable brushes, and edit existing instances.

  • Custom Layout Brushes

    How to create layout brush digital assets you can use to customize the behavior of the Layout LOP.

Shading

  • Shader Framework

    Describes the Solaris shading framework, including shader node translation to USD primitives.

  • Using MaterialX with Karma

    Houdini has VOP node equivalents of the MaterialX shader nodes. You can build a shader network using these nodes, or import an existing MaterialX shader, and use them with Karma (Houdini’s USD renderer).

  • /solaris/materialx_nodes

Karma rendering

Tutorials