🐙

DaVinci FusionのOn Changeと実行の挙動についてあれこれ

に公開
6

この記事について

DaVinci Resolve 20.0 から追加された On Change (INPS_ExecuteOnChange)の意外だった挙動などをまとめています。
お悩みに該当する部分があればその見出しの箇所と、コメントも併せてみるとヒントがあるかもしれません。

マクロ化すると2回以上実行される

print(tool.Name)

と書いて保存します。

toolは変更が行われたノードを指してくれる変数で、.Nameはその名前です。
マクロにするとき、このコントロールを操作できるように指定して、操作できるマクロを作ったとします。

そしてコントロールを操作すると、マクロからでも、もとのノードからでも、どちらから操作してもprintが2回実行されます
オリジナルのノード名とマクロのノード名が表示されると思います。

このようにtoolが変わって2度実行される挙動になっていることを知っておくとよいでしょう。
なお、ButtonExecuteからは2回実行されないようです。

対策

これはスクリプトの書かれた大本のコントロールからの実行である、といった情報の取得方法はバージョン20の時点ではありませんでした。

一番の対策は可能な限り、2回実行されても影響がないようにすることです。
例えば チェックボックスがオンになると この値を N にする、という処理であれば 2回実行されたところで N にする処理が2回走ったところで結果が変わりません。

もしくは、なんらかの特徴を紐付けて判定するロジックを加えるしかありません。

対応策の具体例

親側になるマクロを作成する際には 独自の名前をつけますよね。そのときユニークなものを設定しておけば、親側マクロであるかどうか名前を見て判断ができます。
tool.Name:gsub("[_]%d+$", "")とすれば末尾のアンダーバーと数字を除いたノード名を取得できます。
これが 親側のマクロ名であるかどうかで 場合分けが可能です。

※追記:その他の対応法もコメントにいくつか上がっていますので 自身に合う方法をご確認ください。これはユーザー側の任意実装なので、何らかの方法を適当に選んで適用する必要があります。

自動リネームされない

エクスプレッションはリネームされます。

コピー後↓↓

このとき、5_1のボタンの実行にprint(Transform5:GetInput("Size"))と書いていました。コピー先の5_1_1を見ても変更されていませんでした。

対策

追跡用コントロールを作りエクスプレッションから取得するのが確実だと思い。
上記の例でもエクスプレッションはリネームされていたので、その性質を使おうということですね。
※追記、下記のスクリプトの冒頭2行は、読み取り専用のTextEditControlを使う方法に変更したほうが良さそうです。コメント欄にて教えてもらいました。

local expression_str = tool.Track:GetExpression() -- なんでもよいので追跡用のパラメーターを用意しておく
local nodeName = string.match(expression_str, "^([^.]+)") -- ピリオドより左側を取得 
local node = FindTool(nodeName)
print(node.Name)


コピー前に実行し、コピー後に新しくコピーしたノードで実行しました。コピー先を追跡できていますね。

ほかにもGetChildrenList()で子の一覧を取得してフィルタリングしたり、GetConnectedOutput()で前後を辿るなどの、あれやこれやといった方法もありますが、確実性に欠けます。

Pythonも書ける

スクリプトの先頭に!Py3: (←半角スペースも必須)と入力することでスクリプトをPythonを書くことができます。参考記事:【DaVinci Resolve】AIを使用してButtonControlのExecuteに設定するスクリプトを作る話

1000文字の制限

1000文字までしか入力させてくれません。ちょっと長めのものを書こうとするとすぐに制限が来てしまいます。

対策

文字数を抑える対策です。

  • タブをスペースからインデントにする
  • AIにコードを圧縮してもらう

これに対して、根本的に記述場所を変えるという方法があります。

!Py3: import sys
import os
import importlib

# ホームディレクトリを動的に取得(オプション)
home_dir = os.path.expanduser('~')
script_dir = os.path.join(home_dir, r'davinci_python') # 自分のpythonを置いてある場所
if script_dir not in sys.path:
    sys.path.append(script_dir)

# インポートして使用できる
import python_filename
python_filename.func_name(self, tool, comp)

配布する予定などがある場合は、コントロールに記述して読み込ませる方法がよさそうです。

Discussion

MugMug

2回実行される問題は、操作してる側のツールのときは~ってするのが良さそうかなって思いました
これでいつもうまくいくかどうかは未確認です💦

comp.ActiveTool.Name == tool.Name
2
kcdlrkcdlr

ありがとうございます!
そういえばセカイルさんの記事を見た記憶がなんとなくあって
それで記事にしたほうがいいという気がして書いたのでした。
改めて確認もしてきました。
いろいろと思い出せてよかったです🙌

