composites

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Composites

Composites

This skill carries the shared composite format reference used by other Decentraland skills (
create-scene
,
add-3d-models
,
sdk-scenes
).
The mandatory workflow below applies to EVERY composite you author or edit: compute scene bounds first (Step 0), consult the format catalog in the reference while writing entities, then run the validation gate at the end before finishing.
本 skill 提供其他 Decentraland skill(
create-scene
、
add-3d-models
、
sdk-scenes
)共用的 Composite 格式参考。
以下强制工作流适用于你编写或编辑的每一个 Composite:先计算场景边界(第 0 步),在编写实体时查阅参考中的格式目录,完成后再运行末尾的验证关卡。

Step 0 — Read scene.json and Compute Bounds (MANDATORY)

第 0 步——读取 scene.json 并计算边界(强制)

Before writing a single entity position, read
scene.json
and calculate the scene bounds.
This must happen first — all entity positions must fit within these bounds or they will not render.
**在写入任何一个实体位置之前,读取
scene.json
并计算场景边界。**这必须最先执行——所有实体位置都必须位于这些边界内,否则不会渲染。

How to calculate bounds

如何计算边界

  1. Open
    scene.json
    and locate
    scene.parcels
    (array of
    "x,y"
    strings) and
    scene.base
    .
  2. Parse every parcel as integers. Find the min and max X and Y across all parcels.
  3. Compute:
parcelsWide = max(parcel_x) - min(parcel_x) + 1
parcelsDeep = max(parcel_y) - min(parcel_y) + 1

maxX = parcelsWide * 16
maxZ = parcelsDeep * 16
  1. Valid entity positions: X in [0, maxX], Z in [0, maxZ]. Negative values and values above maxX/maxZ are outside the scene and will not render.
  1. 打开
    scene.json
    ,找到
    scene.parcels
    (由
    "x,y"
    字符串组成的数组)和
    scene.base
    。
  2. 将每个 parcel 解析为整数。找出所有 parcel 中 X 和 Y 的最小值与最大值。
  3. 计算:
parcelsWide = max(parcel_x) - min(parcel_x) + 1
parcelsDeep = max(parcel_y) - min(parcel_y) + 1

maxX = parcelsWide * 16
maxZ = parcelsDeep * 16
  1. 有效实体位置:X 位于 [0, maxX],Z 位于 [0, maxZ]。负值以及高于 maxX/maxZ 的值均位于场景之外,不会渲染。

Step 0b — Account for 3D Model Bounding Boxes (MANDATORY for GLB models)

第 0b 步——考虑 3D 模型包围盒(对 GLB 模型强制要求)

A model's
Transform.position
is its local origin, NOT its visual extent.
Tree and vegetation models commonly extend 6–12 m beyond their origin in one or more directions. Placing a tree at x=2 can cause it to render at x=–10, which is outside the scene bounds.
How to find a model's bounding box — parse the GLB binary and apply node-level transforms. Raw accessor
min
/
max
values alone are not reliable because many GLB models have large scale factors or translations baked into the GLTF node hierarchy (e.g. a model whose accessors say 0.6 m but whose node scale is 24× giving an actual rendered size of 14 m).
js
node -e "
const buf = require('fs').readFileSync('assets/Models/MyModel.glb');
const jsonLen = buf.readUInt32LE(12);
const json = JSON.parse(buf.slice(20, 20+jsonLen));
let minW=[Infinity,Infinity,Infinity], maxW=[-Infinity,-Infinity,-Infinity];
json.nodes?.forEach(n => {
  if (n.mesh === undefined) return;
  const s = n.scale || [1,1,1];
  const t = n.translation || [0,0,0];
  for (const prim of json.meshes[n.mesh].primitives) {
    const acc = json.accessors[prim.attributes.POSITION];
    if (!acc.min || !acc.max) continue;
    for (let i = 0; i < 3; i++) {
      const lo = acc.min[i]*s[i]+t[i], hi = acc.max[i]*s[i]+t[i];
      minW[i] = Math.min(minW[i], lo, hi);
      maxW[i] = Math.max(maxW[i], lo, hi);
    }
  }
});
const w=maxW[0]-minW[0], h=maxW[1]-minW[1], d=maxW[2]-minW[2];
console.log('Rendered size:', w.toFixed(2)+'m x', h.toFixed(2)+'m x', d.toFixed(2)+'m');
console.log('World min:', minW.map(v=>v.toFixed(2)), 'max:', maxW.map(v=>v.toFixed(2)));
"
Measure per model — don't guess or hard-code. Extents vary wildly: running the script above on a typical tree often reveals ~11 m of reach in one horizontal direction from the origin (safe minimum origin z≥12), while a column reaches under 1 m in every direction. Always compute the box for the specific GLB you're placing.
Rule: For every GLB model, compute:
minSafeX = max(0, -bbox.minX) + margin      (≥1 m)
minSafeZ = max(0, -bbox.minZ) + margin      (≥1 m)
maxSafeX = maxX - (bbox.maxX + margin)
maxSafeZ = maxZ - (bbox.maxZ + margin)
Only place the model if its Transform position satisfies all four bounds.
For tree/vegetation models where the bounding box is unknown, assume a 12 m safe buffer from all edges — i.e., place origins in
[12, maxX-12]
×
[12, maxZ-12]
.
**模型的
Transform.position
是其局部原点,而不是其视觉范围。**树木和植被模型通常会从原点向一个或多个方向延伸 6–12 m。将树放在 x=2 处,可能导致它在 x=–10 处渲染,从而超出场景边界。

