地図

MapboxGLで映画のようなルートアニメーションを構築

見出し

これはレイアウト確認用のダミーテキストです。

アニメーションルート追跡シーンとビデオエクスポートの作成方法

アニメーションルート追跡シーンとビデオエクスポートの作成方法

MapboxのツイートやInstagramの投稿で、ツール・ド・フランスの各ステージを示すアニメーションビデオをご覧になったことがあるかもしれません。優雅なカメラの動き、衛星画像、3D地形により、視聴者を没入させ、ツアーのサイクリストが耐えなければならない標高の変化と距離を十分に印象付けることができます。

この記事では、これらのアニメーションビデオの作成に関わるすべての可動部分を確認します。

  • 高高度からルートエリアにズームイン
  • 地図上のルートをアニメーション化する
  • カメラでアニメーションルートの最先端を追跡
  • より視覚的に魅力的なシーンを作成するために、カメラをスムーズに回転(またはゆっくり回転)させること
  • Webキャンバスからビデオへのエクスポート

これらのビデオのアプローチは、Mapbox GL JSドキュメントのQuery Terrain Elevationの例から始まり、3D地形での同様のルート追跡を示していますが、正確なカメラ制御はありません。

ルートの公開

geoJSON LineStringとしてエンコードされたルートから始めて、時間経過に伴う「進行状況」をアニメーション化するために、線の長さを段階的に明らかにする必要があります。 

アニメーションは一連の静止画像をまとめて再生したものなので、目の前のタスクは、前のフレームと比較して必要な変更を加えて各フレームをプログラムで構築することです。このために、ブラウザのwindow.requestAnimationFrame()メソッドを使用します。これは、時間の追跡にも役立ちます。  

時間の経過とともに線を表示するには、line-gradientというペイントプロパティを変更します。

まず、アニメーションの開始から経過した時間をアニメーションのプリセットされた継続時間で割ることにより、animationPhaseの値を計算します。これにより、開始から終了に移動するにつれて、各フレームに対してゼロから1の間の値が得られます。

次に、これをsetPaintProperty()で使用します。

const animationPhase = (currentTime - startTime) / duration;
...

map.setPaintProperty(
    "line",
    "line-gradient",
    [
        "step",
        ["line-progress"],
        "yellow",
        animationPhase,
        "rgba(0, 0, 0, 0)"
    ]
);

平易な英語で言うと、この式は「線に沿った各ポイントが現在の進行ポイントより前にある場合は黄色で、後にある場合は透明で色を付ける」と言っています。 animationPhase はフレームごとに1に近づくため、この setPaintProperty() メソッドが呼び出されるたびに、線の別の小さな部分が表示されます。

これは、この手法を使用して、単純な2点間のLineStringを3秒かけて表示する例です。

カメラを移動させて追跡する

線がフレームごとに表示されるようになりました。カメラを追跡するにはどうすればよいでしょうか。答えは、turf.jsと、Mapbox GL JSのFreeCamera APIに加えて、少しの三角法が必要です。まず、線の「リーディングエッジ」の座標を取得する必要があります。turf.distance()turf.along()animationFrameとともに使用して、線に沿った正しいポイントを選択します。

// アニメーションの前に、線の長さを計算します
const pathDistance = turf.lineDistance(path);

...

// animationPhaseに基づいてパスに沿った距離を計算します
const [lng, lat] = turf.along(path, pathDistance * animationPhase)
    .geometry.coordinates;

カメラを制御するには、位置/高度(どこにあるか?)およびピッチ/ベアリング(どちらを向いているか?)の4つを提供する必要があります。これらのビデオでは、高度とピッチは一定であり、計算する必要があるのはベアリングとカメラの位置だけです。

カメラのベアリングは、パスに沿った進行とは無関係です。微妙な映画のような回転効果のために、一定の速度で変更しています。

const bearing = startBearing - animationPhase * 200.0;

ベアリング、高度、ピッチ、および見たいポイントを使用して、三角法を使用してカメラの位置を推測できます。

