🕒

【DaVinci Resolve】コントロール変更時の処理の設定

に公開2

まえがき

こんにちは。火注ゆかなです。

今回はコントロールの値を変更した場合の処理を設定する方法についてです。
We Suck Less フォーラムで言及されていました。
https://www.steakunderwater.com/wesuckless/viewtopic.php?p=56457#p56457

コントロールの値が変更された時に特定の処理をするというのはこれまで Fuse (自作プラグイン)でしか実装できませんでしたが、これからはユーザーコントロールだけでなくText+などの既存ノードのコントロールにも設定できるようになりました。
これにより、できることの幅がグッと広がる……と思いきや、いざ検証してみると思ったより使い勝手が悪いです。特にアニメーションには使用できないので使いどころに悩みます。

そういう注意点も踏まえて使い方の説明をしていきます。


コントロール変更時の処理設定

DaVinci Resolve ver.20.0 からコントロールの属性にINPS_ExecuteOnChangeが追加されました。
例えば、Fusionページで適当にTextPlusノードを配置し、ノードを選択した状態でコンソールで==ActiveTool.StyledText:GetAttrs()を実行すると属性一覧を確認できます。

バージョン 20.0 からはコントロール属性にINPS_ExecuteOnChangeが確認できる

INPS_ExecuteOnChange属性にスクリプトをリテラル文字列で設定すると該当コントロール変更時に処理が実行されますが、デフォルトでは何も設定されていません。

設定方法

ひとまず、試しに設定してみましょう。

  1. TextPlusノードを選択した状態で右クリックしてコンテキストメニューを表示→コントロールを編集をクリックしてコントロールを編集画面を表示します。
  2. 適当なユーザーコントロールを追加します。とりあえず今回はIDAddControl入力SliderControlにします。
  3. 中央下にOn Changeという欄があるので、そこに下記スクリプトをコピペして保存してください。

    On Change欄にコントロール変更時に実行したいスクリプトを記述
設定するスクリプト
tool:SetInput("StyledText", self.ID.." : "..self[0]))   -- 表示テキスト変更

これでAddControlコントロールを変更すると、その値に連動してStyledTextコントロールのテキストが変更されるようになりました。

AddControlと連動してテキストが更新されるようになった

変数selftoolについて

selfはコントロール自身、toolはコントロールが所属するノードを参照する変数です。
例えば、先ほど実行したコマンドだとselfAddControlコントロール、toolTextPlusノードを参照しています。

tool:SetInput("StyledText", self.ID.." : "..self[0])    -- 表示テキスト変更

現在の再生フレームに応じた処理をしたい場合

キーフレームアニメーションを設定している場合、現在の再生フレームに応じた値を参照して処理したいときもあるでしょう。
現在の再生フレームはcomp.CurrentTimeで参照できます。キーフレームアニメーションが未設定なら0を指定した場合と同じ挙動になります。(現在のコントロール値が表示される)
例えば、下記のスクリプトでは再生フレームに応じてテキストとコントロールのラベルを更新します。

tool:SetInput("StyledText", self.ID.." : "..self[comp.CurrentTime])    -- 表示テキスト変更
self:SetAttrs({INPS_Name=tostring(self[comp.CurrentTime])})       -- ラベルを再生フレーム番号に変更する


AddControlの再生中の値でテキストとラベル更新

ローカル変数とグローバル変数の使用、ログ出力

local を付けて変数を宣言すればローカル変数を使用することもできるため、複雑な処理も記述しやすいです。
下記のようにprint() dump()関数も使えるのでコンソールへログ出力して動作確認も可能です。

local v = 100
print("ローカル変数1:".. v)
v = v + 20
print("ローカル変数2:".. v)


ローカル変数の実行結果

変数にlocalを付けない場合、コンソールからでも参照・操作が可能なグローバル変数として扱われます。

print("グローバル変数:".. v)


変数vは宣言していないのにコンソールで代入した値が表示された

他のコントロールだけでなく他のノードからもアクセスできてしまうため、誤動作の要因となりやすいことを考えると推奨できません。基本的にはlocalを付けてスコープを制限するべきでしょう。
どうしても同じノード内で値を共有したい場合、tool:SetData(key, value) tool:GetData(key)を使用しましょう。ノードにキーを指定して値を保存・読み込みすることが可能です。

同じノード内での値の共有
local key = "ValueName"     -- "Name1.name2"という風に "."(半角ドット)で区切れば入れ子の連想配列としても扱えます
tool:SetData(key, value)    -- ノードへの値の保存
tool:GetData(key)           -- ノードに保存した値の読み込み(キーが存在しない場合は nil を返却)

テキストエディタで記述する

