add-3d-models

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Adding 3D Models to Decentraland Scenes

向 Decentraland 场景添加 3D 模型

RULE: Check bounding boxes before placing models

规则:放置模型前检查 bounding box

A model's
Transform.position
is its local origin, not its visual extent.
Vegetation and large structural models often extend 6-12 m beyond their origin. A tree placed at x=2 can render at x=-10 — outside scene bounds and invisible.
Before placing any GLB model, determine its actual world-space bounding box. Raw accessor
min
/
max
values are NOT sufficient — many models have large node-level scales baked in. You must account for node transforms. Use the bounding box script in
{baseDir}/references/model-patterns.md
.
Then compute the safe placement zone:
safeMinX = -bbox.minX + edgeMargin (>=1m)
etc. Place the origin only within the safe zone. When bounding box is unknown, use a conservative 12 m buffer from all edges for trees/large foliage, or 3 m for small props.
模型的
Transform.position
是其局部原点,而不是其视觉范围边界。
植被和大型结构模型通常会延伸到其原点之外 6–12 m。放置在 x=2 的树木可能渲染在 x=-10——位于场景边界之外且不可见。
在放置任何 GLB 模型前,确定其实际的 world-space bounding box。 仅使用 accessor 的原始
min
/
max
值并不足够——许多模型在 node level 烘焙了较大的 scale。你必须考虑 node transform。使用
{baseDir}/references/model-patterns.md
中的 bounding box 脚本。
然后计算安全放置区域:
safeMinX = -bbox.minX + edgeMargin (>=1m)
等。只能将原点放在安全区域内。当 bounding box 未知时,对树木/大型植被使用距所有边界至少 12 m 的保守缓冲区,对小型道具使用 3 m。

RULE: Account for model depth before neighboring entities

规则:考虑与相邻实体之间的模型纵深

Two models don't overlap just because their origins are different. Always verify that
origin +/- extent
doesn't intersect any neighbor's bounding box. Pay special attention to deep arch/gateway models (can extend 14 m in +/-Z) and rotated models (rotating 90deg around Y swaps X and Z extents).
两个模型不会仅仅因为原点不同就一定不重叠。 始终验证
origin +/- extent
不与任何相邻实体的 bounding box 相交。特别注意纵深较大的 arch/gateway 模型(可沿 +/-Z 延伸 14 m)以及旋转后的模型(绕 Y 旋转 90deg 会交换 X 和 Z 范围)。

RULE: Single-sided models — orient the rendered face toward players

规则:单面模型——让渲染面朝向玩家

Many GLB models use back-face culling. The rendered face is typically toward local -Z. Y rotation transforms facing: 0deg -> south, 90deg -> east, 180deg -> north, 270deg -> west. When players approach from both sides, add a second copy rotated 180deg. Prefer double-sided geometry for elements visible from all angles.
Decentraland uses a Y-up coordinate system — test model orientation after import: a model exported Z-up (common from Blender) loads tipped over and must be re-exported with "Y Up".
许多 GLB 模型使用 back-face culling。渲染面通常朝向局部 -Z。Y rotation 会改变朝向:0deg -> 南,90deg -> 东,180deg -> 北,270deg -> 西。当玩家从两侧靠近时,添加一个旋转 180deg 的副本。对于需要从所有角度查看的元素,优先使用 double-sided geometry。
Decentraland 使用 Y-up 坐标系——导入后测试模型朝向:以 Z-up 导出(Blender 中很常见)的模型加载后会发生倾倒,必须使用 "Y Up" 重新导出。

RULE: Text labels must be in open air — no occlusion by geometry

规则:文本标签必须位于开放区域——不能被几何体遮挡

TextShape
labels are rendered in world space and can be occluded by solid geometry. Place labels with no solid model within 2 m horizontally, ensure height clearance, use
Billboard
with
billboardMode: 2
for readable labels, and prefer open-area placement. Exception: labels mounted on a wall without Billboard.
TextShape
标签在世界空间中渲染,可能会被实体几何体遮挡。
将标签放置在水平方向 2 m 内没有实体模型的位置,确保高度间隙充足,使用
Billboard
并设置
billboardMode: 2
以获得清晰可读的标签,并优先选择开阔区域。例外:安装在墙面上且不使用 Billboard 的标签。

