Open19

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\Fuses
  • C:\ProgramData\Blackmagic Design\DaVinci Resolve\ Fusion\Fuses
  • C:\Program Files\Blackmagic Design\DaVinci Resolve\ Fusion\Fuses

    (パスマップは Fusion ページに切り替えて、上部メニューからFusionFusion設定で Fusion 設定画面を開き、「パスマップ」の項目で確認できる。
    一覧からFusesの行を右クリックすると該当フォルダパスが表示されるので、そのパスをクリックすると該当フォルダを開くことができる)

Fuse を使うために配置するフォルダはパスマップに含まれているところであれば、どのフォルダでも OK。
今回はその内の一つ、下記のフォルダにコピーしたところ無事に読み込むことができた。
C:\ProgramData\Blackmagic Design\DaVinci Resolve\Fusion\Fuses

アプリケーションを再起動する

Fuse ファイルはアプリケーションを起動するタイミングで読み込まれる。
なので、指定フォルダに Fuse ファイルを配置した際、アプリケーションを既に起動していたなら再起動する。

無事に読み込めていれば画像のように Fusion ページでツール選択できるようになっている。

火注ゆかな火注ゆかな

Fuse で記述する各イベント処理

Fuse の記述は以下の関数とイベント処理に分けられる。
基本的に上に位置するほどイベントの処理タイミングが早い。
なお、必ず定義する必要があるのは以下の3つ。

  • FuRegisterClass
  • Create
  • Process

上記に加え、基本的に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 の設定に変更を加えた場合はアプリの再起動が必要となる。

引数

  1. name

    • Fusion が識別するためのプラグイン ID。他の Fuse と被ってはいけない。
      • 被った場合は特にエラーは表示されず、後から読み込まれた Fuse の内容が優先される模様。読み込み順の指定はできないので「Fuse ファイルを更新したのに反映されない!」などの混乱を防ぐためにも極力被せないように注意。
    • 後述の REGS_Name 属性に指定がなければ Fuse の名前としてそのまま使われる。
    • 渡せる文字列には英数字とアンダーバー( _ )のみ使用可能。
    • Create 関数などで参照したいときは self.RegNode.m_ID で取得可能
      • ただし、この方法で取得するとFuse.nameのように先頭にFuse.が付く
        • comp:GetToolList() で該当 Fuse を絞り込んで検索したい場合、Fuse.name というように Fuse. を含めて渡すと良い
  2. ClassType

    Fuse の種類を識別する定数。以下の3種類から一つ指定可能。
    • CT_Tool : ノード
    • CT_Modifier : モディファイア
    • CT_ViewLUTPlugin : ビューLUT
  3. 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 用の関数を優先する)
    • PreCalcProcessProcess 、どちらで処理中かは 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 のコード

モディファイア作成時のオプション属性

  • 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") で取得可能
  • attributes:table, 必須

    • 各コントロールの種類に応じた属性を格納したテーブル。コントロールで指定できる最小値・最大値など、内容は様々。
    • Input:GetAttr(attr_name) で取得可能(引数は取得したい属性名)
      • 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イベントも発生するが、引数paramnilとなる。
ButtonControl ボタン。
クリックするとBTNCS_Execute 属性に設定されたコマンド(文字列で設定)を実行する。また、NotifyChanged イベントが2回連続発生する。
(引数param.Valueが 1 回目は 0 → 1 、2 回目は 1 → 0 に変化)
リロードなど、ユーザに任意のタイミングで処理をさせたいときに使用。
CheckControl チェックボックス。
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", empty_table)で複製できる。

  • object:Input, 必須
    • 複製したいコントロールを指定。
  • clone_id:string, 必須
    • 複製したコントロールのID。複製であること、複製元コントロールがわかるような ID を推奨。
  • attr_table:table, 必須
    • 複製時に変更したい属性テーブル。例えば、複製元からラベル名を変更したい場合{LINKS_Name="クローンコントロール名"}というように指定する。
    • テーブルを指定しないとエラーが出て複製されないので注意。
    • 変更しない場合は空テーブルを渡せば良い。
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引数の空テーブルを忘れないように注意
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通り。

  1. string.format(command_str, arg1, arg2, ...)でコマンド文に変数の内容を埋め込む
  2. 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

全ての属性名を把握するのは難しいので、必要に応じてスクリプトから参照した場合の属性一覧と照らし合わせて確認するのが良い。
下記のスクリプトをコンソールで実行すれば属性一覧を表示可能。

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イベント→コマンド文実行を交互に繰り返すソースコード例。

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 言語とほぼ同じ。
おおまかな使い方としては以下の通り。

  1. 処理に使うパラメータ(構造体)定義と各ピクセル処理関数定義を文字列で記述
  2. Processイベント内でパラメータ定義と処理関数定義を指定し、処理ノードを作成
  3. パラメータに値を格納し、ノードにパラメータを設定
  4. ノードを実行

パラメータ定義

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と同義で、とりあえずパラメータ変数とメンバー名を->で繋げば良いと覚えれば十分。
  • _tex2DVec4(src, x, y):入力画像srcの座標(x,y) のピクセルカラーRGBAを格納したfloat4型として取得。
    • 各色の値は 0.0f1.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 情報を扱うためにfloat2float4という型が存在する。
特にfloat4に関しては画像のピクセル取得・設定を行う際に必ず使用することになる。
float2float4の違いは格納できる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():後述する引数をもとにノードを作成し、返却する。
    • reqProcessイベントなどで引数として渡される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を返却する。
    • reqProcessイベントなどで引数として渡される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_AUTOfilterModeに応じて変わる?
        • filterModeTEX_FILTER_MODE_POINT(最近傍参照)ならTEX_NORMALIZED_COORDS_FALSE?(絶対値指定)
        • filterModeTEX_FILTER_MODE_LINEAR(線形補間)ならTEX_NORMALIZED_COORDS_TRUE(割合指定)?

ちなみにデフォルトサンプラーRowSamplerの設定は下記の通り。

  • filterModeTEX_FILTER_MODE_POINT(最も近い位置のピクセル参照)
  • addressModeTEX_ADDRESS_MODE_CLAMP(最も近い位置?)
  • normCoordModeTEX_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-10 ~ 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進数文字列を渡して実行すると画像に変換することができる。
大まかな手順としては以下のようになる。

  1. 画像サイズに合わせた Image 型の変数を作成
  2. RGBA 情報 を 16 進数文字列に変換、結合する
  3. 変換した 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 配列を画像に変換して表示するコードは下記のとおりになる。

Image2RGBAArray.fuse
--=============================================================

--[[
    指定画像をコード
--]]
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)などでメモリ確保が可能
  • 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 ずつ格納・設定

ImageFromMemoryTest.fuse
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)に渡して接続する方が確実。

モディファイア削除防止用コントロールは非表示に

当然ではあるが、モディファイア削除防止用コントロールからモディファイアを外されると意味がない。
非表示にしてユーザが操作できないようにしておくこと。