mcpField
O
optimize-scene

optimize-scene

github.com/decentraland开发4291 次安装今天更新
代码仓库

关于 optimize-scene

optimize-scene 是面向 Decentraland 场景开发者的性能优化技能,汇总场景限制并提供实体、模型、三角形和纹理优化方法。它也介绍移动端预算及如何依据性能数据选择优化方向。

主要功能

  1. 01查看按地块数量变化的场景限制
  2. 02说明移动端软性与硬性预算
  3. 03通过复用和合并优化重复模型
  4. 04使用 LOD 与低面数资源
  5. 05优化纹理和 Blender 模型资源

适用场景

  1. 01检查场景是否接近资源限制
  2. 02为移动客户端减少场景负载
  3. 03处理重复道具造成的渲染开销
  4. 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,00010,00020,00030,00040,00060,00090,000160,000200,000
实体n x 2002004006008001,2001,8003,2004,000
物理体n x 3003006009001,2001,8002,7004,8006,000
材质log2(n+1) x 202031404656668187
纹理log2(n+1) x 101015202328334043

所有场景的高度限制均为 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,0001,200,000
实体4,8006,000
网格(实体)2,4003,000
几何体1,0002,000
材质400500
纹理400500
碰撞体1,2001,500
VFX 发射器810
活动粒子25,00050,000
光源816
投射阴影的灯光24
内容大小120 MB150 MB
外部内容40 MB50 MB
内存(进程 RSS)1,638 MB2,048 MB
绘制调用1,0002,000
性能(越高越好)90%85%

灯光和粒子按创作的数量计数,而非引擎实际运行的数量。

  • 每种移动端图形配置只会启用一定预算内的最近光源——从“非常低”配置下的 0 个到“高”配置下的 8 个——并且可能完全禁用阴影。即使最多只有 8 盏灯实际发光,创作 20 盏灯仍会承担完整的场景开销。请将创作数量控制在 8/16 的限制内;不要依赖运行时预算来降低过度照明场景的成本。参见 lighting-environment。
  • 粒子还会在运行时受到限制:每个发射器最多 5,000 个,每个场景最多 50,000 个。超过这些上限的创作数量会被静默削减,因此超出上限只会浪费场景预算,不会带来视觉收益。参见 particle-system。

无需设备即可测量:Creator Hub > Preview > Show QR Code for Mobile,然后在手机上打开 Scene Limits Preview(右上角带红色统计信息的显示器图标)。在中端设备的 High 配置下,将**性能 > 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 })

重复模型(复用与合并)

对于重复内容——路灯、椅子、树木、栅栏——**默认每个副本使用一个实体,并全部指向同一个 .glb。**引擎会按来源去重:无论有多少实体使用它,都只需下载一次、进行一次资源包转换,并在内存中保留一份网格和纹理。去重在整个会话中全局生效,因此相邻场景使用同一文件时也无需额外开销。

// 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 个路灯的 .glb,20 个路灯都对应 20 个渲染器。场景内容不支持运行时批处理或 GPU 实例化:客户端会在运行时流式加载并构建场景,因此无法像处理预烘焙内容的引擎那样将重复对象分组。绘制数量在创作时就已固定。

方案下载次数网格与纹理渲染器材质剔除可移动/可点击
N 个实体,共用 1 个 .glb11 份NN × 插槽按对象是
1 个 .glb,内部有 N 个独立对象11 份NN × 插槽按对象否
1 个 .glb,在 Blender 中合并网格1几何体重复11 × 插槽全部或无否

只有第三种方案能减少绘制调用,但代价是文件体积和内存占用增加,并且无法按对象进行剔除。**仅在以下情况推荐这种方案:有许多小型道具、它们始终在同一位置同时可见、从不需要交互,且测量结果表明渲染器数量确实是瓶颈。**其他情况下,建议复用。

对于分散的道具,可以采用折中方案:每个道具簇使用一个 .glb(例如一个街区或一个房间的家具),而不是每个道具一个文件,也不是整个场景只用一个文件。这样既能减少渲染器数量,又能让道具簇保持足够小,使剔除机制发挥作用。