RULE: Use composite for initial models

规则:初始模型使用 composite

Always add models that exist at scene load to
assets/scene/main.composite
, not in TypeScript.
Only use TypeScript for models spawned dynamically at runtime. Use
visibleMeshesCollisionMask: 3, invisibleMeshesCollisionMask: 0
when the model has no
_collider
meshes
(common case). Use
visibleMeshesCollisionMask: 0, invisibleMeshesCollisionMask: 3
when it has
_collider
meshes. Never set both to non-zero simultaneously.
To add behavior to a composite model, fetch it in
index.ts
by name or tag — do NOT re-create it.
始终将场景加载时已存在的模型添加到
assets/scene/main.composite
,而不是添加到 TypeScript 中。
仅对运行时动态生成的模型使用 TypeScript。模型没有
_collider
mesh(常见情况)时,使用
visibleMeshesCollisionMask: 3, invisibleMeshesCollisionMask: 0
;模型有
_collider
mesh 时,使用
visibleMeshesCollisionMask: 0, invisibleMeshesCollisionMask: 3
。绝不要同时将两者设置为非零值。
若要为 composite 模型添加行为,请在
index.ts
中按名称或 tag 获取它——不要重新创建它。

RULE: Swapping a model
src
requires fresh Transform — never inherit scale/position

规则:替换模型的
src
时需要全新的 Transform——绝不继承 scale/position

When you change the
src
of an existing
GltfContainer
(in a composite, in code, or via builder asset replacement), the entity's existing
Transform.scale
,
Transform.position
, and often
rotation
were tuned for the previous model's native dimensions and pivot. They are almost never correct for the new model — applying them blindly can produce buildings that overshoot scene bounds, props at wrong heights, or models visibly shifted from where the user expected them.
Treat every model swap as fresh placement:
  1. Look up the new GLB's native bounding box — use the bounding box script in
    {baseDir}/references/model-patterns.md
    (raw accessor
    min
    /
    max
    is not sufficient; node-level scale/translation must be applied).
  2. Recompute
    scale
    so the world-space size (native size × scale) is sensible for the role. Do not carry over the previous entity's scale — it was calibrated against a different native size.
  3. Verify pivot location. Many architecture/building GLBs have pivots at a corner (e.g.
    (0,0,0)
    at one base corner), not the center. Two models with the same
    position
    but different pivots will visually shift after the swap.
  4. Verify the resulting world-space bounding box stays inside scene bounds. Each parcel is 16 × 16 m horizontally; max height is
    log2(parcels+1) × 20 m
    (1 parcel → 20 m, 4 parcels → 46 m, 9 parcels → 66 m — see
    {baseDir}/../optimize-scene/SKILL.md
    ). Compute
    position +/- bbox
    against scene
    [0, maxX] × [0, maxZ]
    and
    y <= maxHeight
    .
  5. State the audit explicitly. After a swap, the agent must report: native dimensions of the new GLB, chosen scale, chosen position, and the resulting world-space bounding box vs. scene bounds. Do not silently keep the prior Transform.
