目次
1)ローエンドデバイスで街づくり
最新のモバイルデバイスは高い処理能力を備えていますが、大規模で美しいゲーム環境を安定したフレームレートで実行することはいまだに困難です。より古いモバイルデバイス上で、大規模な 3D 環境で安定して 60 fps を達成するのは非常に困難です。
開発者としては、ハイエンドのスマートフォンのみをターゲットとし、大多数のプレイヤーがゲームを滑らかに実行するのに十分なハードウェアを持っていると見なして開発を進めることもできます。ただし、いまだに数多くの古いデバイスが使用されている現状において、これは潜在的なプレイヤーを大量に締め出す結果となります。古いデバイスのユーザーはすべて、できれば排除したくない潜在的な顧客です。
当社のゲーム『Galactic Colonies』では、プレイヤーが異星人の住む星をコロニーとして、数多くの建物が建ち並ぶ大規模なコロニーを構築できます。小さなコロニーの建物の数は 1 ダース程度ですが、大きなコロニーでは優に数百を超えます。
パイプラインの構築を開始したときの目標リストは次のとおりです。
- 多数の建物が建ち並ぶ大きなマップがほしい
- 安価で古いモバイル端末でも早い実行速度で動作するようにしたい
- 光と陰影を美しく見せたい
- 簡単で保守性に優れた制作パイプラインがほしい
2)モバイルゲームでのライティング:よくある課題
ゲームの巧妙なライティングは、3D モデルの見栄えを良くする鍵になります。もちろん Unity なら、手間なく巧みなライティングができます。ゲームのステージを作ってダイナミックライトを配置すれば、すぐ見栄えの良い画面ができあがります。パフォーマンスを注視するなら、全ライトをベイクしてから、ポストプロセッシングスタックを通じて SSAO をいくつか追加してちょっとした装飾を散りばめましょう。そうしたらもう、出荷するだけです!
モバイルゲームでのライティングの設定には、テクニックを駆使したり設定時に何らかの回避策を講じたりしなければならないことも少なくありません。例えば、ハイエンドデバイスを対象としている場合を除けば、ポストプロセッシングエフェクトには一切触れない方が良いでしょう。同様に、大きなシーンでダイナミックライトをふんだんに使ってしまうと、フレームレートが格段に低下してしまいます。
リアルタイムのライティングは、たとえデスクトップ PC であっても非常に負荷が高くなりうるものです。限られているモバイル端末ではリソースが限られているので、こういった魅力的な華々しい機能をすべて盛り込む余裕はありません。
3)便利なライトベイキング
モバイル端末ではリソースが非常に限られているので、シーンに妙にこだわったライトを数多く配置してユーザーのバッテリーを無駄に消耗させるわけにはいきません。
ハードウェアの限界まで端末を酷使し続けると過熱し、その結果、自動的に端末保護のために処理能力がダウンします。このため、リアルタイムで影を描写する必要のないライトをすべてベイクする習慣をつけてください。

(多数のビルが表示された『Galactic Colonies』の典型的なステージ)
しかし私たちにとって、これはできない選択でした。私たちのゲームの世界は、プレイヤーによってリアルタイムで構築されるものだからです。常にプレイヤーにより新しいエリアが発見され、新しい建物が建築され、既存の建物が改築されるので、効果的なライトのベイクができないのです。
4)フェイクで何とかする
(静的)シーンのハイライトと影を事前に計算しておいて、その情報をライトマップに保存するというのが、ライトベイキングのプロセスです。
このプロセスでレンダラーにどこでモデルを明るく(または暗く)するのかを伝え、ライトがあるような錯覚を作り出します。
この方法でレンダリングを非常に高速化できます。負荷が高く時間もかかるライト計算がすべてオフラインで完了していて、レンダラー(シェーダー)は実行時にテクスチャに格納された結果を見るだけで済むからです。
これには代償があって、ライトマップテクスチャを余分に積み込む必要があります。これによりビルドのサイズが大きくなり、実行時に余分なテクスチャメモリが必要になります。
また、メッシュにライトマップ UV が必要になり、若干サイズが大きくなるので、ある程度スペースのロスが生じます。
しかし全体的に見れば、圧倒的な高速化を実現できます。