还有三个值得了解的影响:

  • **复用也不会减少材质数量。**客户端会为每个渲染器实例化一个材质,用于写入场景的边界裁剪,因此统计数据中的 materials 反映的是已渲染对象,而非不同模型的数量。实测:14 个实体共用一个 .glb,会显示 28 个渲染器和 28 个材质,而 geometries 和 textures 仍各为一份。这些实例共用相同的纹理和着色器变体,因此只会少量增加内存开销,不会影响帧时间。在检查 shaderVariants 之前,切勿将材质去重作为解决帧时间问题的建议。
  • **要一次生成许多副本?**先用 AssetLoad 预加载(见下文),这样副本就能从已驻留的资源创建,而不必各自等待同一次下载。预加载并不会让创建副本没有开销:即使资源已驻留,构建副本仍占突发开销的约 90%,因此大量集中生成仍可能占用一帧。
  • **一个超大模型对加载流畅度的影响比许多小模型更大。**资源创建会在帧时间预算内分散到多帧完成,但单个超大模型无法拆分——它会集中在一帧内加载,因此更容易造成明显卡顿。

请参阅 gltf-reuse-vs-merge 基准测试场景,了解具体测量结果:该场景通过复用(一个 .glb,多个实体)、使用重复文件以及使用合并后的 .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,或布满玩家永远看不到的面的网格——应通过驱动 Blender 来直接修复资源,而不是在 SDK 代码中补救。完整指南(无头 CLI 与 Blender MCP、设置、导出规则)位于 {baseDir}/../add-3d-models/references/blender-authoring.md,其中 {baseDir}/../add-3d-models/references/blender-patterns.md 提供了可直接使用的 bpy 脚本。常见的补救编辑:

  • 使用 Decimate 修改器降低导入模型或细节过多模型的三角形数量(这是修复现有资源的工具——新模型从一开始就应采用低多边形建模)。
  • 调整纹理尺寸,使其符合二次幂,尺寸为 1024×1024 或更小,并将纹理重新打包到 GLB 中。
  • 删除永远不可见的面(底面、被遮挡的背面),并启用背面剔除,而不是重复几何体。
  • 将每种颜色的材质合并为单个调色板纹理材质,并将每个部件的纹理合并到一张图集中。
  • 移除 _collider 网格上的灯光、摄像机和材质——这些都是引擎会忽略或重复处理的冗余内容。

使用 patterns 文件中的三角形计数和材质审计脚本,在编辑前后进行检查;导出到同一路径(预览会热重载该文件),然后再次检查统计面板。若想快速进行非 Blender 处理,可使用 gltf-transform(见下文),压缩并精简 GLB,而不更改其几何体设置。

纹理优化

  • 尺寸必须是二次幂:256、512、1024
  • 最大尺寸为 1024x1024。 asset-bundle-converter 会强制执行 DESKTOP_MAX_TEXTURE_SIZE = 1024(AssetBundleConverter.cs → ReduceTextureSizeIfNeeded):任何更大的纹理(例如 2048)都会在转换时缩小到 1024,因此制作超过 1024 的纹理只会增加源文件大小,不会带来视觉收益。
  • 建议尺寸:大多数物体使用 512x512,主体部件使用 1024x1024
  • 透明 UI/精灵图使用 .png
  • 不透明照片和纹理使用 .jpg
  • 尽可能优先使用 WebP 等压缩格式,而非原始 PNG
  • 使用纹理图集(将多个纹理合并到一张图片中)以减少绘制调用和材质数量
  • 在材质间共享纹理引用——不要重复纹理文件
  • 在实体间复用材质:
// 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
按使用场景划分的纹理尺寸指南
使用场景建议尺寸最大尺寸
场景物体(墙壁、地板)1024x10241024x1024
道具和家具512x5121024x1024
UI 元素 / 图标256x256512x512
天空盒 / 环境贴图1024x10241024x1024

纹理不必是正方形——只要两个维度都是二次幂,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[],即要加载的资源路径列表。通常在 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 不提供卸载功能。
  • 预加载路径不会创建或渲染任何内容 — 你仍需在实体上通过 getOrCreateMutable 获取真实组件(GltfContainer、AudioSource、VideoPlayer、Material 纹理)才能使用;预加载只是让之后的使用立即生效。
  • 如果资源在场景启动时就会立即使用,则无需使用 AssetLoad。只预加载启动时不需要的资源 — 即稍后出现或由玩家交互触发的资源。

