色収差再び v1.1.0

at.atarabi.com

v1.1.0に更新した。

Blur Contrast

www.youtube.com

いくら加算だけでなく、比較明合成も扱えるようになったと言っても、それが発揮されるのはサンプリング時の合成時だけで、Steps数を上げないと効果が感じづらかった。 特にブラーを掛けると必然的に鈍ってしまうので、コテコテ感を出すのがむずかった。そこで、ブラーのコントラストを強調出来るようにした。

Blur Quality

ブラーの処理コストが比較的高かったので、CPU処理時はバレない程度に品質を下げていた。しかし、ブラーのコントラストを上げられるようになったことで粗が見えやすくなったので、品質をあげるパラメータを用意した。

パフォーマンス

いくつか無駄な処理を発見したので省いた。

色収差再び v1.0.1

at.atarabi.com

v1.0.1に更新した。

Background

適用しているレイヤーのサイズと背Backgrondに指定したレイヤーのサイズが異なる時に、背景が中央に来ない問題を修正した。ここらへんは今後Transform周りを整備したい。

パフォーマンス

ステップ数が大きい時やRepeat Edge Pixelsを適用した際のパフォーマンスが向上した(CPU処理時)。

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

github.com

パフォーマンスを向上させたのと、スコアリングを見直して、もうちょっといい感じに候補が並ぶようにした。

@effect_launcher

いい感じに候補が並ぶように、@effect_launcherの側も少し弄った。新しいFuzzy Searchである必要があるので、@scriptの更新と後述しますがStartupにある!@script_UI.jsxの削除をお願いします。

プロファイラー

Perf — @script 0.8.0 documentation パフォーマンス計測のために、プロファイラーを作成した。

!@script_UIの埋め込み

@script.aexと!@script_UI.jsxとで、2つの更新箇所もあると面倒だなと思ったので、!@script_UI.jsxの内容を@script.aexに埋め込むようにした。それにともなって、Startupにある!@script_UI.jsxは不要、否、むしろ、@script.aexのとでコンフリクトしてしまうので、削除していただけると幸いです。

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

github.com

シンプルでかつES3に簡単に移植できそうだったのでFuzzy searchを軽く書き直して使っていたのだが、改めて中身を見直したところ、あまりよろしくないなと感じたので、自作のものに切り替えた。 新しいもの好きは、 !@script_UI.jsx を上書きしてください*1

@note

画像を扱うプラグインは以前追加したが、テキストを扱う方もある程度自分の中で煮詰まったので追加した。

例によってスクリプトからも触れるようにしてあるので、それを用いたスクリプトも追加してある。

@expression_baker

Startup用のスクリプト。エクスプレッションを時間に依存させず静的に使用したい場合に、ベイクが非常に面倒なので作った。

*1:@effect_launcher, @effect_searchなどに使用している

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と書く必要がある