After Effectsのエクスプレッションのライブラリを考える。

上記記事の最後の方で、エクスプレッションライブラリを作るならこんな感じじゃないかというのを書いた。 それから色々思索しながら、どういうのがいいのかを探っている。

以下が経過中。

@scriptスクリプトを書く人向けに作っているように、これもエクスプレッションを書く人を想定して作っている*1。 単一のエクスプレッションをコピペするだけで済むだけなら簡単であるが、他のパラメータを参照したり、あるパラメータのエクスプレッション適用前の値を取得するなど、複雑なことをやろうとするとスクリプトを介する必要がある。そういった場合にどうすればスマートになるのか、現状、解は出ていない。

ただそうは言っても、数学だったりアルゴリズムだったりは下界を無視して構築できるし、テキストのスタイルもテキストレイヤーのソーステキストだけで完結させられるので、都合が良い。

テキストスタイル

スクリプトではバージョンの24.3、エクスプレッションでは25.0から、文字ごとにスタイルを変更することが可能となった。スクリプトとエクスプレッション、それぞれに利点があるが、エクスプレッションの場合、非破壊的に色々試せるのが一番の魅力である。重ければベイクすればいい。

使いやすくないと作る意味がないので、複雑性を如何にライブラリ側に閉じ込め、ユーザー側にはいい感じのAPIを提供する必要があるが、そこがやはり一番頭を使う。

静的

混植は、文字種によってスタイルを変える操作である。つまり、ある条件を満たしたものに対し、あるスタイルを適用する、というパターンと見なせる。

const { CharClass, TextStyle } = footage("@text.jsx").sourceData.load();

TextStyle.byCharClass()
    .rule(CharClass.Hiragana, { applyFill: true, fillColor: [0.8, 0.7, 0.2], applyStroke: false, fontSize: 45 })
    .rule([CharClass.Katakana, CharClass.Yakumono], { applyFill: false, applyStroke: true, strokeColor: [0.5, 0.6, 0.9], fontSize: 55, strokeWidth: 2 })
    .rule(CharClass.Kanji, { applyFill: true, fillColor: [0.4, 0.8, 0.3], applyStroke: false, fontSize: 60 })
    .apply();

こういったパターンの場合、宣言的に書けると分かりやすいので、現状こうなってる。

const { TextStyle } = footage("@text.jsx").sourceData.load();

TextStyle.byPosition()
    .line() // 行ごとにカウントを刷新する。
    .countWhen("nonWhitespace") // 空白はカウントしない。
    .rule((index, line) => (index + line) % 2 === 0, { applyFill: true, fillColor: [0.7, 0.8, 0.2], applyStroke: false, fontSize: 50 })
    .rule((index, line) => (index + line) % 2 === 1, { applyFill: false, applyStroke: true, strokeColor: [0.3, 0.6, 0.75], strokeWidth: 2, fontSize: 60 })
    .apply();

条件としては、文字位置や行位置、

const { TextStyle } = footage("@text.jsx").sourceData.load();

TextStyle.bySurrounding("「", "」", { depth: 0 })
    .rule({ applyFill: false, applyStroke: true, strokeColor: [0.7, 0.9, 0.8], strokeWidth: 2 })
    .apply();

はたまた括弧など、色々考えられる。

const { CharClass, TextStyle } = footage("@text.jsx").sourceData.load();

TextStyle.compose()
    .add(
        TextStyle.byCharClass()
            .rule(CharClass.Kanji, { fontSize: 100 })
    )
    .add(
        TextStyle.byPosition()
            .rule((index) => index % 2 === 0, { applyFill: true, applyStroke: false, fillColor: [0.4, 0.5, 0.8], fontSize: 40 })
    )
    .add(
        TextStyle.bySurrounding("「", "」")
            .rule({ applyFill: false, applyStroke: true, strokeColor: [0.7, 0.2, 0.6], strokeWidth: 2, fontSize: 120 })
    )
    .apply();

また、人は往々にして複数のものを見るとそれを積み上げたがる。