Examples

如何查找模型的包围盒

scene.json parcelsparcelsWideparcelsDeepValid XValid Z
["0,0"]
110 – 160 – 16
["0,0","1,0"]
210 – 320 – 16
["0,0","1,0","0,1","1,1"]
220 – 320 – 32
解析 GLB 二进制文件并应用节点级变换。仅使用 accessor 的原始
min
/
max
值并不可靠,因为许多 GLB 模型在 GLTF 节点层级中内置了较大的 scale 或 translation(例如,一个 accessor 标示为 0.6 m、但节点 scale 为 24× 的模型,实际渲染尺寸为 14 m)。
js
node -e "
const buf = require('fs').readFileSync('assets/Models/MyModel.glb');
const jsonLen = buf.readUInt32LE(12);
const json = JSON.parse(buf.slice(20, 20+jsonLen));
let minW=[Infinity,Infinity,Infinity], maxW=[-Infinity,-Infinity,-Infinity];
json.nodes?.forEach(n => {
  if (n.mesh === undefined) return;
  const s = n.scale || [1,1,1];
  const t = n.translation || [0,0,0];
  for (const prim of json.meshes[n.mesh].primitives) {
    const acc = json.accessors[prim.attributes.POSITION];
    if (!acc.min || !acc.max) continue;
    for (let i = 0; i < 3; i++) {
      const lo = acc.min[i]*s[i]+t[i], hi = acc.max[i]*s[i]+t[i];
      minW[i] = Math.min(minW[i], lo, hi);
      maxW[i] = Math.max(maxW[i], lo, hi);
    }
  }
});
const w=maxW[0]-minW[0], h=maxW[1]-minW[1], d=maxW[2]-minW[2];
console.log('Rendered size:', w.toFixed(2)+'m x', h.toFixed(2)+'m x', d.toFixed(2)+'m');
console.log('World min:', minW.map(v=>v.toFixed(2)), 'max:', maxW.map(v=>v.toFixed(2)));
"
**逐个测量模型——不要猜测或硬编码。**各模型的延伸范围差异巨大:对典型树木运行上述脚本,通常会发现其从原点沿某个水平方向延伸约 11 m(安全最小原点 z≥12),而柱子的每个方向都不到 1 m。始终为要放置的具体 GLB 计算包围盒。
**规则:**对于每个 GLB 模型,都需要计算:
minSafeX = max(0, -bbox.minX) + margin      (≥1 m)
minSafeZ = max(0, -bbox.minZ) + margin      (≥1 m)
maxSafeX = maxX - (bbox.maxX + margin)
maxSafeZ = maxZ - (bbox.maxZ + margin)
仅当模型的 Transform 位置满足全部四个边界条件时,才能放置该模型。
对于包围盒未知的树木/植被模型,假设与所有边缘之间有 12 m 的安全缓冲区——即将原点放置在
[12, maxX-12]
×
[12, maxZ-12]
范围内。