On Change欄はとても小さいので複雑な処理を記述するのには不向きです。
これだけならテキストエディタで編集したスクリプトをコピペするだけで良いのですが、コントロールを編集画面で編集すると、編集されたコントロールがページの一番下に移動してしまうというデメリットがあります。

ノードをテキストエディタにコピペしてからINPS_ExecuteOnChange属性を追記すればユーザコントロールの並び順を変更せずに設定・編集が可能になりますし、長い処理を記述する際も視認性が良くなります。

テキストエディタで直接編集した場合
{
    Tools = ordered() {
        Text1 = TextPlus {
            CtrlWZoom = false,
            Inputs = {
                GlobalOut = Input { Value = 995, },
                Width = Input { Value = 1920, },
                Height = Input { Value = 1080, },
                UseFrameFormatSettings = Input { Value = 1, },
                ["Gamut.SLogVersion"] = Input { Value = FuID { "SLog2" }, },
                Wrap = Input { Value = 1, },
                LayoutRotation = Input { Value = 1, },
                TransformRotation = Input { Value = 1, },
                Softness1 = Input { Value = 1, },
                StyledText = Input { Value = "AddControl : 0", },
                Font = Input { Value = "Open Sans", },
                Style = Input { Value = "Bold", },
                VerticalJustificationNew = Input { Value = 3, },
                HorizontalJustificationNew = Input { Value = 3, }
            },
            ViewInfo = OperatorInfo { Pos = { 275, 82.5 } },
            UserControls = ordered() { AddControl = {
                    LINKS_Name = "Addcontrol",
                    LINKID_DataType = "Number",
                    INPID_InputControl = "SliderControl",
                    INP_Default = 0,
                    INP_Integer = false,
                    INP_MinScale = 0,
                    INP_MaxScale = 1,
                    INP_MinAllowed = -1000000,
                    INP_MaxAllowed = 1000000,
                    INP_SplineType = "Default",
                    INPS_ExecuteOnChange = [[   -- 属性追記
                        local v = 100
                        print("ローカル変数1:".. v)
                        v = v + 20
                        print("ローカル変数2:".. v)
                    ]]
                } 
            }
        }
    },
    ActiveTool = "Text1"
}

エクスプレッションとの違い

他のコントロールの値と連動させる機能としては既にエクスプレッションが存在します。
エクスプレッションでは属性値を参照できないなどの制限があるため、INPS_ExecuteOnChangeの方がちゃんとしたLuaで記述できる分、自由度は段違いです。

ではINPS_ExecuteOnChange属性はエクスプレッションの上位互換だから、エクスプレッションはもう不要で、これからはINPS_ExecuteOnChange属性に全て置き換えるべきなのでしょうか?

いいえ、エクスプレッションはINPS_ExecuteOnChange属性に完全に置き換えることはできません。INPS_ExecuteOnChange属性はアニメーションには使えないからです。

アニメーションには使えない

先ほどの例ではINPS_ExecuteOnChangeでコントロールの値を変更していましたが、結果的にアニメーションには使えません。
INPS_ExecuteOnChangeに設定したスクリプトはレンダリング時に動作していないようです。 なんならインスペクタ欄で該当コントロールを表示していないだけでも動作しないことがあります。

例えば、AddControlINPS_ExecuteOnChange属性に下記のスクリプトを設定します。

tool:SetInput("StyledText", self.ID.." : "..self[comp.CurrentTime])    -- 表示テキスト変更
self:SetAttrs({INPS_Name=tostring(self[comp.CurrentTime])})       -- ラベルを再生フレーム番号に変更する

これをレンダリングすれば再生時間に応じて0から1へ徐々に数値が増加していくはずですが、レンダリングしても0のまま変化しません。(正確には最後に表示した値がそのままとなる)
アニメーションに利用できない以上、エクスプレッションの代替はできませんね。

EditページやFusionページでは動いてるから大丈夫! と思っていてもいざレンダリングすると動かないなんてことを防ぐためにも INPS_ExecuteOnChangeを設定するコントロールにはキースプライン(アニメーション)やエクスプレッションの設定はしない方が無難かと思います。

他のコントロール操作を効率化できる?

アニメーションには利用できませんが、一つのコントロールから他のコントロールを操作する場合には効率化できるかもしれません。

手動操作との併用

図のようにコントロール同士で値の連動をさせたいことがあります。

コントロール間の値の連動はエクスプレッションがありますが、エクスプレッションが設定されたコントロールは値の手動変更ができません。
(正確には手動変更した直後に即エクスプレッションで設定された値で上書きされる?)
また、コントロールA を参照するコントロールが複数存在する場合、エクスプレッションで連動させようとすると コントロールB,C,D......といった連動させたいコントロール全てにエクスプレッションを設定する必要がありました。

