読者です 読者をやめる 読者になる 読者になる

チリペヂィア

リンクフリー。サンプルコードなどは関連記事内でライセンスについて明示されない限り商用利用なども自由に行って構いませんが、自己責任でお願いします。またこれら日記内容の著作権自体は放棄していません。引用部分については引用元の権利に従ってください。

AnimationCurveを近似する折れ線を適度に生成

(…誰得?)

とりあえず

  • ある区間内で一定よりもキツい角度で曲がってたら前後二つに分けて再帰する。
  • あんまり細かくなったらやめる。

ていうお決まり感あふれる作戦にて候*1

さて挿入を繰り返すという手順から純粋に理論的に考えますと、データ構造はリンクリストが妥当です。が、今回はパフォーマンスの定石として配列リストにします。というのもAnimationCurveの実態は制御点つまりkeyframeの配列で、最初からこの区間の数が決まっています。で、区間ごとに分割した配列リストにするのがわりと簡単で、しかも分割数もそんなに増えないでしょう多分*2

使い方
        AnimationCurve curve = new AnimationCurve();

        // 最悪でも100分割に制限し、1度以上曲がってたら再帰分割する
        AnimationCurveDivider.DividedResult divided =
            new AnimationCurveDivider.DividedResult(curve, 100, 1);
        // divided.points          /* 配列リストを取得 */
        // divided.points.Length   /* 区間の数 */
        // divided.points[0]       /* 最初の区間を取得 */
        // divided.points[0].Count /* 最初の区間に何個のVector2要素が並んでいるか */
        // divided.points[0][0]    /* 最初の区間の先頭のVector2要素を取得 */
        // divided.pointsCount     /* divided.points[n].Countの合計 */
        // divided.valueMax/Min    /* 全ての要素のyの最大/最小。
        // (x要素の最小はAnimationCurveの最初のkeyframe.timeまたは配列リスト最初のVector2.x) */
        // (x要素の最大はAnimationCurveの最後のkeyframe.timeまたは配列リスト最後のVector2.x) */
        // divided.ToArray()       /* 配列リストを1つのVector2[]に統合して返す。 */


昨日の直線描画サンプルを適当にいじって…(もちろんコレ以外にも多少いじる)

        Keyframe headKey = obj.sourceCurve.keys[0];
        Keyframe endKey = obj.sourceCurve.keys[obj.sourceCurve.length - 1];
        Vector2 center = new Vector2((headKey.time + endKey.time) * 0.5f,
            (divided.valueMax + divided.valueMin) * 0.5f);
        float width = System.Math.Max(endKey.time - headKey.time, 0.1f);
        float height = System.Math.Max(divided.valueMax - divided.valueMin, 0.1f);
        GL.LoadPixelMatrix(center.x - width * 0.5f, center.x + width * 0.5f,
            center.y - height * 0.5f, center.y + height * 0.5f);
        GL.Begin(GL.LINES);
        GL.Color(Color.white);
        GL.Vertex(divided.points[0][0]);
        for (int indexAry = 0; indexAry < divided.points.Length; indexAry++)
        {
            int indexList = (indexAry == 0 ? 1 : 0);
            int indexListMax = divided.points[indexAry].Count;
            if (indexAry == (divided.points.Length - 1)) indexListMax--;
            for (; indexList < indexListMax; indexList++)
            {
                GL.Vertex(divided.points[indexAry][indexList]);
                GL.Vertex(divided.points[indexAry][indexList]);
            }
            if (indexAry == (divided.points.Length - 1)) GL.Vertex(divided.points[indexAry][indexListMax]);
        }
        GL.End();
        GL.PopMatrix();


(数字は分割数)

コード読むのが面倒な人にロジックとかもうちょっと詳しく

ある区間が与えられて、その区間の最初→最後に向かうベクトルA、最初→中間に向かうベクトルBを計算して、どちらも長さを正規化すると、内積Dotがその角度差を示すコサインになります。いわゆるベクトルのなす角という奴です。この角度差がその区間をまだ分割するべきかどうかの目安になっています。

また、keyframe~keyframeの1区間当たり、最初の1回だけは角度計算を飛ばして分割に挑戦します。この曲線の性質上、1区間内でS字までは描きます。3次曲線の真ん中みたいな感じで。本当はくねっていて分割すべき曲線なのに、たまたま、始点→中間の向きが始点→終点の向きと一致してしまい、角度判定で1度も分割されない事態が有りえます。なので最初の一度は角度計算を飛ばして再帰処理に突入し、それでも一度も分割される事なく戻って来たら、最初の一回分の始点→中間・始点→終点の角度を判定して、本当に曲がってない直線のようなら中間点を削除しています。

それと…AnimationCurveの妙な仕様で、普通のドローツールだと、垂直以上の角度を設定した時は横軸方向に戻る感じで曲がりくねるもんですが、UnityのAnimationCurveでは妙な垂直グラフになるという挙動をします。これは今回は面倒なのでスルーする事にします。バグったりはしませんが、最小分割単位でしか再現しませんので、垂直な角度は再現できませんね。微妙に斜めになります。

こんなところでしょうか。

*1:なんかいかにも情報学部の課題とかでありそう

*2:例えば最悪100分割を目途に再帰分割していくとして、3つの制御点に挟まれた2つの区間だとします。すると理想的に半分で分かれる曲線だと区間当たり最悪でも50分割が目安になります。さらに、そもそも滑らかに補間する曲線なんだから、曲線の中心から半分はほとんど分割しないエリアが存在する事がほぼ期待できます。分割数は体感的にだいたい、最悪ケースの4~6割程度でしょうか。とするとこの場合ブロックコピーの発生する挿入コストで想定する配列の長さは25個とかそんなもんです。一方で、floatとかVector2程度の要素にいちいち細切れのヒープをGCに要求しまくる事は安定してヘビー(少なくとも10~100個程度のブロックコピーに比べれば。だいたいこのサイズの配列なら何百文字かの文字列型を切った貼ったするのと大差ない)。これが配列リストを採用する理由になります。