iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🟠

Houdini: Creating a Font SOP with a Preview Feature

に公開

Is it already Advent Calendar season? You've got to be kidding me... (3rd time)

Putting that aside, how is everyone doing? I've been spending my days fully immersed in Houdini again this year.

In this post, I'd like to talk about creating a FontSOP with preview functionality.

This article is for Day 19 of the Houdini Advent Calendar 2024.

Data Distribution

HDA Download

Background and Features of Tool Creation

FontSOP is frequently used for creating test geometries or as a form of expression itself, but I've always found the UI inconvenient because you have to select fonts from a menu list every single time.

Therefore, I created a tool that generates preview string geometries in bulk from an arbitrary string, and switches to a FontSOP with the desired font set simply by clicking the string you want to use. Please refer to the video below for the behavior.

Behavior confirmation video

To explain briefly, it operates with the following steps:

  1. Font PlusSOP (Select this tool)
  2. Enter any text
  3. String geometries are generated for all available fonts (Preview Mode)
  4. Press the Enter key in the viewport
  5. Click the string geometry using the font you like
  6. The font is set in the built-in FontSOP node

For a simpler tool, please refer to the article Houdini: Adding FontSOP Functionality - My First Python in the Houdini Apprentice Advent Calendar.

Behavior confirmation video

The behavior looks like this. It could be called a minimal feature addition. Since it's an Apprentice article, it's also recommended for those trying Houdini Python for the first time, and it includes detailed explanations of Python itself.

Houdini and Python

In Houdini, Python can be used for various purposes. In this tool as well, Python is used for several functions. They can be broadly classified as follows:

  1. Dynamic node usage (nodeVerb)
  2. Parameter switching
  3. Interactive operations in the viewport (Viewer State)

In the simpler tool introduced in the Houdini Apprentice Advent Calendar, Python was used only for the "Parameter switching" mentioned above, but this tool utilizes it from other perspectives as well.

Regarding dynamic node usage (nodeVerb), I have described it in detail in another Advent Calendar article, Houdini: Introducing an HDA for Loading Multiple Geometries.

This tool combines the techniques from the two aforementioned articles with interactive operations in the viewport.

Tool Configuration Description

From here, I will explain the tool configuration step by step while looking at the actual network.

Parameters

The UI of this tool is simple: it exposes all the parameters of the FontSOP and adds a button to switch between Release and Preview (the ispreview parameter).

I will now introduce the settings for the ispreview parameter. (The images below show the Parameter tab and the menu tab.)

Parameter 01

Parameter 02

There isn't anything particularly difficult here, so an explanation shouldn't be necessary. With this, the basic settings are complete, but I've added one more setting as a personal preference.

Parameter 03

This setting makes it so that "the font selection dialog is grayed out while in the preview state." Specifically, it is implemented by setting { ispreview == 1 } in the Disable when parameter.

While this isn't strictly necessary, I've included it as a UI element to more explicitly convey the intention that the font is currently being determined via viewport operations rather than the dialog.

Tool Network

Let's dive into the HDA. The stream on the right is for preview geometry generation, while the stream on the left is a simple FontSOP stream.

Network

The most important point in this network is that the first input is not connected to the PythonSOP (python_create_fonts).

The design uses the FontSOP only for passing information, while delegating the actual geometry generation to the PythonSOP (python_create_fonts). There's nothing particularly difficult about it, but it might be surprising if you're not used to it.

PythonSOP (python_create_fonts) Code

node = hou.pwd()
geo = node.geometry()

# Get the font node
font = node.inputs()[1]

# Get the font parameters
parm_file = font.parm("file")
labels = {label for label in parm_file.menuLabels() if label != "_separator_"}
text = font.parm("text").eval()

fontverb = font.verb()
tmp = hou.Geometry()

for label in labels:
    try:
        # Set font parameters and execute
        fontverb.setParms({"file": label, "text": text})
        fontverb.execute(tmp, [])
        
        # Add name primitive attribute
        attrib = tmp.addAttrib(hou.attribType.Prim, "name", "")
        for prim in tmp.prims():
            prim.setAttribValue(attrib, label)
            
        # Merge geometry
        geo.merge(tmp)
    except Exception as e:
        print(f"Error occurred during processing of '{label}': {str(e)}")

