はじめに
この記事はHoudini 21のCopernicusの新機能を用いてVAT的なものを作ったり使ったりする内容です。
筆者はゲームエンジンを使用しないためUE等で扱うことのできる正式な仕様に沿ったVATではありませんのでご注意ください。
環境
- Houdini 21.0.440
CopernicusでVAT作成
Block COPのFeedback Loopを用いそのフレームでのアトリビュートを逐次書き込むことでVATを作成します。
ブロックはそのフレームでのジオメトリを入力できるようにシムをオンにしています。
入力ジオメトリをSOP Import COPに設定しています。
既存のROPの出力を参考に画像左上からアトリビュートデータを書き込み横解像度よりポイントが大きい場合は折り返しています。
Houdini上で完結するため値の正規化等は行っていません。
主要ノードの説明をします。
1. Python Snippet SOP(init_texture)
このPython Snippet COPでは入力パラメータをもとにVATの書き込みレイヤーの初期化を行っています。
PythonでLayer COP Verbを実行することで空のレイヤーを生成しています。
COP Verbがわからない方に説明するとAPEX Callbackのラッパーです。
COP VerbのPythonでの扱いに関しては以下の公式ドキュメントが参考になります。
#bind geo geo
#bind layer !&dst float4
#bind parm start_frame int
#bind parm end_frame int
#bind parm tgt_width int
import math
import hou
# パラメータ取得
start_frame: int = kwargs["start_frame"]
end_frame: int = kwargs["end_frame"]
tgt_width: int = kwargs["target_width"]
# ジオメトリから情報取得
geo: hou.Geometry = kwargs["geo"]
numpt: int = geo.intrinsicValue("pointcount")
# フレーム数および1フレームの高さ
total_frames: int = end_frame - start_frame + 1
height_per_frame: int = math.ceil(float(numpt) / tgt_width)
# レイヤー全体の高さ
tgt_height: int = height_per_frame * total_frames
# Layer COP Verbの設定
lay_verb: hou.CopVerb = hou.copNodeTypeCategory().nodeVerb("layer")
parms = {
"signature": "f4",
"f4": [0, 0, 0, 0],
"setres": 1,
"res": [tgt_width, tgt_height]
}
lay_verb.setParms(parms)
# Verbの実行
output: dict = lay_verb.execute({})
dst: hou.ImageLayer = output["layer"]
# レイヤーアトリビュートの設定
dst.setAttributes({
"height_per_frame": height_per_frame,
"target_width": tgt_width,
"total_frames": total_frames
})
return {"dst": output["layer"]}
2. SOP Invoke COP(rename_attr)
このSOP Invoke Cop内のSOPネットワークではVATに書き込みたいポイントアトリビュートをOpenCL COPで読み込む名前に変更しつつvector4型に変更しています。
ここで任意のアトリビュート名を指定することでその値をVATとして書き出すことができます。
string pt_attr_name = chs("target_attribute");
float temp[] = point(0, pt_attr_name, @ptnum);
p@_vatdata = set(temp);
3. Python Snippet COP(crop_texture)
このPython Snippet COPでは対象フレームでの書き込み範囲内のみメモリ確保するためにCrop COP Verbを用いクロップしています。
hou.ImageLayerのメソッドでもData Windowの設定はできるのですが他ノードに渡した際、エラーが出るためCrop COP Verbを使用しています。
#bind layer src float4
#bind layer !&dst float4
#bind layer !&crop float4
import hou
src: hou.ImageLayer = kwargs["src"].freeze()
# レイヤーアトリビュートを取得
frame: int = src.attributes().get("frame", 0)
height_per_frame: int = src.attributes()["height_per_frame"]
tgt_width: int = src.attributes()["target_width"]
total_frames: int = src.attributes()["total_frames"]
# Crop COP Verbの設定
crop_verb: hou.CopVerb = hou.copNodeTypeCategory().nodeVerb("crop")
parms = {
"signature": "f4",
"units": 2,
"xy_pixel": [0, height_per_frame * (total_frames - frame - 1)],
"rt_pixel": [tgt_width, height_per_frame * (total_frames - frame)]
}
crop_verb.setParms(parms)
# Verbの実行
output: dict = crop_verb.execute({"source": src})
crop: hou.ImageLayer = output["crop"]
# レイヤーアトリビュートの更新
src.updateAttributes({"frame": frame + 1})
return {"dst": src, "crop": crop}
4. OpenCL COP
このOpenCL COPではアトリビュートの値を取得してピクセルに設定しています。
前処理にてCrop COP Verbでメモリ範囲を絞っているため対象フレームの範囲外のピクセルに対する処理は必要ありません。
#bind layer src float4
#bind layer !&dst float4
#bind point data float4 port=geo name=_vatdata
@KERNEL
{
int ptnum = @ix + @src.xres * (@src.yres - @iy - 1);
// インデックス範囲外は0で埋める
if (@data.len <= ptnum) {
@dst.set(0);
return;
}
float4 data = @data.getAt(ptnum);
@dst.set(data);
}
5. Blend COP
このBlend COPを使用して前フレームまでのレイヤーにマージしています。
BorderがConstant以外だと余計なデータがマージされます。
以下がRubberToyを変形させて24フレーム分PアトリビュートのVATを作成しているキャプチャになります。
1万ちょっとのポイント数と全24フレーム程度であればほぼリアルタイムに書き込むことができます。
ネットワークの仕様上、複数のアトリビュートの書き込みに対応できないのですがその辺はPDGで対応すればよいと思います。
CopernicusでVAT読込
VAT作成と逆の手順でデータを読み込んでアトリビュートに書き込んでいます。
Fetch COPに作成したVAT、SOP Import COPに書き込み対象のジオメトリを設定しています。
1. SOP Invoke COP(set_frame_attr)
このSOP Invoke Cop内のSOPネットワークでは読み込み対象のフレームの設定とVATから読み込んだデータを格納するための一時的なvector4型アトリビュートを初期化を行っています。
i@frame = int(@Frame - 1);
p@_vatdata = 0;
2. Python Snippet COP(crop_texture)
このPython Snippet COPでは読み込み範囲を絞るためにCrop COP Verbを用いクロップしています。
#bind geo geo
#bind layer src float4
#bind layer !&crop float4
import math
import hou
src: hou.ImageLayer = kwargs["src"].freeze()
# ジオメトリから情報取得
geo: hou.Geometry = kwargs["geo"]
numpt: int = geo.intrinsicValue("pointcount")
frame: int = geo.attribValue("frame")
# テクスチャ解像度、フレームの計算
src_width, src_height = src.bufferResolution()
height_per_frame: int = math.ceil(float(numpt) / src_width)
total_frames: int = src_height / height_per_frame
frame = max(0, min(total_frames - 1, frame))
# Crop COP Verbの設定
crop_verb: hou.CopVerb = hou.copNodeTypeCategory().nodeVerb("crop")
parms = {
"signature": "f4",
"units": 2,
"xy_pixel": [0, src_height - height_per_frame * (frame + 1)],
"rt_pixel": [src_width, src_height - height_per_frame * frame]
}
crop_verb.setParms(parms)
# Verbの実行
output: dict = crop_verb.execute({"source": src})
crop: hou.ImageLayer = output["crop"]
return {"crop": crop}
3. OpenCL COP
このOpenCL COPではVATの値を取得して値格納用アトリビュートに設定しています。
#bind layer src float4
#bind point &data float4 name=_vatdata port=geo
@KERNEL
{
int ix = @elemnum % @src.xres;
int iy = @src.yres - (@elemnum / @src.xres) - 1;
int2 ixy = {ix, iy};
float4 data = @src.bufferIndex(ixy);
@data.set(data);
}
4. SOP Invoke COP(set_attr_data)
このSOP Invoke Cop内のSOPネットワークでは一時的な値格納用アトリビュートからポイントアトリビュートに値を書き込んでいます。
string pt_attr_name = chs("target_attribute");
float temp[] = set(p@_vatdata);
resize(temp, pointattribsize(0, pt_attr_name));
setpointattrib(geoself(), pt_attr_name, @ptnum, temp);
以下がオリジナルとVATから読み込んだデータで変位させたRubberToyのキャプチャです。
APEXでVAT読込
APEXでVATを読み込むために必要な手順が多いため3段階に分けて説明します。
【1】VATを読み込むCopernicusネットワークをジオメトリに変換する
CopernicusでVAT読込で作成したネットワークを流用します。
Block COPの入力にはVATテクスチャをジオメトリにしたものと書き込み対象のジオメトリを設定しています。
1. Geometry to Layer COP
APEXの入出力はジオメトリのためこのGeometry to Layer COPでジオメトリからレイヤーに変換します。
2. Block to Geometry COP
このBlock to Geometry COPでBlock COPの内部ネットワークをAPEXグラフジオメトリに変換します。
【2】1のネットワークを実行するネットワークを作成しジオメトリに変換する
【1】のネットワークを直接APEX Graph Invoke SOPで実行しところエラーが出たためこのInvoke Geometry COPを経由しています。
1. Invoke Geometry COP
このInvoke Geometry COPの入力に【1】のAPEXグラフジオメトリと【1】のネットワークの入力を設定します。
2. Block to Geometry COP
このBlock to Geometry COPでBlock COPの内部ネットワークをAPEXグラフジオメトリに変換します。
【3】APEX Graph Invoke SOPで実行
このAPEX Graph Invoke SOPで【2】のネットワークを実行します。
レイヤーのジオメトリ化
特に難しいことをする必要はなくCOP Network SOP内部でレイヤーをディスプレイフラグのついた状態にする、もしくはCOP Network SOPにパスを指定すると出力はジオメトリ化されたレイヤーになります。
応用
APEXリグに入れ込む
APEXでVAT読込で作成したものを流用します。
1. Pack Folder SOP
このPack Folder SOPではキャラクターストリームを作っています。
| 名前 | 説明 |
|---|---|
| Base.rig | COPをInvokeするAPEXグラフジオメトリ |
| VatToAttr.graph | VATの値をアトリビュートに設定するAPEXグラフジオメトリ |
| Vat.shp | ジオメトリ化したVATレイヤー |
| Base.shp | VATの値を適用するジオメトリ |
2. APEX Script SOP
このAPEX Script SOPを使用してグラフを変更します。
HeaderとFooterのコードはAPEX Autorig Component SOPからコピーしてきます。
VATの値を書き込むアトリビュートをコンポーネントパラメータとして表に出します。
rigname: String = BindInput()
character: Geometry = BindInput()
rigname, graph = character.extractCharacterGraph(graph_name=rigname)
# ========== Start Component =============
# VATの値を書き込むアトリビュートをコンポーネントパラメータとして設定
attr_name: String = BindInput()
# 元のネットワークにおける"set_frame_attr"と"rename_attr"の代替
def init_geo_subnet(frame: Int, attr_name: String, geo: Geometry) -> Geometry:
geo.setDetailAttribValue(attribname="frame", value=frame)
geo.setDetailAttribValue(attribname="_vatattr", value=attr_name)
snippet2: String = """
p@_vatdata = 0;
"""
geo = geoutils.wrangle(geo0=geo, snippet=snippet2, dataclass=2)
return geo
# 元のネットワークにおける"vatdata_to_attr"の代替
def vatdata_to_attr_subnet(geo: Geometry) -> Geometry:
snippet: String = """
string pt_attr_name = detail(0, "_vatattr");
float temp[] = set(p@_vatdata);
resize(temp, pointattribsize(0, pt_attr_name));
setpointattrib(geoself(), pt_attr_name, @ptnum, temp);
"""
geo = geoutils.wrangle(geo0=geo, snippet=snippet, dataclass=2)
return geo
# 全てのノードを取得しサブネット化
nodes = graph.findNodes("*")
invoke_copnet: ApexNodeID = graph.packSubnet("invoke_copnet", nodes)
# 各種ノードの作成
tm: ApexNodeID = graph.addNode("tm", "Value<Matrix4>")
to_int: ApexNodeID = graph.addNode("to_int", "Convert<Float,Int>")
# コントローラーの作成
frame_control = graph.addAbstractControl(tm.value_out, "Frame", "x")
# ジオメトリをコピー
base_geo: ApexNodeID = graph.addNode("base_geo", "Value<Geometry>")
# サブネットを作成
init_geo: ApexNodeID = graph.addSubnet(init_geo_subnet, "init_geo")
vatdata_to_attr: ApexNodeID = graph.addSubnet(vatdata_to_attr_subnet, "vatdata_to_attr")
# "init_geo"サブネットのパラメータを設定
init_geo.setParms({"attr_name": attr_name})
# ネットワークの接続
frame_control.x_out.connect(to_int.a_in)
to_int.b_out.connect(init_geo.frame_in)
base_geo.value_out.connect(init_geo.geo_in)
init_geo.geo_out.connect(invoke_copnet.src_geo_in)
invoke_copnet.dst_geo.connect(vatdata_to_attr.geo_in)
# プロモート
base_geo.parm_in.promoteInput("Base.shp")
invoke_copnet.cop_graph_in.promoteInput("VatToAttr.graph")
invoke_copnet.vat_geo_in.promoteInput("Vat.shp")
vatdata_to_attr.geo_out.promoteOutput("Base.shp")
# グラフを整理
graph.layout()
# ========== End Component =============
character.updateCharacterGraph(graph_name=rigname, graph=graph, bypass=False)
BindOutput(character)
3. APEX Autorig Component SOP
このAPEX Autorig Component SOPを使用してコンポーネントパラメータとしてVATから読み込んだ値を適用するアトリビュートを設定します。
4. APEX Configure Controls
コントローラーの見た目とリミットを設定します。
Animate Viewer State
Animate Viewer Stateを起動するとコントローラーで変形できるようになります。
おわりに
このシステムを用いれば他にもキャッシュを時間をずらしながらコピーし配置とかもできると思うので是非使ってみてください。



















