Open3

DaVinci Resolve--Expressionのトリガーとなる条件のメモ

セカイルセカイル

Expressionの発動条件

Expressionは何も参照しない単なる計算式によっても評価されますが、自ノードや他ノードのパラメータを参照する事ができます。また、フレーム(time)を参照したり、ノードにSetData()した値(カスタムデータ)も参照する事が出来ます。
また、iif(time % n == 0, trueの時の値,falseの時の値 )としてnフレーム毎に処理を実行する事もできます。
Expressionが返す値が更新されるにはそのExpressionが参照している値のうちのいずれかが更新される事によって発動する仕組みのようです。


Expressionとレンダースクリプトの評価順

Expressionレンダースクリプトの評価順についてはフレームレンダースクリプトレンダリング時に評価され、Expressionはその前に評価されていると思います。

オーサリング時(フレームを再生して走らせていない状態)にフレームレンダースクリプトで書き換えた値をExpressionで参照しようとすると書き換えられる前の値を参照してしまう現象がありました。
私はこれをフレームレンダースクリプトより前にExpressionが評価されるため、フレームレンダースクリプトによって書き換えられた値をExpressionが正しく参照できていないのだと思いました。

しかし、オーサリング時でもフレームレンダースクリプトで書き換えられた値をExpressionが正しく参照できているケースもあり、単純な評価順の問題だけでは無いと考えるようになりました。

セカイルセカイル

Expressionの発動条件の具体例その1(参照するだけ)

Expressionが返す値が更新されるにはそのExpressionが参照している値のうちいずれかが更新される事が条件だと思いますが、更新のトリガーとなる値は参照している事が条件で、必ずしもreturnされる値に関与している必要は無いという事に気が付きました。

前提条件

具体例として私が作成した小規模なマクロを例にします。
ノードやパラメータの構成は以下のようになっています。

  • Line1というPolygonノードにIDがPoint0,Point1,Point2という3点のPoint型のパラメータがあり、それぞれに「開始点」「中間点」「終了点」という名前を付けています。
    それぞれのPointはパブリッシュしてあり、他のノードからも参照できるようにしています。
  • 上記3点の座標によりポリラインを形成し描画されます。
  • Line1ModeSelectorというIDのComboControlを追加し名前を「モード選択」としています。
    ModeSelectorには5つの選択肢があり、返される値は0~4迄の整数です。
    その選択状況によって各Pointの制御を変えるようにしています。具体的にはこのようになっています。
ModeSelectorの値 モード名 Pointの制御
0 フリーモード 自動補正無し
1 中間点の高さを終了点に合わす Point1のYがPoint2のYと同期
2 中間点の高さを開始点に合わす Point1のYがPoint0のYと同期
3 直線モード Point1がPoint0と完全に同期
4 水平線モード Point1がPoint0と完全に同期
Point2のYがPoint0のYと同期
  • Line1フレームレンダースクリプトは以下のように記述しています。
local Mode = tonumber(self.ModeSelector) or 0
if self.Point0 and self.Point1 and self.Point2 then
    if Mode == 1 then
        self.Point1.Y = self.Point2.Y
    elseif Mode == 2 then
        self.Point1.Y = self.Point0.Y
    elseif Mode == 3 then
        self.Point1.X = self.Point0.X
        self.Point1.Y = self.Point0.Y
    elseif Mode == 4 then
        self.Point1.X = self.Point0.X
        self.Point1.Y = self.Point0.Y
        self.Point2.Y = self.Point0.Y
    end
end
  • RectangleノードPoint0に、TriangleというsNgonノードを→sRenderノードTransformノードと接続しTransformノードCenterパラメータでPoint2と座標が同期するようにしています。
  • Point1Point2の座標から角度を計算し、そこにプロジェクトのアスペクト比で補正した値を三角形の回転角度とし、線の向きと三角形の向きが同じになるようにしています。

Expressionとレンダースクリプトの挙動の違い

当初はTransformノードフレームレンダースクリプトself.Angleに対して角度制御をしていましたが、再生をしてフレームが走り出すと正しい角度で描画されているものの、オーサリング時にはLine1フレームレンダースクリプトで書き換えたPointの値を上手く拾ってくれない(反応しない)という問題がありました。
オーサリング時の見た目と出力される映像に乖離があってはUXとして破綻してしまいます。

ダミー変数を用意してPoint0Point1に依存するような形を作ってみましたが、結果は変わりませんでした。
レンダースクリプトはレンダリング時に評価されるという特徴を考えれば、これは自然な結果だと思います。しかし、その状態でも何故かPoint2をユーザーの入力によって変更した時は三角形の角度が正しく反映されていました。

Point2を動かした時は他と何が違うのかを観察してみるとTransformノードCenterパラメータにExpressionを記述してPoint2の座標と同期してある事に気が付きました。
この事からExpressionで参照して監視する事によってその値が更新された事をトリガーとしてExpressionが再評価されているのではないかという仮説を立てました。

