advanced-rendering
Original:🇺🇸 English
Not Translated
Advanced rendering in Decentraland scenes. Billboard, TextShape, PBR materials, GltfNodeModifiers, and VisibilityComponent. 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).
2installs
Sourcedecentraland/sdk-skills
Added on
NPX Install
npx skill4agent add decentraland/sdk-skills advanced-renderingSKILL.md Content
Advanced Rendering in Decentraland
When to Use Which Rendering Feature
| Need | Component | When |
|---|---|---|
| Entity faces the camera | | Name tags, signs, sprite-like objects |
| Text in the 3D world | | Labels, signs, floating text above entities |
| Custom material appearance | | Metallic, rough, transparent, emissive surfaces |
| Show/hide without removing | | LOD systems, toggling objects, conditional display |
| Modify GLTF model nodes | | 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? → (+
TextShapeto face camera)Billboard - Need glowing/transparent materials? → with emissive/transparency
Material.setPbrMaterial - Need to override material on a model node? → with
GltfNodeModifiersarraymodifiers
Billboard (Face the Camera)
Make entities always rotate to face the player's camera:
typescript
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
typescript
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 over
BM_Yfor 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.
BM_ALL - No flag. The SDK7
oppositeDirectioncomponent exposes onlyBillboard— there is no way to invert which model face points at the camera. If a model shows its back instead of its front, rotate the model 180° on Y (billboardMode). On a parent-Billboard + child-model setup, apply the rotation to the child — the Billboard owns the parent's rotation.Quaternion.fromEulerDegrees(0, 180, 0) - Porting note: SDK6 → SDK7 ports occasionally show a billboarded model facing away from the camera that was correct under SDK6. The two SDKs appear to disagree on which face the billboard points at the camera. Same fix — rotate the displayed model 180° on Y. See [[migrate-sdk6-to-sdk7]] (Common Pitfalls) for context.
Face another entity — targetEntity
targetEntityBillboardtargetEntity?: Entitytypescript
// Face a specific entity instead of the camera
Billboard.create(card, { targetEntity: sphere })
// Yaw-only tracking of a target (BM_Y respected while targeting)
Billboard.create(card, { targetEntity: target, billboardMode: BillboardMode.BM_Y })
// Retarget at runtime
Billboard.getMutable(card).targetEntity = otherEntity- Unset (default) → faces the main camera, exactly as before. is fully backwards-compatible.
targetEntity - Setting to the camera reserved entity (
targetEntity, idengine.CameraEntity) is equivalent to leaving it unset.2 - still applies:
billboardModewith aBM_Yyaws to face the target on the Y axis only.targetEntity - Gotcha: if the referenced target entity does not exist or is deleted, billboard reorientation is disabled (the entity freezes at its last orientation) until the target exists again.
- Multiplayer: a camera-facing billboard is computed locally per player (each player sees it facing themselves, nothing is synced). A billboard instead faces the same way for all players, because the target's position is scene state. Use
targetEntitywhen every player must see the same orientation.targetEntity
TextShape (3D Text)
Render text directly in 3D space:
typescript
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
})- Keep readable — 16-32 for in-world text.
fontSize - Always add and
outlineColorfor legibility against any background.outlineWidth
Text Alignment Options
typescript
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_RIGHTFor the floating-label pattern (Billboard + TextShape combined into a camera-facing label), see the Floating Label (Billboard + TextShape) section in .
{baseDir}/references/rendering-patterns.mdAdvanced PBR Materials
Metallic and Roughness
typescript
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
typescript
// 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/Images/cutout.png' }),
transparencyMode: MaterialTransparencyMode.MTM_ALPHA_TEST,
alphaTest: 0.5
})- is cheaper than
MTM_ALPHA_TEST— use cutout when smooth transparency isn't needed.MTM_ALPHA_BLEND
Emissive (Glow Effects)
typescript
// 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/Images/diffuse.png' }),
emissiveTexture: Material.Texture.Common({ src: 'assets/Images/emissive.png' }),
emissiveIntensity: 1.0,
emissiveColor: Color3.White()
})- Use with a dark
emissiveColorfor maximum glow visibility.albedoColor
Texture Maps
typescript
Material.setPbrMaterial(entity, {
texture: Material.Texture.Common({ src: 'assets/Images/diffuse.png' }),
bumpTexture: Material.Texture.Common({ src: 'assets/Images/normal.png' }),
emissiveTexture: Material.Texture.Common({ src: 'assets/Images/emissive.png' })
})castShadows
Both and accept (default ). Set to stop a surface from casting shadows without changing its appearance:
setPbrMaterialsetBasicMaterialcastShadows: booleantruefalsetypescript
Material.setPbrMaterial(entity, { albedoColor: Color4.Green(), castShadows: false })For disabling shadows on a specific node inside a GLTF model, use with instead (see below).
GltfNodeModifierscastShadows: falseGltfContainer 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):
typescript
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:
typescript
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.visibleFor a distance-based LOD (Level of Detail) system that toggles per frame, see the LOD via VisibilityComponent section in .
VisibilityComponent{baseDir}/references/rendering-patterns.mdpropagateToChildren
Set on a to apply visibility to all children in the hierarchy at once. This avoids having to mark every child entity individually:
propagateToChildren: trueVisibilityComponenttypescript
VisibilityComponent.create(parentEntity, { visible: false, propagateToChildren: true })Rules (verified against the test scene):
1,0-visibility-comp-propagation- If a child has its own , that value wins regardless of what an ancestor propagates — even if the child's own
VisibilityComponentispropagateToChildren, the child stays at its ownfalsevalue and does not re-inherit the parent's.visible - If a child has no , it inherits from the nearest ancestor with
VisibilityComponent.propagateToChildren: true - A child that overrides an invisible parent to can itself set
visible: trueto force its own subtree visible again — propagation re-evaluates at each node that carries apropagateToChildren: true.VisibilityComponent - Propagation follows the live hierarchy: re-parenting an entity at runtime changes which ancestor's propagated visibility applies to it.
Transform.parent
Per-Node Modifiers (GltfNodeModifiers)
Override material or shadow casting on specific nodes within a GLTF model:
typescript
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 material or shadow casting of the entire model () — including the nested shape — see the GltfNodeModifiers — Whole-Model Material Override section in .
path: ''material: { material: { $case: 'pbr' | 'unlit', ... } }{baseDir}/references/rendering-patterns.mdModifier details (from the test scene):
74,-8-gltfnodemodifier- is the GLTF node hierarchy path,
path-separated (e.g./).Scene_root/shark_skeleton/Sphere/Sphere.001targets the whole model; a nested path targets one node and its descendants.path: '' - accepts either
material(full PBR:$case: 'pbr',albedoColor,emissiveColor, textures, …) oremissiveIntensity($case: 'unlit', …). Different nodes in the samediffuseColorarray can use different cases.modifiers - Textures work here too, including video: .
pbr: { texture: Material.Texture.Video({ videoPlayerEntity: someEntityWithVideoPlayer }) } - per node (no
castShadows: falseneeded) disables shadow casting for that node only.material - One array can contain many entries, each targeting a different
modifiersin a single call.path - Debug trick: passing a that does not exist logs the model's full GLTF node hierarchy to the scene console — use a deliberately wrong path to discover the correct node names.
path - Update with ; remove all overrides with
GltfNodeModifiers.createOrReplace(entity, { modifiers: [...] }).GltfNodeModifiers.deleteFrom(entity)
Node paths are engine-visible names baked into the GLB, not arbitrary — if a target node has no material of the requested kind, the override may be ignored.
Avatar Texture
Generate a texture from a player's avatar portrait:
typescript
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:
typescript
import { TextureFilterMode, TextureWrapMode } from '@dcl/sdk/ecs'
Material.setPbrMaterial(entity, {
texture: Material.Texture.Common({
src: 'assets/Images/pixel-art.png',
filterMode: TextureFilterMode.TFM_POINT, // crisp pixels (no smoothing)
wrapMode: TextureWrapMode.TWM_REPEAT // tile the texture
})
})Filter modes: (pixelated), (smooth), (smoothest).
Wrap modes: (tile), (stretch edges), (mirror tile).
TFM_POINTTFM_BILINEARTFM_TRILINEARTWM_REPEATTWM_CLAMPTWM_MIRRORTexture Tweens
For animated texture patterns ( scrolling, slide-once, vs movement types, and looping via ) — all requiring a texture with — see the Texture Tweens section in .
Tween.setTextureMoveContinuousTween.setTextureMoveTMT_OFFSETTMT_TILINGTweenSequencewrapMode: TWM_REPEAT{baseDir}/references/rendering-patterns.mdFlatMaterial Accessors
The component provides shortcut methods that skip the nested union structure, making material access more ergonomic:
Material| Method | Returns | Throws if no material? |
|---|---|---|
| Read-only | Yes |
| Read-only | No |
| Read/write | Yes |
| Read/write | No |
typescript
// 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/Images/new.png' })Example scenes
Engine-team test scenes exercising these APIs against the real runtime:
- https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/74,-8-gltfnodemodifier — overriding PBR/unlit materials, video textures, per-node colors and
GltfNodeModifierson specific GLTF nodes;castShadows/createOrReplace; wrong-path console-dump debug trick.deleteFrom - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/1,0-visibility-comp-propagation —
VisibilityComponentacross a parent/child/grandchild hierarchy with runtime re-parenting, covering every override combination.propagateToChildren - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/0,3-texture-movement — texture tweens via with
Tween.setTextureMoveandTextureMovementType.TMT_OFFSET, paired withTMT_TILINGloops; alsoTweenSequence+Billboard.TextShape - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/52,-52-testing-gallery — PBR material sweeps (metallic/roughness/emissive/normal-map) and collision-mask combinations shown side by side.
GltfContainer