I will leave the usage of nodeVerb to the previous article, but the key point is that this code sets the font name as a name primitive attribute while dynamically generating the FontSOP geometry with nodeVerb. This is used later to assign it to the Packed Primitives.

For-Each Loop Processing

In this part, we perform the process of making font selection easier.

Specifically, the implementation involves creating a bounding box around the font geometry to ensure a clickable area. This is because if only the areas with polygons were clickable, it would cause issues with thin fonts or characters with many holes.

Network

Once the bounding box is prepared, we refer to the name primitive attribute created earlier to transfer the attribute to the Packed Primitive. I used Attribute Create here, but please use whichever method works best for you.

Loop done

After exiting the loop, the Packed Primitive geometries are generated, each carrying its font name as a primitive attribute and having a clickable region covering the string.

Implementing Viewport Operations

Viewport operations are implemented using a Viewer State. Create a new Python script in the State Script section under the Interactive tab of the Edit Operator Type Properties window, as shown in the image below.

Viewer State

Viewer States are typically created from a template. For details on that process, please refer to the following documentation.

How to write custom Viewer States in Python

Next, let's look at the Viewer State for this tool.

"""
State:          Kickbase::font plus
State type:     kickbase::font_plus
Description:    Kickbase::font plus
Author:         kickbase
Date Created:   November 18, 2024 - 19:57:57
"""

import hou
import viewerstate.utils as su

class State(object):
    MSG = "Click Font what you want"

    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer
        self.node = None
        self.geometry = None
        self.name = ""

    def onEnter(self, kwargs):
        self.node = kwargs["node"]
        self.geometry = self.node.geometry()

        self.scene_viewer.setPromptMessage(State.MSG)

    def onMouseEvent(self, kwargs):
        ui_event = kwargs["ui_event"]
        (origin, dir) = ui_event.ray()
        hitprim, _, _, _ = su.sopGeometryIntersection(self.geometry, origin, dir)
        device = ui_event.device()

        # Left Button Click
        if device.isLeftButton():
            if 0 <= hitprim and self.node.parm("ispreview").eval() == 1:
                self.name = self.geometry.iterPrims()[hitprim].attribValue("name")
                # print(self.name, hitprim)
                self.node.parm("file").set(self.name)
                self.node.parm("ispreview").set(0)


def createViewerStateTemplate():
    """Mandatory entry point to create and return the viewer state
    template to register."""

    state_typename = kwargs["type"].definition().sections()["DefaultState"].contents()
    state_label = "Kickbase::font plus"
    state_cat = hou.sopNodeTypeCategory()

    template = hou.ViewerStateTemplate(state_typename, state_label, state_cat)
    template.bindFactory(State)
    template.bindIcon(kwargs["type"].icon())

    return template

I'll omit the details as there isn't anything particularly difficult, but the onMouseEvent starting from line 28 is the core of this code.

  • Intersection detection between mouse position and geometry
  • Left-click detection
  • Method to retrieve arbitrary attributes from the geometry under the cursor
  • Modifying node parameters

It packs several processes into a short piece of code. Please feel free to use it in your own tools.

Controlling the Distribution of String Geometries

This tool was not made for actual production use but was quickly put together for a study group presentation, so the distribution control is left entirely to the Labs Align and Distribute SOP.

Ideally, this part should be built from scratch to be easier to control, and parameters should be exposed to ensure a good UX.

Conclusion

Thank you to everyone who read until the end. Even with built-in nodes, improving areas you find "inconvenient" can lead to deeper understanding and help you gain multifaceted perspectives.

It's also simply fun as a thought experiment, and creating these kinds of disposable tools can help sharpen your quick-thinking skills.

Well then, have a wonderful Houdini life next year too!

Development Environment

  • Windows 10
  • Houdini 20.0.688

References

Best practices for font previewing

Discussion