This applies equally to code (
GltfContainer.createOrReplace(entity, { src: '...' })
while leaving Transform untouched) and to composite edits where only the
core::GltfContainer.data["<id>"].json.src
was modified.
Runtime swap mechanics: to change a model in place, mutate the field directly —
GltfContainer.getMutable(entity).src = newSrc
— the engine reloads the GLB on the same entity. Other
GltfContainer
fields (e.g.
visibleMeshesCollisionMask
) can also be mutated live the same way. Reusing the Transform is only safe when the two models share the same native size and pivot; otherwise re-audit per the steps above.
当更改现有
GltfContainer
的
src
时,无论是 composite 中、代码中还是通过 builder 替换资产,该实体现有的
Transform.scale
、
Transform.position
以及通常还有
rotation
,都是针对上一个模型的原生尺寸和 pivot 调整的。它们几乎永远不适用于新模型——盲目套用可能导致建筑超出场景边界、道具处于错误高度,或模型在视觉上偏离用户预期的位置。
将每次模型替换都视为重新放置:
  1. 查找新 GLB 的原生 bounding box——使用
    {baseDir}/references/model-patterns.md
    中的 bounding box 脚本(原始 accessor
    min
    /
    max
    值并不足够;必须应用 node-level scale/translation)。
  2. 重新计算
    scale
    ,使 world-space 尺寸(原生尺寸 × scale)适合其用途。不要沿用上一个实体的 scale——它是针对另一个原生尺寸校准的。
  3. 验证 pivot 位置。 许多 architecture/building GLB 的 pivot 位于角点(例如
    (0,0,0)
    位于一个底角),而不是中心。两个模型即使
    position
    相同,pivot 不同也会在替换后产生视觉偏移。
  4. 验证最终的 world-space bounding box 位于场景边界内。 每个 parcel 的水平尺寸为 16 × 16 m;最大高度为
    log2(parcels+1) × 20 m
    (1 个 parcel → 20 m,4 个 parcel → 46 m,9 个 parcel → 66 m——参见
    {baseDir}/../optimize-scene/SKILL.md
    )。根据场景的
    [0, maxX] × [0, maxZ]
    和
    y <= maxHeight
    ,计算
    position +/- bbox
    。
  5. 明确说明审查结果。 替换后,agent 必须报告:新 GLB 的原生尺寸、选择的 scale、选择的 position,以及最终的 world-space bounding box 与场景边界的对比。不得静默保留之前的 Transform。
这同样适用于代码(使用
GltfContainer.createOrReplace(entity, { src: '...' })
并保持 Transform 不变)以及只修改了
core::GltfContainer.data["<id>"].json.src
的 composite 编辑。
运行时替换机制:要原地更换模型,直接修改字段——
GltfContainer.getMutable(entity).src = newSrc
——引擎会在同一实体上重新加载 GLB。其他
GltfContainer
字段(例如
visibleMeshesCollisionMask
)也可以通过相同方式实时修改。只有当两个模型具有相同原生尺寸和 pivot 时,复用 Transform 才是安全的;否则必须按照上述步骤重新审查。

RULE: When editing an existing composite, register new entities in
inspector::Nodes

规则:编辑现有 composite 时,在
inspector::Nodes
中注册新实体

If
assets/scene/main.composite
already contains
inspector::Nodes
(the user has opened the scene in the Creator Hub at least once), every new entity you add MUST also be registered there or it will be invisible in the Creator Hub entity tree — the model still renders in-world, but the user cannot select/edit it from the editor. You also need a
core-schema::Name
entry and an
inspector::TransformConfig
entry for the new entity.
See the "Editing an existing composite (edit mode)" section of
{baseDir}/../composites/composite-reference.md
for the exact arrays to append to. Don't skip this when adding 3D models to an existing scene.
如果
assets/scene/main.composite
已包含
inspector::Nodes
(用户至少在 Creator Hub 中打开过该场景一次),则每个新添加的实体也必须注册到其中,否则它在 Creator Hub 实体树中不可见——模型仍会渲染到世界中,但用户无法从编辑器中选择或编辑它。还需要为新实体添加
core-schema::Name
条目和
inspector::TransformConfig
条目。
请参阅
{baseDir}/../composites/composite-reference.md
中“Editing an existing composite (edit mode)”一节,了解需要追加到的确切数组。向现有场景添加 3D 模型时,不要跳过此步骤。

RULE: Parenting a model to the player — pick by item type

规则:将模型挂载到玩家——根据物品类型选择方式

When a
GltfContainer
entity needs to follow the player (held weapon, aiming reticle, cosmetic backpack, halo, torch), there are three SDK7 mechanisms and they are NOT interchangeable. Default for aim-sensitive items: parent to
engine.CameraEntity
— this is the most common porting mistake when coming from SDK6.
  • Aim-sensitive held item (gun, aiming reticle, flashlight — anything the player should be able to point by looking around) →
    Transform.create(entity, { parent: engine.CameraEntity, position: ..., ... })
    . Follows camera yaw + pitch, so the item points where the player is looking. This is the SDK7 analogue of SDK6's
    Attachable.FIRST_PERSON_CAMERA
    . Recommended default for held gameplay items.
  • Yaw-only / body-fixed item (held shield not used for aim, static carried torch, non-aimed inventory item) →
    Transform.create(entity, { parent: engine.PlayerEntity, position: ..., ... })
    . Follows feet + body yaw only — no pitch, no animation. Wrong default for guns: a weapon parented to
    PlayerEntity
    stays flat when the player looks up to aim.
  • Cosmetic item (hat, halo, backpack, name plate, decorative torch visible to other players) →
    AvatarAttach.create(entity, { anchorPointId: AAPT_HEAD | AAPT_SPINE | AAPT_LEFT_HAND | ... })
    . Follows the animated bone — the item visually moves with idle bob, walk cycle, and gestures. Not for aim (animation jitter makes aim-sensitive items unusable).