結論から言うとその仮説は概ね当たっていました。(再評価なのか順当な評価なのかは分かりません)
試しにTransformノードにテスト用のダミーバラメータ(Point型)を追加してそのパラメータのExpressionPoint0Point1を参照させてみた所、レンダースクリプトでPoint1が書き換えられた事に反応するようになりました。

最終的にはダミーパラメータのExpressionで監視するくらいならレンダースクリプトで角度制御するのをやめて、AngleパラメータのExpressionで角度制御して、そのExpressionの式内でPoint0Point1を参照する形にすれば良いと思いました。

Expressionのトリガーは式内で参照するだけで良い

Angleパラメータに以下のExpressionを記述しています。

:
local Mode = tonumber(Line1.ModeSelector ) or 0
local AR = Line1:GetData("aspectRatio") or (16/9)

local P0 = Line1.Point0 or Point(0,0)
local P1 = Line1.Point1 or Point(0,0)
local P2 = Line1.Point2 or Point(0,0)

local dx = P2.X - P1.X
local dy = (P2.Y - P1.Y) / AR

return math.deg(atan2(dy, dx))

このコードにおいて最終的にreturnされる値にはLine1.ModeSelectorLine1.Point0も一切関与していない事が分かります。角度の計算には必要ないパラメータです。
しかし、この式からLine1.ModeSelectorLine1.Point0を参照している部分を削除するとそれらの値が更新された事に反応しなくなります。
つまり、Expressionのトリガーにするには参照して式内に参加させるだけで良く、必ずしもreturnする値に関与している必要は無いという事が分かりました。

セカイルセカイル

Expressionの発動条件の具体例その2(Point型への代入)

具体例その1でExpressionのトリガー条件みたいなものを見つける事が出来たので喜んでいたのですが、意外な落とし穴があり詰まってしまいました。
Mode == 3 (直線モード)の時とMode == 4 (水平線モード)の時には何故かExpressionが上手く反応しないという現象がありました。
その時のLine1フレームレンダースクリプトはこう記述していました。

local Mode = tonumber(self.ModeSelector) or 0
if self.Point0 and self.Point1 and self.Point2 then
    if Mode == 1 then
        self.Point1.Y = self.Point2.Y
    elseif Mode == 2 then
        self.Point1.Y = self.Point0.Y
    elseif Mode == 3 then
        self.Point1 = self.Point0    -- 意外なことに実はこの代入方法が原因でした。
    elseif Mode == 4 then
        self.Point1 = self.Point0    -- ここもそう
        self.Point2.Y = self.Point0.Y
    end
end

ここで、レンダースクリプトで使用できるPoint型のパラメータへの代入方法を私の知る限り列挙したいと思います。

PointA = PointB    -- 同じPoint型同士ならOK
PointA = PointB + PointC    -- 同じPoint型同士でも単純な+演算子等による計算はNG
PointA.X = PointB.X + PointC.X    -- 一度X,Yに分解して個別に代入すれば計算もOK
PointA.Y = PointB.Y    -- 個別代入。当然計算無しでもOK
PointA = { x , y }    -- テーブル型でも代入できた。
PointA = Point( x , y )    -- Point()関数による代入

そこで、改めてじっくりとLine1フレームレンダースクリプトを観察してMode == 3及びMode == 4の時とそれ以外のモードで何が違うのか探しました。
まさかPoint型代入方法が原因だとは思いもしなかったのでその違いに気付くには結構時間がかかってしまいました。
しかし、よく見てみるとMode == 2まではYを個別に代入していますがMode == 3の時とMode == 4の時はPoint型同士をまるっとコピーするような形で代入しています。

Line1のフレームレンダースクリプトでのPoint制御としてはPoint1 = Point0も個別に代入しても結果としては同じで正しく動作します。
しかし、上記で挙げたPoint型への代入方法のうち個別で代入した場合にのみExpressionが上手く反応しました。
つまり、Expressionが監視しているPoint型の値がExpressionから見て更新されたとみなされるにはPoint型のパラメータへの代入は個別代入でなければならないという条件があるようです。

なぜそうなのかは分かりません。「そういう仕様なんだな」と解釈するしかありません。
XもYも同期するのにわざわざ個別代入するのは少し冗長な気がしてしまいましたが、最終的にLine1フレームレンダースクリプトは以下のようにしました。

local Mode = tonumber(self.ModeSelector) or 0
if self.Point0 and self.Point1 and self.Point2 then
    if Mode == 1 then
        self.Point1.Y = self.Point2.Y
    elseif Mode == 2 then
        self.Point1.Y = self.Point0.Y
    elseif Mode == 3 then
        self.Point1.X = self.Point0.X
        self.Point1.Y = self.Point0.Y
    elseif Mode == 4 then
        self.Point1.X = self.Point0.X
        self.Point1.Y = self.Point0.Y
        self.Point2.Y = self.Point0.Y
    end
end