Never change scene.json parcel count without explicit user instruction

示例

Adding parcels to
scene.json
is not always an option, it depends where the scene will be published to. If publishing to Genesis City, parcels must be owned or rented by the deploying wallet; if publishing to a World, it might be an option. If the scene is currently too small for what the user is asking for, ask the user for confirmation to change the scene layout and include more parcels. If they disagree then work within the existing parcel bounds and make the scene as rich as possible within 16×16m. Do not silently expand the parcel list. If more space is truly needed, ask the user first.
scene.json parcelsparcelsWideparcelsDeep有效 X有效 Z
["0,0"]
110 – 160 – 16
["0,0","1,0"]
210 – 320 – 16
["0,0","1,0","0,1","1,1"]
220 – 320 – 32

Composite format catalog (reference)

未经用户明确指示,绝不要更改 scene.json 中的 parcel 数量

Read
{baseDir}/composite-reference.md
for the full specification of the
main.composite
JSON format: the JSON structure and
jsonSchema
rules, entity ID allocation, the per-component format catalog (
core::Transform
,
core::GltfContainer
, …
core::NftShape
), the component-grouping pattern, edit-mode rules (
inspector::Nodes
), runtime spawning, and patterns for fetching composite entities from TypeScript.
向
scene.json
添加 parcel 并非总是可行,这取决于场景将发布到哪里。如果发布到 Genesis City,parcel 必须由部署钱包拥有或租用;如果发布到 World,则可能可行。如果当前场景对于用户要求而言过小,请向用户确认是否更改场景布局并添加更多 parcel。如果用户不同意,则在现有 parcel 边界内工作,并尽可能在 16×16m 范围内丰富场景。不要在未说明的情况下扩展 parcel 列表。如果确实需要更多空间,请先询问用户。

Validation Checklist

Composite 格式目录(参考)

Step 1 — Detect mode. Scan the composite for
inspector::*
,
composite::root
, or
asset-packs::ActionTypes
. If any are present, you are in edit mode — use the edit-mode checklist below. Otherwise use the authoring-from-scratch checklist.
读取
{baseDir}/composite-reference.md
,了解
main.composite
JSON 格式的完整规范:包括 JSON 结构和
jsonSchema
规则、实体 ID 分配、各组件格式目录(
core::Transform
、
core::GltfContainer
、…
core::NftShape
)、组件分组模式、编辑模式规则(
inspector::Nodes
)、运行时生成实体,以及从 TypeScript 获取 Composite 实体的模式。

Authoring-from-scratch checklist

验证清单