動的

宣言的だと、どうしても、条件とスタイルを個別、分離的に指定することになる。なので、

const { TextStyle } = footage("@text.jsx").sourceData.load();
const { Oklch } = footage("@color.jsx").sourceData.load();

TextStyle.forEachGrapheme((g, ctx) => {
    return { applyFill: true, applyStroke: false, fillColor: new Oklch([0.7, 0.1, random(ctx.index)]).toRGB().get() };
}).apply();

書記素を舐めていって、その文脈に応じてスタイルを返すことで、都度適用出来るようにした。


const { TextStyle } = footage("@text.jsx").sourceData.load();
const d = Math.floor(3 * timeToFrames(time));
TextStyle.forEachGrapheme((g, ctx) => {
    const seed = ctx.index + d;
    return {
        applyFill: noise(seed) > 0,
        fillColor: hslToRgb([0.5 * (1.0 + noise(seed + 0.5)), 0.6, 0.5, 1]).slice(0, 3),
        applyStroke: noise(seed + 1.2) > 0,
        strokeColor: hslToRgb([0.5 * (1.0 + noise(seed + 4.2)), 0.6, 0.5, 1]).slice(0, 3),
        fontSize: 30 + 70 * (1.0 + noise(seed + 1.4142)),
        tsume: 150 * (1.0 + noise(seed + 2.236)),
        baselineShift: 150 * noise(seed + 9.2),
    };
}).apply();

余談

スタイルの話