本地资源包预览

在发布前,先在本地重现服务端的资源包转换。这可以发现转换问题(缺失纹理、压缩后模型损坏),并让预览使用经过生产级优化的模型进行渲染。

  • **Creator Hub:**Play Options(Play 按钮上的下拉箭头)> 将鼠标悬停在 Desktop Client 上 > 勾选 Compress Assets。Linux 系统以及 @dcl/sdk-commands 版本过旧、不支持该标志的场景中,此开关会隐藏。
  • CLI:npm run start -- --asset-bundles(该参数会传入 Explorer 深层链接,作为 local-ab=true;--local-ab 是深层链接参数,不是 CLI 标志)

Desktop Explorer 会在你的机器上将所有 .gltf/.glb 模型转换为资源包。大型场景首次运行可能需要几分钟;转换后的模型会缓存,因此后续预览只会重新转换新增或修改的资源。如果资源转换失败,预览会回退到原始模型。仅 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)
透明材质过多额外绘制调用、排序问题尽量减少透明度,使用 alpha cutoff 代替混合
在系统中无保护地添加实体/组件实体数量激增系统每帧都会运行——创建前务必检查
无限制的实体查询CPU 突增使用特定组件进行筛选,并缓存结果
所有细节在所有距离下都加载三角形预算超限实现 LOD 系统
每个副本都使用几乎相同的 .glb(chair_01.glb、chair_02.glb……)加载缓慢、内存膨胀、纹理数量超限多个实体引用同一个共享 .glb——引擎会按源文件去重
整个场景使用一个单体 .glb出现时加载卡顿,且无法剔除任何内容拆分为按群组划分的模型(如一个街区、一间房间)
未预加载资源游戏过程中突然出现使用 AssetLoad 预加载稍后需要的资源(而非启动时的资源)

场景统计监控

预览模式下

使用 npm run start 在本地运行场景时:

  • 按 P 切换性能面板。
  • 监控:FPS、绘制调用、三角形、实体、材质、纹理、内存。
  • 场景限制会与当前用量一同显示,并使用绿色/黄色/红色指示。
需要关注的指标
  • FPS 低于 30:某项开销过大。检查绘制调用和系统执行时间。
  • 三角形数量接近上限:启用 LOD、减少模型细节、移除隐藏面。
  • 实体数量持续增加:很可能存在泄漏——实体被创建后却从未销毁。实现对象池。
  • 绘制调用超过 300(1 个地块):渲染的材质槽过多。使用图集并降低透明度,以减少每个对象的材质槽数量,同时减少渲染对象的数量。请注意,绘制调用数按渲染对象 × 其材质槽数量计算——在多个实体间复用一个模型不会降低该数量(参见上文的 重复模型),将多个独立对象打包到一个 .glb 中也不会降低该数量。

推荐的优化工具

工具用途
Blender Decimate 修改器减少导入模型的三角形数量(通过无头模式或 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 预览 — 模型会加载失败,且完全没有错误提示:没有控制台消息,没有占位符,网格就这样消失了(文档提交 5f212e3,2026 年 10 月;godot-explorer #2432 正在跟踪错误对话框)。

问题在于这种不对称性:已发布的场景在手机上可能看起来正常,但同一个模型在所有移动预览中都不可见 — 而调试该预览的创作者无从查起。如果场景面向移动设备,请在导出时禁用 Draco;跨平台构建时使用 optimize 并添加 --compress false,同时改为减少三角形和纹理。[未验证:其他 gltf-transform 压缩模式(meshopt、quantize)是否能在移动设备上加载 — 文档只特别提到 Draco。]

示例场景

引擎团队的压力测试场景(应视为 API 结构的权威参考):

交叉引用

  • deploy-scene — 发布后资源包的转换时机、故障排查、/detectabs 命令
  • add-3d-models — 模型加载、碰撞体、文件组织,以及驱动 Blender 修复或创作模型(references/blender-authoring.md)
  • game-design — 性能预算、设计模式和 MVP 规划
  • advanced-rendering — 使用 VisibilityComponent 的纹理模式、材质复用和 LOD
  • scene-runtime — 使用 EngineInfo.sceneHidden,在场景被全屏 Explorer UI 遮挡时暂停耗费资源的系统

评论

支持 Markdown