Before writing a fresh composite, verify:
  • version
    is
    1
  • NO
    inspector::*
    components whatsoever — no
    inspector::Nodes
    ,
    inspector::SceneMetadata
    (any version),
    inspector::Selection
    ,
    inspector::TransformConfig
    ,
    inspector::UIState
    . These are all auto-generated by the Creator Hub and including them in a fresh file breaks the entity tree or causes build errors.
  • NO
    composite::root
    or
    asset-packs::ActionTypes
    — auto-generated by engine
  • Every user entity (512+) has
    core::Transform
    and
    core-schema::Name
  • No duplicate entity IDs across the composite
  • No duplicate entity IDs with entities created via code with an explicit ID
  • core::
    components do NOT have
    jsonSchema
    — this is a hard requirement; including jsonSchema on a core:: component will cause the Creator Hub to fail to parse entities correctly
  • Non-core components (
    asset-packs::*
    ,
    core-schema::*
    ) MUST have
    jsonSchema
    (copied from catalog)
  • All
    GltfContainer.src
    paths use slugified name format:
    assets/asset-packs/<slug>/<filename>
  • All referenced asset files were downloaded to disk (GLB, audio, images)
  • Default collision masks set on GltfContainer (
    visibleMeshesCollisionMask: 0
    ,
    invisibleMeshesCollisionMask: 3
    )
  • All positions within parcel bounds — bounds were calculated in Step 0 from the actual
    scene.json
    parcel list. Every entity's X is in
    [0, maxX]
    and Z is in
    [0, maxZ]
    . Negative values and values above maxX/maxZ do not render. If the user requested a "large" scene but parcel count was not changed, all entities fit within the original bounds.
  • For every
    GltfContainer
    entity: checked whether the GLB contains animations (clip names embedded in the file). If it does, an
    core::Animator
    component is present on that entity. A model with animations but no Animator will silently loop its first clip with no way to control it.
  • For every
    GltfContainer
    entity: checked whether the GLB contains collision meshes (any mesh whose name includes the string
    _collider
    ). If yes,
    invisibleMeshesCollisionMask
    is set to
    3
    (CL_POINTER + CL_PHYSICS) to activate them. If no built-in colliders, evaluated whether a
    core::MeshCollider
    box/sphere is needed to cover the model's rough shape (for walkable surfaces, walls, or clickable objects).
  • If
    asset-packs::Actions
    ,
    asset-packs::Triggers
    , or
    asset-packs::States
    exist anywhere in the composite, then
    asset-packs::Counter
    must exist on entity 0, with
    value
    = the highest
    id
    used inside any Actions/Triggers/States data (it is the id allocator for those ids)
  • No
    {self}
    ,
    {assetPath}
    , or placeholder strings — all resolved to concrete values
  • Component names use base names (e.g.,
    asset-packs::Actions
    , not
    asset-packs::Actions-v1
    ). Never use versioned suffixes like
    -v3
    .
  • A composite using only core components needs no extra library. If it contains
    asset-packs::*
    components, older SDKs require
    @dcl/asset-packs
    as a project dependency; current SDKs fall back to the copy bundled inside
    @dcl/inspector
第 1 步——检测模式。扫描 Composite 中是否存在
inspector::*
、
composite::root
或
asset-packs::ActionTypes
。如果存在任何一项,则处于
编辑模式
——使用下方的编辑模式检查清单。否则使用从零编写检查清单。

Edit-mode checklist (composite already contains
inspector::*
)

从零编写检查清单

  • The scene is NOT currently open in the Creator Hub. The inspector autosaves and overwrites
    main.composite
    wholesale from its in-memory engine, discarding external edits with no error. Ask the user to close the scene before you write, and to reopen it afterwards.
