DaVinci Resolve20の新機能`On Change`を試してみる
まだDaVinci Resolve20の新機能を全然追えていませんが、Fusion関係で個人的に注目の機能はマルチテキストとやはりExecute On Changeでしょうか。
今までもButton Executeで実現できたとは思いますが、様々なコントロールから実行できるようになった事で自由度が上がったように思います。
今までのマクロでは難しかった事
DaVinci Resolveにあらかじめ用意されているノードの中にはチェックボックスのオンオフ状態によって表示非表示が切り替わるコントロールや、マルチボタンコントロールの選択状態によって表示非表示が切り替わるコントロールがあります。

現在無効であるコントロールを表示しないとか、特定の編集作業の時に操作するコントロールのみを表示すると言った事はUXに大きく貢献します。
それらをマクロで再現しようと思ったらExpressionなどを駆使して出来る事もあるのですが、少々手間がかかりました。
例えばマルチボタンコントロールの選択状態によってラベルの開閉を制御する場合だと、ラベルコントロールのExpressionに以下のように記述します。
iif(MultiButton == 0, 1 ,0)
この場合制御したいラベル毎にExpressionを記述してそれぞれからマルチボタンコントロールを参照しなければいけません。
しかも、毎フレーム監視する必要があるので、数が多くなってくるとパフォーマンスにも影響があるというデメリットもありました。
Execute On Changeの利点
今回のアップデートで追加されたExecute On Changeはチェックボックスのオンオフが切り替わったとか、マルチボタンのいずれかが選択されたという状態変化によってスクリプトを発火させる仕組みのようです。
1つのコントロールから複数のコントロールを制御できるので処理が集約して管理しやすいと思います。
加えて、Expressionやレンダースクリプトではアクセスできないコントロールの属性値にアクセスできる点が最大の利点だと思います。
この属性値を変更することによってコントロールの表示非表示を制御したりラベルコントロールの表示名や色等も変更でるようです。
アニメーションについて
但し、アニメーションが設定されている値にはアクセスできないようです。
アニメーションを設定していない時点ではExecute On Changeによって何度でも変更できるのですが、アクセスしている値にキーフレームを打つなどしてアニメーションを設定するとその後はこの方法でアクセスできなくなってしまいます。
少し話がややこしいのですが、この方法で変更した値がアニメーションできなくなる訳では無くて、アニメーションを設定した値にはこの方法でアクセスできなくなるという事です。
つまり、初期値などを設定する事は可能だけどアニメーションには関与できないという事ですね。
ひょっとしたら制御しているノードが他ノードに接続されていない事やパススルーを設定している事、INP_External = falseやINP_Passive = true等が関係している可能性があるかもしれないと思って変更してみましたが、私が試した限りではアニメーションに関与できないという事に変わりはありませんでした。
おそらくですが、アニメーションの制御とOn Changeイベントとの衝突を回避するための仕様だと思います。
以上の事から、少なくとも現時点ではExecute On Changeの機能はインスペクタの表示を制御するために使用するものと考えた方が良さそうです。
とはいえ、冒頭でも書きましたがインスペクタの表示を制御する事は編集作業を快適にする事に繋がるので、その仕組み作りがやり易くなったという点で大きな意味があると感じました。
もちろん、Fuseや外部スクリプトによる制御に比べると色々と制限があると思いますが、マクロ完結型の制御は私のような初心者でも比較的少ない学習コストで始める事が出来るし、配布時にはdrfxだけで簡単にインストールできるという点も魅力の一つです。
スクリプトの入力欄
さて、そんなExecute On Changeですが、スクリプトを記述するテキストボックスがあまりにも狭すぎるという問題があります。

これは従来のButton ExecuteやExpressionも同様で、入力欄は1行でコントロールの編集ダイアログにおいてのIDや名前の入力欄と変わりません。
この小さなテキスト入力欄に長いスクリプトを記述するのは書くのにも確認するのにも視認性が非常に悪いので、おそらく多くの人が外部テキストエディタで編集した物を貼り付けているか、settingファイルに直接記述していると思います。
打開策
その狭い入力欄に記述する使い勝手の悪さを緩和する方法をDaVinci Resolveのスクリプトで有名な海外の方のYoutube動画で見つけました。その方法でも手狭感は否めませんが、スクリプトを少し確認する程度の事であれば十分有効な方法だと思います。
その方法とは、少し大きめのテキストコントロールを追加してそこにスクリプトを記述しExecute On Changeからはそのスクリプトを呼び出すための短いコードを記述をすると言った物でした。

