--- name: lighting-environment description: Dynamic lighting and environment in Decentraland scenes. LightSource, shadows, SkyboxTime, realm detection, and emissive materials. Use when the user wants lights, shadows, skybox control, day-night cycle, or glowing materials. Do NOT use for PBR material properties like metallic/roughness (see advanced-rendering). --- # Lighting and Environment in Decentraland ## RULE: Use Smart Items for lights when the Creator Hub MCP is available When the user asks to add a light and the Creator Hub MCP tools are present (`mcp__creator-hub__*`), **always `search_catalog` first** before manually creating an entity with `LightSource`. The Creator Hub catalog includes ready-made light Smart Items: - **Spotlight** (`87c829b3-8e8d-4fcb-9f43-85d3aa9084f6`, category `lights`) -- comes with a real GLB model (`assets/asset-packs/spotlight/spotlight.glb`), a pre-configured `core::LightSource`, built-in Turn On / Turn Off / Toggle actions, and proper `inspector::Config` for the Creator Hub UI. - **Point Light** (category `lights`) -- same pattern: GLB placeholder, pre-configured `core::LightSource` of type Point, built-in actions. - There is also a **decorative** "Spotlight" (category `decorations`) that is a visual-only fixture model with no `LightSource` -- make sure you pick from the `lights` category when the user wants actual illumination. Use `place_smart_item` with the `assetId` and a `position`. It handles everything: downloading the GLB files, resolving asset paths, setting up `asset-packs::Placeholder`, `asset-packs::Script`, `asset-packs::Actions`, and `inspector::Config` components. The entity is immediately usable in the editor with action triggers wired up. **Only fall back to manual `create_entity` + `set_component` `core::LightSource`** when: - No Creator Hub MCP tools are available (pure code workflow). - The user needs a light with no visual model (invisible light source). - The user needs non-standard light parameters that a Smart Item does not expose. The manual `LightSource` component API documented below remains the authoritative reference for both paths -- Smart Items use the same underlying component. ## RULE: 3D model light fixtures do not emit light Light-looking geometry in a GLB model (lamp meshes, bulb shapes, glowing filaments) is purely visual -- it does not cast actual dynamic light in Decentraland. The renderer treats it as regular geometry (possibly with an emissive material for a glow effect, but no illumination of surrounding objects). To get real dynamic lighting from a fixture model, attach a `LightSource` component to the same entity or to a child entity positioned at the light source. When a user has a scene model with built-in light fixtures, proactively mention this: "The light fixtures in your model are decorative geometry only -- I need to add `LightSource` components for actual illumination." ## Point Lights Emit light in all directions from a position: ```typescript import { engine, Transform, LightSource } from '@dcl/sdk/ecs' import { Vector3, Color3 } from '@dcl/sdk/math' const light = engine.addEntity() Transform.create(light, { position: Vector3.create(8, 3, 8) }) LightSource.create(light, { type: LightSource.Type.Point({}), color: Color3.White(), intensity: 16000 // candela }) ``` ### Colored Point Light ```typescript LightSource.create(light, { type: LightSource.Type.Point({}), color: Color3.create(1, 0.5, 0), // Warm orange intensity: 16000, range: 15 // Maximum distance in meters }) ``` Defaults (from the protocol): `active` true, `color` white, `intensity` 16000 candela, `range` -1 (auto), `shadow` false. `color` is `Color3` (RGB, each 0–1). ## Spot Lights Emit a cone of light in a direction: ```typescript import { Quaternion } from '@dcl/sdk/math' const spotlight = engine.addEntity() Transform.create(spotlight, { position: Vector3.create(8, 4, 8), rotation: Quaternion.fromEulerDegrees(90, 0, 0) // Point straight down — +90, not -90 (see below) }) LightSource.create(spotlight, { type: LightSource.Type.Spot({ innerAngle: 25, outerAngle: 45 }), color: Color3.White(), intensity: 16000 }) ``` - `innerAngle` — full-brightness cone angle (degrees). Default `21.8`. Min `0`, max `179`. - `outerAngle` — outer fade angle (degrees). Default `30`. Max `179`. - `innerAngle` cannot exceed `outerAngle` — if it does, the engine clamps them to the same value. - The cone shines along the entity's local **+Z** (its forward): the Explorer parents the Unity `Light` under the entity's transform with an identity local pose (`LightSourceLifecycleSystem.cs`, unity-explorer main 2026-10). **Pitch `+90` about X points it straight down; `-90` points it at the sky** — `Quaternion.fromEulerDegrees(90, 0, 0)` rotates `Vector3.Forward()` to `(0, -1, 0)` and `-90` to `(0, 1, 0)`, computed with `@dcl/ecs-math` (2026-10-22). The SDK docs' spot example and an earlier version of this skill wrote `-90` for "downward", so a ceiling spot that seems to do nothing may be lighting the roof. Not yet re-confirmed in-world — check the cone once with the sanity test under Light Limits. - `type` is a discriminated union. To read/mutate spot params at runtime, narrow first: `if (comp.type?.$case === 'spot') { comp.type.spot.innerAngle = 30 }` ## Shadows Enable shadows on point or spot lights: ```typescript LightSource.create(spotlight, { type: LightSource.Type.Spot({ innerAngle: 25, outerAngle: 45 }), shadow: true, intensity: 800 }) ``` Note: shadows are only rendered for spot lights, not point lights. `shadow` is a top-level boolean on the component (not inside `Spot`/`Point`). ### Shadow Mask Textures (Gobos) Project a pattern through the light: ```typescript const maskedLight = LightSource.getMutable(spotlight) maskedLight.shadowMaskTexture = Material.Texture.Common({ src: 'assets/Images/lightmask1.png' }) ``` - Set `shadowMaskTexture = undefined` to remove the mask again. - The mask projects light shape (e.g. a window pattern) — simulating caustics/soft shadows. Used on spot lights. ## Toggling Lights ```typescript // Toggle on/off const lightData = LightSource.getMutable(light) lightData.active = !lightData.active ``` ## Light Limits - A scene may **create** many lights (the engine test scene spawns ~9 across 2 parcels); the renderer decides how many render. - The desktop quality preset caps scene lights: **Low = 0 (scene lights disabled entirely), Medium = 6, High = 10** (`QualityPresetLow/Medium/High.asset`, unity-explorer main 2026-10; the Settings slider can lower a preset's cap further). If the scene has more than the cap, only the closest lights to the player render. - **Troubleshoot "my light doesn't show" by checking the player's active preset first** — on Low a correctly authored `LightSource` renders nothing, and no amount of intensity, range, material or attachment-offset tuning changes that. State the supported minimum (Medium) rather than demanding High. A visible emissive flame is not proof the candle lights the room: keep the glowing mesh and the light cast onto surroundings as separate checks. - Up to ~3 shadow-casting lights render at once. - The renderer auto-culls lights based on quality settings and proximity. - Intensity is in **candela** (lumens/m² at 1m, i.e. lumens/4π). Default `16000`. - `range` default is `-1` → auto-computed as `intensity^0.25` (fourth root, in meters). Set an explicit `range` to override — this also limits a light's influence and saves performance. - Spread lights out so few are near the player at once (only the closest ones render). - **Keep the scene readable with no scene light at all.** Desktop *Low* and mobile *Very Low* render zero scene lights, and a desktop owner test (2026-10-09) saw a point light while two spots in the same room stayed dark (cause not established). Anything gameplay-critical — the path, the interactable, a "lights turn warm" story beat — must survive on emissive materials, textures and the skybox; `LightSource` is accent. This generalises the Mobile rule under Platform Support. - **One-minute sanity test before tuning anything:** at the default `intensity: 16000`, place one point light 2.5 m above the floor and one spot at the same height pitched `+90` (straight down), next to an emissive plane as a reference. Point visible, spot dark → aim or preset, not intensity. Neither visible → the preset is Low. Only then tune. Eight rounds of 8–60 cd guesses written in code (2026-10-08) preceded the one test that answered this; the catalog's Point Light / Spotlight Smart Items start at 16000 for a reason. ## SkyboxTime (Day/Night Cycle) Use SkyboxTime for atmosphere — nighttime scenes with point lights create dramatic environments. ### Fixed Time in scene.json Set a permanent time of day without code. Two valid locations: ```json // Genesis City scene — top-level { "skyboxConfig": { "fixedTime": 43200 } } // World — inside worldConfiguration { "worldConfiguration": { "name": "my-name.dcl.eth", "skyboxConfig": { "fixedTime": 36000 } } } ``` Time values (seconds since midnight, full day = 86400): 0 = midnight, 21600 = 6 AM, 43200 = noon, 64800 = 6 PM (dusk), 86400 = full day. **Precedence** (verified against the engine test scenes): `worldConfiguration.skyboxConfig.fixedTime` wins over top-level `skyboxConfig.fixedTime`; either JSON value is in turn overridden at runtime by a `SkyboxTime` component on `engine.RootEntity`. ### Read Current World Time ```typescript import { getWorldTime } from '~system/Runtime' executeTask(async () => { const time = await getWorldTime({}) console.log('Seconds since midnight:', time.seconds) }) ``` ### Change Time Dynamically ```typescript import { engine, SkyboxTime, TransitionMode } from '@dcl/sdk/ecs' // Set time of day (must target the root entity) SkyboxTime.create(engine.RootEntity, { fixedTime: 43200 }) // Noon // Change with transition direction SkyboxTime.createOrReplace(engine.RootEntity, { fixedTime: 64800, // Dusk (6 PM) transitionMode: TransitionMode.TM_BACKWARD // TM_FORWARD (0, default) or TM_BACKWARD (1) }) // Remove the component to hand control back to global/world time SkyboxTime.deleteFrom(engine.RootEntity) ``` - `transitionMode` (optional) sets the animation direction when the time changes. Default `TM_FORWARD`. - The component must live on `engine.RootEntity`. Deleting it reverts to the scene.json/world time (or the global day/night cycle). ### Day/Night Cycle System ```typescript let currentTime = 43200 const CYCLE_SPEED = 100 // Time units per second function dayNightCycle(dt: number) { currentTime = (currentTime + CYCLE_SPEED * dt) % 86400 SkyboxTime.createOrReplace(engine.RootEntity, { fixedTime: currentTime }) } engine.addSystem(dayNightCycle) ``` ## Realm Info Detect which realm (server) the player is connected to: ```typescript import { getRealm } from '~system/Runtime' executeTask(async () => { const realm = await getRealm({}) console.log('Realm:', realm.realmInfo?.realmName) console.log('Network:', realm.realmInfo?.networkId) console.log('Base URL:', realm.realmInfo?.baseUrl) }) ``` ## Emissive Materials (Glow Effects) For a visual glow without casting light on surroundings: ```typescript import { engine, Material } from '@dcl/sdk/ecs' import { Color4, Color3 } from '@dcl/sdk/math' // Self-illuminated 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 }) ``` Note: emissive materials don't illuminate other surrounding entities, they just have a glow effect on them. ### Combining Emissive + LightSource For an object that both glows visually and casts light: ```typescript // Visual glow on the mesh Material.setPbrMaterial(bulb, { emissiveColor: Color3.create(1, 0.9, 0.7), emissiveIntensity: 1.5 }) // Actual light emission LightSource.create(bulb, { type: LightSource.Type.Point({}), color: Color3.create(1, 0.9, 0.7), intensity: 200, range: 10 }) ``` ### Shadow Quality `shadow` is a top-level optional boolean on the LightSource component (default `false`). There is no shadow-type enum — quality is automatic and distance-based. `Spot({...})` accepts only `innerAngle?` and `outerAngle?`. ```typescript import { LightSource } from '@dcl/sdk/ecs' // Spot light with shadows enabled LightSource.create(spotEntity, { type: LightSource.Type.Spot({ innerAngle: 25, outerAngle: 45 }), shadow: true, // top-level boolean intensity: 800 }) ``` Constraints: - Shadows are only supported for **spot** lights; point lights do not cast shadows. - Max **3** shadow-casting lights rendered at a time — disable `shadow` on lights that don't need it. Spot lights with shadows suit dramatic effects such as flashlights. - Shadow quality/culling is automatic, based on the light's distance from the player. Exact distances vary by light type and the player's quality settings; general rule: | Distance from player | Result | |----------------------|--------| | < 10 m | Soft shadows (high quality) | | 10–20 m | Hard shadows (low quality) | | > 20 m | No shadows rendered | - The **light itself keeps illuminating** at much larger distances — it is only disabled when the player is more than **160 m away (10 parcels)**. This makes LightSource suitable for large-scale setups like stage lighting at live events, where most of the audience is far from the fixtures. - Lights only render while the player is standing **inside** the scene; outside, they are not rendered. > **Need advanced material effects?** See the **advanced-rendering** skill for metallic, roughness, transparency, texture maps, texture tweens, and texture modes. ## Platform Support - **Desktop (Unity explorer):** Full support (point lights, spot lights, shadows). One owner test (2026-10-09, point + two spots at default intensity) showed only the point light; not reproduced, and the spots may have been pitched up (see Spot Lights) — run the sanity test under Light Limits before calling it a client defect. - **Mobile (Godot explorer):** `LightSource` (scene dynamic lights) is **available since v1.13.0** — no longer a parity gap. Budgeted at runtime, see below. - **Bevy explorer:** Full support (point lights, spot lights, shadows). Verified against docs commits `b4d4c88` (mobile parity tracker) and `8139a7f` (mobile limits), Oct 2026. ### Mobile light budget Mobile enforces flat scene limits on **authored** lights: **8 soft / 16 hard** light sources, **2 soft / 4 hard** shadow-casting lights. Soft = warning in the mobile performance panel; hard = the scene will not load. On top of that, each mobile **graphics profile** only activates a budget of the **nearest** light sources — **0 on Very Low, up to 8 on High** — and lower profiles may disable shadows entirely. So: - Authoring 20 lights still costs the full scene budget even though at most 8 ever emit. Author to 8, do not lean on the runtime budget. - Never make gameplay-critical visibility depend on a specific light being lit: on Very Low **no** scene light is active. Keep a baseline readable with ambient/emissive materials and the skybox, and treat `LightSource` as accent. - Shadows are not guaranteed. Do not use a cast shadow as the only cue for an object's position. Full mobile limit table: **optimize-scene** → *Mobile Scene Limits*. ## Gotchas - `range` left unset (`-1`) is auto-derived from intensity as `intensity^0.25` — small intensities give surprisingly short range. Set `range` explicitly for predictable falloff. - `shadow` only affects spot lights; setting it on a point light has no effect. - Animating a light's direction: put a `Tween`/`TweenSequence` (Rotate mode) on the light entity — the beam follows the entity's forward vector. - `SkyboxTime` on `RootEntity` overrides any scene.json `fixedTime`; `deleteFrom` reverts to it. - **Baked light geometry is inert.** GLB models with lamp/bulb meshes do not emit light -- add a `LightSource` component for real illumination. See the rule above. - **Smart Item `Placeholder` needs a resolved file path**, not a template variable. Setting `{assetPath}/spotlight.glb` as the `src` in an `asset-packs::Placeholder` results in an invisible/broken gizmo. `place_smart_item` resolves this automatically to e.g. `assets/asset-packs/spotlight/spotlight.glb`. If you must set `Placeholder` manually via `set_component`, use the real on-disk path. ## Example scenes Engine-team test scenes (real API, exercised against the engine): - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/0,4-dynamic-lights — point & spot LightSource: toggle active, color, range, intensity, spot inner/outer angle at runtime, shadow-mask (gobo) swapping, and tweened light rotation. - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/2,0-skybox-scene-json — fixed skybox time via top-level `skyboxConfig.fixedTime`; reads it back with `getSceneInformation`. - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/3,0-skybox-world-json — fixed skybox time via `worldConfiguration.skyboxConfig.fixedTime` (World variant); demonstrates the worldConfiguration-wins precedence. - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/2,1-skybox-sdk-scene-a and https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/3,1-skybox-sdk-scene-b — runtime `SkyboxTime` on `RootEntity` with `TransitionMode`, plus `deleteFrom` to return to global time.