DaVinci Resolve Fuse開発に関するメモ
Fuseとは?
概要っぽいもの
Fusionでは各種ツールを組み合わせることで画像処理が可能。
Mergeツールで画像を重ねたり、Transformツールで回転や拡大縮小したり。
そういう画像処理ツールを自作したものがFuseプラグイン。
あとモディファイアも作れるらしい。
使用言語はLua。
変更を加えたらすぐにリロードして試せるので、開発しやすい。(基本的には変更する度にアプリケーションの再起動をする必要はない)
参考資料
このスクラップは下記資料を参考に記述しています。
注意点
このスクラップでは上記資料を完全に翻訳したり、全ての要素を漏れなく記述するものではありません。なので「ここで書いてないから存在しない」と早合点しないように注意してください。
普通に記述漏れとかありますので、完全な情報を知りたい方は各自で調査したり公式資料を翻訳してください。
Fuseの公式テンプレート
Windows の場合、テンプレートは下記フォルダにあった。
C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Developer\Fusion\Fuse」
基本的に Developer フォルダの下を探せば良いと思われる。
Fuse を使うには
Fuse ファイルを指定のフォルダに配置する
Windows の場合、Fuse ファイルを下記のフォルダに配置すると書かれているけど、反映されなかった。
C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Fusion\Fuses
(そもそも Support フォルダの下に Fusion フォルダがない)
パスマップを確認したところ、反映される Fuses フォルダはデフォルトだと3つくらいある。
C:\Users\ユーザ名\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Support\Fusion\FusesC:\ProgramData\Blackmagic Design\DaVinci Resolve\ Fusion\Fuses-
C:\Program Files\Blackmagic Design\DaVinci Resolve\ Fusion\Fuses

(パスマップは Fusion ページに切り替えて、上部メニューからFusion→Fusion設定で Fusion 設定画面を開き、「パスマップ」の項目で確認できる。
一覧からFusesの行を右クリックすると該当フォルダパスが表示されるので、そのパスをクリックすると該当フォルダを開くことができる)
Fuse を使うために配置するフォルダはパスマップに含まれているところであれば、どのフォルダでも OK。
今回はその内の一つ、下記のフォルダにコピーしたところ無事に読み込むことができた。
C:\ProgramData\Blackmagic Design\DaVinci Resolve\Fusion\Fuses
アプリケーションを再起動する
Fuse ファイルはアプリケーションを起動するタイミングで読み込まれる。
なので、指定フォルダに Fuse ファイルを配置した際、アプリケーションを既に起動していたなら再起動する。
無事に読み込めていれば画像のように Fusion ページでツール選択できるようになっている。

Fuse で記述する各イベント処理
Fuse の記述は以下の関数とイベント処理に分けられる。
基本的に上に位置するほどイベントの処理タイミングが早い。
なお、必ず定義する必要があるのは以下の3つ。
FuRegisterClassCreateProcess
上記に加え、基本的にNotifyChangedイベント関数も記述することになる。
(必須ではないものの、大体の Fuse はコントロール操作が必要になるので)
FuRegisterClass(name, ClassType, attribute):必須
- アプリ起動時に実行される関数(イベント処理ではない)。
- そのため、ここを変更したときだけはアプリの再起動が必要になる。
- ツール名や説明、分類カテゴリなどの基本情報の設定。
- 場合によっては複数定義可能。
Create():必須
- ツールを Fusion コンポジションに追加したときのイベント処理。
- イメージの入力と出力、その他コントロール(GUI)の設定が基本。必須のコントロールはここで追加しておくのが良い。
- ここで定義した変数は他のイベント関数で参照可能。
OnAddToFlow()
- ツールをフロー内に配置したときのイベント処理。
- モディファイアはコントロールに追加された時に処理される。
- 配置したコンポジション毎、あるいは配置したタイミングの再生フレームなどに応じて初期化したい場合に処理を記述する。
- 例:フレームタイミングを調整したいので配置したコンポジションの FPS を取得するなど
OnConnected(link, old, new)
- ツールの入力へ接続があった際に呼び出されるイベント処理。
- ノードのイメージ入力だけでなく、コントロールへのモディファイア追加時も処理される。
NotifyChanged(inp, param, time)
- コントロールの値を変更したときのイベント処理。
- 必須ではないが記述しないことの方が少ない。
- キースプラインやモディファイアなどで値が変化したときも処理される。
- 他のコントロールの操作(コントロール間の連動)も行うことが可能。
CheckReqest(req)
-
Process関数より前に処理される。 - コントロールからの値を上書きして
Process関数に渡すことも可能で、不要なレンダリングを回避するなどの用途もある。
PreCalcProcess(req)
-
Process関数が呼び出される前に処理される。 - 予想される画像サイズ、DoD などを Fusion に伝えるために使用される。
- 基本的に使用する必要はないものの、特定のケースにおいてはカスタムする必要がある。
- 例えば、画像の拡大縮小したりマージしたり等、入力と出力で最適な DoD が異なる処理をする場合など
Process(req):必須
- メイン処理はここに記述。
- この関数では最終的にノードの場合は画像を、モディファイアの場合は値を出力する。
FuRegisterClass(string name, enum ClassType, table attribute)
実行タイミング
アプリの起動時、Fuse ツールを初めて読み込むときに実行される。
そのため、FuRegisterClass の設定に変更を加えた場合はアプリの再起動が必要となる。
引数
-
name
- Fusion が識別するためのプラグイン ID。他の Fuse と被ってはいけない。
- 被った場合は特にエラーは表示されず、後から読み込まれた Fuse の内容が優先される模様。読み込み順の指定はできないので「Fuse ファイルを更新したのに反映されない!」などの混乱を防ぐためにも極力被せないように注意。
- 後述の
REGS_Name属性に指定がなければ Fuse の名前としてそのまま使われる。 - 渡せる文字列には英数字とアンダーバー( _ )のみ使用可能。
-
Create関数などで参照したいときはself.RegNode.m_IDで取得可能- ただし、この方法で取得すると
Fuse.nameのように先頭にFuse.が付く-
comp:GetToolList()で該当 Fuse を絞り込んで検索したい場合、Fuse.nameというようにFuse.を含めて渡すと良い
-
- ただし、この方法で取得すると
- Fusion が識別するためのプラグイン ID。他の Fuse と被ってはいけない。
-
ClassType
Fuse の種類を識別する定数。以下の3種類から一つ指定可能。-
CT_Tool: ノード -
CT_Modifier: モディファイア -
CT_ViewLUTPlugin: ビューLUT
-
-
attribure
ツールのその他定義属性。属性ごとに値を格納したテーブルを渡すことで設定する。
次の項目で説明。
必須属性
-
REGS_Category : string
- ツールが表示されるカテゴリを設定する文字列値。
-
Script\\Colorというように記述すると、Script カテゴリの下に Color カテゴリを作成する。 - ツールを分類するときに設定することになる。
-
REGS_OpIconString : string
- ツール名の略語。ツールバーメニューとビンで使用される。
-
REGS_OpDescription : string
- Fusion インターフェースの様々な部分で使用されるツールの簡単な説明。
全タイプ共通のオプション属性
-
REGS_Name : string
- Fuse 名
- FuRegisterClass の引数 name は ID(種類)として運用し、名前が別途必要なときに設定
- 例:Number 型と Point 型の両方に対応したモディファイアを作成する場合、name は別に設定し、REGS_Name は同じ名前にする等
- 設定値は
self.RegNode.m_Nameで取得可能
-
REG_TimeVariant : bool (デフォルトは false)
- カレントフレームの変更時に処理を行うか
- false だと Fuse をリロードしたりモディファイアを適用したタイミングのように、Fuse を読み込んだタイミングに設定されていたフレームを参照して1回だけ処理している?
- 再生時間に合わせてアニメーションさせたいなら true にしとけばOKな模様
- ただし、true にすると毎フレーム処理を行うので描画負荷が高くなる点に注意
-
REGS_HelpTopic : string
- ヘルプを記載したファイルや Web ページの URL
-
REGS_URL : string
- 作成者のページなどの URL ?
-
REG_Fuse_NoEdit : bool
- 編集ボタンの非表示フラグ
- 編集ボタンを押すと
Fusion設定で設定されたスクリプトエディタで該当 Fuse ファイルを開く

赤枠内に設定したエディタで開く
-
REG_Fuse_NoReload : bool
- リロードボタンの非表示フラグ
- Fuse を編集したら、リロードボタンを押すことで反映される
( FuRegisterClass の編集時のみ、アプリの再起動が必要)

Fuse の編集ボタン、リロードボタン
-
REG_Version : string
- バージョン情報
-
REG_NoPreCalcProcess
-
PreCalcProcessイベント実行の際、Processイベント用の処理を呼び出すようになる
(ただし、PreCalcProcess用の関数が定義されている場合、PreCalcProcess用の関数を優先する) -
PreCalcProcessとProcess、どちらで処理中かは Process 関数内でRequest.IsPreCalc()を実行すれば判別可能
-
ノード作成時のオプション属性
-
REGID_DataType : string
-
REGID_InputDataType : string
- 対応するコントロールの種類
-
Image: 画像を扱うコントロール
-
- 対応するコントロールの種類
-
REG_OpNoMask : bool
- マスク用の画像 Input を表示し、処理に使えるようにする
-
REG_NoBlendCtrls : bool
- ブレンドできるかどうか?
-
REG_NoObjMatCtrls : bool
- オブジェクトマットからのマスキングが出来るかどうか?
-
REG_NoMotionBlueCtrls : bool
- モーションブラーを使用できるかどうか?
-
REGS_IconID : string
- エフェクト欄で表示された際のアイコン ID 。
-
Icons.Tools.Icons.Dissolveという風に指定すると、該当する既存ツールのアイコンを割り当てることが可能な模様。
-
REG_Fuse_TilePic : table
- タイル表示したときに表示される画像を Bitmap 形式の文字列で設定できる。

