5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CopernicusとAPEXとVATでいろいろする

Last updated at Posted at 2025-12-09

はじめに

この記事はHoudini 21のCopernicusの新機能を用いてVAT的なものを作ったり使ったりする内容です。
筆者はゲームエンジンを使用しないためUE等で扱うことのできる正式な仕様に沿ったVATではありませんのでご注意ください。

2025-12-03_23h23_45_1.gif

環境

  • Houdini 21.0.440

CopernicusでVAT作成

Block COPのFeedback Loopを用いそのフレームでのアトリビュートを逐次書き込むことでVATを作成します。
ブロックはそのフレームでのジオメトリを入力できるようにシムをオンにしています。
入力ジオメトリをSOP Import COPに設定しています。
既存のROPの出力を参考に画像左上からアトリビュートデータを書き込み横解像度よりポイントが大きい場合は折り返しています。
Houdini上で完結するため値の正規化等は行っていません。

image.png

image.png

image.png

主要ノードの説明をします。

1. Python Snippet SOP(init_texture)

このPython Snippet COPでは入力パラメータをもとにVATの書き込みレイヤーの初期化を行っています。
PythonでLayer COP Verbを実行することで空のレイヤーを生成しています。
COP Verbがわからない方に説明するとAPEX Callbackのラッパーです。
COP VerbのPythonでの扱いに関しては以下の公式ドキュメントが参考になります。

init_texture
#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として書き出すことができます。

image.png

rename_attr:Run Over[Points]
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を使用しています。

crop_texture
#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でメモリ範囲を絞っているため対象フレームの範囲外のピクセルに対する処理は必要ありません。

opencl1
#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を使用して前フレームまでのレイヤーにマージしています。

BorderConstant以外だと余計なデータがマージされます。

image.png

以下がRubberToyを変形させて24フレーム分PアトリビュートのVATを作成しているキャプチャになります。

2025-12-03_22h34_22_1.gif

1万ちょっとのポイント数と全24フレーム程度であればほぼリアルタイムに書き込むことができます。

ネットワークの仕様上、複数のアトリビュートの書き込みに対応できないのですがその辺はPDGで対応すればよいと思います。

CopernicusでVAT読込

VAT作成と逆の手順でデータを読み込んでアトリビュートに書き込んでいます。
Fetch COPに作成したVAT、SOP Import COPに書き込み対象のジオメトリを設定しています。

image.png

1. SOP Invoke COP(set_frame_attr)

このSOP Invoke Cop内のSOPネットワークでは読み込み対象のフレームの設定とVATから読み込んだデータを格納するための一時的なvector4型アトリビュートを初期化を行っています。

image.png

set_frame_attr:Run Over[Detail]
i@frame = int(@Frame - 1);
rename_attr:Run Over[Points]
p@_vatdata = 0;

2. Python Snippet COP(crop_texture)

このPython Snippet COPでは読み込み範囲を絞るためにCrop COP Verbを用いクロップしています。

crop_texture
#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の値を取得して値格納用アトリビュートに設定しています。

opencl1
#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ネットワークでは一時的な値格納用アトリビュートからポイントアトリビュートに値を書き込んでいます。

image.png

vatdata_to_attr
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のキャプチャです。

2025-12-04_23h17_23.gif

APEXでVAT読込

APEXでVATを読み込むために必要な手順が多いため3段階に分けて説明します。

【1】VATを読み込むCopernicusネットワークをジオメトリに変換する

CopernicusでVAT読込で作成したネットワークを流用します。
Block COPの入力にはVATテクスチャをジオメトリにしたものと書き込み対象のジオメトリを設定しています。

image.png

1. Geometry to Layer COP

APEXの入出力はジオメトリのためこのGeometry to Layer COPでジオメトリからレイヤーに変換します。

2. Block to Geometry COP

このBlock to Geometry COPBlock COPの内部ネットワークをAPEXグラフジオメトリに変換します。

【2】1のネットワークを実行するネットワークを作成しジオメトリに変換する

【1】のネットワークを直接APEX Graph Invoke SOPで実行しところエラーが出たためこのInvoke Geometry COPを経由しています。

image.png

1. Invoke Geometry COP

このInvoke Geometry COPの入力に【1】のAPEXグラフジオメトリと【1】のネットワークの入力を設定します。

2. Block to Geometry COP

このBlock to Geometry COPBlock COPの内部ネットワークをAPEXグラフジオメトリに変換します。

【3】APEX Graph Invoke SOPで実行

このAPEX Graph Invoke SOP【2】のネットワークを実行します。

image.png

image.png

レイヤーのジオメトリ化

特に難しいことをする必要はなくCOP Network SOP内部でレイヤーをディスプレイフラグのついた状態にする、もしくはCOP Network SOPにパスを指定すると出力はジオメトリ化されたレイヤーになります。

応用

APEXリグに入れ込む

APEXでVAT読込で作成したものを流用します。

image.png

1. Pack Folder SOP

このPack Folder SOPではキャラクターストリームを作っています。

名前 説明
Base.rig COPをInvokeするAPEXグラフジオメトリ
VatToAttr.graph VATの値をアトリビュートに設定するAPEXグラフジオメトリ
Vat.shp ジオメトリ化したVATレイヤー
Base.shp VATの値を適用するジオメトリ

image.png

2. APEX Script SOP

このAPEX Script SOPを使用してグラフを変更します。
HeaderとFooterのコードはAPEX Autorig Component SOPからコピーしてきます。
VATの値を書き込むアトリビュートをコンポーネントパラメータとして表に出します。

APEX Script
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から読み込んだ値を適用するアトリビュートを設定します。

image.png

4. APEX Configure Controls

コントローラーの見た目とリミットを設定します。

image.png

Animate Viewer State

Animate Viewer Stateを起動するとコントローラーで変形できるようになります。

2025-12-06_21h56_19.gif

おわりに

このシステムを用いれば他にもキャッシュを時間をずらしながらコピーし配置とかもできると思うので是非使ってみてください。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?