INPS_ExecuteOnChangeならコントロール A 変更時にコントロール B,C,D を操作しつつ、手動でも操作することができますし、コントロールA のINPS_ExecuteOnChangeで対象コントロールを一括操作するようにすれば編集箇所をまとめることができます。

常時アニメーションさせる必要はないけど、複数のコントロールの値をまとめて変更したいときもある……という場合に向いてるかもしれません。
例えば、複数のコントロールの値セット①②③~と複数保存しておいて、コンボボックスを①に変更したらデータセット①を初期値として読み込む……みたいなセーブ・ロード機能みたいなのとか。
(でもこれは今までもButtonControlと組み合わせれば普通にできたし、そっちの方が安全だからやるべきではないかも……)

データセットを変更するとコントロール 1~3 の初期値が読み込まれる

ただし、エクスプレッションと違ってどのコントロールにINPS_ExecuteOnChangeが設定されているのか、どのコントロールがINPS_ExecuteOnChangeで変更されるのか一目ではわからないため、管理の仕方に注意する必要があります。
後述するようにラベルをHTMLで記述することで文字色を変更して可視化するのも良いかもしれません。

それからINPS_ExecuteOnChangeの応用として、コントロール A と B を相互に変更するようなことも可能ではあるでしょう。でも推奨はしません。
A を更新→ B を更新→ A を更新→……という無限ループが発生しかねないのでフラグ管理をしっかりする必要がありますし、処理が複雑になる上に意図せぬ挙動をしやすくなるだろうことは想像に難くないからです。コントロール間の主従関係はシンプルにするべきです。
推奨はしませんが後々必要になることもあるかもしれないので、一応書き残しておきます。

属性の一時変更

エクスプレッションは値の変更しかできませんが、INPS_ExecuteOnChangeならInput:SetAttrs()で属性を一時的に変更することができます。
パッと思いつくところでは、以前紹介したインスペクタ欄に画像を表示する場合です。
https://zenn.dev/hitsugi_yukana/articles/fusion_inspector_html
コントロールのラベルはHTMLを記述できることを利用して画像を表示するというテクニックですが、エクスプレッションでは属性値を変更できないので Fuse 以外はコントロールの値に合わせてラベルの動的な書き換えができないという問題がありました。
INPS_ExecuteOnChangeならInput:SetAttrs({INPS_Name=[[HTML文]]})という風にINPS_Name属性を書き換えられるのでこの問題を解決できます。

AddControlの値が0.5以上ならHTMLを書き換えて画像表示、そうでないなら非表示に切り替えるようなスクリプトを設定すると下記のようになります。

{
    Tools = ordered() {
        Text1 = TextPlus {
            CtrlWZoom = false,
            Inputs = {
                GlobalOut = Input { Value = 995, },
                Width = Input { Value = 1920, },
                Height = Input { Value = 1080, },
                UseFrameFormatSettings = Input { Value = 1, },
                ["Gamut.SLogVersion"] = Input { Value = FuID { "SLog2" }, },
                Wrap = Input { Value = 1, },
                LayoutRotation = Input { Value = 1, },
                TransformRotation = Input { Value = 1, },
                Softness1 = Input { Value = 1, },
                StyledText = Input { Value = "AddControl : 0.069346733668342", },
                Font = Input { Value = "Open Sans", },
                Style = Input { Value = "Bold", },
                Size = Input { Value = 0.2677, },
                VerticalJustificationNew = Input { Value = 3, },
                HorizontalJustificationNew = Input { Value = 3, },
                AddControl = Input { Value = 0.2677, }
            },
            ViewInfo = OperatorInfo { Pos = { 165, 49.5 } },
            UserControls = ordered() { 
                AddControl = {
                    LINKS_Name = "0.069346733668342",
                    LINKID_DataType = "Number",
                    INPID_InputControl = "SliderControl",
                    INP_Default = 0,
                    INP_Integer = false,
                    INP_MinScale = 0,
                    INP_MaxScale = 1,
                    INP_MinAllowed = -1000000,
                    INP_MaxAllowed = 1000000,
                    INP_SplineType = "Default",
                    INPS_ExecuteOnChange = [[
                        -- 値が0.5以上なら画像表示
                        if self[comp.CurrentTime] >= 0.5 then
                            tool.HTMLLabel:SetAttrs({INPS_Name=[=[<img src="I:/Image.png" width="100">]=]})    -- 画像表示
                        else
                            tool.HTMLLabel:SetAttrs({INPS_Name=[=[<span style="color:red;">画像非表示</span>]=]})    -- リテラル文字列の入れ子になるので [=[ ]=] と言う風に括弧の間に = を挟む
                        end
                    ]],
                },  
                HTMLLabel = {  -- 画像表示用ラベルコントロール
                    LINKS_Name = [[<span style="color:red;">ラベル</span>]],
                    LINKID_DataType = "Number",
                    INPID_InputControl = "LabelControl",
                    INP_Integer = false,
                    INP_SplineType = "Default",
                    LBLC_DropDownButton = false,
                    INP_External = false,
                    LBLC_MultiLine = true,
                    IC_NoLabel = true,         -- ←ラベル非表示
                },
            }
        }
    },
    ActiveTool = "Text1"
}


