composites
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseComposites
Composites
This skill carries the shared composite format reference used by other Decentraland skills (, , ).
create-sceneadd-3d-modelssdk-scenesThe 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(、、)共用的 Composite 格式参考。
create-sceneadd-3d-modelssdk-scenes以下强制工作流适用于你编写或编辑的每一个 Composite:先计算场景边界(第 0 步),在编写实体时查阅参考中的格式目录,完成后再运行末尾的验证关卡。
Step 0 — Read scene.json and Compute Bounds (MANDATORY)
第 0 步——读取 scene.json 并计算边界(强制)
Before writing a single entity position, read and calculate the scene bounds. This must happen first — all entity positions must fit within these bounds or they will not render.
scene.json**在写入任何一个实体位置之前,读取 并计算场景边界。**这必须最先执行——所有实体位置都必须位于这些边界内,否则不会渲染。
scene.jsonHow to calculate bounds
如何计算边界
- Open and locate
scene.json(array ofscene.parcelsstrings) and"x,y".scene.base - Parse every parcel as integers. Find the min and max X and Y across all parcels.
- Compute:
parcelsWide = max(parcel_x) - min(parcel_x) + 1
parcelsDeep = max(parcel_y) - min(parcel_y) + 1
maxX = parcelsWide * 16
maxZ = parcelsDeep * 16- 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.
- 打开 ,找到
scene.json(由scene.parcels字符串组成的数组)和"x,y"。scene.base - 将每个 parcel 解析为整数。找出所有 parcel 中 X 和 Y 的最小值与最大值。
- 计算:
parcelsWide = max(parcel_x) - min(parcel_x) + 1
parcelsDeep = max(parcel_y) - min(parcel_y) + 1
maxX = parcelsWide * 16
maxZ = parcelsDeep * 16- 有效实体位置: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 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.
Transform.positionHow to find a model's bounding box — parse the GLB binary and apply node-level transforms. Raw accessor / 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).
minmaxjs
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]**模型的 是其局部原点,而不是其视觉范围。**树木和植被模型通常会从原点向一个或多个方向延伸 6–12 m。将树放在 x=2 处,可能导致它在 x=–10 处渲染,从而超出场景边界。
Transform.positionExamples
如何查找模型的包围盒
| scene.json parcels | parcelsWide | parcelsDeep | Valid X | Valid Z |
|---|---|---|---|---|
| 1 | 1 | 0 – 16 | 0 – 16 |
| 2 | 1 | 0 – 32 | 0 – 16 |
| 2 | 2 | 0 – 32 | 0 – 32 |
解析 GLB 二进制文件并应用节点级变换。仅使用 accessor 的原始 / 值并不可靠,因为许多 GLB 模型在 GLTF 节点层级中内置了较大的 scale 或 translation(例如,一个 accessor 标示为 0.6 m、但节点 scale 为 24× 的模型,实际渲染尺寸为 14 m)。
minmaxjs
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 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| scene.json parcels | parcelsWide | parcelsDeep | 有效 X | 有效 Z |
|---|---|---|---|---|
| 1 | 1 | 0 – 16 | 0 – 16 |
| 2 | 1 | 0 – 32 | 0 – 16 |
| 2 | 2 | 0 – 32 | 0 – 32 |
Composite format catalog (reference)
未经用户明确指示,绝不要更改 scene.json 中的 parcel 数量
Read for the full specification of the JSON format: the JSON structure and rules, entity ID allocation, the per-component format catalog (, , … ), the component-grouping pattern, edit-mode rules (), runtime spawning, and patterns for fetching composite entities from TypeScript.
{baseDir}/composite-reference.mdmain.compositejsonSchemacore::Transformcore::GltfContainercore::NftShapeinspector::Nodes向 添加 parcel 并非总是可行,这取决于场景将发布到哪里。如果发布到 Genesis City,parcel 必须由部署钱包拥有或租用;如果发布到 World,则可能可行。如果当前场景对于用户要求而言过小,请向用户确认是否更改场景布局并添加更多 parcel。如果用户不同意,则在现有 parcel 边界内工作,并尽可能在 16×16m 范围内丰富场景。不要在未说明的情况下扩展 parcel 列表。如果确实需要更多空间,请先询问用户。
scene.jsonValidation Checklist
Composite 格式目录(参考)
Step 1 — Detect mode. Scan the composite for , , or . If any are present, you are in edit mode — use the edit-mode checklist below. Otherwise use the authoring-from-scratch checklist.
inspector::*composite::rootasset-packs::ActionTypes读取 ,了解 JSON 格式的完整规范:包括 JSON 结构和 规则、实体 ID 分配、各组件格式目录(、、… )、组件分组模式、编辑模式规则()、运行时生成实体,以及从 TypeScript 获取 Composite 实体的模式。
{baseDir}/composite-reference.mdmain.compositejsonSchemacore::Transformcore::GltfContainercore::NftShapeinspector::NodesAuthoring-from-scratch checklist
验证清单
Before writing a fresh composite, verify:
- is
version1 - NO components whatsoever — no
inspector::*,inspector::Nodes(any version),inspector::SceneMetadata,inspector::Selection,inspector::TransformConfig. These are all auto-generated by the Creator Hub and including them in a fresh file breaks the entity tree or causes build errors.inspector::UIState - NO or
composite::root— auto-generated by engineasset-packs::ActionTypes - Every user entity (512+) has and
core::Transformcore-schema::Name - No duplicate entity IDs across the composite
- No duplicate entity IDs with entities created via code with an explicit ID
- components do NOT have
core::— this is a hard requirement; including jsonSchema on a core:: component will cause the Creator Hub to fail to parse entities correctlyjsonSchema - Non-core components (,
asset-packs::*) MUST havecore-schema::*(copied from catalog)jsonSchema - All paths use slugified name format:
GltfContainer.srcassets/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 parcel list. Every entity's X is in
scene.jsonand Z is in[0, maxX]. 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.[0, maxZ] - For every entity: checked whether the GLB contains animations (clip names embedded in the file). If it does, an
GltfContainercomponent is present on that entity. A model with animations but no Animator will silently loop its first clip with no way to control it.core::Animator - For every entity: checked whether the GLB contains collision meshes (any mesh whose name includes the string
GltfContainer). If yes,_collideris set toinvisibleMeshesCollisionMask(CL_POINTER + CL_PHYSICS) to activate them. If no built-in colliders, evaluated whether a3box/sphere is needed to cover the model's rough shape (for walkable surfaces, walls, or clickable objects).core::MeshCollider - If ,
asset-packs::Actions, orasset-packs::Triggersexist anywhere in the composite, thenasset-packs::Statesmust exist on entity 0, withasset-packs::Counter= the highestvalueused inside any Actions/Triggers/States data (it is the id allocator for those ids)id - No ,
{self}, or placeholder strings — all resolved to concrete values{assetPath} - Component names use base names (e.g., , not
asset-packs::Actions). Never use versioned suffixes likeasset-packs::Actions-v1.-v3 - A composite using only core components needs no extra library. If it contains components, older SDKs require
asset-packs::*as a project dependency; current SDKs fall back to the copy bundled inside@dcl/asset-packs@dcl/inspector
第 1 步——检测模式。扫描 Composite 中是否存在 、 或 。如果存在任何一项,则处于编辑模式——使用下方的编辑模式检查清单。否则使用从零编写检查清单。
inspector::*composite::rootasset-packs::ActionTypesEdit-mode checklist (composite already contains inspector::*
)
inspector::*从零编写检查清单
- The scene is NOT currently open in the Creator Hub. The inspector autosaves and overwrites 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.
main.composite
For every NEW entity you add, in addition to the authoring-from-scratch rules above (with the relaxation that etc. are kept, not stripped):
<id>inspector::*- has been appended to the end of the
<id>array of the entity-childrenentry inside0.inspector::Nodes.data["0"].json.value - A new entry has been appended to the
{ "entity": <id>, "children": [...] }array ofvalue(useinspector::Nodes.data["0"].jsonif the entity has no children of its own).[] - If 's
<id>is notTransform.parent, then0is in the parent entity's<id>array (not the root's).children - has a
core-schema::Name.data["<id>"]entry — names are required for the entity to appear correctly in the entity tree and to be looked up by code.{ "json": { "value": "..." } } - has a
inspector::TransformConfig.data["<id>"]entry (empty object is fine).{ "json": {} } - is either updated to include the new name (in
entity-names.ts) OR left untouched so the Creator Hub regenerates it on next save. Do NOT hand-edit the auto-generated header.EntityNames - You did NOT delete or strip pre-existing ,
inspector::Nodes,inspector::SceneMetadata-*,inspector::Selection,inspector::TransformConfig, orcomposite::root. These are managed by the Creator Hub and must stay.asset-packs::ActionTypes - is unchanged unless
inspector::SceneMetadata-*parcels changed (in which case the layout block must matchscene.json).scene.json - Reserved entities (PlayerEntity) and
1(CameraEntity) are still present in2as their own top-levelinspector::Nodes/{ "entity": 1, "children": [] }entries. They are separate tree roots — they must NOT appear inside entity{ "entity": 2, "children": [] }'s0array (or any otherchildrenarray).children
Verification command (edit mode): after editing, every entity ID present in should also appear:
core::Transform.data- As a top-level entry in
{ "entity": <id>, ... }, ANDinspector::Nodes.data["0"].json.value - In exactly one array within that same
childrenlist (its parent's children).value
Entities , , and are the exception to rule 2 — they are tree roots and appear in no array.
012childrenMissing entries here are the root cause of "entity renders but is invisible in the Creator Hub entity tree".
编写新的 Composite 前,验证:
- 为
version1 - 完全不存在任何 组件——不得包含
inspector::*、inspector::Nodes(任何版本)、inspector::SceneMetadata、inspector::Selection、inspector::TransformConfig。这些组件均由 Creator Hub 自动生成;在全新文件中加入它们会破坏实体树或导致构建错误。inspector::UIState - 不存在 或
composite::root——它们由引擎自动生成asset-packs::ActionTypes - 每个用户实体(512+)都具有 和
core::Transformcore-schema::Name - Composite 内不存在重复实体 ID
- 与通过代码创建的实体及显式指定的 ID 不重复
- 组件没有
core::——这是硬性要求;在 core 组件上包含jsonSchema会导致 Creator Hub 无法正确解析实体jsonSchema - 非 core 组件(、
asset-packs::*)必须包含core-schema::*(从目录中复制)jsonSchema - 所有 路径均采用 slugified name 格式:
GltfContainer.srcassets/asset-packs/<slug>/<filename> - 所有引用的 asset 文件均已下载到磁盘(GLB、audio、images)
- GltfContainer 上已设置默认 collision mask(、
visibleMeshesCollisionMask: 0)invisibleMeshesCollisionMask: 3 - 所有位置均位于 parcel 边界内——边界已在第 0 步根据实际 parcel 列表计算完成。每个实体的 X 均位于
scene.json,Z 均位于[0, maxX]。负值以及高于 maxX/maxZ 的值不会渲染。如果用户要求“大型”场景但 parcel 数量未更改,则所有实体仍须位于原有边界内。[0, maxZ] - 对于每个 实体:检查 GLB 是否包含动画(嵌入文件中的 clip 名称)。如果包含,则该实体上必须存在
GltfContainer组件。带动画但没有 Animator 的模型会静默循环播放第一个 clip,且无法控制。core::Animator - 对于每个 实体:检查 GLB 是否包含 collision mesh(名称包含字符串
GltfContainer的任何 mesh)。如果包含,则将_collider设置为invisibleMeshesCollisionMask(CL_POINTER + CL_PHYSICS)以激活它们。如果不存在内置 collider,则评估是否需要添加3box/sphere,以覆盖模型的大致形状(用于可行走表面、墙壁或可点击对象)。core::MeshCollider - 如果 Composite 中任意位置存在 、
asset-packs::Actions或asset-packs::Triggers,则实体 0 上必须存在asset-packs::States,且asset-packs::Counter等于 Actions/Triggers/States 数据中使用的最高value(它是这些 ID 的分配器)id - 不存在 、
{self}或占位字符串——所有值均已解析为具体值{assetPath} - 组件名称使用基础名称(例如 ,而不是
asset-packs::Actions)。绝不使用asset-packs::Actions-v1等版本化后缀。-v3 - 仅使用 core 组件的 Composite 不需要额外 library。如果包含 组件,较旧的 SDK 需要将
asset-packs::*作为项目依赖;当前 SDK 会回退使用@dcl/asset-packs内置的副本。@dcl/inspector
Post-Write Validation
编辑模式检查清单(Composite 已包含 inspector::*
)
inspector::*After writing the composite, run the SDK build to verify:
bash
npx sdk-commands buildThe build must pass with zero errors. If it fails, the composite is invalid. Common errors:
- (older/released SDKs word this as
Composite references undefined component "X". Ensure provider.schemas was registered pre-seal via setCompositeProvider().) → missing"X is not defined and there is no schema to define it"on non-core component, orjsonSchemacomponent that shouldn't be thereinspector::* - 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,或者保持不变,让 Creator Hub 在下次保存时重新生成。不要手动编辑自动生成的头部。EntityNames - 未删除或剥离已有的 、
inspector::Nodes、inspector::SceneMetadata-*、inspector::Selection、inspector::TransformConfig或composite::root。这些内容由 Creator Hub 管理,必须保留。asset-packs::ActionTypes - 除非 中的 parcel 发生变化,否则
scene.json保持不变;如果发生变化,layout 区块必须与inspector::SceneMetadata-*一致。scene.json - 保留实体 (PlayerEntity)和
1(CameraEntity)仍作为独立的顶层2/{ "entity": 1, "children": [] }条目存在于{ "entity": 2, "children": [] }中。它们是独立的树根——不得出现在实体inspector::Nodes的0数组或任何其他children数组中。children
**验证命令(编辑模式):**编辑后, 中存在的每个实体 ID 都应该同时满足:
core::Transform.data- 作为顶层 条目出现在
{ "entity": <id>, ... }中;以及inspector::Nodes.data["0"].json.value - 出现在同一 列表中恰好一个
value数组内(即其父实体的 children)。children
实体 、 和 是规则 2 的例外——它们是树根,不出现在任何 数组中。
012children遗漏这些条目是“实体已渲染,但在 Creator Hub 实体树中不可见”的根本原因。
—
写入后验证
—
写入 Composite 后,运行 SDK 构建以进行验证:
bash
npx sdk-commands build构建必须通过且零错误。如果失败,则 Composite 无效。常见错误包括:
- (较旧或已发布的 SDK 会将其表述为
Composite references undefined component "X". Ensure provider.schemas was registered pre-seal via setCompositeProvider().)→ 非 core 组件缺少"X is not defined and there is no schema to define it",或包含了本不应存在的jsonSchema组件inspector::* - TypeScript 错误 → 修复生成的脚本