インスペクタの幅いっぱいに広げるとこんな感じ
私はこれまでシンタックスハイライトの機能があるテキストコントロールはレンダースクリプトの記述欄しか知らなかったので、こんな方法があるとは目から鱗でした。
具体的な方法は
-
コントロールやスクリプトを設置するためだけのノードを用意する
これは必須ではありませんが、レンダリングされないスクリプト専用ノードのような物を用意すると管理がしやすいので良いと思います。
-
動画ではよりシンプルな
AlphaDivideノードが使用されていました。
CustomToolノードやExpressionノードもありますが、この使用目的には入力や既存のコントロールが多くて少々オーバースペックだと思います。

-
ノードはどこにも接続せず、パススルーにしておく。
ノードのインスペクタのトグルスイッチでoff状態にしておくかsettingファイル内のノードの設定でPassThrough = trueを記述する。
このノード自体は映像にレンダリングされないのでパススルーにしておく事で負荷の軽減が見込めると思います。
- ノードにテキストコントロールを追加
- できるだけ広く使えるように入力欄のサイズを大きくする。
- スクリプトのシンタックスハイライトが有効になるように設定
以下の様な設定はコントロールの編集ダイアログでは行えない物が多いのでsettingファイルのUserControls = Orderd() {...}ブロック内で編集する事になります。
ScriptedControls = {
INPID_InputControl = "TextEditControl",
LINKID_DataType = "Text",
TEC_ReadOnly = false,
TEC_Lines = 20, -- 行数もそこそこ多くしておく
ICD_Width = 1.1, -- 1だと右端までにならない
INP_External = false,
INP_Passive = true,
IC_NoLabel = true, -- 左いっぱいまで使いたいのでラベルを非表示
TEC_Wrap = false,
TECS_Language = "lua", -- lua言語を指定。シンタックスハイライトが有効になる
LINKS_Name = "スクリプトを記述したテキストコントロール",
},
- テキストコントロールにスクリプトを記述
- 任意のコントロールを追加して
On Change欄に2で作成したスクリプトを呼び出すコードを記述
-
settingファイルでエスケープシークエンスが見辛い場合はクォーテーションにシングルクォーテーションを使うと良いかもしれません。
fusion:Execute(NodeNmame:GetInput('ScriptedControls'))
最も活躍しそうな場面
表示したいコントロールだけを表示してボタン操作で表示するコントロールを切り替えると言った事が今回のアップデートでやり易くなりました。
開閉式のラベルを非表示にしておいてその開閉操作をマルチボタンコントロールやコンボボックスから行うと言った事も可能です。
特にラベルコントロールは閉じた状態でも1行分のスペースを必要とするので数が多くなるとそれだけでも表示領域を圧迫します。

ラベルを表示した状態

ラベル非表示だとスペースが稼げる
Label1 = {
INPID_InputControl = "LabelControl",
LBLC_DropDownButton = true,
LBLC_NumInputs = 2,
INP_Default = 1,
INP_External = false,
IC_Visible = false, -- ラベルコントロール自体を非表示に
LINKS_Name = "Label1",
},
サンプルsettingファイル
Execute On Changeを使ってどんなことができるか簡単なサンプルを作ってみました。

