skills/add-3d-models/SKILL.md
Add 3D models (.glb/.gltf) to a Decentraland scene using GltfContainer. Covers loading, positioning, scaling, colliders, parenting, and browsing 5,700+ free assets from the OpenDCL catalog. Use when the user wants to add models, import GLB files, find free 3D assets, or set up model colliders. Do NOT use for materials/textures (see advanced-rendering) or model animations (see animations-tweens).
npx skillsauth add dcl-regenesislabs/opendcl add-3d-modelsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
main-entities.tsA 3D model placed at author time is a static visible entity. Declare it in main-entities.ts, not via engine.addEntity() in src/index.ts. The build compiles main-entities.ts into main.crdt, the engine preloads it before main() runs, and the editor can drag/rotate the model interactively.
// main-entities.ts
import type { Scene } from '@dcl/sdk/scene-types'
export const scene = {
my_model: {
components: {
Transform: { position: { x: 8, y: 0, z: 8 } },
GltfContainer: {
src: 'models/myModel.glb',
visibleMeshesCollisionMask: 3 // CL_PHYSICS | CL_POINTER
}
}
}
} satisfies Scene
Always set
visibleMeshesCollisionMaskonGltfContainer. Catalog models don't include separate collider meshes — using the visible mesh as the collider ensures the model is solid and clickable. Use the integer value (ColliderLayer.CL_PHYSICS = 1,CL_POINTER = 2, both =3) insidemain-entities.tssince enums aren't allowed in the literal.
When to use engine.addEntity() in src/index.ts instead: only when the model is spawned dynamically (procedurally placed in a loop, dropped on an event, gated by NFT ownership, etc.). For static props, always use main-entities.ts.
Place model files in a models/ directory at the project root:
project/
├── models/
│ ├── building.glb
│ ├── tree.glb
│ └── furniture/
│ ├── chair.glb
│ └── table.glb
├── src/
│ └── index.ts
└── scene.json
// main-entities.ts — declared inline with GltfContainer
building: {
components: {
Transform: { position: { x: 8, y: 0, z: 8 } },
GltfContainer: {
src: 'models/building.glb',
visibleMeshesCollisionMask: 3, // CL_PHYSICS | CL_POINTER
invisibleMeshesCollisionMask: 1 // CL_PHYSICS
}
}
}
For basic shapes (no GLTF), add MeshCollider:
// main-entities.ts
invisible_wall: {
components: {
Transform: { position: { x: 0, y: 1, z: 8 }, scale: { x: 0.1, y: 2, z: 16 } },
MeshCollider: { mesh: { $case: 'box', box: { uvs: [] } } }
}
}
Other shapes: { $case: 'sphere', sphere: {} }, { $case: 'plane', plane: { uvs: [] } }, { $case: 'cylinder', cylinder: {} }.
undefined in Transform FieldsThe SDK serializer crashes if any Transform field (position, rotation, scale) is present but undefined. Omit the key entirely instead — both in main-entities.ts literals and in any runtime helpers. In main-entities.ts this is natural (you just don't write the field).
// main-entities.ts
big_statue: {
components: {
Transform: { position: { x: 8, y: 0, z: 8 }, scale: { x: 2, y: 2, z: 2 } },
GltfContainer: { src: 'models/statue.glb' }
}
}
Quaternions are { x, y, z, w }. For Quaternion.fromEulerDegrees(0, 90, 0) the equivalent literal is { x: 0, y: 0.7071, z: 0, w: 0.7071 }. If you need exact-degree rotations and don't want to compute by hand, set rotation to identity in main-entities.ts and rotate at runtime in src/index.ts using Transform.getMutable(entity).rotation = Quaternion.fromEulerDegrees(0, 90, 0).
Reference the parent by name (a string key from the same scene object). The build resolves names to entity IDs in a second pass.
// main-entities.ts
character: {
components: {
Transform: { position: { x: 8, y: 0, z: 8 } },
GltfContainer: { src: 'models/character.glb' }
}
},
hat: {
components: {
Transform: {
position: { x: 0, y: 2, z: 0 }, // 2m above parent's origin
parent: 'character'
},
GltfContainer: { src: 'models/hat.glb' }
}
}
The catalog file is at {baseDir}/references/model-catalog.md. Each line has this format:
slug | dims | tris | size | category/sub | description [tags] [anim: clips] | curl command | preview: thumbnail_url
Search with one keyword at a time — try the most specific word first:
grep -i "zombie" {baseDir}/references/model-catalog.md
If no results, try synonyms, broader terms, or related words:
Browse all categories to discover what's available:
grep "^##" {baseDir}/references/model-catalog.md
Search within a specific category:
grep "^##\|chair" {baseDir}/references/model-catalog.md
models/GltfContainer.create(entity, { src: 'models/{slug}.glb' })[anim: ...]), use the Animator component to play thempreview: URL) to see what it looks like# Search for zombie models
grep -i "zombie" {baseDir}/references/model-catalog.md
# Found: zombie-purple | 2.8×2.9×0.5m | 1472 tri | 271KB | character/zombie | ...
# [anim: Tpose, ZombieAttack, ZombieUP, ZombieWalk]
# preview: https://models.dclregenesislabs.xyz/blobs/bafkrei...
# Download the model
curl -o models/zombie-purple.glb "https://models.dclregenesislabs.xyz/blobs/bafybeiffc..."
// main-entities.ts — declare the entity with all its initial state
import type { Scene } from '@dcl/sdk/scene-types'
export const scene = {
zombie: {
components: {
Transform: { position: { x: 8, y: 0, z: 8 } },
GltfContainer: { src: 'models/zombie-purple.glb' },
Animator: {
states: [
{ clip: 'ZombieWalk', playing: true, loop: true },
{ clip: 'ZombieAttack', playing: false, loop: false }
]
}
}
}
} satisfies Scene
To switch animations at runtime (e.g., trigger attack on click), use src/index.ts:
import { engine, Animator } from '@dcl/sdk/ecs'
export function main() {
const zombie = engine.getEntityOrNullByName('zombie')
if (zombie) Animator.playSingleAnimation(zombie, 'ZombieAttack')
}
Important:
GltfContaineronly works with local files. Never use external URLs for the modelsrcfield. Always download models intomodels/first. Nevercdinto the models directory. Always run curl from the project root withcurl -o models/slug.glb "URL". Do NOT usecd models && curl -o slug.glb.
Load-state polling is runtime behavior — put it in src/index.ts and reference the entity by name:
import { engine, GltfContainerLoadingState, LoadingState } from '@dcl/sdk/ecs'
export function main() {
engine.addSystem(() => {
const model = engine.getEntityOrNullByName('zombie')
if (!model) return
const state = GltfContainerLoadingState.getOrNull(model)
if (state?.currentState === LoadingState.FINISHED) {
console.log('Model loaded successfully')
} else if (state?.currentState === LoadingState.FINISHED_WITH_ERROR) {
console.log('Model failed to load')
}
})
}
| Problem | Cause | Solution |
|---------|-------|----------|
| Model not visible | Wrong file path | Verify the file exists at the exact path relative to project root (e.g., models/myModel.glb) |
| Model not visible | Position outside scene boundaries | Check Transform position is within 0-16 per parcel. Center of 1-parcel scene is (8, 0, 8) |
| Model not visible | Scale is 0 or very small | Check Transform.scale — default is (1,1,1). Try larger values if model was exported very small |
| Model not visible | Behind the camera | Move the avatar or rotate to look in the model's direction |
| Model loads but looks wrong | Y-up vs Z-up mismatch | Decentraland uses Y-up. Re-export from Blender with "Y Up" checked |
| "FINISHED_WITH_ERROR" load state | Corrupted or unsupported .glb | Re-export the model. Use .glb (binary GLTF) format. Ensure no unsupported extensions |
| Clicking model does nothing | Missing collider | Add visibleMeshesCollisionMask: ColliderLayer.CL_POINTER to GltfContainer or add MeshCollider |
Need to optimize models for scene limits? See the optimize-scene skill for triangle budgets and LOD patterns. Need animations from your model? See the animations-tweens skill for playing GLTF animation clips with Animator.
.glb format (binary GLTF) — smaller than .gltfAnimator componentdevelopment
Capture screenshots of the running Decentraland preview to verify scene changes visually. Covers camera movement, interaction actions, and visual debugging. Use when the preview is running and you need to check what the scene looks like, debug visual issues, or verify layout. Do NOT use for code changes (make changes first, then screenshot).
development
Cross-cutting runtime APIs for Decentraland SDK7 scenes. Use when the user needs async operations (executeTask), HTTP requests (fetch, signedFetch), WebSocket connections, timers, realm/scene detection, restricted actions (movePlayerTo, teleportTo, triggerEmote, openExternalUrl), portable experiences, or the testing framework. Do NOT use for UI (see build-ui), multiplayer sync (see multiplayer-sync), or avatar/player data (see player-avatar).
development
Apply physics forces to the player in Decentraland scenes. Impulses (one-shot pushes), knockback (push away from a point with falloff), continuous forces (wind tunnels, anti-gravity, lift, levitation, hover), timed forces, and repulsion fields. Use when the user wants launch pads, knockback on hit, wind zones, gravity fields, jumps, lifting/floating the player, pushing the player up/sideways/back, hover effects, or any scene-applied force on the player. THIS is also the right skill when an agent's first instinct is to mutate `Transform` on `engine.PlayerEntity` to move/lift/push the player — that does NOT work (the player Transform is engine-controlled and read-only); use the Physics API instead. Do NOT use for player movement speed (see player-avatar AvatarLocomotionSettings) or platform movement (see animations-tweens).
data-ai
Player and avatar system in Decentraland. Read player position/profile, customize appearance (AvatarBase), trigger emotes (triggerEmote/triggerSceneEmote), read equipped wearables (AvatarEquippedData), attach objects to players (AvatarAttach), create NPC avatars (AvatarShape), avatar modifier areas, and locomotion settings. Use when the user wants player data, emotes, wearables, NPC avatars, avatar attachments, or movement speed changes. Do NOT use for wallet/blockchain interactions (see nft-blockchain).