※追記
マクロの内部にマクロがある、という場合、3回実行されるのですが、その際は
アクティブマクロ、非アクティブマクロ、非アクティブノードという並びになります。
1回だけ実行、という条件なら問題なかったのですが、
例えば非アクティブマクロにおいてパススルーをONにしたい、という処理をしたい場合には
また対応が難しいかもしれません。

3
火注ゆかな火注ゆかな

toolがマクロではない場合のみ実行できれば良いということであれば、tool.IDプロパティが"MacroOperator"かどうかで判別すればマクロの多重構造にも対応できます。
(グループの場合は"GroupOperator"

if tool.ID ~= "MacroOperator" then  -- マクロでない場合のみ実施
    print(tool.Name .. "." .. self.Name)
end

あとはtool.ParentToolプロパティは自身が所属するマクロもしくはグループを返却するのでtool.ParentTool == nilという条件なら多重構造マクロの一番外側(最上位)だけ処理する、あるいはtool.ParentToolで親マクロを辿って自身が上から何層目のノード・マクロか判別するなどもできそうです。
追記における非アクティブマクロのみパススルーONにする処理であれば、ノード本体を直接含むマクロだけを対象にすれば可能かと思います。

if tool.ID ~= "MacroOperator" then  -- マクロでない場合
    parent = tool.ParentTool    -- ノードの一つ上の親マクロ or グループ取得(繰り返せば指定分だけ上位のマクロ取得可能)
    parent:SetAttrs({TOOLB_PassThrough = true})    -- パススルーON
end

また、自動リネームされない仕様への対策案ですが、追跡用コントロールをTextEditControlにして追跡対象ノード.Nameというエクスプレッションを追記すればノード名を直接取得できるようになり、正規表現が不要な分だけ少しシンプルにできそうです。

nodename = tool:GetInput("AddControl")
3
kcdlrkcdlr

火注ゆかなさん、コメントいただきありがとうございます。
個人的に改めてDaVinci Resolveに挑戦するにあたり、Mugさんとゆかなさんのお二人の発信内容がとても参考になりました。カスタマイズの過程でソフトに慣れていくタイプの人間のため 助かりました。
改めて感謝申し上げます。
実践で使用をしていたため コメント返信が遅れました。
ーーー
追跡対象を追うために読み取り専用のTextEditControlを使うのは最適と言えそうですね!ありがとうございます。

親子関係を辿るのは良い妥協点になりそうですね。特に下から数えれば、状態は安定しそうです。上から数えると、親子関係の深さによって参照が変わってしまいますからね。下から数えれば、うっかり状態によって参照できなくなるケースも起きづらいように思います。

発火したコントロールが大本の記述対象だったかどうかを明示的に得られるのが、その他の状態に依存しなくていいんですが、やはり難しそうでした。
INPS_ExecuteOnChangeがあるかどうかを追うというのもやってみましたがコピーされていますね…

-- 自身の "INPS_ExecuteOnChange" 属性を取得
local my_attribute = self:GetAttrs()

print("--- 属性チェック開始 ---")
print("実行元コントロール: " .. self.Name .. " (" .. tool.Name .. ")")

if my_attribute and my_attribute.INPS_ExecuteOnChange then
  print("結果: INPS_ExecuteOnChange 属性は存在します。")
  print("内容:")
  -- dump() を使うとテーブル構造が分かりやすく表示される
  dump(my_attribute.INPS_ExecuteOnChange)
else
  print("結果: INPS_ExecuteOnChange 属性は存在しません (nil または空です)。")
  dump(attribute)
end

print("--- 属性チェック終了 ---")

出力:

--- 属性チェック開始 ---
実行元コントロール: EnableOut (Switch1)
結果: INPS_ExecuteOnChange 属性は存在します。
内容:
-- 自身の "INPS_ExecuteOnChange" 属性を取得
local my_attribute = self:GetAttrs()


print("--- 属性チェック開始 ---")
print("実行元コントロール: " .. self.Name .. " (" .. tool.Name .. ")")


if my_attribute and my_attribute.INPS_ExecuteOnChange then
  print("結果: INPS_ExecuteOnChange 属性は存在します。")
  print("内容:")
  -- dump() を使うとテーブル構造が分かりやすく表示される
  dump(my_attribute.INPS_ExecuteOnChange)
else
  print("結果: INPS_ExecuteOnChange 属性は存在しません (nil または空です)。")
  dump(attribute)
end


print("--- 属性チェック終了 ---")
--- 属性チェック終了 ---
--- 属性チェック開始 ---
実行元コントロール: EnableOut (ScaleInOut)
結果: INPS_ExecuteOnChange 属性は存在します。
内容:
-- 自身の "INPS_ExecuteOnChange" 属性を取得
local my_attribute = self:GetAttrs()


print("--- 属性チェック開始 ---")
print("実行元コントロール: " .. self.Name .. " (" .. tool.Name .. ")")


if my_attribute and my_attribute.INPS_ExecuteOnChange then
  print("結果: INPS_ExecuteOnChange 属性は存在します。")
  print("内容:")
  -- dump() を使うとテーブル構造が分かりやすく表示される
  dump(my_attribute.INPS_ExecuteOnChange)
else
  print("結果: INPS_ExecuteOnChange 属性は存在しません (nil または空です)。")
  dump(attribute)
end


print("--- 属性チェック終了 ---")
--- 属性チェック終了 ---
1
火注ゆかな火注ゆかな

マクロが多重構造の時に指定ノード(マクロ)のみ判別→実行する方法ですが、カスタムデータに識別情報を保存・参照する方法はどうでしょうか。
ノードやコントロールにはカスタムデータを設定できます。これはノード自身を含むマクロにはコピーされません。発火させたいコントロールのInput(マクロならInstanceInput)の属性にbool値などを保存しておけばif self:GetData("キー名") then ~ Endで囲うだけで済みますし、他のコントロール操作に影響を与えることもありません。
まあ今度はカスタムデータを付与する手間が生じるのですが……発火元の付け替え(ノード→一つ上の親マクロへの変更など)も容易かと。

中間マクロ名のみ出力するマクロ例
{
    Tools = ordered() {
        Macro_1_Wrapper = MacroOperator {	-- 一番上のマクロ
            Inputs = ordered() {
                Input1 = InstanceInput {
                    SourceOp = "Macro_1",
                    Source = "Input1",
                    Default = 1,
                }
            },
            Outputs = {
                MainOutput1 = InstanceOutput {
                    SourceOp = "Macro_1",
                    Source = "MainOutput1",
                }
            },
            ViewInfo = GroupInfo { Pos = { 0, 0 } },
            Tools = ordered() {
                Macro_1 = MacroOperator {	-- ExecuteOnChange で操作したいマクロ
                    CtrlWZoom = false,
                    Inputs = ordered() {
                        Input1 = InstanceInput {
                            SourceOp = "Background1_1",
                            Source = "AddControl",
                            CustomData = {
                                Original = true	-- 発火対象の指定
                            },
                        }
                    },
                    Outputs = {
                        MainOutput1 = InstanceOutput {
                            SourceOp = "Background1_1",
                            Source = "Output",
                        }
                    },
                    ViewInfo = GroupInfo { Pos = { 220, 115.5 } },
                    Tools = ordered() {
                        Background1_1 = Background {	-- ExecuteOnChange を設定したノード
                            CtrlWZoom = false,
                            CtrlWShown = false,
                            Inputs = {
                                Width = Input { Value = 1920, },
                                Height = Input { Value = 1080, },
                                UseFrameFormatSettings = Input { Value = 1, },
                                ["Gamut.ColorSpaceNest"] = Input { Value = 1, },
                                ["Gamut.GammaSpaceNest"] = Input { Value = 1, },
                                ["Gamut.GammaType"] = Input { Value = FuID { "Log" }, },
                                ["Gamut.SLogVersion"] = Input { Value = FuID { "SLog2" }, },
                                Type = Input { Value = FuID { "Gradient" }, },
                                nest1 = Input { Value = 1, },
                                AddControl = Input { 
                                    Value = 1, 
                                    --CustomData = {
                                    --    Original = true	-- 発火対象の指定(今回はコメントアウトで対象から外す)
                                    --},
                                }
                            },
                            ViewInfo = OperatorInfo { Pos = { 0, 8.75757 } },
                            UserControls = ordered() { 
                                AddControl = {
                                    LINKS_Name = "AddControl",
                                    LINKID_DataType = "Number",
                                    INPID_InputControl = "SliderControl",
                                    INP_Integer = true,
                                    INP_SplineType = "Default",
                                    INPS_ExecuteOnChange = [[
                                        if self:GetData("Original") then    -- カスタムデータで発火対象を判別
                                            print(tool.Name)
                                        end
                                    ]],
                                } 
                            }
                        }
                    },
                }
            },
        }
    },
    ActiveTool = "Macro_1_1"
}
1
kcdlrkcdlr

コメントありがとうございます。いつも参考にさせていただいています。

ご指摘の方法を踏まえた、様々な手段で対応できるとは思います。

そもそも フュージョンが発火元のノードを特定する API を提供していない、というのが 本 記事での指摘したいところでした。

よって ユーザー側で何らかの発火元のノードを特定する ロジックを組まなければなりません。
任意の実装で対策を施すしかないというのが指摘したい現状の問題点です。
ですので 何らかの方法によって発火元のノードを特定できるのではないかという ご指摘はおっしゃる通りです。

記事についてはもう少し分かりやすく 推敲しておきたいと思います。時間が取れればですが、最近多忙気味ですので遅れる場合かあります。

結局のところ フュージョン 側で提供していない 発火 元のノードを特定する ロジックは各々 が納得する形で実装していただく しかないということです。

2