VFXPedia の Switch.Fuse の場合 - サイズは最大で幅 160*120px まで指定可能
- 設定時の table は以下の3つのキーを持つこと。
-
Width: int (1~160) -
Height: int (1~120) -
Bitmap: string- RGBA の値をそれぞれ 2 桁の 16 進数に変換し、8 桁の文字列で 1 つのピクセルとする
- 横方向は半角スペースで区切り、縦方向は改行で区切る
- 実物を見た方が早いので、VFXPedia の Fuse 例のコードを参照することをオススメする
VFXPedia の TilePic.Fuse のコード
-
- タイル表示したときに表示される画像を Bitmap 形式の文字列で設定できる。
モディファイア作成時のオプション属性
-
REGID_DataType : string
-
REGID_InputDataType : string
- 対応するコントロールの種類
-
Text: 文字列を扱うコントロール -
Number: 数値を扱うコントロール -
Point: 座標を扱うコントロール -
FuID: ID (文字列)を扱うコントロール
-
- 複数のコントロールに対応することも可能
- 例:
Number型とPoint型の両方に設定可能なモディファイアを作成したい場合、一つの Fuse ファイルにNumber型用とPoint型用の 2 つの FuRegisterClass を定義すればOK
- 例:
- 対応するコントロールの種類
コントロールの追加について
基本的には Create イベントでコントロールを設定する。
ネストなどの一部を除き、既存ノードに追加できるユーザコントロールと設定できる項目は大体同じ。
MultiMerge ノードのように入力イメージの数に応じて入力コネクタを増やしたい場合
は OnConnected イベントで追加するのが良い。例:Switch.fuse
また、読み込んだファイルに応じてコントロールを追加したい時は NotifyChanged イベントに記述することになる。
コントロールの追加方法
入力は self:AddInput() 関数、出力は self:AddOutput() 関数で追加する。
ほとんどのケースにおいて Output は一つは設定することになる。
(画像を RGB 値で分けたり、複数設定するケースもある)
self:AddInput(labelname, scriptname, attributes)
self:AddOutput(labelname, scriptname, attributes)
-
labelname:string, 必須
- コントロールの横に表示されるラベルの文章。利用者がわかりやすいものを付ける。
-
Input:GetAttr("LINKS_Name")で取得可能 -
Input:SetAttrs({LINKS_Name = "好きな文章"})で変更可能
-
scriptname:string, 必須
- スクリプトから参照するための名前。例えば Loader ノードの FileControl は
Loader1.Clipで参照できるようにClipという scriptname が付けられている。 -
Input:GetAttr("LINKID_ID")で取得可能
- スクリプトから参照するための名前。例えば Loader ノードの FileControl は
-
attributes:table, 必須
- 各コントロールの種類に応じた属性を格納したテーブル。コントロールで指定できる最小値・最大値など、内容は様々。
-
Input:GetAttr(attr_name)で取得可能(引数は取得したい属性名)- Fusion スクリプトと異なり GetAttrs ではない点に注意(末尾に s が付かない)
- 引数を省略して属性設定値一覧を取得、ということができないため
- Fusion スクリプトと異なり GetAttrs ではない点に注意(末尾に s が付かない)
-
Input:SetAttrs({attr_name1 = value1, attr_name2 = value2, ...})で変更可能- 属性名をキー( attr_name )に、設定値( value )を格納したテーブルを引数とする
- ほとんどのコントロールに共通する汎用的な属性については FusionSDK の 49 ~50P に記載
実際の追加は下記のように行う。
( DaVinci Resolve のサンプル「Example 1 Bright Contrast.fuse」から抜粋。)
function Create()
InBright = self:AddInput("Brightness", "Brightness", {
LINKID_DataType = "Number",
INPID_InputControl = "SliderControl",
INP_MaxScale = 1.0,
INP_MinScale = -1.0,
INP_Default = 0.0,
})
InContrast = self:AddInput("Contrast", "Contrast", {
LINKID_DataType = "Number",
INPID_InputControl = "SliderControl",
INP_MaxScale = 1.0,
INP_MinScale = -1.0,
INP_Default = 0.0,
})
InSaturation = self:AddInput("Saturation", "Saturation", {
LINKID_DataType = "Number",
INPID_InputControl = "SliderControl",
INP_MaxScale = 5.0,
INP_MinScale = 0.0,
INP_Default = 1.0,
ICD_Center = 1,
})
InImage = self:AddInput("Input", "Input", {
LINKID_DataType = "Image",
LINK_Main = 1,
})
OutImage = self:AddOutput("Output", "Output", {
LINKID_DataType = "Image",
LINK_Main = 1,
})
end
コントロールの種類(コネクタ)
LINKID_DataType 属性で指定する。
-
Image:画像入力。self:AddInput()で指定すると入力用コネクタが、self:AddOutput()で指定すると出力用コネクタが追加される。
コントロールの種類(インスペクタ)
LINKID_DataType 属性で指定する。
| コントロールの種類 | 概要 |
|---|---|
| ImageControl | Wireless Link ノードのようにコネクタを介さずに画像入力できる。 ノード名を入力するか、ノードをドラッグ&ドロップで設定する。 入力用コネクタの一種なので、このコントロールの値を変更した場合 OnConnectedイベントが発生する。OnConnectedイベントの後にNotifyChangedイベントも発生するが、引数paramがnilとなる。 |
| ButtonControl | ボタン。 クリックすると BTNCS_Execute 属性に設定されたコマンド(文字列で設定)を実行する。また、NotifyChanged イベントが2回連続発生する。(引数 param.Valueが 1 回目は 0 → 1 、2 回目は 1 → 0 に変化)リロードなど、ユーザに任意のタイミングで処理をさせたいときに使用。 |
| CheckControl | チェックボックス。CBC_TriState属性を有効にするとON/OFF以外の第3状態も設定できるようになる。ON=1, OFF=0, 第3状態=-1
|
| ColorControl | カラー設定用コントロール。 スライダーやボタンなどのセット。設定項目(RGBA 等)の数だけ追加する必要がある。 全項目を追加する必要はなく、例えば赤のスライダーだけ追加しても良い。 |
| ComboControl | コンボボックス。選択アイテムのインデックス(数値)を返す。 |
| ComboIDControl | データ型が FuID のコンボボックス。選択アイテムの表示文字列、もしくはアイテム毎に設定した ID を返す。挙動についてはこちらの記事で解説。。 |
| FileControl | ファイル選択用。 ダイアログ表示用のボタンも付く。 |
| FontFileControl | フォント選択用コンボボックス。 端末にインストールされているフォントの一覧を選択できる。 |
| GradientControl | グラデーション設定用。 白~黒のスライダー。 |
| LabelControl | ラベル。 情報表示用。 ラベルの表示文字列は TextEditControl のように値として保有しているわけではないため、LabelControl:SetSource("変更文字列", 0) では設定できない。LabelControl:SetAttrs({LINKS_Name="変更文字列"}) で変更すること。 |
| MultiButtonControl | マルチボタン。Normal タイプだとラジオボタンのような挙動となり、押されているボタンは0 ~ ボタン数 - 1 の数値で取得可能。Toggle タイプだと複数選択が可能となり、押しているボタンは ビット(2進数)で管理される(ただしボタンの状態は 10 進数に変換されて返却される)。 |
| MultiButtonIDControl | データ型が FuID のマルチボタン。押されているボタンに応じた ID を取得可能。 Toggle タイプは押してもボタンが反応しなくなるので併用不可。挙動についてはこちらの記事で解説。 |
| OffsetControl | X 座標と Y 座標の2つの入力欄を持つ。 主に座標設定に用いる。 |
| RangeControl | 2 つのつまみを持つスライダー。 主に範囲の設定に用いる。 |
| ScrewControl | 最小値も最大値もない無限のスライダー。 回転角度のように上下限がない数値の設定向き。 |
| SliderControl | スライダー。 主に一定範囲内の値の設定に用いる。デフォルトでは実数値となるが、属性設定でスライダーの動きを整数値に制限することもかのう。入力範囲が広すぎると細かい数値調整がしづらくなるため、場合によっては ScrewControl の方が向いていることも。 |
| TextEditControl | テキストボックス。 行単位で高さを調整できるため、1 行分の簡単な入力ボックスから長文入力用のボックスなども設定できる。 |
コントロールの種類(OnScreen UI Widgets)
他のコントロールに追加設定する画面コントロール群。
追加すると座標や角度など、UI 上で直感的に設定することが可能。
INPID_PreviewControl 属性で指定する。

ウィンドウ中央の矢印や点線の円が OnScreen コントロール
| コントロールの種類 | 概要 |
|---|---|
| PointControl | 1点を示す小さい■。スポイト機能のような1点の位置調整用。 |
| AngleControl | 円と角度を示す線を持つ。角度の設定用。 |
| CrosshairControl | ↑と→のクロスヘアを持つ。座標などの設定用。 |
| RectangleControl | 四角形を表示。切り抜きなど、画像処理したい範囲の設定向き? |
ネストの設定(インスペクタ)
ネストの開始位置を self:BeginControlNest(labelname, scriptname, open_state)、
ネストの終了位置を self:EndControlNest() で指定する。
self:BeginControlNest() の第3引数はネストの初期状態が開いているかどうかをbool 値で設定する。
デフォルトは true(ネストが開いた状態)。
下記のようにネストに格納したいコントロールを self:BeginControlNest() と self:EndControlNest() で挟んで使う。
self:BeginControlNest("ネストラベル", "TestNest", false) -- ネストの開始(第3引数は初期状態で開くかどうか)
InSlider = self:AddInput("スライダー", "SliderInput", {
LINKID_DataType = "Number",
INPID_InputControl = "SliderControl",
INP_MaxScale = 6.0,
INP_MinScale = 1.0,
INP_Default = 1.0,
IC_Steps = 1.0,
})
self:EndControlNest()

複雑な構造のネストを作成する
self:BeginControlNest() と self:EndControlNest() の間でネストを追加する関数を実行した場合、入れ子構造になる。
function nest2_add()
nest2 = self:BeginControlNest("Nest2", "Nest2", true, {})
InLabel3 = self:AddInput("label3", "Label3", {
LINKID_DataType = "Text",
INPID_InputControl = "LabelControl",
})
self:EndControlNest()
end
function Create()
nest1 = self:BeginControlNest("Nest1", "Nest1", true, {})
InLabel1 = self:AddInput("label1", "Label1", {
LINKID_DataType = "Text",
INPID_InputControl = "LabelControl",
})
InLabel2 = self:AddInput("label2", "Label2", {
LINKID_DataType = "Text",
INPID_InputControl = "LabelControl",
})
nest2_add() -- 関数でネスト追加
self:EndControlNest()
end

