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点の座標によりポリラインを形成し描画されます。
-
Line1
にModeSelector
という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
と座標が同期するようにしています。 -
Point1
とPoint2
の座標から角度を計算し、そこにプロジェクトのアスペクト比で補正した値を三角形の回転角度とし、線の向きと三角形の向きが同じになるようにしています。
Expressionとレンダースクリプトの挙動の違い
当初はTransformノード
のフレームレンダースクリプト
でself.Angle
に対して角度制御をしていましたが、再生をしてフレームが走り出すと正しい角度で描画されているものの、オーサリング時にはLine1
のフレームレンダースクリプト
で書き換えたPointの値を上手く拾ってくれない(反応しない)という問題がありました。
オーサリング時の見た目と出力される映像に乖離があってはUXとして破綻してしまいます。
ダミー変数を用意してPoint0
やPoint1
に依存するような形を作ってみましたが、結果は変わりませんでした。
レンダースクリプトはレンダリング時に評価されるという特徴を考えれば、これは自然な結果だと思います。しかし、その状態でも何故かPoint2
をユーザーの入力によって変更した時は三角形の角度が正しく反映されていました。
Point2
を動かした時は他と何が違うのかを観察してみるとTransformノード
のCenter
パラメータにExpression
を記述してPoint2
の座標と同期してある事に気が付きました。
この事からExpression
で参照して監視する事によってその値が更新された事をトリガーとしてExpression
が再評価されているのではないかという仮説を立てました。
結論から言うとその仮説は概ね当たっていました。(再評価なのか順当な評価なのかは分かりません)
試しにTransformノード
にテスト用のダミーバラメータ(Point型
)を追加してそのパラメータのExpression
でPoint0
とPoint1
を参照させてみた所、レンダースクリプトでPoint1
が書き換えられた事に反応するようになりました。
最終的にはダミーパラメータのExpression
で監視するくらいならレンダースクリプトで角度制御するのをやめて、Angle
パラメータのExpression
で角度制御して、そのExpression
の式内でPoint0
やPoint1
を参照する形にすれば良いと思いました。
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.ModeSelector
もLine1.Point0
も一切関与していない事が分かります。角度の計算には必要ないパラメータです。
しかし、この式からLine1.ModeSelector
とLine1.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