Houdini: プレビュー機能付きFontSOPを作った話
もうアドベントカレンダーの季節とか嘘でしょう…?(3回目)
気を取り直して皆さんいかがお過ごしでしょう。僕は今年もHoudini三昧の毎日でした。
今回はプレビュー機能付きFontSOPを作った話をしたいと思います。
本記事はHoudini アドベントカレンダー2024 19日目の記事です。
データ配布
ツール制作の背景とその特徴
FontSOPはテストジオメトリの作成や表現そのものにもよく用いられますが、フォントを選ぶ際、メニューリストから毎回選択しなければならないことがUIとして不便に思っていました。
そこで任意の文字列からプレビュー用の文字列ジオメトリを一括で生成し、使用したい文字列をクリックすることで好みのフォントがセットされたFontSOPに切り替わるツールを作成しました。動作としては下記動画をご参考下さい。
簡単に説明すると下記手順で動作します。
-
Font PlusSOP
(本ツールを選択状態にする) - 任意のテキストを入力する
- 文字列ジオメトリが使用できるフォントの分だけ生成される(プレビューモード)
- ビューポート上でエンターキーを押す
- 好みのフォントを使っている文字列ジオメトリをクリックする
- ビルトインノードの
FontSOP
にそのフォントが設定される
よりシンプルなツールについてはHoudini ApprenticeアドベントカレンダーのHoudini: FontSOP機能追加 - はじめてのPythonという記事でご紹介しているのでご参考下さい。
挙動としてはこんな感じです。最小限の機能追加と言えましょう。Apprenticeの記事ということもあり、はじめてHoudini Pythonにトライする方にもオススメなツールとなっており、Pythonそのものの解説も厚めにしています。
HoudiniとPython
HoudiniではPythonを様々な用途で使用可能です。今回もPythonをいくつかの機能として使用しています。大きく分けて下記のとおりです。
- 動的なノード利用(nodeVerb)
- パラメータの切り替え
- ビューポートでのインタラクティブ操作(Viewer State)
Houdini Apprenticeアドベントカレンダーでご紹介したシンプルなツールでは上記の「パラメータの切り替え」のみにPythonを使用していましたが、本ツールでは他の視点からも利用しているということになります。
動的なノード利用(nodeVerb)に関してはこちらもアドベントカレンダーの記事であるHoudini: 複数のジオメトリをロードするHDAのご紹介に詳しく記載しました。
今回のツールは前述の2つの記事の複合的な技術と合わせ、そこにビューポートでのインタラクティブ操作を組み合わせたものとなります。
ツール構成の説明
ここから実際のネットワークを見ながらツール構成を順にご説明します。
パラメータ
本ツールのUIはFontSOPのパラメータを全て露出し、そこにReleaseとPreviewを切り替えるボタン(ispreview
パラメータ)を追加しただけというシンプルなものとなります。
早速以下にispreview
パラメータの設定をご紹介します。(以下はParameterタブ
とmenuタブ
の画像です)
特に難しいところはないので解説は不要でしょう。これで基本的な設定は完成ですが、筆者の趣味でもうひとつだけ設定を追加しています。
「プレビュー状態ではフォント選択のダイアログがグレーアウトする」という設定です。具体的には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
の使用方法に関しては前回の記事に譲りますが、ポイントとしてこのコードではnodeVerb
でFontSOP
のジオメトリを動的に生成するのと同時にプリミティブアトリビュートname
としてフォント名をセットしています。これは後ほどPacked Primitiveに付与するために使用します。
For-Eachループの処理
ここではフォント選択をしやすい状態を作るという処理を行っています。
具体的にはフォントのジオメトリを囲うバウンディングボックスを作成し、クリッカブル領域を担保するという実装になります。ポリゴンが存在するところだけクリッカブルにすると細いフォントや穴が多く空いている文字などで支障がでるためです。
バウンディングボックスが用意できたら、先程作成したプリミティブアトリビュートname
を参照してPacked Primitiveにアトリビュートを移植します。ここではAttribute Createを使用しましたが、各人やりやすい方法を採択して下さい。
ループ処理を抜けた段階で、それぞれのフォント名をプリミティブアトリビュートに持ち、文字列を覆う形でクリック領域を持ったPacked Primitiveジオメトリが生成されている状態になります。
ビューポート操作の実装
ビューポート操作の実装はViewer State
で行います。下記画像の通りEdit Operator Type Properties
ウィンドウのInteractive
タブ下にあるState Script
に新規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