(ライトマップを使用した建物と使用していない建物)
しかし、プレイヤーによって常に変化する動的世界を相手にしている場合は、単に「Bake」ボタンを押しただけでは意味がありません。
高度にモジュール化したシーンのライトベイキングでは、さまざまな問題に直面しています。
5)簡単ではないプレハブのライトベイキング
まず問題となるのが Unity ではライトベイキングデータが、シーンデータに直接関連付けられるかたちで保存されるということです。これは、各ゲームステージが独立していてシーンが構築済みで、動的オブジェクトの数が限られている場合であれば問題になりません。ライティングを事前にベイクしておけば、それで十分です。
ところがゲームステージを動的に作成する場合、これではうまくいかないのは自明です。都市建築ゲームの世界は、創られていないのが前提です。世界の大部分はプレイヤーが何を、どこに建築するかによって動的に、その場で組み立てられていきます。これは通常、プレイヤーが何かを建築しようとするたびにプレハブをインスタンス化することで実現されます。
この問題の唯一の解決策は、関連するすべてのライトベイキングデータをシーンではなく、プレハブ内に格納することです。
残念ながら、使用するライトマップのデータ、その座標、スケールをプレハブにコピーする簡単な方法はありません。
6)ライトをベイクしたプレハブ用パイプラインの構築
ライトをベイクしたプレハブを処理するための安定したパイプラインを実現するには、個々のシーンごと(事実上複数のシーン)にプレハブを作成して、必要になったときにメインゲームに読み込むようにするのが、最善のアプローチにです。
各モジュールにライトがベイクされ、必要になったらゲームに読み込まれます。
Unity がライトをベイクする仕組みをよく見ると、実際にはライトがベイクされたメッシュに他のテクスチャが適用され、メッシュの明度が若干調整される(彩色されることもあります)だけのレンダリングであることがわかります。必要なのはライトマップテクスチャと UV 座標だけです。そしてそのどちらも、ライトをベイクするプロセスで Unity により作成されます。
ライトベイキングのプロセスで、Unity は新しい UV 座標(ライトマップテクスチャの場所を示すもの)と、個々のメッシュのオフセットとスケールをワンセットで作成します。ライトを再ベイクするたびに、座標は変化します。
7)UV チャンネルの活用方法
この問題の解決策を練るうえで、UV チャンネルの仕組みと、それを最大限に活用する方法についての知識が役に立ちます。
各メッシュには複数の UV 座標セット(Unity では UV チャンネルと呼びます)を持つことができます。それぞれのテクスチャ(拡散、鏡面、バンプなど)がすべて画像内の同じ場所に情報を格納するので、たいていの場合 UV セットは 1 つで十分です。
しかしオブジェクトがライトマップのようなテクスチャを共有していて、1 つの大きなテクスチャ内の非常に限定的な場所に関する情報を参照する必要がある場合は、この共有テクスチャで使用する別の UV 座標セットを追加する以外の方法がないこともあります。
複数の UV 座標を使うことの欠点は、メモリの消費量が増えることです。UV セットを 1 つではなく 2 つ使うと、メッシュの 1 つ 1 つの頂点で UV 座標が 2 倍になります。すべての頂点に余計な 2 つの浮動小数点数が格納され、さらにレンダリング時にこれらが GPU にアップロードされます。
8)プレハブの作成
Unity を使って、通常のライトベイキング機能で座標とライトマップを生成します。エンジンによってライトマップの UV 座標がモデルの 2 番目の UV チャンネルに書き込まれます。重要なのは、この場合は 1 番目の UV 座標セットが使えないことに注意することです。なぜなら、モデルをアンラップする必要があるからです。
すべての面に同じテクスチャを使っている箱を想像してみてください。同じテクスチャを再利用するので、箱の各面がすべて同じ UV 座標を持ちます。しかし、箱の各面にはそれぞれの光の当たり方と影のつき方があるため、これはライトマップされたオブジェクトでは機能しません。各面について、それぞれのライティングデータに基づくライトマップ内の独自のスペースが必要なのです。そこで、新しい UV セットが必要になります。
このため、新しくライトをベイクしたプレハブの設定に必要なのは、行方不明にならないようにテクスチャとその座標を保存して、それをプレハブにコピーすることだけです。
ライトベイキングが完了したら、シーン内のすべてのメッシュを対象にしたスクリプトを実行し、オフセットとスケールの値が適用された状態で UV 座標をメッシュの UV2 チャンネルに書き込みます。
メッシュを更新するためのコードは、次のように比較的単純です。
Mesh meshToModify = GetComponent().sharedMesh; Vector4 lightmapOffsetAndScale = GetComponent().lightmapScaleOffset; Vector2[] modifiedUV2s = meshToModify.uv2; for (int i = 0; i < meshToModify.uv2.Length; i++) { modifiedUV2s[i] = new Vector2(meshToModify.uv2[i].x * lightmapOffsetAndScale.x + lightmapOffsetAndScale.z, meshToModify.uv2[i].y * lightmapOffsetAndScale.y + lightmapOffsetAndScale.w); } meshToModify.uv2 = modifiedUV2s;
少しだけ説明を加えると、これはメッシュのオリジナルではなくコピーに対して実行されます。ベイキングプロセス中のメッシュの最適化が目的だからです。
コピーが自動的に生成されてプレハブに保存され、新しいマテリアルにはカスタムシェーダーと新しく作成されたライトマップが割り当てられます。これでオリジナルのメッシュには触れないまま、ライトをベイクしたプレハブはすぐに使用できる状態になります。
9)カスタムライトマップシェーダー
これでワークフローがとてもシンプルになります。グラフィックスのスタイルや見た目を更新するには、該当のシーンを開いて納得いくまで変更を加えたらコードを実行して、自動化されたベイクとコピーのプロセスを開始します。このプロセスが終了すればすべて完了です。ゲームでは更新されたプレハブとメッシュが、更新されたライティングで使用されるようになります。
実際のライトマップテクスチャは、カスタムシェーダーにより追加されます。カスタムシェーダーは、レンダリング中にライトマップを 2 番目のライトテクスチャとしてモデルに適用します。
シェーダーはとてもシンプルで短く、カラーとライトマップを適用するだけでなく、低負荷のフェイクスペキュラー/グロスエフェクトの計算を行います。
Shader "Custom/LightmappedPrefabWithSpec" { Properties { _MainTex("Base (RGB)", 2D) = "white" {} _Lightmap("Lightmap", 2D) = "white" {} _Specmap("Specmap", 2D) = "white" {} _SpecularAtt("Glossiness", Range(0.1, 2)) = 0.5 _SpecularAmt("Specular", Range(0, 1)) = 0.5 } SubShader { Tags{ "Queue" = "Geometry+1" } Pass { CGPROGRAM // Defining the name of the vertex shader #pragma vertex vert // Defining the name of the fragment shader #pragma fragment frag // Include some common helper functions, // specifically UnityObjectToClipPos and DecodeLightmap. #include "UnityCG.cginc" // Color Diffuse Map sampler2D _MainTex; // Tiling/Offset for _MainTex, used by TRANSFORM_TEX in vertex shader float4 _MainTex_ST; // Lightmap (created via Unity Lightbaking) sampler2D _Lightmap; // Tiling/Offset for _Lightmap, used by TRANSFORM_TEX in vertex shader float4 _Lightmap_ST; // Grayscale Map indicating which parts of the models have specular // Note: _Specmap_ST is not needed, as this map is using the same // UVs as for the _MainTex. sampler2D _Specmap; // This is the vertex shader input: position, UV0, UV1, normal // UV1 (= second UV channel) needed for the lightmap texture coordinates struct appdata { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; float2 texcoord1: TEXCOORD1; float3 normal: NORMAL; }; // This is the data passed from the vertex to fragment shader struct v2f { float4 pos : SV_POSITION; // position of the pixel float2 txuv : TEXCOORD0; // for accessing the diffuse color map float2 lmuv : TEXCOORD1; // for accessing the light map float3 normalDir : TEXCOORD2; // for fake specular }; // This is the vertext shader, doing nothing special at all. // Most notably it is calculating the surface normal, because that // is needed for the fake specular lighting in the fragment shader. v2f vert(appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.txuv = TRANSFORM_TEX(v.texcoord.xy, _MainTex); // using _MainTex_ST o.lmuv = TRANSFORM_TEX(v.texcoord1.xy, _Lightmap); // using _Lightmap_ST // Calculating the normal of the vertex for the fragment shader float4x4 modelMatrixInverse = unity_WorldToObject; o.normalDir = normalize(mul(float4(v.normal, 0.0), modelMatrixInverse).xyz); return o; } uniform float _SpecularAtt; uniform float _SpecularAmt; // Fragment Shader half4 frag(v2f i) : COLOR { // Reading color directly from the diffuse texture, using first UV channel half4 col = tex2D(_MainTex, i.txuv.xy); // Reading specular (on/off) value from spec map texture half4 specVal = tex2D(_Specmap, i.txuv.xy); // Reading lightmap value from the lightmap texture half4 lm = tex2D(_Lightmap, i.lmuv.xy); // Fake specular light angle calculation with a hard-coded light direction half3 th = normalize(half3(0, 1, -0.25)); float spec = max(0, dot(i.normalDir, th)); // Adjusting by overall specular amount and glossyness (material parameters) spec = _SpecularAmt * pow(spec, 40.0 * _SpecularAtt); // We're just using red value of the specular texture, like a grayscale map, // although technically spec could be colored. // Example: float3 specCol = specVal * spec; spec = spec * specVal.r; // Calculating the final color of the pixel by bringing it all together col.rgb = min(half4(1,1,1,1), col.rgb * DecodeLightmap(lm) + col.rgb * spec); return col; } ENDCG } } Fallback "Diffuse" }
次の画像は、上のシェーダーを使ったマテリアルのセットアップ画面です。

(カスタムシェーダーを使ったマテリアルのセットアップ)
10)セットアップとスタティックバッチング
このケースでは、すべてのプレハブが設定されている 4 つの異なるシーンを使います。私たちのゲームには熱帯、氷原、砂漠など、さまざまな生物群系があり、それに従ってシーンを分割しています。皆さんのゲームでは、要件に応じてシーン数は異なるはずです。
特定のシーンで使われるすべてのプレハブで 1 つのライトマップを共有します。つまり、マテリアルを 1 つだけ共有しているプレハブに、テクスチャが 1 つだけ加わることになります。
その結果、すべてのモデルの静的レンダリングと、ほぼ世界全体のバッチレンダリングを、たった 1 つのドローコールで実行できるようになりました。

(Unity エディターのベイクレベル)
すべてのタイルや建物が設定されているライトベイキングシーンには、ローカライズしたハイライトを生成するために光源を追加しています。いずれにしてもベイクダウンすることになるので、セットアップシーンには必要と思った分だけいくつでもライトを配置しても構いません。
ベイクプロセスは、必要なすべてのステップを示してくれるカスタム UI ダイアログで進めます。おかげで以下のことを忘れずにいられます。
- 正しいマテリアルがすべてのメッシュに割り当てられている
- プロセス中にベイクする必要がないものをすべて非表示にする
- メッシュを組み合わせる/ベイクする
- UV をコピーしてプレハブを作成する
- すべてに正しく名前を付け、必要なファイルをバージョン管理システムからチェックアウトする

(簡易ワークフローとしてのカスタムインスペクター)
適切に命名されたプレハブがメッシュから作成されるので、ゲームコードでそれらを直接読み込んで使うことができます。このプロセス中にメタファイルも変更されるので、プレハブのメッシュへの参照が失われることもありません。
このワークフローのおかげで、好きなだけ建物を調整し、好きなようにライティングして、スクリプトですべてのことを処理することができます。
メインシーンに戻ってゲームを実行すると、何ら問題なく動作します。手作業で何か行ったり、他の部分を更新したりする必要はありません。
11)フェイクライトと動くビット
ライティングを 100% 事前にベイクしたシーンの欠点の 1 つは、動的オブジェクトやモーションを使用しにくくなることです。影を投影する要素はすべて、リアルタイムでライトや影を計算する必要があります。言うまでもなく、これは何としても避けたいものです。しかし、動くオブジェクトがまったくないと、3D 環境は静的で活気のない世界に見えてしまいます。
見栄えするビジュアルを高速レンダリングで実現することが最優先なので、ある程度の制限は仕方ありません。生き生きとした動きのあるスペースコロニーや都市という印象を生み出すためには、大量のオブジェクトをすべて実際に動き回らせる必要はありません。また、動かすものの大部分を影がなくてもよい、または少なくとも影がなくても気にならないものにします。
私たちがどうしたかというと、まずはすべての建物のブロックを 2 つのプレハブに分けました。1 つは静的プレハブで、頂点の大部分とメッシュの複雑なビットがすべて含まれます。もう 1 つが動的プレハブで、含まれる頂点はできるだけ少なくなるようにします。
プレハブの動的な部分は、アニメーション化されたビットで、静的な部分の上に配置されます。一切ライトベイクされず、非常に高速で負荷が軽いフェイクライティングシェーダーを使って、オブジェクトが動的にライティングされているかのような錯覚を作り出します。
Shader "Custom/FakeLighting" { Properties { _Color ("Color", Color) = (1,1,1,1) _Brightness ("Brightness", Range(0,1)) = 0.4 _MainTex ("Albedo (RGB)", 2D) = "white" {} } SubShader { Tags { "RenderType" = "Opaque" } LOD 200 Pass { CGPROGRAM // Define name of vertex shader #pragma vertex vert // Define name of fragment shader #pragma fragment frag // Include some common helper functions, such as UnityObjectToClipPos #include "UnityCG.cginc" float4 _Color; float _Brightness; // Color Diffuse Map sampler2D _MainTex; // Tiling/Offset for _MainTex, used by TRANSFORM_TEX in vertex shader float4 _MainTex_ST; // This is the vertex shader input: position, UV0, UV1, normal struct appdata { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; float3 normal: NORMAL; }; // This is the data passed from the vertex to fragment shader struct v2f { float4 pos : SV_POSITION; float2 txuv : TEXCOORD0; float3 normalDir : TEXCOORD2; }; // This is the vertex shader v2f vert(appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.txuv = TRANSFORM_TEX(v.texcoord.xy,_MainTex); // Calculating normal so it can be used for fake lighting // in the fragment shader float4x4 modelMatrixInverse = unity_WorldToObject; o.normalDir = normalize(mul(float4(v.normal, 0.0), modelMatrixInverse).xyz); return o; } // This is the fragment shader half4 frag(v2f i) : COLOR { // Reading color from diffuse texture half4 col = tex2D(_MainTex, i.txuv.xy); // Using hard-coded light direction for fake lighting half3 th = normalize(half3(0.25, 1, -0.25)); // Using hard-coded light direction for fake specular // This matches the value inside the LightmappedPrefabWithSpec shader half3 sth = normalize(half3(0, 1, -0.25)); // Fake lighting float lightVal = max(0, dot (i.normalDir, th)); float lightScale = 0.75; lightVal = lightVal * lightScale; // Fake spec float spec = max(0, dot(i.normalDir, sth)); float specScale = 0.65; float specAtt = 0.65; spec = specScale * pow (spec, 40.0 * specAtt); // Add in a general brightness (similar to ambient/gamma) and then // calculate the final color of the pixel col.rgb = min(half4(1,1,1,1), col.rgb * _Brightness + col.rgb * lightVal * _Color + col.rgb * spec); return col; } ENDCG } } FallBack "Diffuse" }
また、オブジェクトには影を持たせないか、動くビットの一部としてフェイクシャドウを作成します。私たちのケースではサーフェスの大部分がフラットなので、これが大きな障害になることはありませんでした。

(動的オブジェクトを追加で使用している建物)
動くビットに影は付いていませんが、影が付いていないと知ったうえでよく見て、かろうじて気付く程度です。動的なプレハブのライティングも、完全にフェイクです。リアルタイムのライティングはまったく使われていません。
最初に私たちは負荷を軽くするための近道として、光源(太陽)の位置をフェイクライティングシェーダーにハードコーディングしました。これで、シェーダーがルックアップしてワールドから動的に入力する変数を 1 つ減らせます。
動的な値を使うより定数を使った方が、当然処理は早くなります。これにより基本的なライティング、メッシュの明るい面と暗い面が得られました。
12)スペキュラー/グロス
もう少し光沢を加えるため、私たちは動的オブジェクトと静的オブジェクトの両方のシェーダーにフェイクのスペキュラー/グロス計算を追加しました。スペキュラー反射は金属感を演出する効果があるだけでなく、表面のカーブを表現するのにも役立ちます。
スペキュラーハイライトは反射の一種なので、カメラと光源の相対的な角度を正しく計算する必要があります。カメラが移動したり回転したりすると、スペキュラーは変化します。計算を行うすべてのシェーダーで、シーン内のカメラの位置とすべての光源へのアクセスが必要になります。
ただし、私たちのゲームではスペキュラーのために必要な光源は 1 つだけ、太陽しかありません。ゲーム内では太陽は動かず、また、ディレクショナルライトとみなすことができます。使うライトを 1 つだけにして、ポジションと入射角が固定されているとシンプルに想定したことで、シェーダーを大幅に簡略化できました。
加えて『Galactic Colonies』のカメラが、多くの都市建築ゲームと同じく見下ろし視点でシーンを映すタイプのものであることも功を奏しました。カメラは若干傾けたりズームインやズームアウトしたりできますが、上方軸を中心に回転することはできません。
全体的に見れば、カメラは常に環境を上から見下ろしている状態です。スペキュラーの見た目を低負荷で偽装するため、私たちはカメラが完全に固定されていて、カメラとライトの角度が常に一定であるということにしました。
この方法で、改めて固定値をシェーダーにハードコーディングして、低負荷のスペキュラー/グロスエフェクトを実現できました。

(フェイクのスペキュラー/グロスエフェクト)
スペキュラーに固定角を使うのは、もちろん技術的には正しくありませんが、実際にはカメラの角度がほぼ固定の場合と見分けはつきません。プレイヤーにはあいからわずシーンは問題なく見えるわけで、リアルタイムライティングではそれだけが重要なのです。
リアルタイムビデオゲームでの環境のライティングは、物理的に正しくシミュレートされていることよりも、正しいように見えていることの方が常に重要なのです。
ほとんどすべてのメッシュで 1 つのマテリアルを(ライトマップと頂点に由来する多くのディテールと合わせて)共有していたので、私たちはシェーダーにスペキュラーの値をいつどこに、どのくらいの強度で適用すればよいのかを伝えるためにスペキュラーテクスチャマップを追加しました。テクスチャには 1 番目の UV チャンネルを使ってアクセスするので、座標セットを追加する必要はありません。また、それほどディテールもないので解像度を非常に低くでき、ほとんどスペースを使いません。
頂点数が少ない小さな動くビットの一部については、Unity の自動ダイナミックバッチング機能を利用して、レンダリングをさらにスピードアップできました。
13)ライトをベイクしたプレハブにライトをベイクしたプレハブを重ねる
ベイクした影は、どれも新しい問題を引き起こす可能性を秘めています。特に比較的モジュール化の度合いが高い建造物に使う場合です。ある時私たちは、プレイヤーが建築できる倉庫の前に、保管されている商品の種類を表示させようとしました。
これが問題を引き起こしました。ライトをベイクしたオブジェクトにライトをベイクしたオブジェクトを重ねたからです。いわば、ライトのベイクに例外が発生したのです!
別の負荷軽減テクニックを使ってこの問題に取り組むことにしました。
- 追加オブジェクトが追加される予定であったサーフェスはフラットにし、特定のグレーカラーを使用してベースの建物と調和するようにした
- そうしたことで、より小さなフラットサーフェス上のオブジェクトをベイクし、それをわずかなオフセットを持たせてその領域上に配置できるようにした
- ライト、ハイライト、色付きの光と陰影をすべてタイルにベイクした

(青く光り、影を作っているベイク済みオブジェクト)
14)負荷が軽く効率的なライトベイキング - 多少の制限は我慢できる方向け
この方法でプレハブを構築してベイキングしたことで、ドローコール数を驚くほど少ない水準に抑えつつ、数百もの建造物がある巨大なマップを実現できました。私たちのゲームでは、世界のほぼ全部がたった 1 つのマテリアルでレンダリングされ、ゲームの世界よりも UI の方がたくさんのドローコールを使うほどです。Unity がレンダリングしなければならないマテリアルが少なければ少ないほど、ゲームのパフォーマンスは向上します。
これにより、パーティクル、天候効果、その他の装飾的な要素を追加する余地が生まれます。
この方法なら、比較的古いデバイスを使っているプレイヤーでも、安定した 60fps を維持したまま数百もの建造物が立ち並ぶ巨大な都市を作ることができます。

(ライトをベイクしたプレハブを使ってプレイヤーが作ったコロニー)
15)ゲームが快適に動けば良しとする
賑やかな都市であふれる巨大な惑星をレンダリングするため、私たちはゲームに合わせたカスタマイズを行いました。自分たちのゲームで機能すれば良いという発想です。カメラアングル数を抑え、シーンの光源を 1 つにすることで、シェーダーの複雑さを大幅に軽減しました。また、これによりアングルが固定され、動的オブジェクトのライティングやスペキュラーの計算の観点から無駄を省くこともできました。動的オブジェクトの数を減らして、残りのオブジェウトは静的にバッチ処理しました。
それから、少ないポリゴン数とカラーのみでビジュアルを構築するスタイルにより、さまざまな作業が楽になったことも言うまでもありません。ほとんどすべてのオブジェクトでマテリアルを共有でき、スタティックバッチングもダイナミックバッチングも可能になるからです。
おそらくここで紹介した解決策を、皆さんのゲームにまるごと転用することはできないでしょう。それでいいのです。そのつもりで紹介したわけではないのですから。これらの解決策を、皆さんのゲームを最適化するベストな方法を探るためのたたき台にしてください。ゲームで切り詰められる要素や我慢できる要素を探し出して、それをゲームの高速化に役立てましょう。