GTM No Script

WebGLで、
ダイナミックなデジタル
体験を創出(パート2)

はじめに

こちらのパート2の記事では、WebGLをさらに詳しく見ていきます。まだの方は、ぜひパート1もご覧ください。パート1ではWebGLの概要と、ブラウザ上でグラフィックを描画する方法を簡単に説明しています。

今すぐ読む

この記事では、ブラウザ上でスムーズでインタラクティブなグラフィックスを作成する際に役立つ、最適化方法について説明します。

WebGLを最適化するテクニック

コンピューターグラフィックスのパフォーマンス最適化には、汎用なものから特別な仕様を必要とするものまで、さまざまなアプローチがあります。

そのなかでも、WebGLでよく使われる最適化テクニックとして以下が挙げられます。

  1. JavaScriptコードからシェーダーへの計算の移行
  2. 多数オブジェクトのレンダリング最適化

ここでは、Starとテキサス大学オースティン校Design Institute for Healthとの共同プロジェクトである「インタラクティブヘルスツール」を例に、最適化テクニックを紹介していきます。

GPUで計算を行う方法

コンピューターグラフィックスでよく見られる活用事例に、オブジェクトのアニメーション化があります。例えば、オブジェクトを移動させたり、形状や色を変えたりといったことです。

このようなタスクは、実装の簡単さからJavaScriptを使って解決されるケースがよく見られます。ただ、アプリケーション全体のパフォーマンスの観点では、その方法では不利になる場合があります。

以下はシンプルなアニメーションの例です。空間上のいくつかの経路に沿って、オブジェクトが異なる速度で移動します。

以下のように時間関数として座標を計算すると、たしかにシンプルになります。

const easingValue = (1 + Math.sin(Math.PI * (t - 0.5))) / 2

this.shardsGroup.children.forEach((mesh) => {
    const { initialPosition, explosionVector, rotationSpeed, rotationAxes } = mesh

    mesh.position.set(
        initialPosition.add(explosionVector.multiplyScalar(easingValue))
    )

    mesh.rotation[rotationAxes[0]] += rotationSpeed
    mesh.rotation[rotationAxes[1]] += rotationSpeed
})

ただ、この場合は、すべての計算をフレームごとに行う必要があります。また、アニメーション化されるオブジェクトの数もパフォーマンスに影響してきます。

最適化処理を行う前のアニメーションを見てみましょう。ここでは、オブジェクトの数を約2500個に増やしています。各オブジェクトは4つの頂点で構成されています。

ご覧の通り、フレームレートはかなり低く、アニメーションも滑らかとは言えません。ブラウザのプロファイラーによる解析を見ると、CPUにほぼ100%近い負荷がかかっているのがわかります。緑色のグラフはフレームレート(1秒あたりフレーム数:0~60)、黄色のグラフはCPU負荷(0~100%)を示しています。

そこで、CPUの負荷を軽減させるために、頂点の属性データとユニフォーム変数を使って、アニメーションをGPU(Graphics Processing Unit)に移管します。

mat4 rotationMartix = makeRotationMatrixY(uRotation);

mat3 internalRotationMartix = makeInternalRotationMatrix(
    uInternalRotation * uInternalRotationFactor, rotationAxes
);

vec3 explosionDelta = explosionDestination * easeInOutSin(uExplosionElapsedTime);

vec4 vertexPositionTransformed =
    modelViewMatrix * vec4(
        internalRotationMartix * position + explosionDelta + initialPosition, 1.0
    );

gl_Position = projectionMatrix * vertexPositionTransformed;

変換されるオブジェクト座標の算出には、以下のデータを用います。

  • explosionDelta:各瞬間のオブジェクトの現在地を決定するために使用される平行移動ベクトル
  • initialPosition:ワールド座標系におけるオブジェクトの初期位置
  • internalRotationMatrix:オブジェクトを回転させるための行列

WebGLでは行列計算を使用します。それほど難しいものではありませんが、慣れるまでに少し時間がかかるかもしれません。アニメーションオブジェクトの座標変換に使用した上記の回転行列について、行列の作成例を以下に示します。

mat3 makeInternalRotationMatrix(float theta, vec3 axes) {

    float cX = cos(theta * axes.x);
    float cY = cos(theta * axes.y);
    float cZ = cos(theta * axes.z);
    float sX = sin(theta * axes.x);
    float sY = sin(theta * axes.y);
    float sZ = sin(theta * axes.z);

    return mat3(
        cY * cZ, cY * sZ, -sY,
        sX * sY * cZ - cX * sZ, sX * sY * sZ + cX * cZ, sX * cY,
        cX * sY * cZ + sX * sZ, cX * sY * sZ - sX * cZ, cX * cY
    );
}

mat4 makeRotationMatrixY(float theta) {

    float c = cos(theta);
    float s = sin(theta);

    return mat4(
        c, 0., -s, 0.,
        0., 1., 0., 0.,
        s, 0., c, 0.,
        0., 0., 0., 1.
    );
}

シェーダーに計算を移管して、最適化を行なった結果がこちらの動画です。

アニメーションはずいぶんスムーズになりましたが、フレームレートはまだ60fpsにはほど遠い状態です。

 

多数オブジェクトの描画の最適化

多数のオブジェクトが扱われると、パフォーマンスに大きな影響を与えます。オブジェクトごとに描画呼び出しを行うことで、かなりのリソースが消費されるからです。

これを回避するには、呼び出しの回数を最小限に抑える必要があります。そのためには、似たオブジェクトのジオメトリを組み合わせて統合するアプローチが有効です。

その場合、統合されたオブジェクトの各部分の座標を変換する際などに、シェーダーへのデータの受け渡し方法がやや異なってきます。とくに、ユニフォーム変数はすべての頂点に共通する値であるため、この目的には適さなくなります。

ここでは、各頂点のデータを個別に設定できる「属性」を使用します。オブジェクトの座標を決定するコードについて、ジオメトリを統合する前と後でどう違うかを比較してみましょう。まず以下の例では、ユニフォーム変数を使ってシェーダーに座標を渡しています。

const setMeshVectorUniform = (mesh, vector, uniformName) => {
    mesh.material.uniforms[uniformName] = {
        value: vector
    }
}
属性を使うと以下のようになります。
const setGeometryVectorAttribute = (geometry, attributeName, vector) => {
    const vertexAttributesCount = geometry.attributes.position.array.length

    const attributeValuesArray = new Float32Array(vertexAttributesCount).fill(0)

    for (let i = 0; i < vertexAttributesCount; i = i + 3) {
        attributeValuesArray[i] = vector[0]
        attributeValuesArray[i + 1] = vector[1]
        attributeValuesArray[i + 2] = vector[2]
    }

    geometry.setAttribute(
        attributeName,
        new BufferAttribute(attributeValuesArray, 3),
    )
}

少し複雑に見えるかもしれませんが、この方法により、ジオメトリデータをより効率的に扱えるようになります。

上記のような最適化を行った結果が以下の動画です。60fpsのフレームレートを達成でき、アニメーションも十分に滑らかになりました。CPUとGPUの負荷も大幅に軽減されています。

まとめ

今回の記事で紹介した方法を使って、以下のようなさまざまな目的でWebGLを活用できます。

  • データの可視化(地図、統計データ、医療現場やロボットなどから得られたデータなど)
  • 複雑なデザインソリューションの実装
  • ゲームの開発
  • グラフィックエディターの作成
  • 画像処理
  • インタラクティブなプレゼンテーションの開発(自動車用機器やモバイル機器メーカーなど)
  • 仮想現実(VR)/拡張現実(AR)プロジェクトの実装

WebGLは非常に汎用性の高いツールです。実装に必要なのは、グラフィックプロセッサと想像力だけです。

WebGLの活用事例について、StarとDesign Institute for Healthによるコラボレーションを紹介したケーススタディもぜひご覧ください。

今すぐ読む