Using a bone anchor like
AAPT_RIGHT_HAND
for a gun looks correct ("put the gun in the hand") but the hand bone is animated — the gun jitters every frame and is unaimable. This is a porting trap when coming from SDK6's
Attachable.FIRST_PERSON_CAMERA
pattern; the correct SDK7 mapping is
engine.CameraEntity
, NOT
AvatarAttach
and NOT
engine.PlayerEntity
(which loses pitch).
See [[player-avatar]] (Held items vs cosmetic items) for the full comparison and a worked gun example. For SDK6 porting context, see [[migrate-sdk6-to-sdk7]].
当
GltfContainer
实体需要跟随玩家时(手持武器、瞄准准星、装饰背包、光环、火炬),请按物品类型选择。SDK7 中有三种机制,它们不可互换。对于瞄准敏感物品,默认将
engine.CameraEntity
作为 parent
——这是从 SDK6 迁移时最常见的错误。
  • 瞄准敏感的手持物品(枪械、瞄准准星、手电筒——任何玩家需要通过观察来指向的物品)→
    Transform.create(entity, { parent: engine.CameraEntity, position: ..., ... })
    。它会跟随 camera yaw + pitch,因此物品会指向玩家正在查看的方向。它是 SDK6 中
    Attachable.FIRST_PERSON_CAMERA
    在 SDK7 中的对应机制。推荐作为手持游戏物品的默认方案。
  • 仅跟随 yaw/固定于身体的物品(不用于瞄准的手持盾牌、静态携带的火炬、非瞄准类库存物品)→
    Transform.create(entity, { parent: engine.PlayerEntity, position: ..., ... })
    。它只跟随脚部和身体 yaw——没有 pitch,也没有动画。对枪械使用此默认方式是错误的:挂载到
    PlayerEntity
    的武器会在玩家抬头瞄准时保持平放。
  • 装饰性物品(帽子、光环、背包、名牌、供其他玩家看到的装饰性火炬)→
    AvatarAttach.create(entity, { anchorPointId: AAPT_HEAD | AAPT_SPINE | AAPT_LEFT_HAND | ... })
    。它会跟随带动画的 bone——物品在视觉上会随闲置晃动、行走循环和手势一起移动。不适用于瞄准(动画抖动会使瞄准敏感物品无法使用)。
使用
AAPT_RIGHT_HAND
这样的 bone anchor 挂载枪械看起来是正确的(“把枪放进手里”),但手部 bone 带有动画——枪械会每帧抖动且无法瞄准。这是从 SDK6
Attachable.FIRST_PERSON_CAMERA
模式迁移时容易陷入的陷阱;正确的 SDK7 映射是
engine.CameraEntity
,而不是
AvatarAttach
或
engine.PlayerEntity
(后者会丢失 pitch)。
有关完整对比和枪械示例,请参阅 [[player-avatar]](Held items vs cosmetic items)。有关 SDK6 迁移背景,请参阅 [[migrate-sdk6-to-sdk7]]。

RULE: Always check for animations

规则:始终检查动画

Before finalizing any entity with
GltfContainer
, check if the GLB contains animations. If it has animations, always add an
Animator
component. Without it the engine silently loops the first clip forever. If no animations, omit
Animator
.
在最终确定任何包含
GltfContainer
的实体前,检查 GLB 是否包含动画。如果包含动画,始终添加
Animator
component。
如果没有该 component,引擎会静默地永久循环播放第一个 clip。如果没有动画,则不要添加
Animator
。

RULE: Always check for built-in colliders

规则:始终检查内置 collider

