【エクスプレッション】シェイプグループトランスフォームの変換関数 改訂版(行列を使用)

こちらの関数の改訂版です。

シェイプレイヤーのグループトランスフォームがデフォルト値でないと、toCompやfromCompだけではパス点をヌルなどに変換できません。

なぜなら、 エクスプレッションで用意されている変換メソッドで行き来できる座標空間は
ワールド座標 ⇔ レイヤー座標
だけだからです。

パス点はシェイプグループ内にあり、シェイプグループは独自のトランスフォームを持っています。つまり、レイヤー座標から座標が変わってしまっているわけです。これを辿るには
ワールド座標 ⇔ レイヤー座標 ⇔ シェイプグループ座標
という順で変換せねばなりません。

この『レイヤー座標⇔シェイプグループ座標』の橋渡し関数を作りました。

■toShapeLayerCoord(シェイプグループ座標からレイヤー座標に変換)

//初期値の行列。
var mMtx = [1, 0, 0, 0, 1, 0, 0, 0, 1];
//使いまわし計算用行列。
var mAryM = new Array(16);
var mAryR1 = new Array(16);
var mAryR2 = new Array(16);
//toShapeLayerCoord行列作成関数。戻り値はラスト計算関数。
function mMulToShapeLayerMtx(aMtx, aGp, aTime) {
    //第3引数が入っていなければ現在コンポのインジケータ時間にする。
    var mTime = aTime;
    if (mTime === undefined) { mTime = time; }

    //toShapeLayerCoordはtoWorldと順序が一緒。toWorldならば子から順なので、行列用に親から順で入れる。
    var mPrtTfms = mGetPrtShpTfmsFstPrt(aGp);
    for (var i = 0; i < mPrtTfms.length; i++) {
        var mPrtTfm = mPrtTfms[i];
        mMulToShpLyrFromGpOnceMtx(aMtx, mPrtTfm, mTime);
    }
    return mMulMtxLst;
}
//--------------------------------------
//toShapeLayerCoordの、1Gp分だけ行列計算する関数。
//シェイプグループトランスフォーム内の座標からシェイプレイヤー座標。
//レイヤー座標にする行列だが、処理順はtoWorldと一緒。
function mMulToShpLyrFromGpOnceMtx(aMtx, aTfm, aTime) {
    var mTfm = aTfm;
    var mTime = aTime;
    //toWorldはAp,Sc,(Skw),Rot,Ptの順なので、行列用にその逆順でやる。
    //Pt,Rot,(Skw),Sc,Ap。
    mCrtMtx2DPt(mTfm.position.valueAtTime(mTime), mAryM);
    mMulMtx(aMtx, mAryM, mAryR1);

    mCrtMtxRot(mTfm.rotation.valueAtTime(mTime), mAryM);
    mMulMtx(mAryR1, mAryM, mAryR2);

    mCrtMtx2DSkwAxs(mTfm.skewAxis.valueAtTime(mTime), mAryM);
    mMulMtx(mAryR2, mAryM, mAryR1);
    mCrtMtx2DSkw(mTfm.skew.valueAtTime(mTime), mAryM);
    mMulMtx(mAryR1, mAryM, mAryR2);
    mCrtMtx2DSkwAxsPre(mTfm.skewAxis.valueAtTime(mTime), mAryM);
    mMulMtx(mAryR2, mAryM, mAryR1);

    mCrtMtx2DSc(mTfm.scale.valueAtTime(mTime), mAryM);
    mMulMtx(mAryR1, mAryM, mAryR2);

    mCrtMtx2DAp(mTfm.anchorPoint.valueAtTime(mTime), mAryM);
    mMulMtx(mAryR2, mAryM, aMtx);
}
//--------------------------------------
//行列の掛け算をする関数。
function mMulMtx(a, b, r) {
    r[0] = a[0] * b[0] + a[1] * b[3] + a[2] * b[6];
    r[1] = a[0] * b[1] + a[1] * b[4] + a[2] * b[7];
    r[2] = a[0] * b[2] + a[1] * b[5] + a[2] * b[8];

    r[3] = a[3] * b[0] + a[4] * b[3] + a[5] * b[6];
    r[4] = a[3] * b[1] + a[4] * b[4] + a[5] * b[7];
    r[5] = a[3] * b[2] + a[4] * b[5] + a[5] * b[8];

    r[6] = a[6] * b[0] + a[7] * b[3] + a[8] * b[6];
    r[7] = a[6] * b[1] + a[7] * b[4] + a[8] * b[7];
    r[8] = a[6] * b[2] + a[7] * b[5] + a[8] * b[8];
}
//--------------------------------------
//ラストの、出来上がった行列3*3と値配列3*1を行列計算をする関数。
function mMulMtxLst(a, b) {
    var mb = [b[0], b[1], 1];
    var mRst = [];
    mRst[0] = a[0] * mb[0] + a[1] * mb[1] + a[2] * mb[2];
    mRst[1] = a[3] * mb[0] + a[4] * mb[1] + a[5] * mb[2];
    //mRst[2] = a[6]*b[0] + a[7]*b[1] + a[8]*b[2];
    return mRst;
}
//--------------------------------------
//プロパティから行列を作る関数。シェイプレイヤー版。
function mCrtMtx2DAp(aApVal, aAry) {
    var mApVal = aApVal * -1;
    aAry[0] = 1; aAry[1] = 0; aAry[2] = mApVal[0];
    aAry[3] = 0; aAry[4] = 1; aAry[5] = mApVal[1];
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
function mCrtMtx2DPt(aPtVal, aAry) {
    aAry[0] = 1; aAry[1] = 0; aAry[2] = aPtVal[0];
    aAry[3] = 0; aAry[4] = 1; aAry[5] = aPtVal[1];
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
function mCrtMtx2DSc(aScVal, aAry) {
    var mScX = aScVal[0] / 100;
    var mScY = aScVal[1] / 100;

    aAry[0] = mScX; aAry[1] = 0; aAry[2] = 0;
    aAry[3] = 0; aAry[4] = mScY; aAry[5] = 0;
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
function mCrtMtx2DScRev(aScVal, aAry) {
    //ゼロによる除算対策。正確な値ではないが、0.00001で近似する。
    var mScVal = aScVal;
    if (mScVal[0] === 0) { mScVal[0] = 0.00001; }
    if (mScVal[1] === 0) { mScVal[1] = 0.00001; }
    var mScX = 1 / (mScVal[0] / 100);
    var mScY = 1 / (mScVal[1] / 100);

    aAry[0] = mScX; aAry[1] = 0; aAry[2] = 0;
    aAry[3] = 0; aAry[4] = mScY; aAry[5] = 0;
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
function mCrtMtx2DSkw(aSkwVal, aAry) {
    var mRad = aSkwVal * (Math.PI / 180);
    var mSkw = Math.tan(mRad * -1);
    aAry[0] = 1; aAry[1] = mSkw; aAry[2] = 0;
    aAry[3] = 0; aAry[4] = 1; aAry[5] = 0;
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
function mCrtMtx2DSkwAxs(aSkwAxsVal, aAry) {
    var mRad = aSkwAxsVal * -1 * (Math.PI / 180);
    var mCos = Math.cos(mRad);
    var mSin = Math.sin(mRad);
    var mSinM = mSin * -1;

    aAry[0] = mCos; aAry[1] = mSinM; aAry[2] = 0;
    aAry[3] = mSin; aAry[4] = mCos; aAry[5] = 0;
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
function mCrtMtx2DSkwAxsPre(aSkwAxsVal, aAry) {
    var mRad = aSkwAxsVal * (Math.PI / 180);
    var mCos = Math.cos(mRad);
    var mSin = Math.sin(mRad);
    var mSinM = mSin * -1;

    aAry[0] = mCos; aAry[1] = mSinM; aAry[2] = 0;
    aAry[3] = mSin; aAry[4] = mCos; aAry[5] = 0;
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
function mCrtMtxRot(aRotVal, aAry) {
    var mRad = aRotVal * (Math.PI / 180);
    var mCos = Math.cos(mRad);
    var mSin = Math.sin(mRad);
    var mSinM = mSin * -1;

    aAry[0] = mCos; aAry[1] = mSinM; aAry[2] = 0;
    aAry[3] = mSin; aAry[4] = mCos; aAry[5] = 0;
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
//--------------------------------------
//トランスフォームを集める関数。
//シェイプグループトランスフォーム版。親から順。Gpを入れる(コンテンツではない)。
function mGetPrtShpTfmsFstPrt(aGp) {
    if (aGp instanceof Layer) { return []; }
    var mPrt = aGp;
    var mTfms = [aGp.transform];
    for (var i = 0; i < 100; i++) {
        //Gpの1つ上は必ずコンテンツ、2つ上は必ずGpかレイヤー。
        var mPrtPrt = mPrt.propertyGroup(2);
        if (mPrtPrt instanceof Layer) { break; }
        mTfms.unshift(mPrtPrt.transform);
        mPrt = mPrtPrt;
    }
    return mTfms;
}
//-----------------------------------------------------------------------------

■fromShapeLayerCoord(レイヤー座標からシェイプグループ座標へ変換)

//初期値の行列。
var mMtx = [1, 0, 0, 0, 1, 0, 0, 0, 1];
//使いまわし計算用行列。
var mAryM = new Array(16);
var mAryR1 = new Array(16);
var mAryR2 = new Array(16);
//toShapeLayerCoord行列作成関数。戻り値はラスト計算関数。
function mMulToShapeLayerMtx(aMtx, aGp, aTime) {
    //第3引数が入っていなければ現在コンポのインジケータ時間にする。
    var mTime = aTime;
    if (mTime === undefined) { mTime = time; }

    //toShapeLayerCoordはtoWorldと順序が一緒。toWorldならば子から順なので、行列用に親から順で入れる。
    var mPrtTfms = mGetPrtShpTfmsFstPrt(aGp);
    for (var i = 0; i < mPrtTfms.length; i++) {
        var mPrtTfm = mPrtTfms[i];
        mMulToShpLyrFromGpOnceMtx(aMtx, mPrtTfm, mTime);
    }
    return mMulMtxLst;
}
//--------------------------------------
//toShapeLayerCoordの、1Gp分だけ行列計算する関数。
//シェイプグループトランスフォーム内の座標からシェイプレイヤー座標。
//レイヤー座標にする行列だが、処理順はtoWorldと一緒。
function mMulToShpLyrFromGpOnceMtx(aMtx, aTfm, aTime) {
    var mTfm = aTfm;
    var mTime = aTime;
    //toWorldはAp,Sc,(Skw),Rot,Ptの順なので、行列用にその逆順でやる。
    //Pt,Rot,(Skw),Sc,Ap。
    mCrtMtx2DPt(mTfm.position.valueAtTime(mTime), mAryM);
    mMulMtx(aMtx, mAryM, mAryR1);

    mCrtMtxRot(mTfm.rotation.valueAtTime(mTime), mAryM);
    mMulMtx(mAryR1, mAryM, mAryR2);

    mCrtMtx2DSkwAxs(mTfm.skewAxis.valueAtTime(mTime), mAryM);
    mMulMtx(mAryR2, mAryM, mAryR1);
    mCrtMtx2DSkw(mTfm.skew.valueAtTime(mTime), mAryM);
    mMulMtx(mAryR1, mAryM, mAryR2);
    mCrtMtx2DSkwAxsPre(mTfm.skewAxis.valueAtTime(mTime), mAryM);
    mMulMtx(mAryR2, mAryM, mAryR1);

    mCrtMtx2DSc(mTfm.scale.valueAtTime(mTime), mAryM);
    mMulMtx(mAryR1, mAryM, mAryR2);

    mCrtMtx2DAp(mTfm.anchorPoint.valueAtTime(mTime), mAryM);
    mMulMtx(mAryR2, mAryM, aMtx);
}
//--------------------------------------
//行列の掛け算をする関数。
function mMulMtx(a, b, r) {
    r[0] = a[0] * b[0] + a[1] * b[3] + a[2] * b[6];
    r[1] = a[0] * b[1] + a[1] * b[4] + a[2] * b[7];
    r[2] = a[0] * b[2] + a[1] * b[5] + a[2] * b[8];

    r[3] = a[3] * b[0] + a[4] * b[3] + a[5] * b[6];
    r[4] = a[3] * b[1] + a[4] * b[4] + a[5] * b[7];
    r[5] = a[3] * b[2] + a[4] * b[5] + a[5] * b[8];

    r[6] = a[6] * b[0] + a[7] * b[3] + a[8] * b[6];
    r[7] = a[6] * b[1] + a[7] * b[4] + a[8] * b[7];
    r[8] = a[6] * b[2] + a[7] * b[5] + a[8] * b[8];
}
//--------------------------------------
//ラストの、出来上がった行列3*3と値配列3*1を行列計算をする関数。
function mMulMtxLst(a, b) {
    var mb = [b[0], b[1], 1];
    var mRst = [];
    mRst[0] = a[0] * mb[0] + a[1] * mb[1] + a[2] * mb[2];
    mRst[1] = a[3] * mb[0] + a[4] * mb[1] + a[5] * mb[2];
    //mRst[2] = a[6]*b[0] + a[7]*b[1] + a[8]*b[2];
    return mRst;
}
//--------------------------------------
//プロパティから行列を作る関数。シェイプレイヤー版。
function mCrtMtx2DAp(aApVal, aAry) {
    var mApVal = aApVal * -1;
    aAry[0] = 1; aAry[1] = 0; aAry[2] = mApVal[0];
    aAry[3] = 0; aAry[4] = 1; aAry[5] = mApVal[1];
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
function mCrtMtx2DPt(aPtVal, aAry) {
    aAry[0] = 1; aAry[1] = 0; aAry[2] = aPtVal[0];
    aAry[3] = 0; aAry[4] = 1; aAry[5] = aPtVal[1];
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
function mCrtMtx2DSc(aScVal, aAry) {
    var mScX = aScVal[0] / 100;
    var mScY = aScVal[1] / 100;

    aAry[0] = mScX; aAry[1] = 0; aAry[2] = 0;
    aAry[3] = 0; aAry[4] = mScY; aAry[5] = 0;
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
function mCrtMtx2DScRev(aScVal, aAry) {
    //ゼロによる除算対策。正確な値ではないが、0.00001で近似する。
    var mScVal = aScVal;
    if (mScVal[0] === 0) { mScVal[0] = 0.00001; }
    if (mScVal[1] === 0) { mScVal[1] = 0.00001; }
    var mScX = 1 / (mScVal[0] / 100);
    var mScY = 1 / (mScVal[1] / 100);

    aAry[0] = mScX; aAry[1] = 0; aAry[2] = 0;
    aAry[3] = 0; aAry[4] = mScY; aAry[5] = 0;
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
function mCrtMtx2DSkw(aSkwVal, aAry) {
    var mRad = aSkwVal * (Math.PI / 180);
    var mSkw = Math.tan(mRad * -1);
    aAry[0] = 1; aAry[1] = mSkw; aAry[2] = 0;
    aAry[3] = 0; aAry[4] = 1; aAry[5] = 0;
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
function mCrtMtx2DSkwAxs(aSkwAxsVal, aAry) {
    var mRad = aSkwAxsVal * -1 * (Math.PI / 180);
    var mCos = Math.cos(mRad);
    var mSin = Math.sin(mRad);
    var mSinM = mSin * -1;

    aAry[0] = mCos; aAry[1] = mSinM; aAry[2] = 0;
    aAry[3] = mSin; aAry[4] = mCos; aAry[5] = 0;
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
function mCrtMtx2DSkwAxsPre(aSkwAxsVal, aAry) {
    var mRad = aSkwAxsVal * (Math.PI / 180);
    var mCos = Math.cos(mRad);
    var mSin = Math.sin(mRad);
    var mSinM = mSin * -1;

    aAry[0] = mCos; aAry[1] = mSinM; aAry[2] = 0;
    aAry[3] = mSin; aAry[4] = mCos; aAry[5] = 0;
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
function mCrtMtxRot(aRotVal, aAry) {
    var mRad = aRotVal * (Math.PI / 180);
    var mCos = Math.cos(mRad);
    var mSin = Math.sin(mRad);
    var mSinM = mSin * -1;

    aAry[0] = mCos; aAry[1] = mSinM; aAry[2] = 0;
    aAry[3] = mSin; aAry[4] = mCos; aAry[5] = 0;
    aAry[6] = 0; aAry[7] = 0; aAry[8] = 1;
}
//--------------------------------------
//トランスフォームを集める関数。
//シェイプグループトランスフォーム版。親から順。Gpを入れる(コンテンツではない)。
function mGetPrtShpTfmsFstPrt(aGp) {
    if (aGp instanceof Layer) { return []; }
    var mPrt = aGp;
    var mTfms = [aGp.transform];
    for (var i = 0; i < 100; i++) {
        //Gpの1つ上は必ずコンテンツ、2つ上は必ずGpかレイヤー。
        var mPrtPrt = mPrt.propertyGroup(2);
        if (mPrtPrt instanceof Layer) { break; }
        mTfms.unshift(mPrtPrt.transform);
        mPrt = mPrtPrt;
    }
    return mTfms;
}
//-----------------------------------------------------------------------------

使い方

改定前よりちょっと手順が要りますが、その分高速です。

手順その1 行列式を作る。
手順その2 行列式&変換したい値を、ラスト計算関数に入れて変換値を出す。

ポイントは、行列式は使いまわせるということです。
イメージとしては、以前は「値にスケールをかけ、その結果の値に回転をかけ…」と色々とやって変換値を出していました。そして、出したい値が複数ある場合また一から「スケールをかけ…」と、何度も同じ処理を繰り返していました。
が、行列を使うと、その「スケールをかけ、回転をかけ…」の部分だけを先に計算でき、そして使いまわせるんですね…。

結果、高速なんです。

以下、それぞれの続きの記述を載せます。
シェイプグループを増やしたり、シェイプグループトランスフォームにめちゃめちゃ数値を入れて試してみてください。

■toShapeLayerCoord(シェイプグループ座標からレイヤー座標に変換)
パスとヌルを作り、ヌルに以下を適用する。するとヌルがパス点に追従します。

//この上にtoShapeLayerCoordの関数群がある想定。

//変換したい値を得ておく。
var mLyr = thisComp.layer("シェイプレイヤー 1");
var mPath = mLyr.content("グループ 1").content("シェイプ 1").content("パス 1").path;
var mVtxs = mPath.points();
var mTgtVtx = mVtxs[0];


//Gp座標からレイヤー座標。
//パスから一番近いシェイプグループは3個上にある。
var mGp = mPath.propertyGroup(3);
//行列計算。引数は計算させる行列、トランスフォームが入ったグループ、時間。戻り値でラスト計算関数を得る。
//これを行えば変換作業用にmMtxとmLastFuncが使いまわしできる。
var mLastFunc = mMulToShapeLayerMtx(mMtx, mGp, time);
//ラスト計算関数を使って最後の行列計算をし、値を出す。出したい値が複数あればfor文で回せる。
mTgtVtx = mLastFunc(mMtx, mTgtVtx);


//レイヤー座標からコンポ座標、あるいはワールド座標。
//mTgtVtx = mLyr.toComp(mTgtVtx);
mTgtVtx = mLyr.toWorld(mTgtVtx)

//値を適用する。
mTgtVtx;

注意点です。

変換関数mMulToShapeLayerMtx()のカッコ内の1番目には初期値の行列mMtxを入れます。mMtxは実は一番最初に定義してあります。変数名を変えたい場合は最初の変数名も変えてください。

カッコ内の2番目には、〇〇.toCompと同じく、変換したい座標値の座標空間を作っているプロパティを入れてください。直近の上の層にあるトランスフォームを持つプロパティということです。
この場合はパス点を変換しているので、パス点の座標空間を作っているプロパティ、つまり直近のシェイプグループを入れています。

変換関数は入れたmMtxの中身を直接勝手に書き換えます。そしてそれが戻り値ではなく、戻り値はラスト計算関数であることに注意してください。書き換えられたmMtxと、戻り値のラスト計算関数を使って値を変換します。

直近のシェイプグループは、パスから数えたら3個上にあります。

■fromShapeLayerCoord(レイヤー座標からシェイプグループ座標へ変換)

パスとヌルを作り、パスに以下を適用する。するとパス点がヌルに追従します。

//この上にfromShapeLayerCoordの関数群がある想定

//変換したい値を得ておく。
var mNul1 = thisComp.layer("ヌル 1");
var mNul2 = thisComp.layer("ヌル 2");
//定番の手法で、3Dや親子になってもコンポ座標となるようにしておく。
var mTgtPt1 = mNul1.toComp(mNul1.anchorPoint);
var mTgtPt2 = mNul2.toComp(mNul2.anchorPoint);
var mTgtPts = [mTgtPt1, mTgtPt2];


//まずはレイヤー座標に変換する。
for (var i = 0; i < mTgtPts.length; i++) {
    mTgtPts[i] = fromCompToSurface(mTgtPts[i]);
}

//レイヤー座標からGp座標。
//パスから一番近いシェイプグループは3個上にある。
var mGp = thisProperty.propertyGroup(3);
//行列計算。引数は計算させる行列、トランスフォームが入ったグループ、時間。戻り値でラスト計算関数を得る。
//これを行えば変換作業用にmMtxとmLastFuncが使いまわしできる。
var mLastFunc = mMulFromShapeLayerMtx(mMtx, mGp, time);
//ラスト計算関数を使って最後の行列計算をし、値を出す。出したい値が複数あればfor文で回せる。
//引数は計算済み行列、変換したい値。
for (var i = 0; i < mTgtPts.length; i++) {
    mTgtPts[i] = mLastFunc(mMtx, mTgtPts[i]);
}

//値を適用する。
mPath = thisProperty;
mVtxs = mPath.points();
mIns = mPath.inTangents();
mOuts = mPath.outTangents();
mCls = mPath.isClosed();

mVtxs[0] = mTgtPts[0];
mVtxs[1] = mTgtPts[1];

createPath(mVtxs, mIns, mOuts, mCls);

基本的な注意点はto~と一緒です。

違いは、mMulFromShapeLayerMtx()のカッコ内の2番目には『変換後の座標空間を作っているプロパティ』を入れることです。 mMulToShapeLayerMtx() が〇〇.toCompと一緒なように、これも〇〇.fromCompと一緒ですね。

スクリプト版を作るときのヒント

instanceof Layer ではレイヤーかどうかが判断できないので、レイヤーのマッチネームかどうか、あるいはグループのマッチネームではないかどうかにする。

スクリプトではタイムはタイムだけ表記ではエラーが出るので直す。とくにスクリプトではaGpにレイヤーが入ることもあるので、レイヤーだった場合は レイヤー.containingComp.time、Gpだった場合はGp.propertyGroup(Gp.propertyDepth)でレイヤーを指定、さらにレイヤー.contaningComp.timeとする。

出典:
画像処理ソリューション様 アフィン変換
高校数学の基本問題様 行列の乗法の性質 結合法則が成立の欄

行列に関する要点:トランスフォームの位置やスケール、回転などは全て行列の掛け算で表せる。
また、行列の掛け算は(AB)C=A(BC)が成り立つ。つまり、左右の並べ替えさえしなければ、どこから掛けてもよい。なので、値を最後に掛けてもよいことになる。ならば、その前の計算式を使いまわすことができる!
行列の掛け算は、値からやっていくときと並び順が逆になることに注意(右側が値になる)。

よかったらシェアしてね!
  • URLをコピーしました!