const computeCameraPosition = (
    pitch,
    bearing,
    targetPosition,
    altitude,
    smooth = false
) => {
    const bearingInRadian = bearing / 57.29;
    const pitchInRadian = (90 - pitch) / 57.29;

    const lngDiff =
        ((altitude / Math.tan(pitchInRadian)) *
            Math.sin(-bearingInRadian)) /
        70000; // ~70km/degree longitude

    const latDiff =
        ((altitude / Math.tan(pitchInRadian)) *
            Math.cos(-bearingInRadian)) /
        110000; // 110km/degree latitude

    const correctedLng = targetPosition.lng + lngDiff;
    const correctedLat = targetPosition.lat - latDiff;

    const newCameraPosition = {
        lng: correctedLng,
        lat: correctedLat
    };

    ...

    return newCameraPosition;
};

注:経度の角度からメートルへの変換は緯度に依存します。上記の関数は、1度あたり70kmという固定変換を使用していますが、これはフランスでは十分に正確ですが、どこでも機能するわけではありません。

これは、computeCameraPosition() で行われている計算を説明するのに役立つ図です。 高度と方位、そしてピッチとともに、見たい地上の場所がわかっています。 カメラの新しい位置は、ターゲット位置を基準としたxオフセットとyオフセットとして計算されます。

GeoGebraで作成された3Dチャート (https://www.geogebra.org/3d/z8czvzzw)

LERPによるスムーズ化

以前のバージョンのビデオでは、カメラは急なカーブを含むパスを直接追跡していました。カメラはラインの先端にレーザーのように集中しているため、アニメーションに揺れが生じていました。

線形補間は、「lerp」とも呼ばれ、フレーム間で急激に移動するのを防ぐことで、カメラの動きをスムーズにするために使用できます。  

// from https://codepen.io/ma77os/pen/OJPVrP
function lerp(start, end, amt) {
    return (1 - amt) * start + amt * end;
}

André Mattos によるこの codepen は、円の動きをスムーズにする lerp 関数を示しています。マウスを動かすと、円がポインタにスムーズに追従する様子がわかります。同じ lerp 関数を使用して、ルートアニメーションにスムージングを追加しました。

前の位置と新しいリーディングエッジをラープ関数に通し、「よりスムーズな」新しい位置を取得します。これは、見たいリーディングエッジがフレームの絶対的な中心にあるとは限らないが、動きが急激でない限り、時間の経過とともに中心に戻る傾向があることを意味します。

地球からズームイン

各ビデオは、選挙速報から始まり、ルートアニメーションで世界のどの地域が表示されるかを示します。Mapbox GL JSには便利なflyTo()メソッドがありますが、flyTo()の終了状態とFreeCamera APIコントロールの開始状態との間をシームレスに移行するのが難しかったため、ここでは使用できませんでした。代わりに、カメラを初期の地球儀ビューとトラックアニメーションの開始ビューの間で移行させるカスタム関数を作成しました

キャンバスからビデオへのエクスポート

動画をエクスポートするために、Mapbox GL JSコードベースのこちらの例で説明されている手法を使用します。これは、mp4-encoder javascriptライブラリを利用し、Mapbox GL JSキャンバスの各レンダリングを使用してフレームを保存します。

function frame() {
    // increment stub time by 16.6ms (60 fps)
    now += 1000 / 60;

    mapboxgl.setNow(now);

    const pixels = encoder.memory().subarray(ptr); // get a view into encoder memory

    gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); // read pixels into encoder

    encoder.encodeRGBPointer(); // encode the frame
}

map.on("render", frame); // set up frame-by-frame recording

キャンバスのサイズは、地図コンテナのCSSで制御されます。16:9の動画の場合、1280px x 720pxの寸法を持つコンテナを使用します。(Instagramで使用される)正方形の動画の場合、寸法は1080px x 1080pxです。

ポストプロダクション

アニメーションが完了すると、これらのフレームが組み立てられ、mp4としてダウンロードされます。最後に、出力mp4はCLIツールffmpegを使用して圧縮されます。

ffmpeg -i my_video.mp4 my_video_compressed.mp4

その後、Webデザインチームのメンバーがイントロとアウトロのグラフィックを追加し、完成した製品をソーシャルメディアチームに渡して公開します。

これらのアニメーションの作成に使用されたコードは、githubのimpact tools repositoryに公開されており、参照および再利用できます。FreeCamera APIを試したり、Mapbox GL JSプロジェクトから高品質のビデオをエクスポートしたりした経験について、ぜひお聞かせください。共有する準備ができたら、Twitterで@mapboxをタグ付けしてください。

これはレイアウト確認用のダミーテキストです。

これはレイアウト確認用のダミーテキストです。

関連記事