Check whether the GLB contains
_collider
meshes (mesh or node name includes
_collider
). Use the collider detection script in
{baseDir}/references/model-patterns.md
.
检查 GLB 是否包含
_collider
mesh(mesh 或 node 名称中包含
_collider
)。使用
{baseDir}/references/model-patterns.md
中的 collider detection script。

Two correct patterns — pick one, never mix

两种正确模式——选择一种,绝不能混用

Model HAS
_collider
meshes
:
visibleMeshesCollisionMask: 0, invisibleMeshesCollisionMask: 3
Model has NO
_collider
meshes
:
visibleMeshesCollisionMask: 3, invisibleMeshesCollisionMask: 0
Choose the mask based on role:
RolevisibleMeshesCollisionMaskWhy
Interactive (clicks)
3
Physics + pointer
Structural / decorative
3
Block walking + clicks
Clickable-only, no bulk
1
Detect clicks only
Purely decorative
0
No interaction
Anti-pattern — DO NOT USE:
visibleMeshesCollisionMask: 2, invisibleMeshesCollisionMask: 3
— mixes both patterns and causes pointer detection failures.
模型包含
_collider
mesh
:
visibleMeshesCollisionMask: 0, invisibleMeshesCollisionMask: 3
模型不包含
_collider
mesh
:
visibleMeshesCollisionMask: 3, invisibleMeshesCollisionMask: 0
根据角色选择 mask:
角色visibleMeshesCollisionMask原因
交互式(点击)
3
物理 + pointer
结构/装饰性
3
阻止行走 + 点击
仅可点击、无实体阻挡
1
仅检测点击
纯装饰性
0
无交互
反模式——不要使用:
visibleMeshesCollisionMask: 2, invisibleMeshesCollisionMask: 3
——这会混用两种模式,并导致 pointer detection 失败。

RULE: Always validate entity positions against parcel bounds

规则:始终根据 parcel 边界验证实体位置

Entities entirely outside scene parcels are not rendered — no error shown; a model that straddles the boundary still renders the part that is inside. Each parcel is 16x16 m. Valid range:
0 <= x <= 16 * parcelsWide
,
0 <= z <= 16 * parcelsDeep
,
y >= 0
.
完全位于场景 parcel 之外的实体不会被渲染——不会显示任何错误;跨越边界的模型仍会渲染位于边界内部的部分。每个 parcel 的尺寸为 16x16 m。有效范围:
0 <= x <= 16 * parcelsWide
、
0 <= z <= 16 * parcelsDeep
、
y >= 0
。

Loading a 3D Model in TypeScript (dynamic entities only)

在 TypeScript 中加载 3D 模型(仅限动态实体)

Use
GltfContainer.create(entity, { src: 'assets/Models/myModel.glb' })
for runtime-spawned entities. Place files in
assets/Models/
.
对于运行时生成的实体,使用
GltfContainer.create(entity, { src: 'assets/Models/myModel.glb' })
。将文件放在
assets/Models/
中。

Asset folder conventions

Asset 文件夹约定

  • Default for models you download yourself:
    assets/Models/
    .
  • Legacy scenes may already have models under
    assets/scene/Models/
    — that path still works; reuse it for any new models in those scenes instead of creating a parallel
    assets/Models/
    folder.
  • Creator Hub assets: models imported directly through the Creator Hub UI land in
    assets/Models/
    (same as the standard path). Items from free DCL asset packs land in
    assets/asset-packs/
    and custom items in
    assets/custom/
    . Older scenes may also have user imports directly under
    assets/scene/
    . Reference these paths as-is — never move or rename them.
Always check the scene's existing folders before deciding where to put a new model.
  • 自行下载的模型默认放在
    assets/Models/
    。
  • 旧版场景可能已经在
    assets/scene/Models/
    下包含模型——该路径仍然可用;对于这些场景,应在其中放置新模型以复用现有目录,而不是创建并行的
    assets/Models/
    文件夹。
  • Creator Hub asset:通过 Creator Hub UI 直接导入的模型会进入
    assets/Models/
    (与标准路径相同)。免费 DCL asset pack 中的项目会进入
    assets/asset-packs/
    ,自定义项目会进入
    assets/custom/
    。某些旧版场景还可能直接在
    assets/scene/
    下包含用户导入的资产。请按原样引用这些路径——绝不要移动或重命名它们。