エクスプレッションで文字ごとにスタイルを設定する際、text.sourceText.style.setFillColor([1, 0, 0, position, count);のように位置と文字数を指定するのであるが、オーバーラップするような区間に対して設定する場合の優先順序がややこしい。後で適用した側が優先される仕組みではなく、範囲の指定の仕方によって優先順位が決定されるようなので、弥縫策として一文字ずつ設定するようにしてあり*2、必ず後で指定した方が優先されるようになっている。

ライブラリの話

TypeScriptの小話
{
    method() {
    },
}

のように、いきなり{を書くとブロック文だと認識されるので、

({
    method() {
    },
})

のように、 ( ) で囲んでやって、これはオブジェクトですよと主張する必要がある。幸いなことに、丸括弧で囲んでも.jsx であれば、AE側できちんと読み込んでくれる。ただし、トランスパイルをすると勝手に末尾に ; が付けられるので、これは取り除く必要がある。

あと、同じプロジェクト内でライブラリ側ではなくて、エクスプレッション側として

const L = thisComp.layer("Parent");

のようなものを複数のファイルで書くと、二重定義で怒られる。つまり、TypeScript側でグローバルだと解釈されてしまうので、

export { };

const L = thisComp.layer("Parent");

のように、空の export {}; を書いて、ファイル単位ですよとアピールする必要がある。これも、後処理で取り除きたい。

構成の小話

上のようなMotion Developerさんとこのように、一機能ずつプロジェクトを分けたり、モジュールバンドラーなどを用いるのは、AEのスクリプトやエクスプレッションの開発では、あまりにも過剰だと思っている。なので環境を出来るだけシンプルに保ちたいが、上で書いたようにトランスパイル後に ; などを取り除く必要があるので、何かしらの後処理はする必要がある。TypeScript前提の環境であるから、必然的にNode.js環境でもあるので、適当にJavaScriptで補助用のスクリプトを書くのが最適なのかな。

*1:ライブラリであるから当然のことではあるが

*2:無駄に重くする行為であるので、範囲が重複したら分割して、みたいな方向が考えられるが、要思索

色収差再び

キリスト降誕おめでとうございます。

映像制作ゆる Advent Calendar 2025の25日目の記事です。




惰眠を貪っていたら、上記記事から1年経ってしまっていた。遅ればせながら、自分なりの回答として以下を提示したい。

at.atarabi.com



分解

2色3色のみならず、大量の色を指定出来るようにした。また、色数と何分割するかは独立した話であるので、分割数を別途変えられるようにした。


色の指定に自由度に幅が出ると、複雑なグラデーションも構成出来るので表現に幅が出る。


合成

一般的な加算による合成だけでなく、比較明による合成も出来るようにした。

分割数を増やしつつ元の色調を維持しようとする場合*1、加算による合成だと、一つ一つの重みの寄与が小さくなるため、指定した色に比べ暗くなってしまう。


一方、比較明だと指定した色を維持しやすい。


Additional Colorというパラメータも追加した。一見摩訶不思議なパラメータであるが、

比較明において、このように比較的暗い色を指定してNormalizeさせると、指定した色よりどうしても明るくなってしまう。

そこで、Additional Colorで白色を追加することで、色補正のしわ寄せをその白色だけに押し付けることができ、指定色をそのまま出すことが出来る。



パワー

色収差の掛かり方を調整するために冪函数を用いている。

 \displaystyle
y = x^p \quad (0 \le x \le 1)

指数であるpを増やしていくと、中心部にはあまり掛からず周辺部で急に掛かるようになる。


入力値を0から1の範囲に正規化するために、以前は、画面中心から各画素までの距離を、中心から四隅までの距離で割った比率を用いていたが、Centerというパラメータで中心を移動させることが出来るという特性上、扱いが難しい部分があった*2。そこで、正規化の基準となる距離をOuter Radiusというパラメータで指定できるようにした*3



GPU

WinかつCUDA環境で、GPUを用いた処理が出来るようになっている。CPUとGPUでは得意なアルゴリズムが異なるので、出力結果が異なる場合がある。その点はご了承ください。

*1:SapphireだとWhite Balance、自分の場合はNormalizeという語を使うことが多い

*2:xを[0,1]でクランプしていなかったので、変化が急峻になりすぎた

*3:デフォルト値は、中心から四隅までの距離の 150%

After Effectsのエクスプレッションの四方山。

映像制作ゆる Advent Calendar 2025 の13日目の記事です。

After EffectsはCC2019から、エクスプレッションのエンジンを従来のExtendScriptのエンジンからモダンなJavaSriptのエンジン(Windows版はV8)へ切り替えられるようになった。そこら辺の話をメインに、エクスプレッションに関して書いていく。

ExtendScriptの話

ExtendScriptはES3をベースにしているが、独自拡張も多い*1。代表例としてE4X*2があげられる。

var xml = <root><a foo="bar">1</a><b>2</b></root>;
xml.a.@foo; // bar

こんな感じで、JSX*3のようにXMLを扱える代物である。おそらくFlashのAction Script 3.0でES4の要素を取り入れたので*4、それが回り回ってExtendScriptにも取り入れられたものだと思われる。

エクスプレッションに関わってくる話だと、演算子オーバーロード*5がある。

function V(x, y) {
    this.x = x;
    this.y = y;
}

V.prototype["+"] = function (v) {
    return new V(this.x + v.x, this.y + v.y);
}

var v = new V(3, 4);
var w = new V(4, 5);
v + w; // => new V(7, 9)

のような感じで、演算子の挙動を変えられることが出来て便利である。この仕組みはAEで多分に利用されており、Objectの演算子があらかじめオーバロードされている。 そのおかげで、

2 * [3, 4, 5]; // => [6, 8, 10]
[6, 7] + [8, 9]; // => [14, 16]

のように、エクスプレッションやスクリプトで配列を用いた計算が楽に出来るようになっている。

前処理の話

エクスプレッションのコードはそのままの状態では評価できない。そのため、前もって処理がなされる。

前処理の話は、上記記事に詳しく載ってあるので、詳細を見たい場合も見たくない場合も行ってみてください*6

演算子オーバーロードの対応

まず、JavaScriptの仕様には演算子オーバーロードがないので、演算子が使われている箇所は別の処理に置き換える必要がある。そのために、コードをrecastというライブラリで一度AST*7 に変換し、該当する演算子があれば別の処理に置き換えるということを行っている。

たとえば、

[1, 2] + [3, 4];
a *= 3;

というコードは、

__add([1,2], [3, 4]);
a = __mul(a, 3);

のように変換される*8

with文の付加

これはExtendScript時代から行われていた処理だと思われるが、エクスプレッションでは、属性やメソッドを手軽に書けるようにするために、それらを"開いて"使えるようにしている。たとえば、本来はthisProperty.wiggle(1, 100)と書くべきところを、wiggle(1, 100)とだけ書けるようになっていて、この仕組みを実現するためにwith文が使われている。

そもそもwith文とは、

var obj = {A: 1, B: 2};
with(obj) {
    A = 5; // obj.A = 5;
    B = 2 * A; // obj.B = 2 * obj.A;
}

のように、指定したオブジェクトのプロパティに簡易にアクセスするために導入されたもので、現在は非推奨となっている。

このwith文を用いて、thisLayerthisPropertyが展開されるようになっている。

with(thisProperty)  {
     with(thisLayer) {
        // ここにコードが展開される
    }
}

のような感じである*9。そのおかげで、thisLayer、thisPropertyの属性、メソッドを簡単に書くことができる。

エクスプレッションには__preprocessという関数があり、文字列を与えてやると、実際にどのように変換されるのかが確認できる。いとまがあれば試してみてください。

ヘルパーオブジェクトの話

$オブジェクトは JavaScript エンジンに切り替わったことで、一部の機能が削減された一方で、新たな可能性も秘めている。

この記事にある通り、$オブジェクトは同じインスタンスが使い回される。よって、

$.settings = {
    mainColor: [0.33, 0.22, 0.18, 1],
    accentColor: [0.75, 0.15, 0.20, 1],
    backgroundColor: [0.96, 0.93, 0.88, 1]
};

のように$麾下に値を代入するエクスプレッションを一度でもAEに評価させることができれば、他のエクスプレッションからも$.settingsの値を参照できる。ただし、いの一番に当該エクスプレッションを評価させるというのが存外難しい*10

しかも、この$インスタンスの使い回しはMFRがオフの時に限られる*11。MFRがオンのときは、複数の実行環境が並列に生成され、$も環境ごとに別のインスタンスになる。そのインスタンスもRAMプレビューや、レンダリングを掛けたときなどに初期化されるようである。

ただ、毎回新たに生成されるわけではないというところに、使い道に関しての一筋の光明が見えないでもない。

エクスプレッションライブラリの話

AEではデータ駆動云々のときに、jsoncsv、tsv、txtを読み込めるようになった。

これを用いて、

{
    "mainColor": [0.33, 0.22, 0.18, 1],
    "accentColor": [0.75, 0.15, 0.20, 1],
    "backgroundColor": [0.96, 0.93, 0.88, 1]
}

のようなjsonファイルを作って、AEに読み込み、エクスプレッション側で、

footage("palette.json").sourceData.accentColor

こんな感じで値を読み込むことが出来る。上で書いた$オブジェクトを橋渡しに用いるよりよほど真っ当である。関数を書きたい場合は、通常のjsonだと書くことが出来ないので、関数を書けるように拡張したjsonであるjsx*12ファイルを用いる必要がある*13

この話もMotion Developerの上記記事が詳しい*14

jsxで書かれた関数は、AE側で上で書いたような前処理がなされないため、thisLayerthisPropertyをいちいち書く必要があるという点は注意である*15

つまり、

{
    getTime() {
        return thisLayer.time;
    }
}

のように書く必要がある。

シンプルな関数の詰め合わせであれば、

{
    func1() {
    },
    func2() {
    },
    func3() {
    },
}

と書けばいい。より複雑なのを構築したければ、

{
    load() {
        class Vector {
        }
        class Matrix {
        }
        return {
            Vector,
            Matrix,
        };
    },
}

のようにエントリーポイントとなる関数を一つ用意し、その関数が色んな関数などを詰め込んだオブジェクトを返すようにすればいい。更に、$オブジェクトを駆使して、

{
    load() {
        if ($.__lib) return $.__lib;
        class Vector {
        }
        class Matrix {
        }
        const lib = {
            Vector,
            Matrix,
        };
        $.__lib = lib;
        return lib;
    },
}

のように、キャッシュすることで幾分かパフォーマンスの向上を望める。

と、色々書いたが、現状エクスプレッション用のライブラリを構築できていないので、作って終わりの自己満足に終わらない、実用と思想にまみれたライブラリをいずれは作りたい。

尚々

何も例がないのはよろしくないので、それっぽいものをこしらえた。

*1:ExtendScriptについて知ってることをまとめた – TAWAMIラボに詳しい

*2:Integrating XML into JavaScript - Adobe Extendscript Scripting Guide

*3:Reactの方

*4:JavaScript: 最初の 20 年 (翻訳) - inzkyk.xyzに詳しい

*5:Operator overloading - Adobe Extendscript Scripting Guide

*6:かなり具体的なコードが掲載されているが、aelib.dllを覗けば誰でも見れる

*7:抽象構文木 - Wikipedia

*8:__addは単にthisLayer.addを呼び出している。__mulも同様

*9:実際はProxyを用いてひとつのwith式ですむようにしてある

*10:プロジェクトを閉じれば初期化されるので、プロジェクトを再オープンした際にも注意を払う必要がある

*11:上記記事はMFR導入前に書かれている

*12:スクリプトの拡張子を流用している

*13:AE15.0ではjsonに関数を書いても読み込むことが出来たが、AE15.1で厳格化したようである

*14:機械翻訳の質が向上したこのご時世に、言語の壁を利用し、ただ左から右へ流すだけの東京のようなつまらないことをしている

*15:rgbToHsldegreesToRadiansなど、グローバル関数として定義されているかのように説明されているものも実際はLayerのメソッドで、これらもthisLayer.rgbToHslthisLayer.degreesToRadiansと書く必要がある

After Effectsのスクリプトの可能性を広げる。 その7

github.com

型定義をTypes-for-Adobeのものに

AEスクリプト周りの型定義は個人的なものを使用していたが(Types-for-AdobeのAEの箇所の元になったもの)、ユーザーも散見されるようになり、そこらへんの指摘が入ったので切り替えた。

Mouseのhookをmouseupにも対応した

実際に使用される方が出てきて、その方の要望で追加した。

@cropped_mesh_warp

番外

SVGが公式で対応されるようになった

SVG ファイルを読み込み https://community.adobe.com/t5/after-effects-beta-discussions/new-vector-workflow-enhancements-in-after-effects-beta/td-p/15559138 公式がやるのならシェイプレイヤーに変換するのではなくて、まずsvgをaiのように一つのvector画像として扱えるようにすべきではある。複雑なものを扱うと重くなるし、そもそも静的な画像として扱いたい場合が大半だろうから。

After Effectsのスクリプトの可能性を広げる。 その6

github.com

github.com

画像ファイルを介することなく気軽に画像データを扱いたいと開発当初より思っていて、やっと煮詰まった。

カスタムパラメータ

 プロジェクト内に画像データを埋め込みたいので、エフェクトのカスタムパラメータを用いる。カスタムパラメータは、通常のパラメータと違い、UI、データ構造を自由に出来るが面倒が増える。また、スクリプトから値の取得、設定が出来ない。よって、様々なエフェクトに流用出来るような汎用性の高いものを目指す、かつ、スクリプトから加工できる機能を提供するというのを、個人的な指針としている*1ピクセルデータをそのまま保存するのは容量が嵩むので、以前個人的にプリレンダリング用としてZPngを参考にして作ったフォーマットを流用した。そして、スクリプトから画像ファイルやクリップボードに含まれる画像データを読み込めるようにした。

@image

 画像を取り扱うエフェクト群として、@image, @image_file, @images_in_folderの3種を用途に応じて用意した。@imageは上記のカスタムパラメータを用いて、エフェクト内にピクセルデータを保持するエフェクト。@image_fileは画像ファイルのパスを保持して、その画像を表示するエフェクト。@images_in_folderはフォルダのパスを保持し、そのフォルダ内に含まれる画像を表示するエフェクトである。この3つさえあれば大抵の用途に対応できると現時点では思っている。画像ファイルの読み込み自体はstb_imageに委譲しているので、stb_imageで読み込めるフォーマットは読み込める。

クリップボード

 CLCLPasteboard Viewerを用いてクリップボードの中を眺めると、いろいろな形式が登録されているのが分かる。そこで、登録されている形式、ファイルパス、画像の情報を取得出来るようにし、クリップボードを介したやりとりをやりやすくした。

RIFX

 IllustratorはCC 2014あたりから、コピー時にクリップボードSVGを含められるようになった。すでに@svgを作り、最低限度のSVGを扱えるようにはしてあるが、Illustratorが出力するSVGにもある程度対応したい*2。そこでまず、グラデーション周りに取り掛かることにした。

 カスタムパラメータやグラデーションのようなスクリプトから値を設定出来ないパラメータに対して値を設定する方法としては、プリセットを適用するという手段がよくとられる。プリセット(.ffx)はプロジェクト(.aep)と同様、RIFX*3というファイルフォーマットが用いられている。そこで、RIFXファイルをparseしてjavascriptのオブジェクトに変換、そしてそのオブジェクトを再びバイナリデータに変換する仕組みを作った*4。こうすることで、あらかじめテンプレートとなるオブジェクトを保持しておき、必要に応じてそのオブジェクトに変更を加えバイナリに変換することで、動的にプリセットを生成できる。バイナリをテンプレートとして保持し、それに必要に応じて変更を加えるという手法が大抵使われるが、かなりわずらわしい*5ので割りかし楽になった。

@svg

 グラデーションを弄ることが出来るようになったので、@svgもアップデートした。

Pseudo

 RIFXを弄るツールを整備したので、ついでにPseudo Effect周りも調べることにした。調査過程でパラメータの定義を司るpardチャンクのデータ構造がPF_ParamDefであることが分かったため、Pathパラメータ*6やButtonパラメータ*7も追加できるようになった。現時点だと*8Pseudo Effect Makerを導入する方が賢明。

メッシュワープ

www.youtube.com

 メッシュワープの値をスクリプトから弄られるようにした。rows, columnsの数は頂点の数なので、面の数を表すRows, Columnsパラメータの値に1足す必要がある。

API

 他のスクリプトの機能を呼び出したいなというときが時たまある。例えば、@hook_solidを導入しているならば@hook_solidライクに平面を追加したいな、といったときである。そういうことがやりやすい仕組みを整備した。

@paste_clipboard

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

 今回の更新の集大成として、Alt(Option)+Vで、クリップボードに含まれている形式に応じて色々読み込むスクリプトを作成した。

*1:カスタムパラメータにアクセスするAPIの提供も@scriptの目的の一つではある

*2:具体的には.klass、#id程度のセレクタに対応したスタイルタグを用いた間接的なスタイルの適用及びグラデーション

*3:RIFFのビッグエンディアン

*4:aepの構造はLottieのParsing AEP Filesに幾分かの解析結果が載っており大いに参考にした

*5:RIFXの仕様として、親チャンクが配下の子チャンク群のデータサイズを保持するため、子チャンクのデータを弄ると親チャンクに対して再帰的に変更を加える必要がある。

*6:ファイルパスではなくマスクパスの方

*7:押された時の処理が書けないため完全に無用

*8:そして今後も

色収差再考

キリスト降誕おめでとうございます。

adventar.org

https://atarabi.com/kikaku/plugin/old/#ChromaticAberration

2011年にChromatic AberrationというAEプラグインを公開した。ありがたいことに今もちょくちょくダウンロードされているのだけど、当時の技術的限界もあり、特にパフォーマンス面において難がある。 なので、しばらく前からコンセプトはそのままに一から作り直している。そこで今一度、色収差(風)エフェクトの有り様について考えてみたい*1


色収差風エフェクトは基本的には、色の分解、サンプリング、合成からなる。まずはこれらの処理について大まかに見ていく。

分解

神は「光あれ」と言われた。すると光があった。

色を分解しないことには始まらない。最も単純なのは、R, G, Bの3色に分割することである。そのうち2色を連動させれば、容易に青と黄、マゼンタと緑、赤とシアンの反対色を構成出来る。

サンプリング

神はまた言われた、「水の間におおぞらがあって、水と水とを分けよ」。

分解した色ごとにサンプリング位置を異にすることで色ずれの効果が生じる。倍率色収差ライクにしたいならスケールを変えたり、レンズディストーションさせて、動径方向にずらせばよい。

RowbyteのSeparate RGBはそれから一歩進めて、R, G, Bチャンネルそれぞれに対して、トランスフォーム出来るようにしてある。

https://aescripts.com/separate-rgb/

SapphireのS_TimeWarpRGBは時間軸方向にずらしている。

組み合わせ次第で色々な方向を目指すことが出来る。ただ、実現可能な領域が増えたとしても、"容易に"実現可能な領域が増えるとは限らないので(むしろ減ることもある)、目指したい方向を明確化し、"容易に"実現可能な領域を単調増加させつつ、実現可能な領域も増やしていくのがいいとは思う。

合成

神はまた言われた、「天の下の水は一つ所に集まり、かわいた地が現れよ」。そのようになった。

通常加算で合成するが、後ほど他の可能性もないか検討する。例えば、乗算が絡むものは黒に沈むので難しい。




分解の分解

分解についてもう少し推し進めたい。例えば、単純にR, G, Bに分割するだけだとパキッとしすぎる。

分割数を増やせばスムーズにすることが出来る。

この際、徒に分割したら色調が変わってしまう。なので、元の色調を維持するために、分割した色を足した場合に白になるように調整する必要がある(加算で合成する場合)。

 \displaystyle
(r, g, b) = \sum_{i}(r_{i}, g_{i}, b_{i}) = (1, 1, 1)

最初からそれを考慮するのは面倒なので、とりあえず色を選んでからそれらを足し合わせ、その合計で各色を割れば良い。

 \displaystyle
(r', g', b') = \frac{(r_{i}, g_{i}, b_{i})}{\sum_{i}(r_{i}, g_{i}, b_{i})}

SapphireのS_WarpChromaの場合、White Balanceをチェックすることでその計算を自動でしてくれる。

単純にR, G, Bを分割する場合でも色々な方向性があり得る。

つくね氏によるPixelBenderフィルタ群であるRetinaFiltersに含まれていたChromatic Aberrationや、SapphireのS_WarpChromaでは各色間を補間する形になっている。

一方、Plugin EverythingのQuick Chromatic Aberration 3や、PSOFTのP_ChromaticAberrations(のモードFix Green)では、Gは固定し、R, Bを分割している。

この方式の利点は、効果を強めてもGがブレないため、ディティールが潰れないところだろうか。

ちなみに、P_ChromaticAberrationsは2023年に更新されて、色選択のモードが増えて便利になった。

サンプリングと分解

上のような、サンプリング数を調整できる類のプラグインは、いい感じに分割出来るようにトランスフォームに一定の制約をかけている。

例えば、S_WarpChromaはFromとToとを指定する形にしており、その間を容易に補間出来るようにしてあり、

Quick Chromatic Aberration 3は、Gは固定で、パラメータを弄ると、R, Bがそれぞれ連動して逆方向に変化するようになっており、これまた補間しやすくしてある(位置をずらすパラメータであるPositionが、左上、右下への移動に固定されているのが解せないが)。

一方、Separate RGBのようにR, G, Bそれぞれを自由にトランスフォーム出来るようにしてあると、いい感じに割るのが難しくなる。

これらの違いは当然想定している表現の方向性による。

合成の可能性

加算以外の可能性を考える。色調を保つために重要なのは、分割した色を合成していって白を作れるかである。加算の場合は単純に足していけばいいだけなので簡単である。他に容易なのに比較明がある。比較明だと、分割した色の中に値が1となるようなものが含まれていれば良い。つまり、

 \displaystyle
(r, g, b) = \max_{i}(r_{i}, g_{i}, b_{i}) = (1, 1, 1)

を満たせば良い。

加算の場合、分割数を増やすと、一つ一つの色の重みが必然的に小さくなる。

比較明の場合、そういった縛りはなくなるため、望み通りの色をそのままどぎつく出せる。

表現の方向性

どういった表現を想定して設計するかで、パラメータの設定の仕方、とんがり方は当然異なる。グリッチ表現を想定するのであれば、ちょっとしたパラメータの変化で大きく表情を変えるようなものが望まれるだろう。

自分の場合、周辺にちょっとした味をつける、周辺の解像度を落とすことで視線誘導させる、といった使い方にウェイトを置いている。

例えば、EFX Chromatic Aberrationにおいて、Scalingをlinear, square, cubicと切り替えられるが、

https://aescripts.com/efx-chromatic-aberration/

このように掛かり具合を変えられるようにすることで、周辺部だけに掛かるようにする、見せたい中心部の解像度は保つ、といったことが可能となる。

また、ズレ量に応じて、放射ブラーを掛けられるようにすれば視線誘導の効果が際立つ。

その先に

こうして天と地とその万象が完成した。

今回は色収差系のプラグインを色々取り上げたが、みなさんも同種のプラグインを比較しその差異を比べることで、その背景にある設計思想を想像してみてはいかがだろうか。

*1:色収差の光学な話については、ComperK氏のVFXと色収差 - コンポジゴクという記事に詳しい

After Effectsのスクリプトの可能性を広げる。 その5

github.com

github.com

ちょっとした修正といくつかのスクリプトの追加と更新。

修正

エフェクトマスクのパラメータのバグ

エフェクトマスク周りのパラメータの扱いに難があったのを修正した。エフェクト麾下のパラメータは、0番目のパラメータがそのエフェクトに対する入力を扱うパラメータとして予約されているので、他のパラメータと扱いが少し異なる。そこらへんの処理が少々雑だった。

UI.showContextMenu()のバグ(Windows下)

上のようなコンテクストメニューを表示させるAPIで、変数のライフタイムの管理が雑な箇所があったので修正。

Fuzzy Searchのバグ

計算コスト低減のためにキャッシュ機能を実装しているが、そこにバグがあったので修正。

スクリプト

@script_panel

使用頻度の小さいScriptUI系のスクリプトをScriptUI Panelsフォルダに入れるのが煩わしい時に、あるフォルダに入れておいて、ドロップダウンリストから呼び出せるようにする。挙動(特にUI周りの)は当然、対象のスクリプト次第。

@proxy_maker

www.youtube.com

重いコンポをプリレンダしてプロキシに設定するというのはままあるが、それをレイヤー単位でしたかったので作成。

@effect_launcher

ある文字列を入力すれば、即座にエフェクトが適用されるショートカット機能を追加した。スクリプト、プリセットもショートカットの対象に出来る。

スクリプト、プリセットも検索の対象に含められるようにした。検索窓に\\を入力すれば設定画面に入れる。

ドキュメント

APIばかりの説明で、スクリプトの説明がなかったので、簡易的だが説明を追加した。

Fuzzy Searchとdebounce

@effect_launcherや@effect_searchなどでは、キー入力ごとにFuzzy Searchを発火させると、処理が追いつかず固まるので、debouneで処理を間引いているが、debounce timeはちょっと安全側に寄せて200msに設定していた。しかしこの設定だと特にショートカット機能を用いた際の遅延が気になったので、100msに変更した。また、使用者が自分で変更出来るように、変数に切り出してスクリプトの上の方に置いた。