For every NEW entity
<id>
you add, in addition to the authoring-from-scratch rules above (with the relaxation that
inspector::*
etc. are kept, not stripped):
  • <id>
    has been appended to the end of the
    children
    array of the entity-
    0
    entry inside
    inspector::Nodes.data["0"].json.value
    .
  • A new entry
    { "entity": <id>, "children": [...] }
    has been appended to the
    value
    array of
    inspector::Nodes.data["0"].json
    (use
    []
    if the entity has no children of its own).
  • If
    <id>
    's
    Transform.parent
    is not
    0
    , then
    <id>
    is in the parent entity's
    children
    array (not the root's).
  • core-schema::Name.data["<id>"]
    has a
    { "json": { "value": "..." } }
    entry — names are required for the entity to appear correctly in the entity tree and to be looked up by code.
  • inspector::TransformConfig.data["<id>"]
    has a
    { "json": {} }
    entry (empty object is fine).
  • entity-names.ts
    is either updated to include the new name (in
    EntityNames
    ) OR left untouched so the Creator Hub regenerates it on next save. Do NOT hand-edit the auto-generated header.
  • You did NOT delete or strip pre-existing
    inspector::Nodes
    ,
    inspector::SceneMetadata-*
    ,
    inspector::Selection
    ,
    inspector::TransformConfig
    ,
    composite::root
    , or
    asset-packs::ActionTypes
    . These are managed by the Creator Hub and must stay.
  • inspector::SceneMetadata-*
    is unchanged unless
    scene.json
    parcels changed (in which case the layout block must match
    scene.json
    ).
  • Reserved entities
    1
    (PlayerEntity) and
    2
    (CameraEntity) are still present in
    inspector::Nodes
    as their own top-level
    { "entity": 1, "children": [] }
    /
    { "entity": 2, "children": [] }
    entries. They are separate tree roots — they must NOT appear inside entity
    0
    's
    children
    array (or any other
    children
    array).
Verification command (edit mode): after editing, every entity ID present in
core::Transform.data
should also appear:
  1. As a top-level
    { "entity": <id>, ... }
    entry in
    inspector::Nodes.data["0"].json.value
    , AND
  2. In exactly one
    children
    array within that same
    value
    list (its parent's children).
Entities
0
,
1
, and
2
are the exception to rule 2 — they are tree roots and appear in no
children
array.
Missing entries here are the root cause of "entity renders but is invisible in the Creator Hub entity tree".
编写新的 Composite 前,验证:
  • version
    为
    1
  • 完全不存在任何
    inspector::*
    组件——不得包含
    inspector::Nodes
    、
    inspector::SceneMetadata
    (任何版本)、
    inspector::Selection
    、
    inspector::TransformConfig
    、
    inspector::UIState
    。这些组件均由 Creator Hub 自动生成;在全新文件中加入它们会破坏实体树或导致构建错误。
  • 不存在
    composite::root
    或
    asset-packs::ActionTypes
    ——它们由引擎自动生成
  • 每个用户实体(512+)都具有
    core::Transform
    和
    core-schema::Name
  • Composite 内不存在重复实体 ID
  • 与通过代码创建的实体及显式指定的 ID 不重复
  • core::
    组件没有
    jsonSchema
    ——这是硬性要求;在 core 组件上包含
    jsonSchema
    会导致 Creator Hub 无法正确解析实体
  • 非 core 组件(
    asset-packs::*
    、
    core-schema::*
    )必须包含
    jsonSchema
    (从目录中复制)
  • 所有
    GltfContainer.src
    路径均采用 slugified name 格式:
    assets/asset-packs/<slug>/<filename>
  • 所有引用的 asset 文件均已下载到磁盘(GLB、audio、images)
  • GltfContainer 上已设置默认 collision mask(
    visibleMeshesCollisionMask: 0
    、
    invisibleMeshesCollisionMask: 3
    )
  • 所有位置均位于 parcel 边界内——边界已在第 0 步根据实际
    scene.json
    parcel 列表计算完成。每个实体的 X 均位于
    [0, maxX]
    ,Z 均位于
    [0, maxZ]
    。负值以及高于 maxX/maxZ 的值不会渲染。如果用户要求“大型”场景但 parcel 数量未更改,则所有实体仍须位于原有边界内。
  • 对于每个
    GltfContainer
    实体:检查 GLB 是否包含动画(嵌入文件中的 clip 名称)。如果包含,则该实体上必须存在
    core::Animator
    组件。带动画但没有 Animator 的模型会静默循环播放第一个 clip,且无法控制。
  • 对于每个
    GltfContainer
    实体:检查 GLB 是否包含 collision mesh(名称包含字符串
    _collider
    的任何 mesh)。如果包含,则将
    invisibleMeshesCollisionMask
    设置为
    3
    (CL_POINTER + CL_PHYSICS)以激活它们。如果不存在内置 collider,则评估是否需要添加
    core::MeshCollider
    box/sphere,以覆盖模型的大致形状(用于可行走表面、墙壁或可点击对象)。
  • 如果 Composite 中任意位置存在
    asset-packs::Actions
    、
    asset-packs::Triggers
    或
    asset-packs::States
    ,则实体 0 上必须存在
    asset-packs::Counter
    ,且
    value
    等于 Actions/Triggers/States 数据中使用的最高
    id
    (它是这些 ID 的分配器)
  • 不存在
    {self}
    、
    {assetPath}
    或占位字符串——所有值均已解析为具体值
  • 组件名称使用基础名称(例如
    asset-packs::Actions
    ,而不是
    asset-packs::Actions-v1
    )。绝不使用
    -v3
    等版本化后缀。
  • 仅使用 core 组件的 Composite 不需要额外 library。如果包含
    asset-packs::*
    组件,较旧的 SDK 需要将
    @dcl/asset-packs
    作为项目依赖;当前 SDK 会回退使用
    @dcl/inspector
    内置的副本。

Post-Write Validation

编辑模式检查清单(Composite 已包含
inspector::*
)

After writing the composite, run the SDK build to verify:
bash
npx sdk-commands build
The build must pass with zero errors. If it fails, the composite is invalid. Common errors:
  • Composite references undefined component "X". Ensure provider.schemas was registered pre-seal via setCompositeProvider().
    (older/released SDKs word this as
    "X is not defined and there is no schema to define it"
    ) → missing
    jsonSchema
    on non-core component, or
    inspector::*
    component that shouldn't be there
  • TypeScript errors → fix generated scripts
  • **场景当前未在 Creator Hub 中打开。**Inspector 会自动保存,并使用内存中引擎的数据整体覆盖
    main.composite
    ,从而无声丢弃外部编辑。请让用户在写入前关闭场景,并在完成后重新打开。
对于每一个新增实体
<id>
,除上述从零编写规则外(但保留而不剥离
inspector::*
等组件),还需满足:
  • <id>
    已追加到
    inspector::Nodes.data["0"].json.value
    中实体
    0
    条目的
    children
    数组末尾。
  • 已将新条目
    { "entity": <id>, "children": [...] }
    追加到
    inspector::Nodes.data["0"].json
    的
    value
    数组末尾(如果实体没有自己的子实体,则使用
    []
    )。
  • 如果
    <id>
    的
    Transform.parent
    不为
    0
    ,则
    <id>
    必须位于父实体的
    children
    数组中,而不是根实体的
    children
    数组中。
  • core-schema::Name.data["<id>"]
    中存在
    { "json": { "value": "..." } }
    条目——实体必须具有名称,才能正确显示在实体树中并供代码查找。
  • inspector::TransformConfig.data["<id>"]
    中存在
    { "json": {} }
    条目(空对象即可)。
  • entity-names.ts
    已更新并在新名称中加入
    EntityNames
    ,或者保持不变,让 Creator Hub 在下次保存时重新生成。不要手动编辑自动生成的头部。
  • 未删除或剥离已有的
    inspector::Nodes
    、
    inspector::SceneMetadata-*
    、
    inspector::Selection
    、
    inspector::TransformConfig
    、
    composite::root
    或
    asset-packs::ActionTypes
    。这些内容由 Creator Hub 管理,必须保留。
  • 除非
    scene.json
    中的 parcel 发生变化,否则
    inspector::SceneMetadata-*
    保持不变;如果发生变化,layout 区块必须与
    scene.json
    一致。
  • 保留实体
    1
    (PlayerEntity)和
    2
    (CameraEntity)仍作为独立的顶层
    { "entity": 1, "children": [] }
    /
    { "entity": 2, "children": [] }
    条目存在于
    inspector::Nodes
    中。它们是独立的树根——不得出现在实体
    0
    的
    children
    数组或任何其他
    children
    数组中。
**验证命令(编辑模式):**编辑后,
core::Transform.data
中存在的每个实体 ID 都应该同时满足:
  1. 作为顶层
    { "entity": <id>, ... }
    条目出现在
    inspector::Nodes.data["0"].json.value
    中;以及
  2. 出现在同一
    value
    列表中恰好一个
    children
    数组内(即其父实体的 children)。
实体
0
、
1
和
2
是规则 2 的例外——它们是树根,不出现在任何
children
数组中。
遗漏这些条目是“实体已渲染,但在 Creator Hub 实体树中不可见”的根本原因。
—

写入后验证

—
写入 Composite 后,运行 SDK 构建以进行验证:
bash
npx sdk-commands build
构建必须通过且零错误。如果失败,则 Composite 无效。常见错误包括:
  • Composite references undefined component "X". Ensure provider.schemas was registered pre-seal via setCompositeProvider().
    (较旧或已发布的 SDK 会将其表述为
    "X is not defined and there is no schema to define it"
    )→ 非 core 组件缺少
    jsonSchema
    ,或包含了本不应存在的
    inspector::*
    组件
  • TypeScript 错误 → 修复生成的脚本