决定新模型的位置之前,务必检查场景现有的文件夹。

Free 3D Models — OpenDCL Catalog (8,800+ models)

免费 3D 模型——OpenDCL Catalog(超过 8,800 个模型)

Always check the scene's local asset folder first. Before fetching any model, confirm with the user.
The catalog is at
{baseDir}/references/model-catalog.md
. Search with
grep -i "keyword"
. Try synonyms if no results. Browse categories with
grep "^##"
.
Workflow: Search catalog → review dimensions/triangles/animations → download with curl into
assets/Models/
→ reference in code → add Animator if model has animations.
Important:
GltfContainer
only works with local files. Always download into
assets/Models/
first. Never
cd
into the models directory — run curl from the project root.
始终先检查场景的本地 asset 文件夹。在获取任何模型之前,先与用户确认。
Catalog 位于
{baseDir}/references/model-catalog.md
。使用
grep -i "keyword"
搜索。如果没有结果,请尝试同义词。使用
grep "^##"
浏览分类。
工作流:搜索 catalog → 查看尺寸/triangle/动画 → 使用 curl 下载到
assets/Models/
→ 在代码中引用 → 如果模型包含动画,则添加 Animator。
重要:
GltfContainer
仅支持本地文件。始终先下载到
assets/Models/
。绝不要
cd
到模型目录——请从项目根目录运行 curl。

Troubleshooting

故障排除

ProblemCauseSolution
Model not visibleWrong file pathVerify path relative to project root
Model not visibleOutside scene boundariesCheck position is within 0-16 per parcel
Model not visibleScale is 0 or very smallCheck
Transform.scale
Model loads but looks wrongY-up vs Z-up mismatchRe-export from Blender with "Y Up"
"FINISHED_WITH_ERROR"Corrupted .glbRe-export as
.glb
(binary GLTF)
Clicking does nothingCL_POINTER not setSet
visibleMeshesCollisionMask: 3
if no
_collider
meshes
Click through wallsCL_POINTER not on visible meshSet
visibleMeshesCollisionMask: 3
(or at minimum
1
)
问题原因解决方案
模型不可见文件路径错误验证相对于项目根目录的路径
模型不可见位于场景边界之外检查位置是否在每个 parcel 的 0-16 范围内
模型不可见Scale 为 0 或非常小检查
Transform.scale
模型可以加载但显示异常Y-up 与 Z-up 不匹配从 Blender 使用 "Y Up" 重新导出
"FINISHED_WITH_ERROR".glb 已损坏重新导出为
.glb
(binary GLTF)
点击后没有任何反应未设置 CL_POINTER如果没有
_collider
mesh,则设置
visibleMeshesCollisionMask: 3
点击穿透墙壁visible mesh 未启用 CL_POINTER设置
visibleMeshesCollisionMask: 3
(或最低设置为
1
)

Model Best Practices

模型最佳实践

  • Keep models under 50MB per file for good loading times
  • Use
    .glb
    format (binary GLTF) — smaller than
    .gltf
  • Optimize triangle count: aim for under 1,500 triangles per model for small props
  • Use texture atlases when possible to reduce draw calls
  • Materials in models should use PBR for best results
For full code examples (loading, colliders, operations, catalog workflow), see
{baseDir}/references/model-patterns.md
. For the asset catalog (8,800+ models), see
{baseDir}/references/model-catalog.md
.
  • 为缩短加载时间,每个模型文件应控制在 50MB 以下。
  • 使用
    .glb
    格式(binary GLTF)——比
    .gltf
    更小。
  • 优化 triangle 数量:小型道具尽量控制在 1,500 个 triangle 以下。
  • 尽可能使用 texture atlas,以减少 draw call。
  • 模型中的 material 应使用 PBR,以获得最佳效果。
有关完整代码示例(加载、collider、操作和 catalog 工作流),请参阅
{baseDir}/references/model-patterns.md
。有关 asset catalog(超过 8,800 个模型),请参阅
{baseDir}/references/model-catalog.md
。

Example scenes

示例场景

Engine-team test scenes exercising these APIs against the real runtime:
Engine 团队提供了针对真实 runtime 验证这些 API 的测试场景: