見出し画像

[Unity]世界を少しだけ曲げて遠くの景色の描画を減らす技術

3Dのオープンワールドゲームを開発していると、必ず問題になるのが遠景の景色の描画です。
はるか遠くまで樹木などを配置するとそれだけリアルにはなりますが、描画するオブジェクトの数が飛躍的に増え、負荷が増します。
そのための対策として、オブジェクトのLODを使用するなど、試すべき最適化の手法は無数にあります。
弊社が現在開発中のフィットネスゲームJoggleでは、地球の球体としての曲率を強調することで、遠くの景色を地面に隠してしまう手法を採用しています。

遠くの景色を地面に隠すメリット

この手法は描画負荷の軽減だけではなく、さまざまなメリットがあると感じています。
以下に、曲率の強調を適用する前と適用後の画像を並べてみました。

曲率ゼロの状態
曲率を強調して遠景を地面に隠した状態

メリット① 画面がスッキリする

適用前の状態では、遠景の木の幹の隙間から空が見えてしまい、風景がごちゃごちゃしているのが分かるかと思います。
曲率を強調することで、遠くの木の幹が自然に地面に隠れてくれて、さらに遠くの木の葉が、下草のように自然に隙間を埋めてくれているのが分かるかと思います。

メリット②遠景の表示切り替えを隠せる

もうひとつのメリットは、描画領域の更新のタイミングを自然に隠すことができることです。
このゲームは実際の地球と同じサイズのワールドを採用しているため、当然ワールドの全領域を常時描画することはできません。そのためワールドを1辺が約100mのタイルに分割して、カメラの位置が移動するごとに表示するタイルを変更しています。
キャラクターが前方向に移動し続けると、次々と前方に新たにタイルが追加されるため、今まで存在していなかったオブジェクトが突然遠方に描画されることになります。

ですが、曲率を強調することで、タイルの追加と削除の瞬間を遠景の地面の下に隠すことができます。
そのため、カメラが前方に進むごとに、遠くの木は最上部の葉がまず姿を表し、それから少しずつ全体が表示されるようになります。

ちなみに、地球の実際の曲率は10km先の物体がわずか8m押し下げられる程度らしいので、ある意味では適用前の画像の方がリアルな景色とも言えます。
ですが、背景は所詮背景です。プレイヤーを目の前に起きている出来事に集中させるためには、背景はできるだけ目立たない存在であるべきです。
そのため、背景をごちゃつかせない事と、背景の表示を突然切り替えないことはとても重要であり、強調した曲率の採用はそのための有効な施策であると言えます。

Curved Worldアセットもオススメ

ちなみに、3D空間をもっとぐにゃぐにゃに曲げたい場合は、下記のアセットが超オススメです。
弊社でもハイパーカジュアルゲームなどを作る際には、結構お世話になっていたりします。
ですが、このアセットの難点は、アセットが用意している独自のシェーダーにのみ空間変形が適用されるというところです。
ゲーム内で独自の雰囲気を出すために自前のシェーダーを使いたい場合、Curved Worldのシェーダーは使うわけにはいきません。

曲率強調シェーダーは簡単に書ける

ということで、Joggleでは自作のY位置調整シェーダーを使用しています。
下記のGIFアニメは、そのシェーダーをシンプルなキューブに適用したサンプルです。
左側のウインドウで動かしているのはキューブではなくカメラの方です。
実際のキューブの位置は変わっていないのですが、カメラからの距離に合わせてY位置が少しずつ沈み込んでいるのが分かるかと思います。

実はシェーダーで遠景を曲げるのはとても簡単で、既存のシェーダーにたった数行の記述を足すだけです。
下記に、必要な記述を抜粋しました。

やっていることは、通常の頂点関数の中でワールド座標のY位置をカメラの距離に合わせて下に移動しているだけです。それ以外の記述は通常の頂点関数と何も変わりません。

float ComputeBendOffset(float distance)
{
    float nearDistance = 200;
    float bendAmount = 0.00025f;

    if (distance <= nearDistance)
        return 0.0;  // 200mの範囲内は下げ幅0

    // 200mを超えた距離
    float farDistance = distance - nearDistance;

    // この例では球形の下げ方を模倣するための簡易的な計算を採用します。
    // 実際には、より詳細な形状やカーブを求めるための計算が必要です。
    float bendOffset = bendAmount * pow(farDistance, 2);

    return bendOffset;
}

v2f vert (appdata v)
{
    v2f o;

    float4 worldPosition = mul(UNITY_MATRIX_M, v.vertex);  // モデル座標をワールド座標に変換

    float distance = length(_WorldSpaceCameraPos - worldPosition);
    worldPosition.y -= ComputeBendOffset(distance);

    o.position = worldPosition.xyz; 
    o.vertex = mul(UNITY_MATRIX_VP, worldPosition);  // ワールド座標をプロジェクションスペースに変換
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    o.fogFactor = ComputeFogFactor(o.vertex.z);
    o.normal = normalize(mul((float3x3)UNITY_MATRIX_M, v.normal));  // モデルの法線をワールドの法線に変換

    return o;
}

また、下げ幅を計算する関数の中身を読むと分かるかと思いますが、地面をリアルな球面に沿って曲げているわけでは全くありません。
単純にY位置を下げているだけですし、200mよりも近くにあるオブジェクトには全く調整が行われないようにしています。
つまり、完全なフェイクです。

リアルである必要はない

これらは意図的にそのようにしています。
200mというのは、このゲームの中でモンスターやアイテムが登場する最大距離であり、これより内側の領域では地面を曲げたくないのです。
というのも、この距離にまで地面の球面効果を適用すると、そのようなシェーダーを使用していないモンスターやアイテムが微妙に地面から浮いてしまいます。地面はカメラ越しの映像でのみ、シェーダの効果で曲がっているように見えるだけで、実際の地面のメッシュは平面に存在しているからです。
それを回避するためには、モンスターやアイテムを含めた全オブジェクトにY値調整済みのシェーダーを適用する必要がありますが、それだとアセットストアなどから購入できる優秀なシェーダーがすべて使えなくなってしまいますね。
それはそれで不便なので、近景の地面は敢えて曲げずに、自由なシェーダーが使える領域にしています。
また、モンスターの存在はできる限り遠くから気づかせたいですし、この領域の地面は曲げない方が、ゲームとしても理にかなっていると言えます。



この記事が気に入ったらサポートをしてみませんか?