optimize-scene について
optimize-scene は、Decentraland シーン開発者向けのパフォーマンス最適化スキルです。シーンの制限と、エンティティ、モデル、三角形、テクスチャの調整方法をまとめ、モバイル向け予算や性能データに基づく診断も扱います。
主な機能
- 01区画数に応じたシーン制限の確認
- 02モバイルの警告・上限予算の把握
- 03再利用や結合による反復モデルの最適化
- 04LOD と低ポリゴン素材の利用
- 05テクスチャと Blender モデル素材の最適化
活用シーン
- 01シーンがリソース制限に近いか確認
- 02モバイルクライアント向けに負荷を軽減
- 03繰り返し配置した小道具の描画負荷に対処
- 04テクスチャ圧縮とモデル形状の簡素化
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ decentraland/ sdk-skills --skill optimize-sceneインストール後、Claude Code で/optimize-sceneを入力して読み込まれたか確認します。
詳細
Decentraland シーンの最適化
シーンの上限(区画数ごと)
以下のすべての上限は、区画数 n に応じて変わります。三角形、エンティティ、物理ボディの上限は線形に増加します。マテリアルとテクスチャの上限は対数的に増加します。高さの上限は変わらず、すべてのシーンで一律 330 m です(下記参照)。
| リソース | 計算式 | 1 区画 | 2 区画 | 3 区画 | 4 区画 | 6 区画 | 9 区画 | 16 区画 | 20 区画 |
|---|---|---|---|---|---|---|---|---|---|
| 三角形 | n x 10,000 | 10,000 | 20,000 | 30,000 | 40,000 | 60,000 | 90,000 | 160,000 | 200,000 |
| エンティティ | n x 200 | 200 | 400 | 600 | 800 | 1,200 | 1,800 | 3,200 | 4,000 |
| 物理ボディ | n x 300 | 300 | 600 | 900 | 1,200 | 1,800 | 2,700 | 4,800 | 6,000 |
| マテリアル | log2(n+1) x 20 | 20 | 31 | 40 | 46 | 56 | 66 | 81 | 87 |
| テクスチャ | log2(n+1) x 10 | 10 | 15 | 20 | 23 | 28 | 33 | 40 | 43 |
高さの上限:区画数にかかわらず、すべてのシーンで 330 m です。 ワールド座標がこの高さを超えるエンティティは非表示になります。⚠️ 約 200 m を超える場所では、マルチプレイヤー同期に問題が発生する可能性があります。ゲームプレイに関係するコンテンツやネットワーク経由のコンテンツは、この高さより下に配置してください。
マテリアルの行は注意して読んでください。 クライアントは描画されるオブジェクトごとにマテリアルをインスタンス化するため、この数値はシーンが定義する異なるマテリアルの数ではなく、描画するオブジェクトの数を表します。正しく同じモデルを何度も再利用するシーンであれば、この上限を超えず、適切に動作します。この数値はメモリ使用量の目安として扱い、フレーム時間への影響は代わりに shaderVariants で判断してください(下記の モデルの繰り返し使用 を参照)。
ファイルの上限: 区画ごとに 15 MB、合計最大 300 MB、区画ごとに 200 ファイル、ファイルごとに最大 50 MB です。
ファイルの上限で数えられるのは、デプロイ時に実際にアップロードされるものだけです。.dclignore(プロジェクトルートにあります)で、作業用ファイル(Blender/FBX のソース、ドラフトモデル、コンセプトアート、スプレッドシート、Markdown ドキュメント)をすべて除外してください。これらはプロジェクトのサイズの大部分を占めることが多く、実行時には必要ありません。deploy-scene スキルの .dclignore セクションを参照してください。
重要:MB 単位のサイズ上限を除き、その他の上限はすべて超過できます。パフォーマンスへの影響があるため、一般には上限を超えないことをおすすめしますが、ユーザーがシーンをテストして十分に良好だと判断した場合は、公開しても問題ありません。
モバイルシーンの上限
シーンをモバイルクライアントで実行する場合は、個別の固定上限が適用されます。区画数に応じて上限が増えることはありません。ソフト上限を超えるとモバイルのパフォーマンスパネルに警告が表示され、ハード上限を超えるとシーンを読み込めなくなります。ドキュメントのコミット 8139a7f(2026年10月)で確認済みです。
| 指標 | ソフト上限 | ハード上限 |
|---|---|---|
| 三角形 | 1,000,000 | 1,200,000 |
| エンティティ | 4,800 | 6,000 |
| メッシュ(ボディ) | 2,400 | 3,000 |
| ジオメトリ | 1,000 | 2,000 |
| マテリアル | 400 | 500 |
| テクスチャ | 400 | 500 |
| コライダー | 1,200 | 1,500 |
| VFX エミッター | 8 | 10 |
| 稼働中のパーティクル | 25,000 | 50,000 |
| 光源 | 8 | 16 |
| シャドウライト | 2 | 4 |
| コンテンツサイズ | 120 MB | 150 MB |
| 外部コンテンツ | 40 MB | 50 MB |
| メモリ(プロセス RSS) | 1,638 MB | 2,048 MB |
| ドローコール | 1,000 | 2,000 |
| パフォーマンス(高いほど良好) | 90% | 85% |
ライトとパーティクルの数には、エンジンが実行するものではなく、作成されたものがカウントされます。
- 各モバイルのグラフィックスプロファイルで有効になるのは、最も近い光源のうち予算内のものだけです。数は**「非常に低い」では 0 個、「高い」では最大 8 個**で、シャドウが完全に無効になる場合もあります。ライトを 20 個作成したシーンは、最大 8 個しか発光しない場合でも、シーン全体のコストがそのままかかります。8/16 個の上限内で作成してください。実行時の予算によって、ライトが多すぎるシーンの負荷が軽くなるとは考えないでください。lighting-environmentを参照してください。
- パーティクルにも、実行時に上限が適用されます。エミッターごとに5,000 個、シーン全体で50,000 個です。これらの上限を超えて作成されたパーティクルは通知なく削減されるため、上限超過分は視覚的な効果を生まないままシーンの予算を消費します。particle-systemを参照してください。
デバイスを使わずに測定するには、Creator Hub > Preview > Show QR Code for Mobileを選択し、スマートフォンでScene Limits Preview(右上にある赤い統計情報付きモニターのアイコン)を開きます。ミドルスペックのデバイスで、「高い」プロファイルのパフォーマンスを 90% 超にすることを目標にしてください。
エンティティ数の最適化
エンティティを再利用する
このパターンは、シーンでインスタンスを動的に生成および削除する場合にのみ使用してください。
// BAD: Creating new entity each time
function spawnBullet() {
const bullet = engine.addEntity() // Creates entity every call
// ...
}
// GOOD: Object pooling
const bulletPool: Entity[] = []
function getBullet(): Entity {
const existing = bulletPool.find((e) => !ActiveBullet.has(e))
if (existing) return existing
const newBullet = engine.addEntity()
bulletPool.push(newBullet)
return newBullet
}
未使用のエンティティを削除する
const removed = engine.removeEntity(entity) // returns boolean
// true: components purged, id released for reuse
// false: entity is renderer-reserved (avatar range) — components untouched
親子関係を使用する
各子エンティティに個別のトランスフォーム値を設定する代わりに、エンティティの階層構造を使用します。
const parent = engine.addEntity()
Transform.create(parent, { position: Vector3.create(8, 0, 8) })
// Children inherit parent transform
const child1 = engine.addEntity()
Transform.create(child1, { position: Vector3.create(0, 1, 0), parent })
const child2 = engine.addEntity()
Transform.create(child2, { position: Vector3.create(1, 1, 0), parent })
繰り返し使うモデル(再利用と結合)
街灯、椅子、木、フェンスなどの繰り返しコンテンツでは、デフォルトではコピーごとにエンティティを1つ作成し、すべて同じ .glb を参照するようにします。 エンジンはソース単位で重複を排除するため、エンティティの数にかかわらず、ダウンロードは1回、アセットバンドルへの変換は1回、メッシュとテクスチャのメモリ上のコピーは1つだけです。この重複排除はセッション全体に適用されるため、隣接するシーンで同じファイルを使う場合も、追加コストなしで利用できます。
// GOOD: one file, many entities — engine shares the loaded asset
for (const position of lampPostPositions) {
const lampPost = engine.addEntity()
Transform.create(lampPost, { position })
GltfContainer.create(lampPost, { src: 'assets/scene/lampPost.glb' })
}
// BAD: near-identical files — 20 downloads, 20 copies in memory
// lampPost_01.glb, lampPost_02.glb, ... lampPost_20.glb
再利用しても削減されないのは、ドローコールです。 街灯が20本あれば、20個のエンティティから構成されていても、20本の街灯をモデル化した1つの .glb から構成されていても、レンダラーは20個です。シーンコンテンツにはランタイムのバッチ処理も GPU インスタンシングもありません。クライアントは実行時にシーンをストリーミングして構築するため、エンジンが事前ベイク済みコンテンツに対して行うように、繰り返し使うオブジェクトをまとめることはできません。ドローコール数は作成時に決まります。
| 方法 | ダウンロード数 | メッシュとテクスチャ | レンダラー | マテリアル | カリング | 移動/クリック |
|---|---|---|---|---|---|---|
N 個のエンティティ、共有する .glb は1つ | 1 | コピー1つ | N | N × スロット数 | オブジェクトごと | はい |
1つの .glb 内に個別のオブジェクトを N 個配置 | 1 | コピー1つ | N | N × スロット数 | オブジェクトごと | いいえ |
Blender でメッシュを結合した .glb を1つ作成 | 1 | ジオメトリを複製 | 1 | 1 × スロット数 | 一括 | いいえ |
ドローコールを減らせるのは3行目だけですが、ファイルサイズとメモリが増加し、オブジェクトごとのカリングもできなくなります。この方法を推奨するのは、測定の結果、レンダラー数がボトルネックだと判明しており、プロップが小さく、同じ場所で常に一緒に表示され、操作の対象にならない場合に限ります。 それ以外の場合は、再利用を推奨してください。
プロップがあちこちに散らばっている場合の中間案は、プロップごと、あるいはシーン全体で1つにするのではなく、クラスターごとに .glb を1つ用意する方法です(街区や部屋の家具など)。クラスターをカリングの効果が得られる大きさに保ちながら、レンダラー数を減らせます。
知っておくべき、ほかの影響が3つあります。
- 再利用してもマテリアル数は減りません。 クライアントはシーンの境界クリッピングを書き込むため、レンダラーごとにマテリアルをインスタンス化します。そのため、統計の
materialsは個別のモデル数ではなく、レンダリングされるオブジェクト数を示します。実測では、同じ.glbを共有する14個のエンティティはレンダラー28個、マテリアル28個と表示されますが、geometriesとtexturesはコピー1つのままです。これらのインスタンスは同じテクスチャとシェーダーバリアントを共有するため、メモリコストは小さく、フレーム時間には影響しません。shaderVariantsを最初に確認せず、フレーム時間の改善策としてマテリアルの重複排除を推奨してはいけません。 - コピーを一度に多数生成しますか? まず
AssetLoadでプリロードしてください(下記参照)。同じダウンロードをそれぞれ待たせるのではなく、常駐アセットからコピーを作成できます。ただし、コピーのコストがなくなるわけではありません。アセットがすでに常駐していても、生成処理は一斉生成のコストの約90%を占めるため、大量に生成すると1フレーム分の負荷になることがあります。 - 大きなモデル1つは、読み込みの滑らかさの点で小さなモデルを多数使うより不利です。 アセットの作成処理はフレーム時間の予算に合わせて複数のフレームに分散されますが、巨大なモデル1つは分割できません。1フレームで処理されるため、目に見えるカクつきが発生しやすくなります。
具体的な測定については、gltf-reuse-vs-merge ベンチマークシーンを参照してください。同じ街灯モデルを、再利用(.glb 1つと多数のエンティティ)、ファイルの複製、結合した .glb の各方法で生成し、レンダラー数/マテリアル数、メモリ、読み込み動作を比較します。また、一斉生成に備えた AssetLoad によるプリロードと、コリジョンマスクの調整も実演しています。
三角形数の最適化
ポリゴン数の少ないモデルを使う
- 小さなプロップ:100~500 三角形
- 中サイズのオブジェクト:500~1,500 三角形
- 大きな建物:1,500~5,000 三角形
- 主役となるオブジェクト:最大10,000 三角形
LOD(詳細度)を使う
距離に応じて、よりシンプルなモデルを表示します。
engine.addSystem(() => {
// Check distance to player and swap models
const playerPos = Transform.get(engine.PlayerEntity).position
const objPos = Transform.get(myEntity).position
const distance = Vector3.distance(playerPos, objPos)
const gltf = GltfContainer.getMutable(myEntity)
if (distance > 30) {
gltf.src = 'models/building_lod2.glb' // Low poly
} else if (distance > 15) {
gltf.src = 'models/building_lod1.glb' // Medium poly
} else {
gltf.src = 'models/building_lod0.glb' // High poly
}
})
モデルの代わりにプリミティブを使う
単純な形状の場合、MeshRenderer は .glb を読み込むより軽量です。
MeshRenderer.setBox(entity) // Very cheap
MeshRenderer.setSphere(entity) // Cheap
MeshRenderer.setPlane(entity) // Very cheap
Blender で重いモデルを修正する
統計情報から特定のモデル(数万個の三角形を持つ小道具、2048 以上のテクスチャを含む GLB、プレイヤーから見えない面が大量にあるメッシュなど)が原因だと分かった場合は、SDK コードで補うのではなく、Blender を操作してアセット自体を修正します。ヘッドレス CLI と Blender MCP の比較、セットアップ、エクスポート規則を含む完全なガイドは {baseDir}/../add-3d-models/references/blender-authoring.md にあります。すぐに使える bpy スクリプトは {baseDir}/../add-3d-models/references/blender-patterns.md にあります。一般的な修正例:
- デシメートモディファイアーを使って、インポート済みモデルや詳細すぎるモデルの三角形数を減らします(既存アセットの修正に使います。新しいモデルは最初からローポリで作成してください)。
- テクスチャのサイズを変更して、2 のべき乗かつ 1024×1024 以下にし、GLB に再パックします。
- 表示されない面を削除し(下面、隠れている背面など)、ジオメトリを二重にする代わりにバックフェースカリングを有効にします。
- 色ごとのマテリアルを統合して 1 つのパレットテクスチャマテリアルにし、パーツごとのテクスチャを 1 つのアトラスにまとめます。
_colliderメッシュ上のライト、カメラ、マテリアルを削除します。これらはエンジンに無視されるか、二重にコストがかかる無駄な要素です。
patterns ファイルにある三角形数とマテリアル監査のスクリプトを使って修正前後を監査し、同じパスにエクスポートします(プレビューではファイルがホットリロードされます)。その後、統計パネルを再確認してください。Blender を使わずに手早く処理する場合は、gltf-transform(以下を参照)を使うと、ジオメトリの選択を変えずに GLB を圧縮して不要な要素を削除できます。
テクスチャの最適化
- 寸法は 2 のべき乗にする必要があります:256、512、1024
- 最大サイズは 1024x1024 です。 asset-bundle-converter は
DESKTOP_MAX_TEXTURE_SIZE = 1024(AssetBundleConverter.cs→ReduceTextureSizeIfNeeded)を適用します。これより大きいサイズ(例:2048)は変換時に 1024 へ縮小されるため、1024 を超えるサイズで作成しても、ソースサイズが増えるだけで視覚的なメリットはありません。 - 推奨サイズ:ほとんどのオブジェクトは 512x512、主要なパーツは 1024x1024
- 透明度のある UI/スプライトには
.pngを使用します - 写真や透明度のないテクスチャには
.jpgを使用します - 可能であれば、非圧縮の PNG より圧縮形式(WebP)を優先します
- テクスチャアトラス(複数のテクスチャを 1 枚の画像にまとめたもの)を使用して、ドローコール数とマテリアル数を減らします
- マテリアル間でテクスチャ参照を共有し、テクスチャファイルを重複させないでください
- エンティティ間でマテリアルを再利用します:
// GOOD: Define material once, apply to many
Material.setPbrMaterial(entity1, {
texture: Material.Texture.Common({ src: 'images/wall.jpg' }),
})
Material.setPbrMaterial(entity2, {
texture: Material.Texture.Common({ src: 'images/wall.jpg' }),
})
// Same texture URL = shared in memory
用途別テクスチャサイズガイド
| 用途 | 推奨サイズ | 最大サイズ |
|---|---|---|
| シーンオブジェクト(壁、床) | 1024x1024 | 1024x1024 |
| 小道具、家具 | 512x512 | 1024x1024 |
| UI 要素/アイコン | 256x256 | 512x512 |
| スカイボックス/環境マップ | 1024x1024 | 1024x1024 |
テクスチャは正方形である必要はありません。両方の寸法が 2 のべき乗であれば、512x1024 も有効です。
バックフェースカリング
バックフェースカリングを有効にすると、プレイヤーが裏側から見ることのないポリゴンの内側の面は描画されません。設定するのは 3D モデリングツール(Blender、Maya など)であり、SDK コードではありません。
目安: デフォルトではすべてのマテリアルでバックフェースカリングを有効にします。表面を両面から見せる必要がある場合(例:木の葉の平面、薄い壁)に限り、無効にしてください。
システムの最適化
フレームごとのメモリ割り当てを避ける
// BAD: Creates new Vector3 every frame
engine.addSystem(() => {
const target = Vector3.create(8, 1, 8) // Allocation!
})
// GOOD: Reuse constants
const TARGET = Vector3.create(8, 1, 8)
engine.addSystem(() => {
// Use TARGET
})
負荷の高い処理を間引く
let lastCheck = 0
engine.addSystem((dt) => {
lastCheck += dt
if (lastCheck < 0.5) return // Only run every 0.5 seconds
lastCheck = 0
// Expensive operation here
})
不要になったシステムを削除する
const systemFn = (dt: number) => {
/* ... */
}
engine.addSystem(systemFn)
// When no longer needed:
engine.removeSystem(systemFn)
アセットの事前読み込み(AssetLoad コンポーネント)
AssetLoad を使うと、アセットをあらかじめメモリに読み込んでおき、必要になったときにすぐ表示できます。PBAssetLoad のフィールドは assets: string[] の 1 つだけで、読み込むアセットパスのリストです。通常は engine.RootEntity に作成しますが、どのエンティティでも使用できます。
import {
engine,
AssetLoad,
assetLoadLoadingStateSystem,
LoadingState,
} from '@dcl/sdk/ecs'
// Queue assets to pre-load (any entity works — commonly a dedicated cube/root).
// AssetLoad.getOrCreateMutable lets you also PUSH more paths later:
// AssetLoad.getOrCreateMutable(entity, { assets: [...] }).assets.push(morePath)
AssetLoad.create(entity, { assets: ['models/big.glb', 'sounds/win.mp3'] })
// React to loading state. The callback fires PER ASSET (once per path in the
// list), receiving { asset, currentState } — NOT one batch-level event.
assetLoadLoadingStateSystem.registerAssetLoadLoadingStateEntity(
entity,
(state: { asset: string; currentState: LoadingState }) => {
if (state.currentState === LoadingState.FINISHED) {
// state.asset finished loading and is now cached
}
},
)
// Stop listening: assetLoadLoadingStateSystem.removeAssetLoadLoadingStateEntity(entity)
LoadingState の列挙値は、LOADING、FINISHED、FINISHED_WITH_ERROR(アセットは見つかったものの、読み込みに失敗)、NOT_FOUND(パスが存在しない)、UNKNOWN(初期値/デフォルト状態)です。src が存在しない場合やタイプミスの場合、例外はスローされず、NOT_FOUND になります。
注意点:
- 状態コールバックはアセットごとに呼び出され、
assetパス文字列をキーとします。state.assetに基づいて処理を分岐し、適切なエンティティを更新してください。すべての読み込み完了を示す単一のイベントはありません。アセットごとのFINISHED/エラー状態を数えて、完了を追跡してください。 AssetLoadはアセットをメモリに追加するだけです。assetsリストからパスを削除してもメモリは解放されません。AssetLoadでアンロードすることはできません。- パスを事前読み込みしても、何かが作成・描画されるわけではありません。アセットを使うには、エンティティ上で実際のコンポーネント(
GltfContainer、AudioSource、VideoPlayer、Materialのテクスチャ)をgetOrCreateMutableで取得する必要があります。事前読み込みによって、その後の使用がすぐに行えるようになります。 - シーンの起動直後にアセットを使う場合、
AssetLoadは不要です。事前読み込みするのは、起動時には必要なく、後から表示されるものやプレイヤーの操作に応じて表示されるものだけにしてください。
ローカルのアセットバンドルプレビュー
公開前に、サーバー側のアセットバンドル変換をローカルで再現できます。これにより、変換の問題(テクスチャの欠落、圧縮後のモデルの破損)を検出でき、プレビューでは本番品質に最適化されたモデルを表示できます。
- Creator Hub: 再生オプション(再生ボタンのドロップダウン矢印)> デスクトップクライアントにカーソルを合わせる > アセットを圧縮にチェックを入れます。この切り替え項目は、Linux の場合や、フラグに対応していない古い
@dcl/sdk-commandsを使用するシーンでは表示されません。 - CLI:
npm run start -- --asset-bundles(Explorer のディープリンクにlocal-ab=trueとして渡されます。--local-abはディープリンクのパラメーターであり、CLI フラグではありません)
Desktop Explorer は、すべての .gltf/.glb モデルをお使いのマシン上でアセットバンドルに変換します。大規模なシーンでは、初回の実行に数分かかることがあります。変換済みのモデルはキャッシュされるため、2 回目以降のプレビューでは、新しいアセットまたは変更されたアセットのみが再変換されます。アセットの変換に失敗した場合、プレビューでは元のモデルが使用されます。Desktop Client でのみ利用でき、Bevy Web では利用できません。
これは、公開後に毎回実行されるサーバー側の変換と同等の処理です(deploy-scene スキルを参照)。特にライブイベントの前には、公開前に定期的に使用してください。
読み込み時間の最適化
- 可能であれば、大きな共有アセットには CDN URL を使用してください。
大規模なシーンでの読み込みエリア
3D モデルが多数あるシーン(家具付きの複数部屋の建物など)では、すべてを一度に描画しないようにしてください。トリガーエリアを使って、プレイヤーがシーン内を移動するのに合わせてコンテンツを読み込み、アンロードします。
import { engine, Transform, GltfContainer, TriggerArea, triggerAreaEventsSystem, ColliderLayer } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'
// Keep furniture hidden initially
let furnitureLoaded = false
// When player enters the building, spawn interior furniture
const trigger = engine.addEntity()
Transform.create(trigger, {
position: Vector3.create(8, 1, 8),
scale: Vector3.create(3, 3, 3)
})
TriggerArea.setBox(trigger, ColliderLayer.CL_PLAYER)
triggerAreaEventsSystem.onTriggerEnter(trigger, () => {
if (!furnitureLoaded) loadInterior()
furnitureLoaded = true
})
triggerAreaEventsSystem.onTriggerExit(trigger, () => {
if (furnitureLoaded) unloadInterior()
furnitureLoaded = false
})
このパターンにより、初期の三角形数とエンティティ数を抑え、必要になったときだけ詳細を読み込めます。
よくあるパフォーマンスの落とし穴
| 落とし穴 | 症状 | 対策 |
|---|---|---|
| 固有のマテリアルが多すぎる | メモリとテクスチャの予算を圧迫し、バッチ処理が失われる | テクスチャアトラスを作成し、モデルを再利用します。フレーム時間を疑う前に shaderVariants を確認します |
| 2 の累乗でないテクスチャ | メモリの肥大化、表示上の問題 | すべてのテクスチャを 256/512/1024 にリサイズします(最大 1024) |
| エンティティを頻繁に作成・破棄する | フレームのカクつき | エンティティプーリングを使用します |
| 毎フレーム、負荷の高い計算を行う | FPS が常に低い | タイマーによるガードを追加し、実行頻度を下げます |
| 装飾に未使用のコライダーがある | 物理ボディの上限を超える | インタラクションのないオブジェクトから MeshCollider を削除します |
| 圧縮されていない大きなテクスチャ | 読み込みが遅い、ファイルサイズの上限を超える | WebP を使用し、解像度を下げ、アトラスを使用します |
| デプロイ時に作業ファイルがアップロードされる | 「Scene too large」というデプロイエラー | Blender/FBX のソース、コンセプトアート、ドキュメントを .dclignore に追加します(deploy-scene を参照) |
| 透明マテリアルが多すぎる | ドローコールが増え、ソートで問題が発生する | 透明度を最小限に抑え、ブレンドの代わりにアルファカットオフを使用します |
| ガードなしでシステム内にエンティティ/コンポーネントを追加する | エンティティ数が急増する | システムは毎フレーム実行されます。作成前に必ずチェックします |
| 範囲を限定しないエンティティクエリ | CPU 使用率が急上昇 | 特定のコンポーネントでフィルタリングし、結果をキャッシュします |
| すべての距離でディテールをすべて読み込む | 三角形の予算を超える | LOD システムを実装します |
コピーごとにほぼ同一の .glb を用意する(chair_01.glb、chair_02.glb など) | 読み込みが遅く、メモリが肥大化し、テクスチャの上限を超える | 多数のエンティティから参照する共有 .glb を 1 つ用意します。エンジンはソースに基づいて重複を排除します |
シーン全体を 1 つの巨大な .glb にまとめる | 表示時に読み込みが滞り、カリングが一切行われない | クラスター単位のモデル(街区や部屋など)に分割します |
| アセットを事前に読み込まない | ゲームプレイ中に突然表示される | 後で必要になるアセット(起動時のアセットではありません)を AssetLoad で事前に読み込みます |
シーン統計のモニタリング
プレビューモード
npm run start でシーンをローカル実行する場合:
- P を押すとパフォーマンスパネルを切り替えられます。
- FPS、ドローコール、三角形、エンティティ、マテリアル、テクスチャ、メモリを監視します。
- シーンの上限と現在の使用量が、緑/黄/赤のインジケーターとともに表示されます。
確認する項目
- FPS が 30 未満:何かの負荷が高すぎます。ドローコールとシステムの実行時間を確認します。
- 三角形の数が上限に近づいている:LOD を有効にし、モデルのディテールを減らし、隠れている面を削除します。
- エンティティ数が増え続けている:リークの可能性があります。エンティティが作成されても破棄されていません。プーリングを実装します。
- ドローコールが 300 を超えている(1 パーセル):描画されるマテリアルスロットが多すぎます。オブジェクトごとのスロット数を減らすには、テクスチャアトラスを使用して透明度を下げ、描画するオブジェクトの数も減らします。ドローコール数は 描画オブジェクト数 × それらのマテリアルスロット数 で決まる点に注意してください。多数のエンティティで 1 つのモデルを再利用しても減少しません(上記の Repeated Models を参照)。多数の独立したオブジェクトを 1 つの
.glbにまとめても減少しません。
推奨される最適化ツール
| ツール | 用途 |
|---|---|
| Blender Decimate modifier | インポートしたモデルの三角形数を減らします(ヘッドレスまたは MCP 経由で実行します — Blender で重いモデルを修正するを参照) |
| Blender Limited Dissolve | 平面から不要な頂点を削除します |
| Squoosh (squoosh.app) | 画像を WebP に変換し、サイズを 2 の累乗に変更します |
| TexturePacker | 複数の画像からテクスチャアトラスを作成します |
| gltf-transform CLI | Draco で GLB ファイルを圧縮し、未使用データを削除します |
| glTF Validator | DCL にインポートする前にエクスポートエラーを確認します |
| Creator Hub Scene Inspector | エンティティ数、三角形数、配置を確認するビジュアルツール |
| Preview Debug Panel (P key) | npm run start の実行中にパフォーマンス指標をリアルタイムで表示します |
# Optimize a GLB with Draco compression
npx @gltf-transform/cli optimize input.glb output.glb --compress draco
⚠️ Draco はモバイルではデコードされず、失敗は通知されません。プレビューでのみ確認できます。 モバイルアプリには実行時の Draco デコーダーが含まれていません。Draco 圧縮された .gltf/.glb がスマートフォンで読み込まれるのは、シーンの最適化済みアセットから読み込む場合に限られます。この場合、アセットサーバーがすでに展開しています。シーンに最適化済みアセットがない場合 — ローカルプレビュー、Creator Hub のモバイルプレビュー、QR プレビュー — モデルはエラーが一切表示されないまま読み込みに失敗します。コンソールメッセージもプレースホルダーもなく、メッシュが表示されないだけです(docs commit 5f212e3、2026 年 10 月。エラーダイアログは godot-explorer #2432 で追跡されています)。
問題は非対称性にあります。公開済みシーンはスマートフォンで正常に表示される一方、同じモデルがあらゆるモバイルプレビューで見えなくなることがあります。そのプレビューをデバッグするクリエイターには、手掛かりが何もありません。モバイルを対象とするシーンでは、Draco を使わずにエクスポートしてください。クロスプラットフォームビルドでは --compress false を指定して optimize を実行し、代わりに三角形数やテクスチャを削減してください。[未検証:他の gltf-transform 圧縮モード(meshopt、quantize)がモバイルで読み込めるかどうか — ドキュメントで言及されているのは Draco のみです。]
シーンの例
エンジンチームのストレステスト用シーンです(API の形状を示す信頼できる情報源として扱ってください)。
- https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/0,2-cube-wave-32x32 — 1 つの区画に約 961 個のプリミティブキューブを配置しています(200 エンティティというソフトリミットを大幅に超過)。単一の
engine.getEntitiesWith(MeshRenderer)クエリを使い、フレームごとにすべてのTransform.position.yを変更します。ソフトリミットを超過できることと、数百のエンティティに対するフレームごとのクエリが想定されたパターンであることを示しています(固定された静的セットにはプーリングは不要です。キューブはmain()で一度だけ作成され、フレームごとには作成されません)。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/73,-2-dbmonster — UI のストレステストです。
Math.random()の値を使って、ReactEcs ツリー内のネストされた<Label>を数十個、フレームごとに再構築します。UI のレンダー関数がフレームごとに再実行されるため、.tsxでのフレームごとの大量のメモリ割り当てが実際のコストになることを示しています。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/88,-12-asset-load — アセットごとの状態コールバック
assetLoadLoadingStateSystemを使ったAssetLoadのプリロード(mp3/テクスチャ/動画/glb に加え、NOT_FOUNDに解決される、意図的に存在しないパス)。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/94,-10-gltf-reuse-vs-merge — ベンチマークです。GltfContainer の再利用(1 つの
.glb、N 個のエンティティ)と、ファイルの複製、メッシュの結合を比較し、レンダラー数、マテリアル数、メモリ、読み込みの滑らかさを測定します。AssetLoadによるプリロードと、バースト生成に向けた衝突マスクの調整を使用しています。
相互参照
- deploy-scene — 公開後のアセットバンドル変換のタイミング、トラブルシューティング、
/detectabsコマンド - add-3d-models — モデルの読み込み、コライダー、ファイルの整理、モデルの修正や作成のために Blender を操作する方法(
references/blender-authoring.md) - game-design — パフォーマンス予算、デザインパターン、MVP の計画
- advanced-rendering — テクスチャモード、マテリアルの再利用、VisibilityComponent を使った LOD
- scene-runtime — 全画面の Explorer UI の背後にシーンが隠れているときに高負荷なシステムを一時停止するための
EngineInfo.sceneHidden