つまり、再帰関数によるネストの設定が可能になる。
これを利用すると、複雑な階層のネストも作りやすくなる。
コントロールを異なるページに複製する
「特定のコントロールを他のページでも操作できるようにしたい」という場合はself:CloneInput(object , "clone_id", attr_table)で複製できる。
-
object:Input, 必須
- 複製したいコントロールを指定。
-
clone_id:string, 必須
- 複製したコントロールのID。複製であることと、複製元コントロールがわかるような ID を推奨。
-
attr_table:table, 必須
- 複製時に変更したい属性テーブル。例えば、複製元からラベル名を変更したい場合
{LINKS_Name="クローンコントロール名"}というように指定する。 - テーブルを指定しないとエラーが出て複製されないので注意。
- 変更しない場合は空テーブルを渡せば良いが、一部属性は変更を推奨。
-
INP_DoNotifyChanged:false(無効化)- 値変更時に
NotifyChangedイベントを発生させる属性。
連動するオリジナルコントロールの方でイベント処理すれば良いので有効化する意味がない 。 - むしろ有効化すると異常終了の原因となり得るのでそういう意味でも無効化しておいた方が無難。
- 値変更時に
-
INP_InteractivePassive:true- 値変更時にレンダリング要求されない(
Processイベントが発生しない)ようになるが、Ctrl+Zなどによる操作の取り消し・やり直しは有効にする。 - オリジナルとクローンで合わせて2回レンダリング要求が走るのは無駄なので
trueでパッシブ化しておくと場合によっては少しだけ軽くなるかもしれない。
- 値変更時にレンダリング要求されない(
-
- 複製時に変更したい属性テーブル。例えば、複製元からラベル名を変更したい場合
function Create()
InSlider = self:AddInput("スライダー", "SliderInput", {
LINKID_DataType = "Number",
INPID_InputControl = "SliderControl",
INP_MaxScale = 6.0,
INP_MinScale = 1.0,
INP_Default = 1.0,
IC_Steps = 1.0,
})
self:AddControlPage("新ページ") -- 新しいページを追加
InSliderClone = self:CloneInput(InSlider, "SliderInput_Clone", { -- コントロール複製。第3引数の属性テーブルを忘れないように注意
INP_DoNotifyChanged = false, -- オリジナルのみ処理できれば良いので無効化
INP_InteractivePassive = true, -- オリジナルの変更と合わせて Process イベントが無駄に2回走るので Passive 化
})
end
複製したコントロールは複製元と同期しており、片方を変更するともう片方も同じ値に変更される。
そのため、複製コントロールのためにNotifyChangedイベント用の処理を追加する必要はない。
むしろInput:GetSource(time)でエラー落ちする要因の一つにもなり得るので記述するべきではない。
ページの設定(インスペクタ)
ページの追加は self:AddControlPage(pagename) で、
ページの削除は self:RemoveContorolPage(pagename) で可能。
新規コントロールは直前に追加されたページに追加される。
新規コントロールを別のページに追加したい場合、既存ページの名前を引数に渡して実行すると切り替えられる。
self:AddControlPage()でできることまとめ
| 実施したい処理 | 実施方法 |
|---|---|
| 新規ページの追加 | 第1引数を既存ページとは異なる名前にして実行 |
| 既存ページへの切り替え | 第1引数を切り替えたい既存ページと同じ名前にして実行 |
| 既存ページを非表示にする | 第1引数を非表示にしたい既存ページと同じ名前に、 第2引数を {CT_Visible = false} にして実行 |
| 既存ページを再表示する | 第1引数を再表示したい既存ページと同じ名前に、 第2引数を {CT_Visible = true} にして実行 |
ちなみに Fuse にはデフォルトでコントロールページがあるが、下記のようにコントロールページを削除したり非表示にすることで最初のページを変更できる。
(アプリ上はページ名がコントロールだが、ページを非表示にするときは Controls で指定すること)
-- self:RemoveControlPage("Controls") -- コントロールページの削除
self:AddControlPage("Controls", { CT_Visible = false }) -- コントロールページを非表示
self:AddControlPage("新ページ")

デフォルトだとコントロールページが一番左にある

コントロールページを非表示にして、追加ページを一番左にした
NotifyChangedイベントでコントロールページで条件分岐したい場合
特定のページにあるコントロールの値変更時に条件分岐したい場合、コントロールページ番号を照合すればOK。
self:AddControlPage("新ページ名")は追加ページのインデックスを返却するため、Input:GetAttr("IC_ControlPage")でコントロールの所属するページ番号と比較すると良い。
function Create()
NumPageIndex = self:AddControlPage("New Page") -- ページ追加、番号保存
InSlider = self:AddInput("スライダー", "SliderInput", {
LINKID_DataType = "Number",
INPID_InputControl = "SliderControl",
INP_MaxScale = 6.0,
INP_MinScale = 1.0,
INP_Default = 1.0,
IC_Steps = 1.0,
})
end
function NotifyChanged(inp, param, time)
if inp:GetAttr("IC_ControlPage") == NumPageIndex then
print(inp:GetAttr("LINKS_Name") .. "のページ番号" .. NumPageIndex)
end
end
コントロールと出力の値の取得・設定方法
スクリプト、およびフレームレンダースクリプトとはやり方が違うので注意が必要。
コントロール値の取得方法
Input:GetValue(req)
レンダリング要求が来た時点の値を取得できる。
引数はProcessイベント関数などに引数として渡されるRequestオブジェクトをそのまま渡して使用する。
Input:GetSource(time)
指定した時間(フレーム)の状態を取得できる。
NotyfiChangedイベントなどで値を取得したい場合や、前後の数フレーム分の値を参照して計算したい場合などに用いる。
基本的にキースプラインなどでアニメーションさせない設定の場合、Input:GetSource(0).Valueという風に値を取得すると良い。
アニメーションしている場合、引数timeに適切な値を渡すこと。
コントロール値の設定方法
Input:SetSource(value, time) で指定した時間(フレーム)に値を設定できる。
第 1 引数と第 2 引数の順番を間違えないように注意。
値設定時のデータ型
第 1 引数に渡せる値はコントロールのデータ型に応じて変換する必要がある。
画像を扱うImageクラスを除いた数値・文字列タイプのデータ型の変換対応表は下記の通り。
| コントロールのデータ型 | 変換関数 | 変換元のデータ型 |
|---|---|---|
| Number | Number(v) | number |
| Point | Point(x, y) | number(2~3つ) |
| Text | Text(v) | string |
| FuID | FuIDParam(v) | string |
いずれも Fuse で定義されてるオリジナルクラスなので、第 1 引数に渡す際はText("str value")という風に変換する必要がある。
もしText型に変換せずにstring型のまま、あるいはNumber型に変換せずにnumber型のまま引数へ渡したりした場合、値が反映されないので注意すること。
コンソールにもエラーが出ないので気が付きにくい。
基本的にキースプラインなどでアニメーションさせない設定の場合、Input:SetSource(Text("text"), 0)という風に値を設定すると良い。
アニメーションさせる場合、引数timeに適切な値を渡すこと。
出力(Output)の値の設定方法
Output:Set(req, value) で設定できる。
第1引数のreq (Reqestクラス)はProcessイベント、PreCalcProcessイベントハンドラへ引数として渡されるものなので、Outputの値設定は実質的にこの2つのイベント専用。
Input:SetSource()のように時間を指定して値を設定することはできないので注意。
以下は固定値を設定する簡単な例。
function Process(req)
print("再生フレーム:" .. req.Time)
OutValue:Set(req, Number(1))
end
メモ(後でコントロールの属性、あるいはトラブルシューティングにまとめます)
コントロールには INP_Required 属性がある。デフォルトは true。
INP_Required = true の場合、該当する全コントロールの値が設定されていないとProcessイベントが処理されない。
NotifyChangedなどの他のイベントは処理されるのにProcessイベントが処理されない場合、コントロールの値の設定忘れがないか確認したり、設定が必須ではないコントロールは INP_Required = false に設定し忘れていないか確認すること。
メモ(後でコントロールの属性、あるいはトラブルシューティングにまとめます)
AddInput()でイメージ入力コネクタを追加した場合、何かノードを接続しないとエラーを吐いてレンダリングができなくなることがある。
(Processイベント処理でコネクタからの入力を使用しているかは関係ない模様)

ノードにイメージ入力コネクタがある場合、特に何も指定していないとProcessイベントの前のPreCalcProcessイベントで上流ノードを元に DoD 計算などの処理を行おうとするため、コネクタ接続がないと存在しない上流ノードを処理しようとしてエラーを吐く模様。
対策としてはPreCalcProcessイベント関数を記述する。
あるいはFuRegisterClass関数で REG_NoPreCalcProcess = true を設定し、Processイベント関数内にPreCalcProcessイベント用の処理を記述する。
下記は後者の例。
if req:IsPreCalc() then -- 事前計算
local img_precalc = Image({IMG_Like = out, IMG_NoData = true}) -- ダミーデータ生成
OutImage:Set(req, img_precalc)
else
OutImage:Set(req, out)
end
また、Processイベント関数が記述されていない場合も同様のエラーが出る。
process(頭文字が小文字)になっていないか気を付けること。
画像ファイルから画像を読み込む方法
Loaderノードのように選択した映像メディアを読み込むにはClipオブジェクトを使う。
Clipオブジェクトのプロパティ
画像を読み込む例
function Process(req)
local clip = Clip(filename, false) -- 画像ファイル読み込み設定
clip:Open()
local img = clip:GetFrame(0) -- 画像フレーム読み込み
clip:Close()
OutImage:Set(req, img) -- 画像出力
end
Clip(string filepath, boolean save, FusionDoc doc)
- filepath:読み込むファイルパス
- save:存在しないパスを許容するか? 読み込むときはfalseで良い。
- doc:よくわからず。
Clip:GetFrame(int frame)
- frame:取得するフレーム番号
事前にClip:Open(filename)を実行しておくこと。
また、Loaderノードのように画像ファイル名末尾が数値の場合、シーケンシャル(連番)ファイルとして認識されるため、適切なフレーム番号を指定する必要がある。
シーケンシャルファイルではない場合、0を指定しておけば特に問題はない。
Clip:PutFrame(int frame, Image img)
- frame:出力する際のフレーム番号?
- img :出力するイメージ
事前にClip:Open()を実行しておくこと。
イメージを保存する。(※実際の挙動は確認していません)
Clip:Open()
指定ファイルを開き、画像の取得や保存を可能とする。
Clip:Close()
指定ファイルを閉じる。必要な処理が終わったらちゃんとクローズすること。
シーケンシャルファイルの画像をアニメーションさせずに表示する方法①(ファイル名制限あり)
正規表現でファイル名末尾の数値を取得し、Clip:GetFrame()の引数に渡せばOK。
表示フレームはFileControlのNotifyChangedイベントで予め抽出しておき、self:SetData()で保存、self:GetData() でProcessイベントから参照する方法もある。
function Process(req)
local filename = self.Comp:MapPath(InFile:GetValue(req).Value) -- ファイルパス取得
local frame = tonumber(filename:match(".?(%d+)%.%w+$")) -- ファイル名末尾の数値取得
if frame == nil then -- シーケンシャルファイルではない場合、0を指定
frame = 0
end
local clip = Clip(filename, false) -- 画像ファイル読み込み設定
clip:Open()
local img = clip:GetFrame(frame) -- 画像フレーム読み込み
clip:Close()
OutImage:Set(req, img) -- 画像出力
end
シーケンシャルファイルの画像をアニメーションさせずに表示する方法②(ファイル名制限なし)
どうしてもファイル名末尾の数値が連番でかつ抜けがないという保証ができない(例えば撮影日の日付や時刻が付いてしまうなど)場合、正規表現で数値を抜き出してシーケンシャルファイルの中で何番目なのかを調べる必要がある。
ファイル名の一覧から、該当ファイル数値が何番目か調べるソースコードの例は下記のとおり。
-- 検索対象となるファイル名リスト(テスト用)
local filenamelist = {"Test12345.jpg", "TestExtend10000.jpg", "Test9999", "Test128.jpg", "Test99999.jpg"}
-- ファイルパスをフォルダパス、拡張子、拡張子なしのファイル名に分割し、ファイル名末尾の数値を検索
local filepath = "C:/TestDir1/dir2/Test12345.jpg" -- 検索対象ファイルパス(テスト用)
local dirpath = filepath:sub(1, -filepath:reverse():find("/"))
local ext = filepath:sub(-filepath:reverse():find("%."))
local filename = filepath:sub(#dirpath+1, -(#ext+1))
local num_pos, num_len = filename:reverse():find("%d+")
-- ファイル名末尾が数値の場合、数値がシーケンシャルファイルの中で何番目になるかソートして調べる
-- ファイル名末尾が数値ではない場合は 0
local idx = 0
if num_pos == 1 then
local commonname = filename:sub(1, num_len-1)
local filenumber = tonumber(filename:sub(-num_len, -1))
local ptrn = commonname .. "%d+" .. ext
local numberlist = {}
for i,v in ipairs(filenamelist) do
if v:match(ptrn) then
numberlist[#numberlist+1] = tonumber(v:match("%d+"))
end
end
table.sort(numberlist) -- 数値ソート
for i,v in ipairs(numberlist) do
if v == filenumber then
idx = i - 1
break
end
end
end
print(idx) -- 結果は 2-1 = 1 となる
上記を踏まえて画像として読み込む場合は下記のコードとなるはず。(後日、動作確認予定)
bmdライブラリのreaddir(path)関数は指定ファイルパスに合致するファイル・フォルダのデータを一覧で取得する。ワイルドカードによる拡張子指定も可能。(例:JPGファイルのみ取得したい場合は「C:/dir/*.jpg」など)
-- ファイルパスをフォルダパス、拡張子、拡張子なしのファイル名に分割し、ファイル名末尾の数値を検索
local filepath = InFile:GetSource(0).Value -- ファイルパス
local dirpath = filepath:sub(1, -filepath:reverse():find("/")) -- フォルダパス
local ext = filepath:sub(-filepath:reverse():find("%.")) -- 拡張子
local filename = filepath:sub(#dirpath+1, -(#ext+1)) -- 拡張子抜きのファイル名
local num_pos, num_len = filename:reverse():find("%d+") -- ファイル名の数値の出現位置と長さ
-- ファイル名末尾が数値の場合、数値がシーケンシャルファイルの中で何番目になるかソートして調べる
-- ファイル名末尾が数値ではない場合は 0
local idx = 0
if num_pos == 1 then
local commonname = filename:sub(1, num_len-1)
local filenumber = tonumber(filename:sub(-num_len, -1))
-- 同名のシーケンシャルファイルから数値抽出
local ptrn = commonname .. "%d+" .. ext
local numberlist = {}
for i,v in ipairs(readdir(dirpath .. "*" .. ext)) do
if v.Name:match(ptrn) then
numberlist[#numberlist+1] = tonumber(v.Name:match("%d+"))
end
end
table.sort(numberlist)
for i,v in ipairs(numberlist) do
if v == filenumber then
idx = i - 1
break
end
end
end
-- クリップを開いて画像読み込み
local clip = Clip(filepath, false)
clip:Open()
local img = clip:GetFrame(idx) -- 画像フレーム読み込み
clip:Close()
他のノードやタイムラインを参照・操作する方法
基本的に Fuse は他のノードやタイムライン等に直接干渉はできない。
しかし、CompositionオブジェクトはExecute(command_str)というメソッドを持っているため、self.Comp:Execute(command_str)で間接的にスクリプト実行が可能。
例えば、下記の処理を実行するとカレントタイムライン名を参照できる。
local exe_str = string.format([[
dump(resolve)
local project = resolve:GetProjectManager():GetCurrentProject()
dump(project:GetName())
local tl = project:GetCurrentTimeline()
dump(tl:GetName())
]])
self.Comp:Execute(exe_str)

実行結果
通常のスクリプトの範囲内であればself.Comp:Execute(command_str)で大体なんとかできる。
とはいえ、プロジェクトやタイムライン、他のノードについては情報の参照程度に留めておき、操作はしない方が無難。そういう総括的な操作は Fuse ではなくスクリプトで実装した方が良い。
注意点①:変数の渡し方
上記の方法では変数を直接渡せないので工夫する必要がある。
とりあえず思いつくのは下記の2通り。
-
string.format(command_str, arg1, arg2, ...)でコマンド文に変数の内容を埋め込む -
tool:SetData(key_str, value)で一度値を保存しておき、コマンド内でtool:GetData(key_str)で取り出す
1つ目の方法は直感的でやりやすいが、テーブルを渡すのには向かない。(できるにはできるが、コマンド文に埋め込む手間を考えると推奨できない)
2つ目の方法はひと手間かかるものの、キー名で値を取り出せる。テーブルを渡すならこちらの方が楽だと思われる。
注意点②:オブジェクトの参照は渡せない
上記の方法ではオブジェクト自体を渡すことができない。
渡せるのは基本的な型(数値・文字列・真偽値・テーブル)くらい。
そのため、例えば、ノード自体を参照・操作したいなら下記のようにコマンド文の中で改めて取得する必要がある。
local exe_str = string.format([[
tool = comp:FindTool("%s") -- ノード名で参照してノードを取得する
print("FuseName : " .. tool.Name)
]], self.Name)
self.Comp:Execute(exe_str)
注意点③:コントロールの属性名が変わる
Fuse・マクロから参照する場合とスクリプトから参照する場合でコントロールの属性名が変わるので注意が必要。

Fuse・マクロ からとスクリプトから参照したときで属性名が若干異なる
基本的には Fuse ・マクロから参照した際の属性名の頭にINP〇_が追加される。
(元からINP_IntegerのようにINP_から開始する属性名の場合、INPB_Integerのように〇の部分だけ挿入される)
〇の部分は属性値の型によって変わる模様。
-
INPB:Bool(真偽値)の B -
INPN:Number(数値)の N -
INPI:Integer(整数)の I -
INPS:String(文字列)の S -
INPID:ID -
INP○T:○のデータ型の Table(テーブル)の T- 例:Bool 型のテーブルを設定する属性なら
INPBT
- 例:Bool 型のテーブルを設定する属性なら
全ての属性名を把握するのは難しいので、必要に応じてスクリプトから参照した場合の属性一覧と照らし合わせて確認するのが良い。
下記のスクリプトをコンソールで実行すれば属性一覧を表示可能。
tool = comp:FindTool("NodeName") -- 属性名を取得したいノード名をここで指定
input = tool.ControlName -- コントロールはメンバー参照か、
-- input = tool["ControlName"] -- テーブルのキー指定で参照可能
dump(input:GetAttrs()) -- GetAttrs を引数なしで実行すると全属性が表示される
注意点④:実行タイミングは記述したイベントの後
self.Comp:Execute(command_str)を記述した位置ではなく、それを記述したイベント処理が終わってから実行される模様。(ちゃんと検証したわけではないので正確ではないかも)
例えば、NotifyChangedイベント内で実行した場合、NotifyChangedイベントが終了してから実行される。
function NotifyChanged(inp, param, time)
print("TestFuse_NotifyChanged開始==================")
dump(inp:GetAttr("LINKID_ID"))
dump(param.Value)
local exe_str = string.format([[
dump(resolve)
local project = resolve:GetProjectManager():GetCurrentProject()
dump(project:GetName())
local tl = project:GetCurrentTimeline()
dump(tl:GetName())
]])
self.Comp:Execute(exe_str)
print("TestFuse_NotifyChanged終了==================")
end

NotifyChangedイベントが終了してからself.Comp:Execute(command_str)が実行されている
イベントが終わった後にコマンド文が実行される、くらいの感覚。
基本的にはコントロールの値を設定するだけに留めるのが無難そう。
注意点⑤:コマンド実行結果は直接受け取れない
self.Comp:Execute(command_str)はコマンド文の実行をするだけであり、関数のように返却値を設定したりはできない。
実行結果を保持しておきたい場合、コマンド文の中でtool:SetData(key_str, value)で値を保存し、Fuse 側でtool:GetData(key_str)で読みだすことが可能。
また、注意点④に述べた実行タイミングのズレを考えると、コマンド文の実行結果を受け取って即座に Fuse での別処理に使用する、という場合は工夫が必要になる。
コマンド実行結果を元に Fuse での処理を続けたい場合
コマンド文の中でコントロールの値を変更することで、コマンド文実行後に続けてNotifyChangedイベントを誘発できる。
処理段階に応じて複数のコントロールを変更していくのも良いが、それだと処理段階に応じてコントロールを追加する羽目になり、操作しないコントロールが増えて管理が煩雑になる。
self:SetData() self:GetData() で実行ステップを更新していけば単体のコントロールの分岐処理で実装可能。
以下はボタンを押すと、3回連続でNotifyChangedイベント→コマンド文実行を交互に繰り返すソースコード例。
elseif inp == StartButton and param.Value == 1 then
print("StartButton NotifyChanged =============")
if self:GetData("ExecuteStep") == nil then
local exe_str = string.format([[
print("実行ステップ:1回目")
tool = comp:FindTool("%s")
tool:SetData("ExecuteStep", 1) -- 次の処理ステップ設定
tool:SetInput("%s", 1) -- コントロール操作
]], self.Name, "StartButton")
self.Comp:Execute(exe_str)
elseif self:GetData("ExecuteStep") == 1 then
local exe_str = string.format([[
print("実行ステップ:2回目")
tool = comp:FindTool("%s")
tool:SetData("ExecuteStep", 2) -- 次の処理ステップ設定
tool:SetInput("%s", 1) -- コントロール操作
]], self.Name, "StartButton")
self.Comp:Execute(exe_str)
elseif self:GetData("ExecuteStep") == 2 then
local exe_str = string.format([[
print("実行ステップ:3回目")
tool = comp:FindTool("%s")
tool:SetData("ExecuteStep") -- 処理ステップ削除(次実行時は最初から)
tool:SetInput("%s", 0) -- コントロール操作(最後の操作なので 0 に戻しておく)
]], self.Name, "StartButton")
self.Comp:Execute(exe_str)
end

実行結果
コンボボックスやマルチボタンの選択アイテムを後から追加
SetAttrs()メソッドで選択アイテムを動的に追加できる。
ただし、追加しただけでは表示に反映されない(選択肢が増えない)。
ComboIDControl:SetAttrs({CCS_AddString = "文字列", CCID_AddID ="add_id"})
MultiButtonIDControl:SetAttrs({MBTNC_AddButton = "文字列", MBTNCID_AddID ="add_id"})
self:UpdateControls()というメソッドもあるようだが、2025/1/21現在では実行しても特に何も起きない。
SetAttrs()で追加されたアイテムはインスペクタ欄の表示を更新すると反映される模様。
インスペクタ欄を更新する方法としては下記の3種類を確認している。
1. 別のコントロールページに切り替えてから元のコントロールページに再度切り替える

Fusion ページのノードの場合

Edit ページのクリップの場合
2. アプリのFusionページなら他のノードを選択し、元のノードを選択し直す
3. アプリのEditページなら他のクリップを選択し、元のクリップを選択し直す
1つ目のページ切り替えについてはtool:ShowControlPage("PageName")を2回実行することで自動化できる。
cnt = cnt + 1 -- 実行したカウント(ID重複回避用)
MultiIDTest:SetAttrs({MBTNC_AddButton = "Add_"..cnt, MBTNCID_AddID = "AddID_"..cnt}) -- マルチIDボタンに選択肢の追加
-- ページを切り替えて追加したアイテムを表示できるようにする
local exe_str = string.format([[
tool = comp:FindTool("%s")
tool:ShowControlPage("Common")
tool:ShowControlPage("Controls")
]], self.Name)
self.Comp:Execute(exe_str)
ただし、Fusionページのノード限定であり、Editページに配置したクリップのインスペクタ欄には行うことができない。
例えば、ノードAのコントロールを表示・操作できるように設定したマクロファイル(TestMacro.setting)をEditページでタイムラインに配置したとする。
本体であるマクロ自体はFusionページのフロー上に存在するが、このマクロのページを切り替えてもEditページに配置されたクリップのインスペクタ欄は元のページのままであり、連動しない。
そしてEditページに配置されたクリップのインスペクタ欄のページを変更するコマンドは存在しないため、スクリプトからページ切り替えは不可能。ユーザーに手動で切り替えてもらうしかない。
ちなみにInput:SetAttrs()でIC_Visible属性を操作してコントロールの表示・非表示を切り替えたり、ネストの開閉を行っても追加選択肢の表示はできない。インスペクタ欄のページ単位での更新が必要。
DCTL について1(定義編)
Fuse では DCTL と呼ばれる機能がある。
DCTL は GPU で各ピクセルを並列処理することが可能であり、実行環境にもよるが大幅な高速化が見込める。
ただし、基本的な画像処理に関しては Fuse に実装されている関数(Image:Merge()メソッドなど)を使う方が高速な場合もあるため、どんな処理も DCTL で記述することが最適解となるわけではない点は留意する必要がある。
既存の関数では実装が難しい、あるいは実装できたものの速度的に使い物にならない場合の解決策としては非常にアリ。
例外的にImage:MultiProcessPixels()(複数CPUによる各ピクセルの並列処理)で画像処理を実装するくらいなら最初から DCTL を使った方が大体高速になる。
記述・実行方法の概要
構文はC 言語とほぼ同じ。
おおまかな使い方としては以下の通り。
- 処理に使うパラメータ(構造体)定義と各ピクセル処理関数定義を文字列で記述
-
Processイベント内でパラメータ定義と処理関数定義を指定し、処理ノードを作成 - パラメータに値を格納し、ノードにパラメータを設定
- ノードを実行
パラメータ定義
DCTL 処理で参照するパラメータを定義する。
パラメータの記述方法は C 言語の構造体と似ているが、メンバーだけ宣言すれば良い。
以下はパラメータの定義例。
ImageParams = [[
int src_size[2]; // 入力画像サイズ
int dst_size[2]; // 出力画像サイズ
]]
なお、パラメータが不要なら特に定義する必要はない。
カーネル関数(メイン処理部)の定義
DCTL で実行するカーネル関数を定義する。
各ピクセルごとに処理する内容はここで実行することになる。
以下に入力画像をそのまま出力する例を示す。
StraightSource = [[
__KERNEL__ void StraightKernel(__CONSTANTREF__ ImageParams *params, __TEXTURE2D__ src, __TEXTURE2D_WRITE__ dst)
{
DEFINE_KERNEL_ITERATORS_XY(x, y) // 処理座標 x, y を取得
if (x < params->src_size[0] && x < params->dst_size[0] // 入力・出力画像サイズ内なら処理
&& y < params->src_size[1] && y < params->dst_size[1]){
float4 col = _tex2DVec4(src, x, y); // 入力画像の座標(x,y)のピクセル RGBA を取得
_tex2DVec4Write(dst, x, y, col); // 出力画像の座標(x,y)にピクセル RGBA を設定
}
}
]]
コードの各箇所の説明。
-
__KERNEL__ void KernelName:カーネル関数名。 -
__CONSTANTREF__ ParamName *param:パラメータ名とその引数名。- 複数指定は不可能?
- 不要な場合は省略可能。
-
__TEXTURE2D__ src:入力画像の引数名。- 複数指定可能。
- 参照しないなら省略可能
-
__TEXTURE2D_WRITE__ dst:出力画像の引数名。- 複数指定可能。つまり異なる処理をした画像を同時に出力できる。
-
_tex2DVec4``_tex2DVecNによるピクセルの読取はできない(ノード作成で失敗し、エラーを吐く)
-
DEFINE_KERNEL_ITERATORS_XY(x, y): 処理座標を取得して変数x,yに代入。- 末尾に
;(セミコロン)は不要。
- 末尾に
-
params->src_size:パラメータのメンバー参照。- Lua であれば
params.src_sizeと同義で、とりあえずパラメータ変数とメンバー名を->で繋げば良いと覚えれば十分。
- Lua であれば
-
_tex2DVec4(src, x, y):入力画像srcの座標(x,y) のピクセルカラーRGBAを格納したfloat4型として取得。- 各色の値は
0.0f~1.0fの範囲となる模様。 - ピクセルカラー情報の取得方法は他にも3種類ほどある。
- 各色の値は
-
_tex2DVec4Write(dst, x, y, col):出力画像dstの座標(x,y) にピクセルカラー情報colを書きこむ。-
colは_tex2DVec4(src, x, y)で取得するなど、RGBA情報を格納したfloat4型であれば良い。
-
ユーザ関数の定義
プログラミングをする上で、特定の処理を効率化・コードの簡略化のために関数を自作することはほぼ必須である。
といっても難しいことはなく、関数定義の頭に__DEVICE__を付ければOK。
ユーザ関数は下記のように、メイン処理と同じソース文字列の中に記述する。
StraightSource = [[
// ユーザ関数定義(ここではRGBAにそれぞれ0.5をかける)
__DEVICE__ float4 harf_color(float4 col)
{
float old_col[4] = {col.x, col.y, col.z, col, w}; // float4 は float 型を4つ持っており、最初から順番に x, y, z, w のメンバーに格納される
float4 new_col = to_float4_v(old_col); // to_float4_v は配列 → float4 へ変換する関数
new_col *= 0.5f; // RGBA 全てに 0.5 を乗算
return new_col;
}
// カーネル処理関数
__KERNEL__ void StraightKernel(__CONSTANTREF__ ImageParams *params, __TEXTURE2D__ src, __TEXTURE2D_WRITE__ dst)
{
// ... 処理
}
]]
float2, float4 型について
画像処理で頻繁に利用する座標系や RGBA 情報を扱うためにfloat2、float4という型が存在する。
特にfloat4に関しては画像のピクセル取得・設定を行う際に必ず使用することになる。
float2とfloat4の違いは格納できるfloat型の値が 2 つか 4 つかというだけで使い方は変わらない。
値の設定方法
-
to_float4(val1, val2):2 つのfloat型の引数からfloat2型の値を生成する。 -
to_float4(val1, val2, val3, val4):4 つのfloat型の引数からfloat4型の値を生成する。- 引数は省略不可。
-
to_float2_v(float val[2]):長さ 2 のfloat型配列からfloat2型の値を生成する。 -
to_float4_v(float val[4]):長さ 4 のfloat型配列からfloat4型の値を生成する。- 引数の配列の長さが 4 未満の場合、RGBA の順番に配列の値が格納され、不足分は 0.0f、A(透明度)のみ 0.5f が設定される?
- 引数の配列の長さが 5 以上の場合、最初~4番目の要素までが RGBA の順番に格納され、残りは無視される?
-
_tex2DVec4(src, x, y):入力画像srcの座標(x,y) のピクセルカラーRGBAを格納したfloat4型として取得。 -
_tex2DVecN(src, x, y, order):入力画像srcの座標(x,y) のピクセルカラーRGBAを格納したfloat4型として取得。-
_tex2DVec4(src, x, y)との違いとしては、RGBAのどの色を取得するかを引数orderへ1~15の値で指定して設定可能。orderの値については後述。
-
また、この他にも_tex2DSamplerVec4(src, sampler, x, y) _tex2DSamplerVecN(src, sampler, x, y, order)がある。_tex2DVec4(src, x, y) _tex2DVecN(src, x, y, order)との違いについては応用編のサンプラーの設定の項目で説明する。
_tex2DVecN() _tex2DSamplerVecN()関数の引数orderに渡す値に応じて取得できるRGBA情報パターンは以下の通りになると思われる……が、後述の理由であまり使い道がない。
| 引数orderの値(2進数) | 取得できるRGBA情報 |
|---|---|
| 15(1111) | RGBA全て |
| 8(1000) | R(赤)のみ |
| 4(0100) | G(緑)のみ |
| 2(0010) | B(青)のみ |
| 1(0001) | A(透明度)のみ |
_tex2DVecN() _tex2DSamplerVecN()関数の説明ページはこちら。
値の参照方法
float4に格納された値は順番にx,y,z,wというメンバで参照できる。
例えば、入力画像のピクセル情報を取得→参照する場合は下記のようなコードになる。
float4 col = _tex2DVec4(src, x, y);
float4 new_col = to_float4(col.x, col.y, col.z, col.w);
RGBA を個別に操作したい場合の参考として、上記の例だと
col.x → Red(赤)col.y → Green(緑)col.z → Blue(青)-
col.w → Alpha(透明度)
という風に格納されている。
値の計算方法
float2, float4に四則演算をする方法は2通りある。
-
float2,float4同士の演算- 各要素(座標XY, RGBA)毎にバラバラの値を計算できる
-
float4 col = to_float4(1.0f, 1.0f, 1.0f, 1.0f); col *= to_float4(0.1f, 0.2f, 0.3f, 0.4f); // RGBA それぞれに別の値を乗算
-
float型との演算- 各要素(座標XY, RGBA)全てに同じ値を計算できる
-
float4 col = to_float4(1.0f, 1.0f, 1.0f, 1.0f); col *= 0.5f; //col *= to_float4(0.5f, 0.5f, 0.5f, 0.5f); // 計算結果はこれと同じになる
各要素ごとにバラバラの値を計算したい場合はfloat2, float4同士、全要素に一括で同じ計算をしたい場合はfloat型で計算する、という風に使い分けをすると、計算の度に一々float4を生成する必要が減るので実装効率が少し良くなるかもしれない。
数学系の処理について
DCTL では計算用の関数が予め用意されている。
どのような関数があるかは Fusion SDK を参照。
DCTL について2(実行編)
DCTL で実行するパラメータとソースコードの定義が終了したら、Processイベントで処理ノード作成→パラメータ設定→ノードを実行する。
下記は該当部分の抜粋。
-- ノード作成
local node = DVIPComputeNode(req, "StraightKernel", StraightSource, "ImageParams", ImageParams)
-- パラメータの設定
local params = node:GetParamBlock(ImageParams)
params.src_size = {[0] = src.Width, [1] = src.Height}
params.dst_size = {[0] = out.Width, [1] = out.Height}
node:SetParamBlock(params)
-- カーネル関数の引数の内、入力・出力用イメージを設定
node:AddInput("src", src) -- 参照イメージ設定
node:AddOutput("dst", out) -- 出力先イメージ設定
-- ノード実行
local success = node:RunSession(req)
-- ノードのエラーログ表示
if not success then
print(node:GetErrorLog())
end
ノードの作成
DVIPComputerNode()でカーネル関数ソースとパラメータを指定し、ノードを作成する。
local node = DVIPComputeNode(req, "KernelName", KernelSource, "ParamsName", ParamBlock)
-
DVIPComputeNode():後述する引数をもとにノードを作成し、返却する。-
req:Processイベントなどで引数として渡されるRequest型の値。 -
"KernelName":KernelSourceで定義されているカーネル関数名。 -
KernelSource:カーネル関数ソースを格納した文字列。 -
"ParamsName":KernelSourceで定義されているカーネル関数の引数のパラメータ型名。引数名ではなく引数の型の名前。 -
ParamBlock:パラメータ定義を格納した文字列。
-
パラメータの設定
ノードを作成したらnode:GetParamBlock()でパラメータ設定用の変数を取得→パラメータに値を格納→node:SetParamBlock()でノードにセットする。
local params = node:GetParamBlock(ParamBlock) -- パラメータ設定用変数取得
params.src_size = {[0] = src.Width, [1] = src.Height} -- パラメータへ値を格納
node:SetParamBlock(params) -- ノードにパラメータセット
-
node:GetParamBlock():パラメータ設定用の変数を作成し、返却する-
ParamBlock:パラメータ定義を格納した文字列。
-
-
node:SetParamBlock():値を格納したパラメータ用変数をノードに設定する。
参照・出力先画像の設定
カーネル関数の引数で定義している参照用画像の__TEXTURE2D__ と、結果出力先となる画像__TEXTURE2D_WRITE__型の引数にImage型の変数を設定する。
node:AddInput("src", src) -- 参照イメージ設定
node:AddOutput("dst", out) -- 出力先イメージ設定
- node:AddInput("ArgumentName", Image):参照用画像
__TEXTURE2D__型引数の設定。- "ArgumentName":カーネル関数で定義されている引数名。
-
Image:引数に設定するImage型変数。
- node:AddOutput("ArgumentName", Image):参照用画像
__TEXTURE2D_WRITE__型引数の設定。- "ArgumentName":カーネル関数で定義されている引数名。
-
Image:引数に設定するImage型変数。
参照用画像__TEXTURE2D__型引数を複数定義している場合、それぞれにnode:AddInput()で画像をセットする必要がある。
引数が多い場合、対応する引数名と画像をテーブルに格納し、ループ処理でテーブル参照しつつ設定していくと短く記述できる。
ノード実行
パラメータと画像の設定が終わったらノードを実行する。
実行結果が正常か異常かをbool型で返却されるので、それを元に条件分岐すると良い。
local success = node:RunSession(req)
-
node:RunSession():ノードを実行する。正常に終了すればtrue、異常終了した場合はfalseを返却する。-
req:Processイベントなどで引数として渡されるRequest型の値。
-
エラーログの表示
node:RunSession()をすると、ソースコードの記述にミスがあってビルドができなかったり、ノードの実行に失敗した場合はコンソールにエラー表示されるが、 「ビルドに失敗した」などの簡素な内容なので問題点の特定がしにくい。
具体的なエラー内容はnode:GetErrorLog()で取得できる。
print(node:GetErrorLog())
例えば、未定義の関数を呼び出そうとしていると画像のようなエラーログを取得できる。

未定義の関数harf_colorを実行しようとしている箇所が表示された
DCTL コード例
上記までを踏まえて実装したDCTLはこんな感じ。
画像サイズによって処理速度は変動する(画像サイズが小さいほど速い)。
--[[
DCTL で入力画像をそのまま出力するテストコード
--]]
FuRegisterClass("DCTLStraight", CT_Tool, {
--REGS_Name = "DCTLStraight", -- Fuse 名
REGS_Category = "Fuses",
REGS_OpIconString = "DST",
REGS_OpDescription = "DCTLテスト用",
REGID_DataType = "Image",
REGID_InputDataType = "Image",
-- REG_TimeVariant = true,
-- REGB_Unpredictable = true, -- キャッシュを持たない
REG_NoCommonCtrls = true,
REG_NoPreCalcProcess = true, -- 事前計算あり
-- REG_Fuse_NoEdit - false, -- 編集ボタン不可視化
-- REG_Fuse_NoReload = false, -- リロードボタン不可視化
REG_Version = 1.0,
})
--=============================================================
-- DCTL用パラメータ
ImageParams = [[
int src_size[2];
int dst_size[2];
]]
--=============================================================
-- 渡された画像をそのまま出力する処理ソース
StraightSource = [[
__KERNEL__ void StraightKernel(__CONSTANTREF__ ImageParams *params,
__TEXTURE2D__ src, __TEXTURE2D_WRITE__ dst)
{
DEFINE_KERNEL_ITERATORS_XY(x, y)
if (x < params->src_size[0] && x < params->dst_size[0]
&& y < params->src_size[1] && y < params->dst_size[1]){
float4 col = _tex2DVec4(src, x, y);
_tex2DVec4Write(dst, x, y, col);
}
}
]]
--=============================================================
function Create()
InFile = self:AddInput("画像ファイル", "File", {
LINKID_DataType = "Text",
INPID_InputControl = "FileControl",
FC_IsSaver = false, -- 存在しないパスを指定可能か
FCS_FilterString = "PNGファイル (*.png)|*.png|", -- ファイルフィルタ
INP_Required = true, -- 入力がなければ Process関数 を実行しない
INP_DoNotifyChanged = true,
INP_External = false, -- キーフレーム、モディファイア追加不可
})
OutImage = self:AddOutput("Output", "Output", {
LINKID_DataType = "Image",
LINK_Main = 1,
})
end
function Process(req)
local filepath = InFile:GetSource(0).Value -- 入力画像ファイルパス
if not fileexists(filepath) then
local out = Image({IMG_Width = 1, IMG_Height = 1})
out:Fill(Pixel({R=1, G=0, B=0, A=0}))
print("ファイルが存在しません")
return
end
-- 入力画像読み込み(今回はシーケンシャルファイルの場合、一番小さい番号のファイルを参照する)
local clip = Clip(filepath, false)
clip:Open()
local src = clip:GetFrame(0)
clip:Close()
print("ファイル読み込み完了")
if req:IsPreCalc() then -- 事前計算
print("事前計算")
local out = Image({ IMG_Like = src, -- 入力画像の属性コピー
IMG_NoData = true, -- ダミーデータ指定(ピクセルデータ用のメモリ確保をしない)
})
OutImage:Set(req, out)
return
end
-- 出力画像初期化
local out = Image({ IMG_Width = src.Width, -- 入力画像と同じサイズの Image 作成
IMG_Height = src.Height,
})
out:Fill(Pixel({R=0, G=0, B=0, A=0})) -- 透明ピクセルで塗りつぶし
local node = DVIPComputeNode(req, "StraightKernel", StraightSource, "ImageParams", ImageParams)
if node then
local params = node:GetParamBlock(ImageParams) -- パラメータの設定
params.src_size = {[0] = src.Width, [1] = src.Height}
params.dst_size = {[0] = out.Width, [1] = out.Height}
node:SetParamBlock(params)
node:AddInput("src", src) -- 参照イメージ設定
node:AddOutput("dst", out) -- 出力先イメージ設定
local success = node:RunSession(req)
print("DCTL処理結果:" .. tostring(success))
print(node:GetErrorLog()) -- ノードのエラーログ表示
else
print("ノード作成に失敗しました")
end
OutImage:Set(req, out)
end
DCTL について3(応用編)
ここから先は DCTL の実行で必須ではないものの覚えておくと便利かもしれない要素を記述していく。
サンプラーの設定(必須)
定義編で少し触れたが、指定座標のRGBA情報を取得する方法として_tex2DSamplerVec4(src, sampler, x, y) _tex2DSamplerVecN(src, sampler, x, y, order)を挙げた。
_tex2DVec4(src, x, y) _tex2DVecN(src, x, y, order)との違いはサンプラーを明示的に指定できることである。
サンプラーについて
サンプラーは座標の参照方法について定義したものである。
定義できるのは以下の3つの要素。
- 座標指定時に返すピクセルの色
- 画像サイズ外を座標指定した際、どのように処理するか
- 座標の指定方法(絶対値か、画像全体に対する割合か)
サンプラーはnode:AddSampler()で追加できるが、RowSamplerという名前のサンプラーがデフォルトで存在するため、他のサンプラーが不要なら追加する必要もない。
サンプラーの追加方法
サンプラーの追加はnode:AddSampler()で行う。
各設定は専用の定数を指定する。
node:AddSampler("SamplerName", filterMode, addressMode, normCoordMode)
-
node:AddSampler():ノードにサンプラーを追加する。-
"SamplerName":処理定義ソース内で参照するためにつけるサンプラー名。 -
filterMode:座標指定時に返すピクセルの色の参照方法。-
TEX_FILTER_MODE_POINT:Nearest Sampling。最も近い座標のピクセルの色を返す? -
TEX_FILTER_MODE_ LINEAR:BiLinear Sampling。周囲のピクセルの色を線形補間した結果を返す?
-
-
addressMode:画面サイズ外を座標指定した際に返すピクセル情報。-
TEX_ADDRESS_MODE_BORDER:黒・透明ピクセル? -
TEX_ADDRESS_MODE_CLAMP:最も近い位置のピクセル? -
TEX_ADDRESS_MODE_MIRRAR:はみ出した分だけ境界から逆方向の位置を参照する?- 例えば座標を右側に5はみ出した場合、右側から-5の位置を参照する? 要は反射。
-
TEX_ADDRESS_MODE_WRAP:はみ出した分だけ逆側の境界からの位置を参照する?- 例えば座標を右側に5はみ出した場合、左側から5の位置を参照する? 要は折り返し。剰余的な処理というか。
-
-
normCoordMode:座標指定方法。-
TEX_NORMALIZED_COORDS_FALSE:絶対値での参照。- X 軸は 0 〜 画像幅-1、Y 軸は0 〜 画像の高さ-1 で指定する。
-
TEX_NORMALIZED_COORDS_TRUE:画像サイズに対する割合で参照。- X 軸、Y 軸共に画像サイズに対する割合を 0.0 〜 1.0 で指定する。
-
TEX_NORMALIZED_COORDS_AUTO:filterModeに応じて変わる?-
filterModeがTEX_FILTER_MODE_POINT(最近傍参照)ならTEX_NORMALIZED_COORDS_FALSE?(絶対値指定) -
filterModeがTEX_FILTER_MODE_LINEAR(線形補間)ならTEX_NORMALIZED_COORDS_TRUE(割合指定)?
-
-
-
ちなみにデフォルトサンプラーRowSamplerの設定は下記の通り。
-
filterMode:TEX_FILTER_MODE_POINT(最も近い位置のピクセル参照) -
addressMode:TEX_ADDRESS_MODE_CLAMP(最も近い位置?) -
normCoordMode:TEX_NORMALIZED_COORDS_FALSE(絶対値指定)
ちなみに_tex2DVec4(src, x, y) _tex2DVecN(src, x, y, order)はこのRowSamplerを参照している。
RowSamplerの設定を変更したい場合、下記のようにnode:AddSampler()でサンプラー名を"RowSampler"と指定して実行すれば良い。
node:AddSampler("RowSampler", TEX_FILTER_MODE_ LINEAR, TEX_ADDRESS_MODE_CLAMP, TEX_NORMALIZED_COORDS_FALSE)
複数のサンプラーを使い分けたい場合は_tex2DSamplerVec4(src, sampler, x, y) _tex2DSamplerVecN(src, sampler, x, y, order)を使うことになるが、サンプラーの設定は変更したいけど 1 種類しか必要ないという場合、RowSamplerの設定を変更しておけば_tex2DVec4(src, x, y) _tex2DVecN(src, x, y, order)で十分だと思われる。
パラメータで可変長配列を扱う
パラメータ定義文ではポインタを利用することができない。
後述するImage:GetScanLine()で取得したり、FFIなどで作成したポインタ型変数ならパラメータに設定は可能だが、ポインタの参照先の値を利用しようとするとWarning : exception during GPU buffer download(GPU バッファのダウンロード中に例外が発生しました)との警告メッセージが表示され、アプリが落ちる。
ポインタが利用できない場合、配列を宣言する際に配列サイズも決めなければならず、これでは処理したい要素に応じて配列サイズを変えることができない……というわけでもない。
パラメータ定義文はノード作成するまでは単なる文字列なので、DVIPComputeNode()関数でノード作成する前に配列サイズを書き換えてしまえば良い。
例えば、下記のようにパラメータの配列のサイズ部分だけ%dにしておく。
-- DCTL用パラメータの雛形
VariableParams = [[
int arr_size;
int arr[%d][2];
]]
これを雛形として、Processイベント内でstring.format()関数で配列サイズを書き換えてからノード作成をすれば良い。
また、パラメータ定義文はnode:GetParamBlock()でも引数に使用するので誤って雛形の方を指定しないように注意。
local SetParams = string.format(VariableParams, 10) -- 配列サイズを10に置換
local node = DVIPComputeNode(req, "TestKernel", TestSource, "VariableParams", SetParams) -- 配列サイズを書き換えたパラメータでノード作成
local params = node:GetParamBlock(SetParams) -- パラメータの設定時も置換後の文字列を使用
-- パラメータに値格納
params.arr_size = 10
for i=0, 9 do
params.arr[i] = i
end
node:SetParamBlock(params)
なお、誤って配列サイズ外へアクセスしないように配列サイズも別途パラメータに追加しないといけない。( DCTL に 配列サイズを取得する関数はないので)
少し注意点はあるが、パラメータの柔軟性が上がるので色々と実装しやすくなるはず。
入力画像を可変にする
入力画像は DCTL カーネル関数の引数で指定するため、入力画像数を柔軟に変更したいならソース文字列を書き換えれば良い。ただし、配列サイズだけ変更すれば良いパラメータに比べればやや手間がかかる。
ソース文字列の雛形例は下記の通り。
置換する箇所%sは引数部分と、入力画像を扱いやすくするために配列に格納する部分の2か所となる。
KernelSource = [[
__KERNEL__ void MultiKernel(
__CONSTANTREF__ VariableParams *params,
__TEXTURE2D__ %s, // <- 引数部分
__TEXTURE2D_WRITE__ dst)
{
__TEXTURE2D__ src_arr[] = {%s}; // <- 引数を格納する配列部分
DEFINE_KERNEL_ITERATORS_XY(x, y)
if(x < params->dst_size[0] && y < params->dst_size[1]){
float4 col = _tex2DVec4(src_arr[0], x, y); // 適当に最初の画像のピクセル取得
_tex2DVec4Write(dst, x, y, col);
}
}
]]
置換する場合はこんな感じになる。
引数名の結合は一度テーブルに格納してからtable.concat(tbl, "sep")で行うと楽だが、Lua の仕様により結合されるのは1以上の数値キーの値のみで0のキーの値は結合されないため、誤って最初の要素のキーを0にしてると結合漏れが生じる……といううっかりミスに注意。私はやりました。
-- 入力画像の格納(Input から取得する、あるいは Clip 関数で読み込むなど)
src_imgs = {} -- 入力画像リスト
for i=1, 10 do
src_imgs[i] = self:FindInput("ImageInput_" .. i):GetSource(req.Time) -- 再生フレームの画像取得
end
-- 入力画像の引数設定
img_args = {} -- 引数名リスト
for i=1, #src_imgs do
img_args[i] = "src" .. i - 1
end
-- ソース文字列書き換え
local SetSource = string.format(
KernelSource,
table.concat(img_args, ", __TEXTURE2D__ "),
table.concat(img_args, ", ")
)
-- パラメータ文字列書き換え
local SetParams = string.format(VariableParams, #src_imgs)
-- ノード作成
local node = DVIPComputeNode(req, "MultiKernel", SetSource, "VariableParams", SetParams)
-- パラメータに値格納
local params = node:GetParamBlock(SetParams)
params.arr_size = #src_imgs
for i=1, #src_imgs do
params.arr[i-1] = i
end
node:SetParamBlock(params)
-- 入力画像の設定
for i=1, #src_imgs do
node:AddInput(img_args[i], src_imgs[i])
end
node:AddOutput("dst", out) -- 出力先イメージ設定
画像とRGBA配列の相互変換
画像をRGBA配列へ変換するGetScanLine()、逆にRGBA配列を画像に変換する_FromHexString() _FromMemoryで既存の画像処理関数に頼らずに画像処理が可能になる。
まあ実行速度の検証はしていないのでどの程度の速度が出るかはさておき、他のライブラリと画像情報のやりとりがしやすくなるのは確実。
(実行速度の検証はしていないものの、処理負荷は画像サイズに比例して重くなる模様。体感では毎フレーム実行するには無視できない重さなので、画像ファイル読み込み時などに1度だけ実行しておき、Processイベントでは予め変換しておいたデータを利用するような使い方が想定される)
基本的には画像データを外部処理する場合に、データと画像の相互変換に用いることになると思われる。
画像からRGBA配列を取得
Image:GetScanLine()を実行すると画像から RGBA 配列を取得できる。
返却値は 2 次元配列であり、配列サイズは[Image.Height][Image.Width * チャンネル数]となっている。配列のインデックスは0 ~ Image.Height-1、0 ~ Image.Width * チャンネル数 -1となっているので、配列のサイズ外にアクセスしないように注意。
画像のチャンネル数についてはImage.Depthプロパティで判別可能。Image.Depth が 1~4 なら 1 チャンネル、5~8 なら4チャンネルとなる。
4チャンネルの画像から RGBA 情報を取得し、配列に格納するコードは下記の通り。
-- 画像からRGBA情報を取得し、格納する
local scanline = img:GetScanLine() -- RGBA 配列取得
local rgba = {r={}, g={}, b={}, a={}}
for h=0, img.Height-1 do
for _, v in pairs(rgba) do
v[h+1] = {}
end
for w=0, img.Width-1 do
rgba.r[h+1][w+1] = scanline[h][w*4]
rgba.g[h+1][w+1] = scanline[h][w*4+1]
rgba.b[h+1][w+1] = scanline[h][w*4+2]
rgba.a[h+1][w+1] = scanline[h][w*4+3]
end
end
RGBA 配列から画像へ変換(文字列から)
Image:_FromHexString()の引数に16進数文字列を渡して実行すると画像に変換することができる。
大まかな手順としては以下のようになる。
- 画像サイズに合わせた Image 型の変数を作成
- RGBA 情報 を 16 進数文字列に変換、結合する
- 変換した 16 進数文字列を
Image:_FromHexString()の引数にして実行
RGBA 情報を 16 進数に変換する際のフォーマットは下記の通り。
- 1 行に付き 1 ピクセル分の RGBA 情報(つまり画像の高さ×幅の分だけ改行することになる)
- 1 チャンネル あたりのバイト数に応じて16進数の桁を調整する(8bit なら2桁になるように0埋めする)(※ 4ch かつ 1byte/ch の画像でしか挙動を確かめていないため、他の形式では上手くいかない可能性もあります)
1 チャンネルあたりのバイト数についてはImage.Depthで確認できる。
| Image.Depthの値 | チャンネル数 | 1チャンネルのバイト数 |
|---|---|---|
| 1 | 1ch | 1byte(8bit) |
| 2 | 1ch | 2byte(16bit) |
| 3 | 1ch | 2byte(16bit) |
| 4 | 1ch | 4byte(32bit) |
| 5 | 4ch | 1byte(8bit) |
| 6 | 4ch | 2byte(16bit) |
| 7 | 4ch | 2byte(16bit) |
| 8 | 4ch | 4byte(32bit) |
Fusion SDK の IMG_Depth 属性についてはこちらを参照。
例えば、1byte の RGBA(4ch) のピクセルデータを変換したい場合、以下のような文字列を作成する
--RRGGBBAA
00000000
01010101
02020202
-- ~ これを画像の 高さ×幅 の分だけ繰り返し ~
FEFEFEFE
FFFFFFFF
数値を 16 進数に変換する場合は string.format("%02x%02x%02x%02x", r, g, b, a)という風に string.format関数を利用すると良い。0 埋めの数は バイト数×2となるように調整。
また、Image:GetScanLine()で読み取った画像を変換してImage:_FromHexString()にそのまま渡すと画像が上下逆になる。
これはImage:GetScanLine()だと Y 軸を下から参照するのに対し、Image:_FromHexString()では Y 軸を上から描写するためである。
そのため、RGBA 配列を 16 進数に変換する場合は Y 軸を反転させる必要がある。
RGBA 配列を 16 進数に変換するコードは下記のような感じになる。
-- RGBA 情報を2桁の16進数に変換して結合していく(1行につき1px)
-- Image:_FromHexString(str) ではY座標が上から読み込んでいくようなので、上下逆にして格納・結合する
local hex_colors = {}
for h=1, img.Height do
local row = {}
for w=1, img.Width do
-- RGBA の値を「2*byte数」桁の16進数文字列に変換
-- (IMG_Depth に応じてチャンネル数や1チャンネル当たりのバイト数が異なるので適切なものを選択。今回は1チャンネルのみのものは考慮しない)
local format = {[5] = "%02x%02x%02x%02x", -- 1px につき 4ch, 1ch あたり 8bit(int)
[6] = "%04x%04x%04x%04x", -- 1px につき 4ch, 1ch あたり16bit(int)
[7] = "%04x%04x%04x%04x", -- 1px につき 4ch, 1ch あたり16bit(float)
[8] = "%08x%08x%08x%08x"} -- 1px につき 4ch, 1ch あたり32bit(float)
local px = string.format(format[img.Depth],
rgba.r[h][w],
rgba.g[h][w],
rgba.b[h][w],
rgba.a[h][w]
)
row[w] = px
end
hex_colors[img.Height-h] = table.concat(row, "\n") -- 1行分格納
end
-- 画像の属性とRGBA配列(16進数×4の文字列)を格納
self:SetData("ImageAttr", {IMG_Width = img.Width, IMG_Height = img.Height, IMG_Depth = img.Depth})
self:SetData("ImageColorHexString", table.concat(hex_colors, "\n"))
テスト用 Fuse
入力された画像から RGBA 配列を読み込み、Processイベントで RGBA 配列を画像に変換して表示するコードは下記のとおりになる。
--=============================================================
--[[
指定画像をコード
--]]
FuRegisterClass("Image2RGBAArray", CT_Tool, {
--REGS_Name = "DCTLStraight", -- Fuse 名
REGS_Category = "Fuses",
REGS_OpIconString = "I2A",
REGS_OpDescription = "DCTLテスト用",
REGID_DataType = "Image",
REGID_InputDataType = "Image",
-- REG_TimeVariant = true,
-- REGB_Unpredictable = true, -- キャッシュを持たない
REG_NoCommonCtrls = true,
REG_NoPreCalcProcess = true, -- 事前計算あり
-- REG_Fuse_NoEdit - false, -- 編集ボタン不可視化
-- REG_Fuse_NoReload = false, -- リロードボタン不可視化
REG_Version = 1.0,
})
--=============================================================
--=============================================================
function Create()
InFile = self:AddInput("画像ファイル", "File", {
LINKID_DataType = "Text",
INPID_InputControl = "FileControl",
FC_IsSaver = false, -- 存在しないパスを指定可能か
FCS_FilterString = "PNGファイル (*.png)|*.png|", -- ファイルフィルタ
INP_Required = true, -- 入力がなければ Process関数 を実行しない
INP_DoNotifyChanged = true,
INP_External = false, -- キーフレーム、モディファイア追加不可
})
OutImage = self:AddOutput("Output", "Output", {
LINKID_DataType = "Image",
LINK_Main = 1,
})
end
function NotifyChanged(inp, param, time)
if inp == InFile then
if not fileexists(param.Value) then
print("ファイルが存在しません")
return
end
-- 入力画像読み込み(今回はシーケンシャルファイルの場合、一番小さい番号のファイルを参照する)
local clip = Clip(param.Value, false)
clip:Open()
local img = clip:GetFrame(0)
clip:Close()
-- 画像からRGBA情報を取得し、格納する
local scanline = img:GetScanLine()
dump(scanline)
local rgba = {r={}, g={}, b={}, a={}}
for h=0, img.Height-1 do
for _, v in pairs(rgba) do
v[h+1] = {}
end
for w=0, img.Width-1 do
rgba.r[h+1][w+1] = scanline[h][w*4]
rgba.g[h+1][w+1] = scanline[h][w*4+1]
rgba.b[h+1][w+1] = scanline[h][w*4+2]
rgba.a[h+1][w+1] = scanline[h][w*4+3]
end
end
-- RGBA 情報を2桁の16進数に変換して結合していく(1行につき1px)
-- Image:_FromHexString(str) ではY座標が上から読み込んでいくようなので、上下逆にして格納・結合する
local hex_colors = {}
for h=1, img.Height do
local row = {}
for w=1, img.Width do
-- RGBA の値を2桁の16進数文字列に変換
-- (IMG_Depth に応じてチャンネル数や1チャンネル当たりのバイト数が異なるので適切なものを選択。今回は1チャンネルのみのものは考慮しない)
local format = {[5] = "%02x%02x%02x%02x", -- 1px につき 4ch, 1ch あたり 8bit(int)
[6] = "%04x%04x%04x%04x", -- 1px につき 4ch, 1ch あたり16bit(int)
[7] = "%04x%04x%04x%04x", -- 1px につき 4ch, 1ch あたり16bit(float)
[8] = "%08x%08x%08x%08x"} -- 1px につき 4ch, 1ch あたり32bit(float)
local px = string.format(format[img.Depth],
rgba.r[h][w],
rgba.g[h][w],
rgba.b[h][w],
rgba.a[h][w]
)
row[w] = px
end
hex_colors[img.Height-h] = table.concat(row, "\n") -- 1行分格納
end
-- 画像の属性とRGBA配列(16進数×4の文字列)を格納
self:SetData("ImageAttr", {IMG_Width = img.Width, IMG_Height = img.Height, IMG_Depth = img.Depth})
self:SetData("ImageColorHexString", table.concat(hex_colors, "\n"))
end
end
function Process(req)
local filepath = InFile:GetSource(0).Value -- 入力画像ファイルパス
if not fileexists(filepath) then
local out = Image({IMG_Width = 1, IMG_Height = 1})
out:Fill(Pixel({R=1, G=0, B=0, A=0}))
print("ファイルが存在しません")
return
end
-- 画像属性読み込み、初期化
local img_attr = self:GetData("ImageAttr")
local out = Image(img_attr)
-- RGBA配列→画像変換
out:_FromHexString(self:GetData("ImageColorHexString"))
OutImage:Set(req, out)
end
RGBA 配列から画像へ変換(メモリから)
Image:_FromMemory()にピクセルデータを格納したメモリアドレスとフォーマットを指定すると画像に変換できる。
Image:_FromMemory(void *ptr, string format, boolean topdown)
-
ptr:ピクセルデータを格納したメモリ領域のアドレス- Fuse でも
FFIライブラリを使用できるため、ffi.C.malloc(n)などでメモリ確保が可能
- Fuse でも
-
format:ピクセルデータの読み込みフォーマットを記した文字列-
"RGBA8"、"BGRA8"、"rgba8"、"bgra8"は挙動を確認済み(大文字と小文字の違いは特になし) - デフォルト値:
"BGRA8" - 1~4 桁はメモリの値をどのカラーチャンネルに割り当てるかの順番。(GBRAなどは受け付けなかった)
- 5 桁目以降は 1ch として読み込むデータサイズ( bit )だと思われるが、
8以外を指定した場合は受け付けない模様?
-
-
topdown:画像を上から描画するか- デフォルト値:
false(左下から順に描画する)
- デフォルト値:
フォーマットについては仕様が把握しきれなかったため、全てのカラーチャンネル数・サイズに適した使用方法を記述することはできない。
その中で挙動が確認できた方法、ひとまずカラーチャンネルが 4Ch かつ 1Ch のデータサイズが 1byte の画像の変換方法のみ記述する。(Image.Depth == 5のパターン)
テスト用 Fuse
入力された画像から RGBA 配列を読み込み、Processイベントで RGBA 配列を画像に変換して表示するコードは下記の通りになる。
基本的にはImage:_FromHexString()と同じような流れになるが、文字列に変換する必要がないこと、4ch 分のデータをまとめて一つの数値(number型)として扱えることから使い勝手はこちらの方が良いかもしれない。
そのため、下記ソースコードでは2通りの変換方法を記述している。(②の方はコメントアウトしている)
① 4ch 分のデータをまとめて一つの数値(number型)として格納・設定
② 1ch ずつ格納・設定
local ffi = require("ffi")
ffi.cdef [[
void free(void *p);
]]
--[[
このモディファイアはPNGイメージを読み込んで表示します。
--]]
FuRegisterClass("ImageFromMemoryTest", CT_Tool, {
REGS_Category = "Fuses",
REGS_OpIconString = "IFMT",
REGS_OpDescription = "FromMemoryテスト用",
-- REG_TimeVariant = true,
REGID_DataType = "Image",
REGID_InputDataType = "Image",
REG_NoCommonCtrls = true,
REG_NoPreCalcProcess = true, -- 事前計算あり
-- REG_Fuse_NoEdit - false,
-- REG_Fuse_NoReload = false,
REG_Version = 1.0,
})
function Create()
InFile = self:AddInput("画像ファイル", "File", {
LINKID_DataType = "Text",
INPID_InputControl = "FileControl",
FC_IsSaver = false, -- 存在しないパスを指定可能か
FCS_FilterString = "PNGファイル (*.png)|*.png|", -- ファイルフィルタ
INP_Required = true, -- 入力がなければ Process関数 を実行しない
INP_DoNotifyChanged = true,
INP_External = false, -- キーフレーム、モディファイア追加不可
})
OutImage = self:AddOutput("Output", "Output", {
LINKID_DataType = "Image",
LINK_Main = 1,
})
end
function NotifyChanged(inp, param, time)
if inp == InFile then
if not fileexists(param.Value) then
print("ファイルが存在しません")
return
end
-- 入力画像読み込み(今回はシーケンシャルファイルの場合、一番小さい番号のファイルを参照する)
print("ファイル読み込み")
local clip = Clip(param.Value, false)
clip:Open()
local img = clip:GetFrame(0)
clip:Close()
-- ピクセル情報を取得し、SetData で格納
local scanline = img:GetScanLine()
local px_arr = {}
for h=0, img.Height-1 do
local line = {}
for w=0, img.Width-1 do
-- ①1byte/ch のデータを 4byte のデータ型にまとめる(こっちの方が高速)
line[w] =
scanline[h][w*4]
+ bit.lshift(scanline[h][w*4+1], 8)
+ bit.lshift(scanline[h][w*4+2], 16)
+ bit.lshift(scanline[h][w*4+3], 24)
-- -- ②もちろん、1byte のデータのまま保存しても良い
-- line[w] = {
-- scanline[h][w*4],
-- scanline[h][w*4+1],
-- scanline[h][w*4+2],
-- scanline[h][w*4+3],
-- }
end
px_arr[h] = line
end
-- ピクセルデータを保存
self:SetData("PixelData", px_arr)
self:SetData("Size.Y", #px_arr+1)
self:SetData("Size.X", #px_arr[0]+1)
print("ファイル読み込み完了")
end
end
function Process(req)
local width = self:GetData("Size.X")
local height = self:GetData("Size.Y")
if req:IsPreCalc() then -- 事前計算
local out = Image({
IMG_Width = width,
IMG_Height = height,
IMG_NoData = true, -- ダミーデータ指定(ピクセルデータ用のメモリ確保をしない)
})
OutImage:Set(req, out)
return
end
-- 背景画像初期化
local out = Image({
IMG_Width = width,
IMG_Height = height,
IMG_Depth = 5, -- 4ch, 1byte/ch (大きいほど_FromMemory() での変換時に負荷がかかる)
})
out:Fill(Pixel({R=0,G=0,B=0,A=1}))
local px_arr = self:GetData("PixelData")
-- メモリ確保・データ格納
local ch_num = 4 -- Ch数
local ch_byte = 1 -- Chごとのサイズ
-- ① 1ピクセル 4ch 分のデータサイズの型にキャスト
local adr = ffi.cast("unsigned int *", ffi.C.malloc(height * width * ch_num * ch_byte))
-- -- ② 1ch ずつ格納する場合は 1byte/ch となるように 1byte 分のデータサイズの型にキャスト
-- local adr = ffi.cast("uint8 *", ffi.C.malloc(height * width * ch_num * ch_byte))
for h=0, height-1 do
for w=0, width-1 do
-- ① 1 ピクセル 4ch 分を 一気に格納
adr[h*width+w] = px_arr[h][w]
-- -- ② 1ch ずつ格納する場合はこちら
-- adr[(h*width+w)*ch_num] = px_arr[h][w][1]
-- adr[(h*width+w)*ch_num+1] = px_arr[h][w][2]
-- adr[(h*width+w)*ch_num+2] = px_arr[h][w][3]
-- adr[(h*width+w)*ch_num+3] = px_arr[h][w][4]
end
end
-- 画像描画
-- 第1引数:変換するメモリアドレス
-- 第2引数:フォーマット。例:"RGBA8"。
-- 省略した場合、"BGRA8" が適用される?
-- 第3引数:画像を上から描画するか(デフォルトは false (下から描画する))
out:_FromMemory(adr, "RGBA8", true)
ffi.C.free(adr) -- メモリ解放
OutImage:Set(req, out)
end
NotifyChanged・Process イベント間でデータをやりとりする方法
イベント間での変数の共有は制限がかかる
Fuse において NotifyChangedイベントとProcessイベントは別スレッドで動いているらしい。
そのためか変数はそれぞれのイベントで独立している。
簡単に言えば NotifyChangedイベントで変更・設定した値をProcessイベントでは参照できないし、その逆も同様ということ。
これはCreateイベントやOnAddToFlowイベントで設定したグローバル変数でも同じ。
例えば、Createイベントで設定した値はNotifyChangedイベントとProcessイベントの両方で参照できる。しかし、仮にNotifyChangedイベントで変数の値を変更したとしても、Processイベントで参照して得られるのは変更前のCreateイベントで設定した値になる。

グローバル変数も NotifyChangedイベントとProcessイベントで独立する
そのため、NotifyChangedイベントで変更した内容をProcessイベントに伝えるには変数以外の方法を用いなければならない。
値を共有するための2種類の方法
イベント間で値を共有する方法は2種類ある。
1. コントロールを介する
2. tool:SetData(), tool:GetData() を用いる
1つ目はInput:SetSource(value, time)でコントロールに値を設定し、Input:GetSource(time)でコントロールから値を取得する方法。
2つ目は tool:SetData("key", value)で Fuse に値を保存し、tool:GetData("key")で値を取得する方法。
Fusion Composition Tool(Fuseなどのノード)オブジェクトはそれぞれコントロール等とは別に専用のデータ保存領域を持っており、SetData() GetData()で設定・参照が可能。
ただし、設定できるのは基本的なデータ型数値型 文字列型 テーブル(Pythonなら配列・辞書型)のみとなっており、Luaのメタテーブルや関数などは設定できない。(あるいは設定できてもおかしな挙動をするので非推奨)
コントロールを介する方法は共有したい値の種類だけコントロールを追加しなければならないため、基本的にはテーブルで値をまとめて管理しやすいSetData() GetData()を利用することになる。
もし値共有用のコントロールを追加する場合、ユーザからコントロールを見えなくすると良い。(コントロールの属性でIC_Visible = falseに設定する)
メタテーブルについて
Lua ではテーブルにメタテーブル メタメソッドを設定できる。これは存在しないキーを参照しようとしたり、テーブルに対して算術子(+ - * / etc)での計算でどのような処理をするかなどの設定ができる。これを応用してクラスとインスタンスを定義することも可能。
グローバル変数のテーブルにメタメソッドを設定している場合、NotifyChangedイベントではそのまま利用できるがProcessイベントではメタメソッドが引き継がれない。
そのため、Processイベントで該当テーブルを初めて参照する際にメタメソッドの再設定を行う必要がある。
コンボボックスに区切り線を挿入する
ComboControl ComboIDControlの属性に{CCS_AddSeparator=""}を追加すると区切り線を設定できる。
ComboIDTest = self:AddInput("コンボIDボックス", "ComboIDTest", {
LINKID_DataType = "FuID",
INPID_InputControl = "ComboIDControl",
INP_DoNotifyChanged = true,
{CCS_AddString = "一", CCID_AddID = "One_ID"},
{CCS_AddString = "二", CCID_AddID = "Two_ID"},
{CCS_AddString = "三", CCID_AddID = "Three_ID"},
{CCS_AddSeparator=""}, -- 代入する値は空文字列で良い(どうせ選択できないので)
{CCS_AddString = "四", CCID_AddID = "Four_ID"},
{CCS_AddString = "五"}, -- IDを設定していない場合、AddStringで設定した文字列を取得する
INPID_DefaultID = "Four_ID",
})

「三」と「四」の間に区切り線が挿入された
特定コントロールに接続されたモディファイアを外せないようにする
ベジェスプライン(キーフレームアニメーション)を含むモディファイアはコントロールとの接続を固定できない。たとえマクロなどの動作に影響があるから接続を切らないで欲しいと思っていても、ユーザが誤ってモディファイアを外してしまう可能性がある。
現状は Fuse 限定ではあるものの、工夫をすることで以下のようにコントロールに接続されたモディファイアを外せないようにすることが可能。

一部コントロールのモディファイアを外そうとしても外れない
OnConnectedイベントでモディファイアを即時再接続
Fuse ではノードの出力からノードの入力へ接続・切断された場合だけでなく、コントロールにモディファイアを接続・切断した場合にもOnConnectedイベントが発生する。
イベント発生時の引数は
-
link:切り替えが起きたリンク、コントロール -
old:接続してた古い出力(未接続だったならnil) -
new:これから接続される新しい出力(切断の場合はnil)
linkがモディファイアを固定したいコントロールで、かつnew == nil(接続を切る)場合にself.Comp:Execute()で再接続用コマンドを実行することで、疑似的にモディファイアを固定することが可能。
モディファイアの削除条件と保存方法
しかし、モディファイアは接続先コントロールが一つもないと削除されてしまうため、そのままではcomp:FindTool("ToolName"), comp:GetToolList()のどちらのメソッドでも取得できないという問題が生じる。
逆に言えば一つでも接続先コントロールが存在すれば消えないので、もう一つモディファイア削除防止用のコントロールに接続しておけば良い。
アプリケーションからの操作であればコントロール右の◇を右クリック→接続→配置されているノード名→モディファイアを共有するコントロール名。
スクリプトから接続するならtool["ControlID"]:ConnectTo("ModifierName")を実行する。
上記 GIF では「コンボIDボックス」というコントロールとモディファイアを共有している。
ソースコード例
予め別コントロールにもモディファイアを接続しておいた上で、モディファイア接続を外せないようにする場合のソースコードは下記のような感じになる。
function OnConnected(link, old, new)
dump(link) -- 接続、あるいは切断が起きたコントール
dump(old) -- これまで接続されてたノードあるいはモディファイアの出力
dump(new) -- これから接続されるノードあるいはモディファイアの出力
-- コントロールA に接続されたモディファイアが切断された場合
if link == ControlA and new == nil then
local exe_str = string.format([[
tool = comp:FindTool("%s") -- ノード取得
modifier_output = tool["ControlB"]:GetConnectedOutput() -- モディファイア消去防止用コントロールBに接続されたモディファイアのアウトプット取得
dump(modifier_output)
if modifier_output ~= nil then
tool["%s"]:ConnectTo(modifier_output) -- モディファイア再接続
end
]], self.Name, link.ID)
self.Comp:Execute(exe_str)
end
end
再接続するモディファイアの取得であればcomp:FindTool("ToolName")でも良いのだが、ベジェスプラインに関してはアプリを再起動すると名前が変わる可能性がある。
ベジェスプラインは作成された際、接続先コントロールに合わせて接続先ノード名と接続先コントロール名を結合したものになる。複数のコントロールに接続されている場合、アプリ再起動時の再設定時にどのコントロールに最初に接続し直すかによって名前が変わっているのかもしれない。
要するに、ベジェスプラインはアプリ再起動で名前が変わる可能性があるので名前指定で取得するのは推奨できない。Input:GetConnectedOutput()でコントロールに接続されているモディファイアの出力を取得できるので、それをInput:ConnectTo(Output)に渡して接続する方が確実。
モディファイア削除防止用コントロールは非表示に
当然ではあるが、モディファイア削除防止用コントロールからモディファイアを外されると意味がない。
非表示にしてユーザが操作できないようにしておくこと。
間接操作用コントロールによるアニメーション遅延の可能性
NotifyChangedイベントを活用すると複数のコントロールを一つのコントロールからまとめて変更するということも可能。
例えば、コントロールAを操作 → コントロールB~Dの3つが変更されるなど。
ただし、Fusionページ以外での描画で問題が起きる可能性がある。
ひとまず操作用のコントロールAを上位コントロール、変更されるコントロールB~Dを下位コントロールと呼称する。
上位コントロールにモディファイアやキースプラインを設定すると、当然ながら再生時間に応じて下位コントロールも変更される。
Fusionページでは問題にならないが、Editページなどで再生するとアニメーション遅延が発生する可能性がある。
(Editページなどにおけるアニメーションの最適化が関係している?)
FuRegisterClass関数においてREG_TimeVariant、REGB_Unpredictableを有効にしても遅延解消には至らなかった。
シンプルな解決方法としては、アニメーションを上位コントロールではなく下位コントロールに設定すること。間接操作用のコントロールが挟まった分だけ遅延が発生していると思われるので、なるべく最下層(最終的な出力に関係する)コントロールに設定するのが望ましい。
ただ、ここまでやっても解消しない可能性は残る。そもそも原因も推測しかできないので、他の要因も絡んでいるかもしれないし、外れているかもしれない。
アニメーション設定による NotifyChanged イベント発生条件の変化
基本的にInput:SetSource(Param, time)でコントロールの値を変更するとNotifyChangedイベントが発生する。そのため、Input:GetSource(time)でコントロールの値を参照した場合はイベントは発生しない。
しかし、モディファイアやエクスプレッションなどアニメーション設定した場合、再生フレーム変更時と、Input:GetSource(time)でコントロールの値を参照した際にもNotifyChangedイベントが発生するようになる。

上はベジェスプライン(モディファイア)、下はエクスプレッションを設定した状態
なぜ参照時にNotifyChangedイベントが発生するのか?
あくまで推測となるが、下記のような挙動になっていると思われる。
アニメーション設定したコントロールの場合、コントロールの値はモディファイアやエクスプレッションの出力値となる。
つまり、コントロール自身が値を持っていないため、値を参照する度にモディファイアやエクスプレッションに計算結果を要求することになっているのではないか。
処理の順番としてはおそらくこんな感じ。
1. 処理中のイベントからコントロールの値を参照しようとする
2. コントロールからモディファイア(エクスプレッション)へ値の更新要請
3. モディファイア(エクスプレッション)で値を処理してコントロールへ出力、値更新
4. コントロールの値が更新されたので割り込みでNotifyChangedイベントが発生
5. 処理中のイベントが参照した値を取得

処理のイメージ
NotifyChangedイベントの割り込み確認 Fuse と、実行結果は下記の通り。
--[[
このモディファイアは GetSource(time) によって NotifyChanged イベントが発火するかの確認用 Fuse です。
--]]
FuRegisterClass("NotifyChangedTestFuse", CT_Tool, {
--REGS_Name = "TestFuse",
REGS_Category = "Fuses",
REGS_OpIconString = "Test",
REGS_OpDescription = "テスト用",
-- REG_TimeVariant = true,
REGID_DataType = "Image",
REGID_InputDataType = "Image",
REG_NoCommonCtrls = true,
REG_NoPreCalcProcess = true, -- 事前計算あり
-- REG_Fuse_NoEdit - false,
-- REG_Fuse_NoReload = false,
REG_Version = 1.0,
})
function Create()
Test1 = self:AddInput("Test1", "Test1", {
LINKID_DataType = "Number",
INPID_InputControl = "CheckboxControl",
INP_Integer = true,
INP_Default = 1,
INP_DoNotifyChanged = true,
})
Test2 = self:AddInput("Test2", "Test2", {
LINKID_DataType = "Number",
INPID_InputControl = "CheckboxControl",
INP_Integer = true,
INP_Default = 1,
INP_DoNotifyChanged = true,
})
Test3 = self:AddInput("Test3", "Test3", {
LINKID_DataType = "Number",
INPID_InputControl = "CheckboxControl",
INP_Integer = true,
INP_Default = 1,
INP_DoNotifyChanged = true,
})
OutImage = self:AddOutput("Output", "Output", {
LINKID_DataType = "Image",
LINK_Main = 1,
})
end
function NotifyChanged(inp, param, time)
print("○ NotifyChanged イベント======================")
-- nil の場合は処理をしない(コンボボックスの未選択の場合など)
if param == nil then
print("param : nil")
print("○ NotifyChanged イベント 終了======================")
return
end
if self:GetData("ProcessFlg") then
print("プロセス処理中フラグ:"..tostring(self:GetData("ProcessFlg")))
end
print("変化コントロール:"..inp:GetAttr("LINKS_Name"))
dump("変更後の値:"..param.Value)
-- Test3 で Test1 を参照することで、NotifyChangedイベントの割り込みを確認
if inp == Test3 then
print("Test1の値:"..Test1:GetSource(time).Value)
end
print("○ NotifyChanged イベント 終了======================")
end
function Process(req)
print()
print("◇ Process イベント==================")
self:SetData("ProcessFlg", true)
print("Test1の値:"..Test1:GetSource(req.Time).Value) -- モディファイア設定コントロール参照
-- print("Test2の値:"..Test2:GetSource(req.Time).Value) -- エクスプレッション設定コントロール参照
self:SetData("ProcessFlg", false)
print("◇ Process イベント終了==================")
end

コントロールTest3を操作した結果
Test3のコントロールを操作するとTest1のNotifyChangedイベントが割り込み発生している。また、コントロール変更によってレンダリング処理(Processイベント)が発生しており、そのProcessイベント内でTest1の値を参照しようとした場合もTest1のNotifyChangedイベントが割り込み発生している。
この割り込み処理が色々と問題を引き起こす可能性がある。
なお、再生フレーム中の値も自動更新されているのか、再生フレームを変更していない=Test1とTest2の値が変わっていなくてもNotifyChangedイベントが発生するようになっていることが確認できる。(ただ、これ自体はそれほど問題にならないはず)
Processイベント内で割り込み発生した場合の問題点
NotifyChangedイベントとProcessイベントは本来別々のスレッドで動いているが、Processイベント内でNotifyChangedイベントが割り込み発生した場合、このNotifyChangedイベントはProcessイベントスレッドで動作することになる。
NotifyChangedイベントとProcessイベントは Fuse 生成時に初期設定されたグローバル変数を除いて、変数を共有できない。また、片方のイベントスレッドでグローバル変数を変更してももう片方のイベントスレッドには反映されず、古い値のままとなる。
これにより、NotifyChangedイベントスレッドの処理で更新しているグローバル変数があった場合、Processイベント内で割り込み発生したNotifyChangedイベントは同じ処理でも最新の値を参照できない(更新前の値を参照してしまう)という問題が発生する。

NotifyChanged イベント用のグローバル変数も、スレッドが異なるために値の整合性が取れない可能性がある
対策:割り込み処理を抑制する
Input:GetSource(time)で値を参照するタイミングだと値を変更するつもりなどないのでNotifyChangedイベント処理は余計である。
そうなると値を変更した時のNotifyChangedイベントのみ処理をして、値を参照したときの割り込み処理の場合は処理をしない(中断する)という条件分岐が必要となる。
Input:GetSource(time)で発生した割り込み処理かどうかを判断する方法として、NotifyChangedイベントの割り込みの深さをカウントしておく方法が考えられる。
カウントはSetData(), GetData()で管理し、NotifyChangedイベントとProcessイベントにおいて、①イベント開始時にカウントアップ(0→1)し、②イベント終了時にカウントダウン(1→0)する。
①と②の間で割り込み処理が発生した場合、割り込み処理イベントの開始時のカウントアップで1→2となるので、カウントが2以上なら割り込み処理と判断できる。
以上の考えを元に割り込み処理を抑制するテストコードと実行結果は下記の通り。
--[[
このモディファイアは GetSource(time) によって NotifyChanged イベントが発火するかの確認用 Fuse です。
--]]
FuRegisterClass("NotifyChangedTestFuse", CT_Tool, {
--REGS_Name = "TestFuse",
REGS_Category = "Fuses",
REGS_OpIconString = "Test",
REGS_OpDescription = "テスト用",
-- REG_TimeVariant = true,
REGID_DataType = "Image",
REGID_InputDataType = "Image",
REG_NoCommonCtrls = true,
REG_NoPreCalcProcess = true, -- 事前計算あり
-- REG_Fuse_NoEdit - false,
-- REG_Fuse_NoReload = false,
REG_Version = 1.0,
})
function Create()
Test1 = self:AddInput("Test1", "Test1", {
LINKID_DataType = "Number",
INPID_InputControl = "CheckboxControl",
INP_Integer = true,
INP_Default = 1,
INP_DoNotifyChanged = true,
INP_SplineType = "StepIn", -- キースプラインの設定タイプを StepIn に指定
})
Test2 = self:AddInput("Test2", "Test2", {
LINKID_DataType = "Number",
INPID_InputControl = "CheckboxControl",
INP_Integer = true,
INP_Default = 1,
INP_DoNotifyChanged = true,
})
Test3 = self:AddInput("Test3", "Test3", {
LINKID_DataType = "Number",
INPID_InputControl = "CheckboxControl",
INP_Integer = true,
INP_Default = 1,
INP_DoNotifyChanged = true,
})
OutImage = self:AddOutput("Output", "Output", {
LINKID_DataType = "Image",
LINK_Main = 1,
})
end
function NotifyChanged(inp, param, time)
-- イベントの深さをカウントアップ
local event_cnt = self:GetData("EventCount")
if event_cnt == nil then
event_cnt = 1
else
event_cnt = event_cnt + 1
end
self:SetData("EventCount", event_cnt)
print("○ NotifyChanged イベント:[".. event_cnt .."]======================")
-- イベントの深さカウントが 2 以上なら割り込み処理と判断し、処理を中断
if event_cnt > 1 then
print("イベントの深さが 2 以上です。")
print("割り込み処理として判断し、処理を中断します")
goto GOTO_NOTIFYCHANGED_EVENT_END -- イベント最後までスキップ
end
-- nil の場合は処理をしない(コンボボックスの未選択の場合など)
if param == nil then
print("param : nil")
goto GOTO_NOTIFYCHANGED_EVENT_END -- イベント最後までスキップ
end
if self:GetData("ProcessFlg") then
print("プロセス処理中フラグ:"..tostring(self:GetData("ProcessFlg")))
end
print("変化コントロール:"..inp:GetAttr("LINKS_Name"))
dump("変更後の値:"..param.Value)
-- Test3 で Test1 を参照することで、NotifyChangedイベントの割り込みを確認
if inp == Test3 then
print("Test1の値:"..Test1:GetSource(time).Value)
end
::GOTO_NOTIFYCHANGED_EVENT_END::
print("○ NotifyChanged イベント:[".. event_cnt .."] 終了======================")
-- NotifyChanged イベントの深さをカウントダウン
self:SetData("EventCount", event_cnt-1)
end
function Process(req)
-- イベントの深さをカウントアップ
local event_cnt = self:GetData("EventCount")
if event_cnt == nil then
event_cnt = 1
else
event_cnt = event_cnt + 1
end
self:SetData("EventCount", event_cnt)
print()
print("◇ Process イベント==================")
self:SetData("ProcessFlg", true)
print("Test1の値:"..Test1:GetSource(req.Time).Value) -- モディファイア設定コントロール参照
-- print("Test2の値:"..Test2:GetSource(req.Time).Value) -- エクスプレッション設定コントロール参照
self:SetData("ProcessFlg", false)
print("◇ Process イベント終了==================")
-- NotifyChanged イベントの深さをカウントダウン
self:SetData("EventCount", event_cnt-1)
end

コントロールTest3を操作した結果
イベントの深さカウントで割り込み処理の場合のみ即処理を中断することで、実質的に割り込み処理のみを抑制できた。
ちなみにCloneInput()で複製したコントロールはオリジナルコントロールとの値連動機能があるので対策による悪影響が懸念されたが、複製コントロールとオリジナルコントロールの値の連動機能は割り込み処理ではないらしく特に問題はなかった。