🟠

Houdini: プレビュー機能付きFontSOPを作った話

2024/12/19に公開

もうアドベントカレンダーの季節とか嘘でしょう…?(3回目)

気を取り直して皆さんいかがお過ごしでしょう。僕は今年もHoudini三昧の毎日でした。

今回はプレビュー機能付きFontSOPを作った話をしたいと思います。

本記事はHoudini アドベントカレンダー2024 19日目の記事です。

データ配布

HDAダウンロード

ツール制作の背景とその特徴

FontSOPはテストジオメトリの作成や表現そのものにもよく用いられますが、フォントを選ぶ際、メニューリストから毎回選択しなければならないことがUIとして不便に思っていました。

そこで任意の文字列からプレビュー用の文字列ジオメトリを一括で生成し、使用したい文字列をクリックすることで好みのフォントがセットされたFontSOPに切り替わるツールを作成しました。動作としては下記動画をご参考下さい。

動作確認用動画

簡単に説明すると下記手順で動作します。

  1. Font PlusSOP(本ツールを選択状態にする)
  2. 任意のテキストを入力する
  3. 文字列ジオメトリが使用できるフォントの分だけ生成される(プレビューモード)
  4. ビューポート上でエンターキーを押す
  5. 好みのフォントを使っている文字列ジオメトリをクリックする
  6. ビルトインノードのFontSOPにそのフォントが設定される

よりシンプルなツールについてはHoudini ApprenticeアドベントカレンダーのHoudini: FontSOP機能追加 - はじめてのPythonという記事でご紹介しているのでご参考下さい。

動作確認用動画

挙動としてはこんな感じです。最小限の機能追加と言えましょう。Apprenticeの記事ということもあり、はじめてHoudini Pythonにトライする方にもオススメなツールとなっており、Pythonそのものの解説も厚めにしています。

HoudiniとPython

HoudiniではPythonを様々な用途で使用可能です。今回もPythonをいくつかの機能として使用しています。大きく分けて下記のとおりです。

  1. 動的なノード利用(nodeVerb)
  2. パラメータの切り替え
  3. ビューポートでのインタラクティブ操作(Viewer State)

Houdini Apprenticeアドベントカレンダーでご紹介したシンプルなツールでは上記の「パラメータの切り替え」のみにPythonを使用していましたが、本ツールでは他の視点からも利用しているということになります。

動的なノード利用(nodeVerb)に関してはこちらもアドベントカレンダーの記事であるHoudini: 複数のジオメトリをロードするHDAのご紹介に詳しく記載しました。

今回のツールは前述の2つの記事の複合的な技術と合わせ、そこにビューポートでのインタラクティブ操作を組み合わせたものとなります。

ツール構成の説明

ここから実際のネットワークを見ながらツール構成を順にご説明します。

パラメータ

本ツールのUIはFontSOPのパラメータを全て露出し、そこにReleaseとPreviewを切り替えるボタン(ispreviewパラメータ)を追加しただけというシンプルなものとなります。

早速以下にispreviewパラメータの設定をご紹介します。(以下はParameterタブmenuタブの画像です)

パラメータ01

パラメータ02

特に難しいところはないので解説は不要でしょう。これで基本的な設定は完成ですが、筆者の趣味でもうひとつだけ設定を追加しています。

パラメータ03

「プレビュー状態ではフォント選択のダイアログがグレーアウトする」という設定です。具体的にはDisable whenパラメータに{ ispreview == 1 }をセットすることで実装しています。

これはやらなくてもよいのでが、より明示的に今はダイアログではなくビューポート操作でフォントを決定するよという意図を伝えるUIとしています。

ツールのネットワーク

HDAの中にダイブしてみましょう。右のストリームがプレビュー用のジオメトリ生成、左のストリームが単純なFontSOPのストリームとなっています。

ネットワーク

このネットワークで最も重要な点はPythonSOP(python_create_fonts)には第1インプットがつながっていないという部分です。

FontSOPは情報の受け渡しのみに使用し、実際のジオメトリ生成はPythonSOP(python_create_fonts)に任せているという設計になっています。特に難しいところはないのですが、慣れていないとギョッとするところかもしれませんね。

PythonSOP(python_create_fonts)のコード

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

# フォントノードを取得
font = node.inputs()[1]

# フォントパラメータを取得
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:
        # フォントパラメータを設定して実行
        fontverb.setParms({"file": label, "text": text})
        fontverb.execute(tmp, [])
        
        # nameプリミティブアトリビュートを追加
        attrib = tmp.addAttrib(hou.attribType.Prim, "name", "")
        for prim in tmp.prims():
            prim.setAttribValue(attrib, label)
            
        # ジオメトリをマージ
        geo.merge(tmp)
    except Exception as e:
        print(f"'{label}' の処理中にエラーが発生: {str(e)}")

nodeVerbの使用方法に関しては前回の記事に譲りますが、ポイントとしてこのコードではnodeVerbFontSOPのジオメトリを動的に生成するのと同時にプリミティブアトリビュートnameとしてフォント名をセットしています。これは後ほどPacked Primitiveに付与するために使用します。

For-Eachループの処理

ここではフォント選択をしやすい状態を作るという処理を行っています。

具体的にはフォントのジオメトリを囲うバウンディングボックスを作成し、クリッカブル領域を担保するという実装になります。ポリゴンが存在するところだけクリッカブルにすると細いフォントや穴が多く空いている文字などで支障がでるためです。

ネットワーク

バウンディングボックスが用意できたら、先程作成したプリミティブアトリビュートnameを参照してPacked Primitiveにアトリビュートを移植します。ここではAttribute Createを使用しましたが、各人やりやすい方法を採択して下さい。

Loop done

ループ処理を抜けた段階で、それぞれのフォント名をプリミティブアトリビュートに持ち、文字列を覆う形でクリック領域を持ったPacked Primitiveジオメトリが生成されている状態になります。

ビューポート操作の実装

ビューポート操作の実装はViewer Stateで行います。下記画像の通りEdit Operator Type PropertiesウィンドウのInteractiveタブ下にあるState Scriptに新規Pythonコードを作成して下さい。

Viewer State

Viewer Stateは一般的にテンプレートから作成することが多いのですが、その方法などについては下記ドキュメントをご参考下さい。

Pythonで独自のViewer Stateを記述する方法

続けて本ツールのViewer Stateを見ていきます。

"""
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

特に難しいところはないので詳細は割愛しますが、28行目からのonMouseEventが本コードのキモになります。

  • マウス位置とジオメトリの交差判定
  • 左クリックの判定
  • マウス下にあるジオメトリから任意のアトリビュートを取得する方法
  • ノードのパラメータ変更

など、短いコードでいくつかの処理が詰まっています。ぜひご自身のツールにご活用下さい。

文字列ジオメトリの分布コントロール

本ツールは実際に使用するものではなく、勉強会で発表する用で雑に作成したため分布コントロールはLabs Align and DistributeSOPに一任しています。

本来であればここはコントロールしやすく自前で組むべきですし、パラメータアウトも行いUXを担保する必要があります。

まとめ

最後まで読んでくださった方、ありがとうございます。ビルトインノードであっても「不便だな」と思ったところは改善していくと、より理解が深まったり多角的な視点を持つ事ができるようになったりします。

また単純に思考実験としても面白く、こういった使い捨てのツール制作を行うことで瞬発力を磨くこともできるでしょう。

ではでは、来年も素敵なHoudiniライフを!

開発環境

  • Windows10
  • Houdini 20.0.688

参考

フォントプレビューのベストプラクティス

Discussion