もしこの問題の解決方法をご存じの方が居たら教えて頂けると大変助かります。
{
Tools = ordered() {
OnChangeSample = GroupOperator {
CtrlWZoom = false,
Inputs = ordered() {
Input1 = InstanceInput {
SourceOp = "Background1",
Source = "TopLeftRed",
Name = "カラー",
ControlGroup = 2,
Default = 0,
},
Input2 = InstanceInput {
SourceOp = "Background1",
Source = "TopLeftGreen",
ControlGroup = 2,
Default = 0,
},
Input3 = InstanceInput {
SourceOp = "Background1",
Source = "TopLeftBlue",
ControlGroup = 2,
Default = 0,
},
Input4 = InstanceInput {
SourceOp = "Background1",
Source = "TopLeftAlpha",
ControlGroup = 2,
Default = 1,
},
Input5 = InstanceInput {
SourceOp = "ControlTool",
Source = "ColourBTN",
},
Input6 = InstanceInput {
SourceOp = "ControlTool",
Source = "Separator",
},
Input7 = InstanceInput {
SourceOp = "ControlTool",
Source = "SelectBTN",
},
Input8 = InstanceInput {
SourceOp = "ControlTool",
Source = "Spacer",
},
Input9 = InstanceInput {
SourceOp = "ControlTool",
Source = "Label1",
},
Input10 = InstanceInput {
SourceOp = "Ellipse1",
Source = "SoftEdge",
},
Input11 = InstanceInput {
SourceOp = "Ellipse1",
Source = "BorderWidth",
},
Input12 = InstanceInput {
SourceOp = "ControlTool",
Source = "Label2",
},
Input13 = InstanceInput {
SourceOp = "Ellipse1",
Source = "Center",
},
Input14 = InstanceInput {
SourceOp = "Ellipse1",
Source = "Angle",
},
Input15 = InstanceInput {
SourceOp = "ControlTool",
Source = "Label3",
},
Input16 = InstanceInput {
SourceOp = "Ellipse1",
Source = "Width",
Default = 0.5,
},
Input17 = InstanceInput {
SourceOp = "Ellipse1",
Source = "Height",
Default = 0.5,
},
},
Outputs = {
MainOutput1 = InstanceOutput {
SourceOp = "Background1",
Source = "Output",
}
},
ViewInfo = GroupInfo {
Pos = { 330, 82.5 },
Flags = {
AllowPan = false,
GridSnap = true,
AutoSnap = true,
RemoveRouters = true
},
Size = { 282.938, 132.364, 141.469, 24.2424 },
Direction = "Horizontal",
PipeStyle = "Direct",
Scale = 1,
Offset = { 0, 0 }
},
Tools = ordered() {
Ellipse1 = EllipseMask {
CtrlWShown = false,
Inputs = {
Filter = Input { Value = FuID { "Fast Gaussian" }, },
MaskWidth = Input { Value = 1920, },
MaskHeight = Input { Value = 1080, },
PixelAspect = Input { Value = { 1, 1 }, },
UseFrameFormatSettings = Input { Value = 1, },
ClippingMode = Input { Value = FuID { "None" }, },
Center = Input { Value = { 0.5, 0.5 }, },
Width = Input { Value = 0.5, },
Height = Input { Value = 0.5, }
},
ViewInfo = OperatorInfo { Pos = { -82.9688, 74.7576 } },
},
Background1 = Background {
CtrlWShown = false,
Inputs = {
EffectMask = Input {
SourceOp = "Ellipse1",
Source = "Mask",
},
GlobalOut = Input { Value = 299, },
Width = Input { Value = 1920, },
Height = Input { Value = 1080, },
UseFrameFormatSettings = Input { Value = 1, },
["Gamut.SLogVersion"] = Input { Value = FuID { "SLog2" }, },
},
ViewInfo = OperatorInfo { Pos = { 82.0312, 74.7576 } },
},
ControlTool = AlphaDivide {
PassThrough = true,
CtrlWZoom = false,
CtrlWShown = false,
NameSet = true,
Inputs = {
SelectBTN = Input { Value = 0,},
LabelScript = Input {
Value = [[
-- マルチボタンの戻り値が0-based indexing なので+1
local selected = ControlTool:GetInput("SelectBTN") + 1
local function SelectLabel(LabelGroup,TotalLabels)
for i = 1, TotalLabels do
local state = (i == selected and 1 or 0)
ControlTool:SetInput(LabelGroup .. i, state)
end
end
local function AllLabelsOpen(LabelGroup,TotalLabels)
for i = 1, TotalLabels do
ControlTool:SetInput(LabelGroup .. i, 1)
end
end
local function AllLabelsVisibility(isVisible,LabelGroup,TotalLabels)
for i = 1, TotalLabels do
ControlTool[LabelGroup .. i]:SetAttrs({INPB_IC_Visible = isVisible})
end
end
if selected == 4 then
AllLabelsOpen("Label",3)
AllLabelsVisibility(true,"Label",3)
else
SelectLabel("Label",3)
AllLabelsVisibility(false,"Label",3)
end
]]
},
ColourScript = Input {
Value = [[
local selected = ControlTool:GetInput("ColourBTN")
--r,g,b,aは0~1迄の数値(小数点第三位まで)
local function SetRGB(BGNode,r,g,b,a)
BGNode:SetInput("Type","Solid")
BGNode:SetInput("TopLeftRed",r)
BGNode:SetInput("TopLeftGreen",g)
BGNode:SetInput("TopLeftBlue",b)
BGNode:SetInput("TopLeftAlpha",a or 1) -- 引数省力時のディフォルト
end
local function SetRandomRGB(BGNode)
local r = math.floor(math.random() * 1000) / 1000
local g = math.floor(math.random() * 1000) / 1000
local b = math.floor(math.random() * 1000) / 1000
SetRGB(BGNode,r,g,b)
end
if selected == 0 then
SetRandomRGB(Background1)
else
SetRGB(Background1,0,0,0)
end
]]
}
},
ViewInfo = OperatorInfo { Pos = { 82.0312, 8.75758 } },
UserControls = ordered() {
SelectBTN = {
{ MBTNC_AddButton = "Label1" },
{ MBTNC_AddButton = "Label2" },
{ MBTNC_AddButton = "Label3" },
{ MBTNC_AddButton = "overview" },
INPID_InputControl = "MultiButtonControl",
LINKID_DataType = "Number",
INP_MinScale = 0,
INP_MaxScale = 3,
INP_MinAllowed = 0,
INP_MaxAllowed = 3,
INP_Integer = true,
MBTNC_ShowBasicButton = true,
MBTNC_ShowName = true,
INP_External = false,
MBTNC_StretchToFit = true,
MBTNC_ShowToolTip = false,
IC_NoLabel = true,
LINKS_Name = "SelectBTN",
INPS_ExecuteOnChange = "fusion:Execute(ControlTool:GetInput('LabelScript'))",
},
LabelScript = {
INPID_InputControl = "TextEditControl",
LINKID_DataType = "Text",
TEC_ReadOnly = false,
TEC_Lines = 20,
ICD_Width = 1.1,
INP_External = false,
INP_Passive = true,
IC_NoLabel = true,
TEC_Wrap = false,
TECS_Language = "lua",
LINKS_Name = "LabelScript",
},
ColourBTN = {
{ MBTNC_AddButton = "Random" },
{ MBTNC_AddButton = "Reset" },
INPID_InputControl = "MultiButtonControl",
LINKID_DataType = "Number",
INP_MinScale = 0,
INP_MaxScale = 1,
INP_MinAllowed = 0,
INP_MaxAllowed = 1,
INP_Integer = true,
MBTNC_ShowBasicButton = true,
MBTNC_ShowName = true,
INP_External = false,
MBTNC_StretchToFit = true,
MBTNC_ShowToolTip = false,
IC_NoLabel = false,
LINKS_Name = "カラーセット",
INPS_ExecuteOnChange = "fusion:Execute(ControlTool:GetInput('ColourScript'))",
},
ColourScript = {
INPID_InputControl = "TextEditControl",
LINKID_DataType = "Text",
TEC_ReadOnly = false,
TEC_Lines = 20,
ICD_Width = 1.1,
INP_External = false,
INP_Passive = true,
IC_NoLabel = true,
TEC_Wrap = false,
TECS_Language = "lua",
LINKS_Name = "ColourScript",
},
Label1 = {
INPID_InputControl = "LabelControl",
LBLC_DropDownButton = true,
LBLC_NumInputs = 2,
INP_Default = 1,
INP_External = false,
IC_Visible = false,
LINKS_Name = "Label1",
},
Label2 = {
INPID_InputControl = "LabelControl",
LBLC_DropDownButton = true,
LBLC_NumInputs = 2,
INP_Default = 0,
INP_External = false,
IC_Visible = false,
LINKS_Name = "Label2",
},
Label3 = {
INPID_InputControl = "LabelControl",
LBLC_DropDownButton = true,
LBLC_NumInputs = 2,
INP_Default = 0,
INP_External = false,
IC_Visible = false,
LINKS_Name = "Label3",
},
Spacer = {
INPID_InputControl = "SpacerControl",
},
Separator = {
INPID_InputControl = "SeparatorControl",
},
}
}
},
}
},
ActiveTool = "OnChangeSample"
}
Discussion
別の記事で同じ問題が扱われててそちらにコメントしたんですけど
操作してるツール側を判定してそっちだけ処理するというのがどうかなーって思ってます
Mugさん ありがとうございます。
ご紹介の記事を拝見しました。
なるほど、問題はインスタンス化したコントロールとコピー元のコントロールでそれぞれにイベントが発生してしまっているという事ですね。
試しに
INPS_ExecuteOnChangeをprint(self.ID)としてみると確かにコピー元のコントロールのIDとInput1等のマクロ化する際に自動的に付けられたInstanceInputのIDが表示されています。そして、ご提案の方法を試してみました。
結果はうまくいきました!!
print(self.ID)は一度しか実行されず、呼び出したコントロールのIDが表示されています。その上でインスタンス化した際に処理が重複するこの挙動が本当にBMDさんの意図した挙動なのか疑問ですねぇ。
個人的には
ButtonのExecuteではこの問題が起きていない所から仕様ではなくバグの可能性もあると思っています。仮に何度実行されても冪等性が担保されている処理だとしても重い処理だとパフォーマンス的に問題があるので困ったなぁと思っていました。
ひょっとしたら将来的にこの挙動が修正される可能性もありますが、それまではこの方法で対処できそうですね。
教えて頂いて本当にありがとうございます。
そうですよね、私も普通にバグだと思ってます🤢
新参者ですが何かのお役に立てたようでうれしく思います。コメント失礼いたします。
そういえば、FuseのNotifyChangedも、InstanceInputで2回以上実行されるようです。
よくわからないですね。
kcdlrさん 初めまして。コメントありがとうございます。
僕はプログラムの事はよく分かっていないので、AIに教えて貰いながら勉強しています。
マクロ化した時にOnChangedが2回実行されてしまう問題は、その現象には気が付いたものの
なぜそうなるのか原因が解らなくて困っていたので本当に助かりました。
その原因を解明したkcdlrさんと対処法を考えたMugさんには感謝しか無いです。
何かわからない事があったら質問するかもしれませんがよろしくお願いします。