skills/advanced-rendering/SKILL.md
Advanced rendering in Decentraland scenes. Billboard (face camera), TextShape (3D world text), PBR materials (metallic, roughness, transparency, emissive glow), GltfNodeModifiers (per-node shadow/material overrides), VisibilityComponent (show/hide entities), and texture modes. Use when the user wants billboards, floating labels, 3D text, material effects, glow, transparency, or model node control. Do NOT use for screen-space UI (see build-ui) or loading 3D models (see add-3d-models).
npx skillsauth add dcl-regenesislabs/opendcl advanced-renderingInstall 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.
Billboard, TextShape, Material, MeshRenderer, GltfContainer, VisibilityComponent, and GltfNodeModifiers are all supported in main-entities.ts — declare the visual entity (sign, label, billboard, glowing prop, model with per-node overrides) fully there with its visual components. Examples below that show engine.addEntity() followed by Transform.create + MeshRenderer / Billboard / TextShape are pre-main-entities.ts patterns — translate them by moving the entity declaration into main-entities.ts and keeping only runtime modifications (e.g., VisibilityComponent.getMutable(entity).visible = false) in src/index.ts.
// Example: floating label
// main-entities.ts
shop_label: {
components: {
Transform: { position: { x: 8, y: 3, z: 8 } },
TextShape: { text: 'OPEN', fontSize: 4, textColor: { r: 1, g: 1, b: 1, a: 1 } },
Billboard: { billboardMode: 7 } // BM_ALL
}
}
| Need | Component | When |
|------|-----------|------|
| Entity faces the camera | Billboard | Name tags, signs, sprite-like objects |
| Text in the 3D world | TextShape | Labels, signs, floating text above entities |
| Custom material appearance | Material.setPbrMaterial | Metallic, rough, transparent, emissive surfaces |
| Show/hide without removing | VisibilityComponent | LOD systems, toggling objects, conditional display |
| Modify GLTF model nodes | GltfNodeModifiers | Override materials or shadow casting on specific mesh nodes |
Decision flow:
TextShape (+ Billboard to face camera)Material.setPbrMaterial with emissive/transparencyGltfNodeModifiers with modifiers arrayMake entities always rotate to face the player's camera:
import { engine, Transform, Billboard, BillboardMode, MeshRenderer } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'
const sign = engine.addEntity()
Transform.create(sign, { position: Vector3.create(8, 2, 8) })
MeshRenderer.setPlane(sign)
// Rotate only on Y axis (most common — stays upright)
Billboard.create(sign, {
billboardMode: BillboardMode.BM_Y
})
BillboardMode.BM_Y // Rotate on Y axis only (stays upright) — most common
BillboardMode.BM_ALL // Rotate on all axes (fully faces camera)
BillboardMode.BM_X // Rotate on X axis only
BillboardMode.BM_Z // Rotate on Z axis only
BillboardMode.BM_NONE // No billboard rotation
BM_Y over BM_ALL for most use cases — it looks more natural and is cheaper to render.BM_ALL is useful for particles or effects that should always directly face the camera.Render text directly in 3D space:
import { engine, Transform, TextShape, TextAlignMode } from '@dcl/sdk/ecs'
import { Vector3, Color4 } from '@dcl/sdk/math'
const label = engine.addEntity()
Transform.create(label, { position: Vector3.create(8, 3, 8) })
TextShape.create(label, {
text: 'Hello World!',
fontSize: 24,
textColor: Color4.White(),
outlineColor: Color4.Black(),
outlineWidth: 0.1,
textAlign: TextAlignMode.TAM_MIDDLE_CENTER
})
TextAlignMode.TAM_TOP_LEFT
TextAlignMode.TAM_TOP_CENTER
TextAlignMode.TAM_TOP_RIGHT
TextAlignMode.TAM_MIDDLE_LEFT
TextAlignMode.TAM_MIDDLE_CENTER
TextAlignMode.TAM_MIDDLE_RIGHT
TextAlignMode.TAM_BOTTOM_LEFT
TextAlignMode.TAM_BOTTOM_CENTER
TextAlignMode.TAM_BOTTOM_RIGHT
Combine Billboard and TextShape to create labels that always face the player:
const floatingLabel = engine.addEntity()
Transform.create(floatingLabel, { position: Vector3.create(8, 4, 8) })
TextShape.create(floatingLabel, {
text: 'NPC Name',
fontSize: 16,
textColor: Color4.White(),
outlineColor: Color4.Black(),
outlineWidth: 0.08,
textAlign: TextAlignMode.TAM_BOTTOM_CENTER
})
Billboard.create(floatingLabel, {
billboardMode: BillboardMode.BM_Y
})
import { engine, Transform, MeshRenderer, Material, MaterialTransparencyMode } from '@dcl/sdk/ecs'
import { Color4, Color3 } from '@dcl/sdk/math'
// Shiny metal
Material.setPbrMaterial(entity, {
albedoColor: Color4.create(0.8, 0.8, 0.9, 1),
metallic: 1.0,
roughness: 0.1
})
// Rough stone
Material.setPbrMaterial(entity, {
albedoColor: Color4.create(0.5, 0.5, 0.5, 1),
metallic: 0.0,
roughness: 0.9
})
// Alpha blend — smooth transparency
Material.setPbrMaterial(entity, {
albedoColor: Color4.create(1, 0, 0, 0.5), // 50% transparent red
transparencyMode: MaterialTransparencyMode.MTM_ALPHA_BLEND
})
// Alpha test — cutout (binary visible/invisible based on threshold)
Material.setPbrMaterial(entity, {
texture: Material.Texture.Common({ src: 'assets/cutout.png' }),
transparencyMode: MaterialTransparencyMode.MTM_ALPHA_TEST,
alphaTest: 0.5
})
// Glowing material (emissiveColor uses Color3, not Color4)
Material.setPbrMaterial(entity, {
albedoColor: Color4.create(0, 0, 0, 1),
emissiveColor: Color3.create(0, 1, 0), // Green glow
emissiveIntensity: 2.0
})
// Emissive with texture
Material.setPbrMaterial(entity, {
texture: Material.Texture.Common({ src: 'assets/diffuse.png' }),
emissiveTexture: Material.Texture.Common({ src: 'assets/emissive.png' }),
emissiveIntensity: 1.0,
emissiveColor: Color3.White()
})
Material.setPbrMaterial(entity, {
texture: Material.Texture.Common({ src: 'assets/diffuse.png' }),
bumpTexture: Material.Texture.Common({ src: 'assets/normal.png' }),
emissiveTexture: Material.Texture.Common({ src: 'assets/emissive.png' })
})
Control visibility and collision of specific mesh layers within a GLTF model using collision masks:
import { engine, Transform, GltfContainer, ColliderLayer } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'
const model = engine.addEntity()
Transform.create(model, { position: Vector3.create(4, 0, 4) })
GltfContainer.create(model, {
src: 'models/myModel.glb',
visibleMeshesCollisionMask: ColliderLayer.CL_PHYSICS | ColliderLayer.CL_POINTER,
invisibleMeshesCollisionMask: ColliderLayer.CL_PHYSICS
})
Show or hide entities without removing them:
import { engine, VisibilityComponent } from '@dcl/sdk/ecs'
// Hide an entity
VisibilityComponent.create(entity, { visible: false })
// Toggle visibility
const visibility = VisibilityComponent.getMutable(entity)
visibility.visible = !visibility.visible
// Useful for LOD (Level of Detail)
function lodSystem() {
const playerPos = Transform.get(engine.PlayerEntity).position
for (const [entity, transform] of engine.getEntitiesWith(Transform, MeshRenderer)) {
const distance = Vector3.distance(playerPos, transform.position)
if (distance > 30) {
VisibilityComponent.createOrReplace(entity, { visible: false })
} else {
VisibilityComponent.createOrReplace(entity, { visible: true })
}
}
}
engine.addSystem(lodSystem)
Override material or shadow casting on specific nodes within a GLTF model. Supported in main-entities.ts:
// main-entities.ts
armored_knight: {
components: {
Transform: { position: { x: 8, y: 0, z: 8 } },
GltfContainer: { src: 'models/knight.glb' },
GltfNodeModifiers: {
modifiers: [
{
path: 'RootNode/Armor', // GLTF hierarchy path
castShadows: false // disable shadows for this node
}
]
}
}
}
Whole-model override: pass path: '' (empty string) to apply the modifier to every node in the model. Useful for re-skinning an entire model with a single material swap:
red_team_unit: {
components: {
Transform: { position: { x: 4, y: 0, z: 8 } },
GltfContainer: { src: 'models/unit.glb' },
GltfNodeModifiers: {
modifiers: [
{
path: '',
material: {
material: {
$case: 'pbr',
pbr: { albedoColor: { r: 1, g: 0, b: 0, a: 1 } }
}
}
}
]
}
}
}
Animate a material's texture offset/tiling over time — useful for water, lava, conveyor belts, scrolling backgrounds. Tween component is supported in main-entities.ts:
// main-entities.ts — continuous texture scroll
conveyor: {
components: {
Transform: { position: { x: 8, y: 1, z: 8 } },
MeshRenderer: { mesh: { $case: 'plane', plane: { uvs: [] } } },
Material: { material: { $case: 'pbr', pbr: { texture: { tex: { $case: 'texture', texture: { src: 'images/belt.png' } } } } } },
Tween: {
duration: 2000,
easingFunction: 0, // EF_LINEAR
mode: {
$case: 'textureMoveContinuous',
textureMoveContinuous: { direction: { x: 1, y: 0 }, speed: 0.5 }
// movementType defaults to 0 = TMT_OFFSET; use 1 = TMT_TILING to scale instead
}
}
}
}
For a finite from-to texture move, use { $case: 'textureMove', textureMove: { start: { x: 0, y: 0 }, end: { x: 1, y: 0 }, movementType: 0 } } with a duration.
Runtime helpers (in src/index.ts): Tween.setTextureMove(entity, from, to, durationMs) and Tween.setTextureMoveContinuous(entity, direction, speed).
Generate a texture from a player's avatar:
Material.setPbrMaterial(portraitFrame, {
texture: Material.Texture.Avatar({ userId: '0x...' })
})
Control how textures are filtered and wrapped:
import { TextureFilterMode, TextureWrapMode } from '@dcl/sdk/ecs'
Material.setPbrMaterial(entity, {
texture: Material.Texture.Common({
src: 'images/pixel-art.png',
filterMode: TextureFilterMode.TFM_POINT, // crisp pixels (no smoothing)
wrapMode: TextureWrapMode.TWM_REPEAT // tile the texture
})
})
Filter modes: TFM_POINT (pixelated), TFM_BILINEAR (smooth), TFM_TRILINEAR (smoothest).
Wrap modes: TWM_REPEAT (tile), TWM_CLAMP (stretch edges), TWM_MIRROR (mirror tile).
BillboardMode.BM_Y instead of BM_ALL — looks more natural and renders fasterfontSize readable (16-32 for in-world text)outlineColor and outlineWidth to TextShape for legibility against any backgroundemissiveColor with a dark albedoColor for maximum glow visibilityMTM_ALPHA_TEST is cheaper than MTM_ALPHA_BLEND — use cutout when smooth transparency isn't neededdevelopment
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).