値が 0.5 以上なので画像非表示

値が 0.5 未満なので画像非表示

HTMLなら文字色の変更などもできますが、HTMLが動作するのはユーザコントロールのみで既存のコントロール(TextPlusノードならStyledTextSize等)だとHTMLが上手く動作しないようです。
下記のようにSizeコントロールのラベル文字色を変更しようとしても素のHTML文がそのまま表示されます。

tool.Size:SetAttrs({INPS_Name=[=[<span style="color:red;">サイズ</span>]=]})    -- リテラル文字列の入れ子になるので [=[ ]=] と言う風に括弧の間に = を挟む


ラベル文字色を変えるはずの HTML が反映されない

HTMLで文字色を変更したりしたいならユーザコントロールを利用しましょう。

余談:エクスプレッションについて

今回の記事を書くためにエクスプレッションについて調べ直したので、そのあたりをちょっと書いておきます。

マルチステートメントで複数のコントロールの値変更は可能(非推奨)

マルチステートメントに関してはこちらを参考にさせていただきました。
https://zenn.dev/muglab3/articles/1d32711f7eb419

エクスプレッションってマルチステートメント(1行に複数の命令を記述)ができるんですね。エクスプレッションは最初の頃に少し触れたっきりだったので全く知りませんでした。

で、マルチステートメントを使えば一つのコントロールのエクスプレッションで複数コントロールの値を一括変更することもできないかなって試してみました。
イメージとしてはこんな感じですね。

一か所のエクスプレッションによる複数のコントロールの値変更
:
Size = AddControl * 10
CharacterSpacingClone = AddControl
LineSpacingClone = AddControl

return AddControl * 10

試してみたらできたにはできたんですが……なんか挙動がおかしいのでやらない方が良いですね。

再生するとコントロールの値は変更されているんですが、ユーザコントロールに設定するとレンダリングに反映されなかったり、ユーザコントロールじゃなくても手動で弄ると実際の値とレンダリング結果が一致しなくなったりして何がなにやら。
変なことせず、1つのエクスプレッションで変更するのは設定コントロール一つだけにするべきですね。

エクスプレッションで if 文が使えた

エクスプレッションってif文で条件分岐できるんですね。条件分岐はiif()関数しか使えないものかと思っていましたが、なんとなく試してみたら普通に動いてビックリしました。

エクスプレッションでの if 文処理
:
v = 0
-- 指定のコントロールの値が0.5以上なら1、0.5未満なら0に設定
if AddControl >= 0.5 then
    print("big")
    v = 1
elseif AddControl == 0 then
    print("zero")
else
    print("less")
end

return v

他所様の記事では「条件分岐はifじゃなくてiifだよー」って書いてあるところばかりなので、どこかでシレっと追加されたんですかね? 
まあ条件分岐処理が書きやすくなったならいつからでも構いませんけれども。

あとがき

INPS_ExecuteOnChangeについての説明は以上となります。
Fuse のNotifyChangedイベントをイメージしていたのですが、思ったより機能が制限されてて使いづらいなぁという印象ですね。

今年は暑くなるのが早いので大変だと思いますが、皆さん体調管理にお気を付けて。
冷房はケチらず使いましょうね。節約するのも良いことですが、冷房ケチって熱中症になったら節約した電気代以上の医療費が飛んでいきますし、最悪死んじゃいますからね。

それでは、今回の内容が皆さんのお役に立てば幸いです。

Discussion

MugMug

すごくいいことを聞きました、ありがとうございます👍
ComboControlとかは選択したときに処理をしたいとずっと思ってたので色々できそうです🤔
色々試してみますー

他所様の記事では「条件分岐はifじゃなくてiifだよー」って書いてあるところばかりなので

これはシングルステートメントだとiifじゃないと戻り値返せないのでiifじゃないとだめだよーって言われてるんだと思います

火注ゆかな火注ゆかな

ComboControl との組み合わせについては間違って変更しちゃいやすいかなと思いましたが、一々ボタン押さずに済むのでサクサク変更できるのは確かに良いかもしれません。
なるほど、if 文に関しては以前から使えたけど、そもそも前提条件としてのマルチステートメント自体が知られてなかったってことなんですかね。