advanced-rendering
Advanced Rendering in Decentraland
When to Use Which Rendering Feature
| 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:
- Need text on screen? → Use build-ui (React-ECS Label) instead
- Need text in 3D space? →
TextShape(+Billboardto face camera) - Need glowing/transparent materials? →
Material.setPbrMaterialwith emissive/transparency - Need to override material on a model node? →
GltfNodeModifierswithmodifiersarray
Billboard (Face the Camera)
Make 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
})
Billboard Modes
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
- Prefer
BM_YoverBM_ALLfor most use cases — it looks more natural and is cheaper to render. BM_ALLis useful for particles or effects that should always directly face the camera.
TextShape (3D Text)
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
})
Text Alignment Options
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
Floating Label (Billboard + TextShape)
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
})
Advanced PBR Materials
Metallic and Roughness
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
})
Transparency
// 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/scene/Images/cutout.png' }),
transparencyMode: MaterialTransparencyMode.MTM_ALPHA_TEST,
alphaTest: 0.5
})
Emissive (Glow Effects)
// 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/scene/Images/diffuse.png' }),
emissiveTexture: Material.Texture.Common({ src: 'assets/scene/Images/emissive.png' }),
emissiveIntensity: 1.0,
emissiveColor: Color3.White()
})
Texture Maps
Material.setPbrMaterial(entity, {
texture: Material.Texture.Common({ src: 'assets/scene/Images/diffuse.png' }),
bumpTexture: Material.Texture.Common({ src: 'assets/scene/Images/normal.png' }),
emissiveTexture: Material.Texture.Common({ src: 'assets/scene/Images/emissive.png' })
})
GltfContainer Collision Masks
Use collision masks to control which collision layers respond to the different mesh layers in a GLTF model. GLTF models have two mesh layers: visible meshes (what players see rendered), and invisible layers (collider meshes, named internally with _collider):
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
})
VisibilityComponent
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)
propagateToChildren
Set propagateToChildren: true on a VisibilityComponent to apply visibility to all children in the hierarchy at once. This avoids having to mark every child entity individually:
VisibilityComponent.create(parentEntity, { visible: false, propagateToChildren: true })
Rules:
- If a child has its own
VisibilityComponent, that overrides what the parent propagates. - If a child has no
VisibilityComponent, it inherits from the nearest ancestor withpropagateToChildren: true.
Per-Node Modifiers (GltfNodeModifiers)
Override material or shadow casting on specific nodes within a GLTF model:
import { GltfNodeModifiers } from '@dcl/sdk/ecs'
GltfNodeModifiers.create(entity, {
modifiers: [
{
path: 'RootNode/Armor', // GLTF hierarchy path
castShadows: false // Disable shadow casting for this node
}
]
})
To override the materials or shadow casting of the entire model, set the path to ''.
import { GltfNodeModifiers } from '@dcl/sdk/ecs'
GltfNodeModifiers.create(entity, {
modifiers: [
{
path: '',
material: {
material: {
$case: 'pbr',
pbr: {
albedoColor: Color4.Red(),
},
},
},
}
]
})
Avatar Texture
Generate a texture from a player's avatar portrait:
Material.setPbrMaterial(portraitFrame, {
texture: Material.Texture.Avatar({ userId: '0x...' })
})
This will fetch a thumbnail image with a closeup of the player's face, wearing the wearables that this player currently has on.
Texture Modes
Control how textures are filtered and wrapped:
import { TextureFilterMode, TextureWrapMode } from '@dcl/sdk/ecs'
Material.setPbrMaterial(entity, {
texture: Material.Texture.Common({
src: 'assets/scene/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).
## Texture tweens
You can use tweens to make a texture slide sideways or shrink or zoom in, this can be used to achieve very cool effects.
Material.setPbrMaterial(myEntity, {
texture: Material.Texture.Common({
src: 'materials/water.png',
wrapMode: TextureWrapMode.TWM_REPEAT,
}),
})
// move continuously
Tween.setTextureMoveContinuous(myEntity, Vector2.create(0, 1), 1)
You can also make a texture move once, lasting a specific duration
// slide once, for 1 second
Tween.setTextureMove(myEntity, Vector2.create(0, 0), Vector2.create(0, 1), 1000)
FlatMaterial Accessors
The Material component provides shortcut methods that skip the nested union structure, making material access more ergonomic:
| Method | Returns | Throws if no material? |
|---|---|---|
Material.getFlat(entity) |
Read-only FlatMaterial |
Yes |
Material.getFlatOrNull(entity) |
Read-only FlatMaterial | null |
No |
Material.getFlatMutable(entity) |
Read/write FlatMaterial |
Yes |
Material.getFlatMutableOrNull(entity) |
Read/write FlatMaterial | null |
No |
// Read a property safely
const src = Material.getFlatOrNull(entity)?.texture?.src
// Mutate a texture in-place without knowing PBR vs Basic
Material.getFlatMutableOrNull(entity)!.texture = Material.Texture.Common({ src: 'assets/scene/Images/new.png' })
Best Practices
- Use
BillboardMode.BM_Yinstead ofBM_ALL— looks more natural and renders faster - Keep
fontSizereadable (16-32 for in-world text) - Add
outlineColorandoutlineWidthto TextShape for legibility against any background - Use
emissiveColorwith a darkalbedoColorfor maximum glow visibility MTM_ALPHA_TESTis cheaper thanMTM_ALPHA_BLEND— use cutout when smooth transparency isn't needed- Combine Billboard + TextShape for floating name labels above NPCs or objects
- Use VisibilityComponent for LOD systems instead of removing/re-adding entities
More from decentraland/sdk-skills
decentraland-sdk-skills
Build, extend, and deploy Decentraland SDK7 scenes. Contains agent behavioral guidelines, the composite-first rule, and an index of all topic skills with reference files. Install with a single command — no flags needed.
18build-ui
Build 2D screen-space UI for Decentraland scenes using React-ECS (JSX). Create HUDs, menus, health bars, scoreboards, dialogs, buttons, inputs, and dropdowns. Use when the user wants screen overlays, on-screen UI, HUD elements, menus, or form inputs. Do NOT use for 3D in-world text (see advanced-rendering) or clickable 3D objects (see add-interactivity).
3advanced-input
System-level input polling and player movement control in Decentraland. Covers inputSystem (isTriggered/isPressed for held keys, WASD polling), InputModifier (freeze/restrict player movement), PointerLock (cursor capture detection), PrimaryPointerInfo (cursor screen coords and world ray), and number-key action bar patterns. Use when the user wants continuous key polling, WASD-controlled entities, to freeze the player during a cutscene, FPS-style cursor lock, or multi-key combo patterns. For event-driven clicks and hover on entities see add-interactivity.
3nft-blockchain
NFT display and blockchain interaction in Decentraland. NftShape (framed NFT artwork), wallet checks (getPlayer, isGuest), signedFetch (authenticated requests), smart contract interaction (eth-connect, createEthereumProvider), and RPC calls. Use when the user wants NFTs, blockchain, wallet, smart contracts, Web3, crypto, or token gating. Do NOT use for player avatar data or emotes (see player-avatar).
3player-physics
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), timed forces, and repulsion fields. Use when the user wants launch pads, knockback on hit, wind zones, gravity fields, or any scene-applied force on the player. Do NOT use for player movement speed (see player-avatar) or platform movement (see animations-tweens).
3camera-control
Control camera behavior in Decentraland scenes. CameraMode detection (first/third person, onChange listener), CameraModeArea (force a mode inside a box), VirtualCamera (cinematic scripted cameras with Speed/Time transitions and lookAtEntity), MainCamera (activate/deactivate virtual cameras), and camera vs collider interactions (CL_PHYSICS + CL_POINTER). Use when the user wants camera control, cutscenes, cinematic views, forced camera modes, or camera tracking. Do NOT use for input restriction during cutscenes (see advanced-input for InputModifier) or cursor lock detection (see advanced-input for PointerLock).
3