# Godot 4 — 2D Lights and Shadows

> **Purpose**: Reference document for Claude Code. This file describes the complete 2D lighting and shadow system in Godot 4, including node types, configuration, shadow setup, normal/specular maps, and performance considerations.

---

## Overview

By default, 2D scenes in Godot are **unshaded** — no lights, no shadows. Everything renders at full brightness as-is. The 2D lighting system adds real-time lights and shadows to give depth to the scene.

The visual progression is:

1. **Unshaded** — flat, no lighting applied
2. **Lights only** — areas are brightened by light sources, but no shadows are cast
3. **Lights + Shadows** — full setup with occluders casting shadows from light sources

---

## Node Architecture

A complete 2D lighting setup involves these node types:

### `CanvasModulate`

- **Role**: Darkens the entire scene by setting a base "ambient" color.
- **Why it's needed**: Without it, lights only *add* brightness on top of an already fully-lit scene, so everything looks washed out. `CanvasModulate` sets the darkness floor — the color visible in areas where no light reaches.
- **Usage**: Add one `CanvasModulate` node to the scene. Set its `Color` property to control the ambient darkness (e.g., a dark blue-gray for nighttime).

### `PointLight2D`

- **Role**: Omnidirectional or spot light source (torches, fire, projectiles, etc.).
- **How it works**: Projects a texture onto the scene to simulate a light cone/blob. The texture defines the light's shape and falloff.

#### Properties

| Property | Description |
|---|---|
| `Texture` | The image used as the light source. Its size determines the light's radius. Can have an alpha channel (useful for `Mix` blend mode). |
| `Offset` | Shifts the light texture without moving the node itself. **Does not affect shadow direction** (shadows are always cast from the node's position, not the texture offset). |
| `Texture Scale` | Multiplier for the light's size. Larger lights = more pixels affected = higher GPU cost. |
| `Height` | Virtual height relative to surfaces, used for **normal mapping** calculations. Default is very low (nearly parallel to surfaces), which makes normal maps barely visible. Increase this to see normal map effects. Has no impact without normal maps. |

#### Creating a Light Texture

The texture **defines the shape of the light** — it's not a physical simulation, it's an image displayed in additive blending over the scene. This means you can create any light shape by changing the texture:

1. **Radial gradient (standard lamp)**: A soft white circle fading to transparent edges. Use a pre-made PNG or create one procedurally via `GradientTexture2D` → set `Fill > Fill Mode` to **Radial** → gradient from opaque white (center) to transparent white (edges) → move the gradient's start position to the center.
2. **Cone texture (flashlight/torch beam)**: Use a cone-shaped or wedge-shaped PNG (white fading to transparent along the cone edges). This creates a directional beam from a point source — useful for flashlights, headlights, spotlights. Rotate the `PointLight2D` node to aim the cone.
3. **Any custom shape**: The texture can be any shape (rectangle for a window light spill, irregular blob for magical effects, etc.). The engine doesn't care — it just renders the texture additively.

### `DirectionalLight2D`

- **Role**: Simulates sunlight or moonlight — parallel rays coming from a single direction.
- **Key difference from `PointLight2D`**: Rays are parallel (infinite distance source), not radial.

#### Properties

| Property | Description |
|---|---|
| `Height` | Virtual height for normal mapping (0.0 = parallel to surfaces, 1.0 = perpendicular). Same principle as `PointLight2D`. **Does not affect shadow appearance.** |
| `Max Distance` | Max distance (in pixels) from the camera center beyond which object shadows are culled. Lower values = better performance (off-screen objects stop casting shadows). **Camera2D zoom is NOT factored in**, so at high zoom levels, shadows may fade out sooner than expected. |

#### Directional Shadow Limitation

Directional shadows are **always infinitely long** regardless of the `Height` property. This is a rendering method limitation. To get finite-length directional shadows, you must:

1. Disable shadows on the `DirectionalLight2D`
2. Use a **custom shader** that reads from the **2D signed distance field** (SDF), which is auto-generated from `LightOccluder2D` nodes in the scene.

### `LightOccluder2D`

- **Role**: Defines geometry that **casts shadows** when hit by a light source.
- **Requirement**: Must have an `OccluderPolygon2D` resource assigned — without it, the node does nothing visually.
- Shadows appear only in areas covered by a `PointLight2D` or `DirectionalLight2D`, and their direction is based on the light's center position.
- Occluders can be placed as **independent nodes** or as part of a **`TileMapLayer`** node (useful for tile-based games where walls/obstacles are in the tilemap).

#### Properties

| Property | Description |
|---|---|
| `Occluder` (polygon resource) | The polygon shape that blocks light. **Must be set** for the occluder to work. |
| `SDF Collision` | If enabled, the occluder contributes to the real-time signed distance field (usable in custom shaders). No visual or performance impact if you don't use SDF shaders. Enabled by default. |
| `Occluder Light Mask` | Bitmask controlling which lights this occluder casts shadows for. Works in tandem with each light's `Shadow > Item Cull Mask`. |

#### Occluders on Moving Entities (Characters, Enemies)

Don't limit occluders to static level geometry (walls, pillars, furniture). Adding `LightOccluder2D` as a **child node of moving entities** (player character, enemies, NPCs) makes them cast dynamic shadows that follow their movement. This is what visually **anchors** entities in the lit environment — without it, characters look like they float on top of the scene, disconnected from the lighting.

Practical setup: add a `LightOccluder2D` child to the entity's scene root, draw a simplified polygon approximating the character's silhouette (doesn't need to be pixel-perfect — a rough outline is enough), and it will cast shadows from every light that has `Shadow > Enabled`.

---

## Common Light Properties (shared by `PointLight2D` and `DirectionalLight2D`)

These come from the base class `Light2D`:

| Property | Description |
|---|---|
| `Enabled` | Toggles the light on/off. Unlike hiding the node, disabling this does **not** hide child nodes. |
| `Editor Only` | Light is visible only in the editor, auto-disabled at runtime. |
| `Color` | The light's color tint. |
| `Energy` | Intensity multiplier. Higher = brighter. |
| `Blend Mode` | **Add** (default): additive blending, standard lighting. **Subtract**: "negative light", not physically accurate but useful for effects. **Mix**: linear interpolation between light texture and underlying pixels. |
| `Range > Z Min / Z Max` | Z index range of nodes affected by this light. |
| `Range > Layer Min / Layer Max` | Visual layer range affected by this light. |
| `Range > Item Cull Mask` | Bitmask controlling which nodes receive light, based on their visual layers' `Occluder Light Mask`. Use this to exclude specific objects from a light. |

---

## Shadow Setup

### Enabling Shadows

1. On the light node (`PointLight2D` or `DirectionalLight2D`), enable `Shadow > Enabled`.
2. **Nothing will change visually yet** — shadows require `LightOccluder2D` nodes in the scene with valid polygon resources.

### Creating Occluders

#### Method 1: Auto-generate from Sprite

1. Select a `Sprite2D` node in the scene.
2. Click the **Sprite2D** menu at the top of the 2D editor.
3. Choose **Create LightOccluder2D Sibling**.
4. In the dialog, adjust **Grow (pixels)** and **Shrink (pixels)** until the outline matches the sprite's edges.
5. Click **Update Preview** to iterate, then **OK** to confirm.

This creates a sibling `LightOccluder2D` node with a polygon that approximates the sprite's shape.

#### Method 2: Manual Drawing

1. Create a `LightOccluder2D` node.
2. Select it, click the **"+"** button at the top of the 2D editor.
3. Confirm to create a polygon resource.
4. Click to place points and draw the occluder shape.
5. Right-click existing points to remove them.
6. Click on an existing line segment and drag to insert new points.

### Shadow Properties (on the light node)

| Property | Description |
|---|---|
| `Shadow > Color` | Color of shaded areas. Default is full black. The alpha channel controls tint intensity. Can be changed for artistic effect (e.g., dark blue shadows for moonlight). |
| `Shadow > Filter` | Shadow filtering mode. See table below. |
| `Shadow > Filter Smooth` | Softness amount when using PCF5 or PCF13. Higher = softer, but may cause **banding artifacts** (especially with PCF5). |
| `Shadow > Item Cull Mask` | Bitmask controlling which `LightOccluder2D` nodes cast shadows for this light, based on their `Occluder Light Mask`. |

#### Shadow Filter Modes

| Mode | Quality | Performance | Notes |
|---|---|---|---|
| **None** | Hard/blocky edges | Fastest | Best for pixel art aesthetics |
| **PCF5** | Moderate softness | Medium | Good general-purpose option |
| **PCF13** | Smoothest | Most expensive | Use sparingly — only a few lights at once |

---

## Pixel Art Lighting

### The Problem

The engine computes 2D lighting and shadows at the **Viewport's pixel resolution**, not at the sprite texture's texel resolution. Setting `Nearest` texture filtering on sprites does **not** make lighting pixelated — it only affects texture sampling.

### The Solution: Custom Shader

Use this shader on sprites to snap light sampling to a pixel grid:

```gdshader
shader_type canvas_item;

uniform float pixel_size = 4.0;

void fragment() {
    // Snap lighting and shadows to pixel grid
    LIGHT_VERTEX.xy = floor(LIGHT_VERTEX.xy / pixel_size) * pixel_size;
    SHADOW_VERTEX = floor(SHADOW_VERTEX / pixel_size) * pixel_size;

    // Normal rendering
    COLOR = texture(TEXTURE, UV);
}
```

**How it works**: Divides position by `pixel_size` (converting to grid space), floors it (rounding down to nearest grid point), then multiplies back. This forces the engine to sample lighting from discrete positions, creating a chunky/pixelated look that matches pixel art.

---

## Normal Maps and Specular Maps

Both `PointLight2D` and `DirectionalLight2D` support normal and specular mapping. These can be assigned to **any 2D element** (nodes inheriting from `Node2D` or `Control`).

### Normal Maps

- Represent the direction each pixel is "pointing" toward.
- The engine uses this to vary lighting intensity per-pixel based on surface angle → less flat-looking light.
- Typically generated from a hand-painted **height map**, or auto-generated from textures using tools like [Laigter](https://azagaya.itch.io/laigter) (free/open source).

### Specular Maps

- Define how much each pixel reflects light (and in what color).
- Brighter values = stronger reflection at that point.
- Usually created manually, using the diffuse texture as a base.

### Setup via `CanvasTexture`

To apply normal/specular maps to a 2D node (e.g., `Sprite2D`):

1. On the property that holds the node's texture (e.g., `Sprite2D > Texture`), create a **New CanvasTexture** resource.
2. Configure the `CanvasTexture` properties:

| Property | Description |
|---|---|
| `Diffuse > Texture` | The base color texture (the sprite's usual texture). |
| `Normal Map > Texture` | The normal map texture (generated from a height map). |
| `Specular > Texture` | The specular map (usually grayscale, can contain color for tinted reflections). |
| `Specular > Color` | Color multiplier for reflections. |
| `Specular > Shininess` | Specular exponent. **Low values** = bright, diffuse reflections. **High values** = tight, localized reflections (wet-looking surfaces). |
| `Texture > Filter` | Overrides the node's texture filter mode (regardless of node property or project setting `Rendering > Textures > Canvas Textures > Default Texture Filter`). |
| `Texture > Repeat` | Overrides the node's texture repeat mode (regardless of node property or project setting `Rendering > Textures > Canvas Textures > Default Texture Repeat`). |

### Post-Setup Adjustment

After enabling normal maps, lights may appear weaker. Fix this by:

- Increasing the `Height` property on your light nodes.
- Slightly increasing the `Energy` property to restore previous brightness levels.

---

## Performance: Additive Sprites as Light Alternatives

If 2D lights cause performance issues, consider replacing **short-lived dynamic effects** (bullets, explosions, muzzle flashes) with `Sprite2D` nodes using **additive blending**.

### Setup

1. Create a `Sprite2D`, assign a light-like texture.
2. In the inspector: `CanvasItem > Material` → create **New CanvasItemMaterial**.
3. Set `Blend Mode` to **Add**.

### Comparison

| Aspect | Additive Sprite | PointLight2D |
|---|---|---|
| Render cost | Very low (no separate pipeline) | Higher (separate light pass) |
| Animation support | Yes (`AnimatedSprite2D` or `AnimationPlayer`) | Limited to property tweening |
| Shadow casting | **No** | Yes |
| Normal/specular map interaction | **No** (ignored) | Yes |
| Dark area lighting accuracy | **Inaccurate** (can't properly brighten fully dark areas) | Accurate |
| Best for | Ephemeral VFX, particles, projectile glow | Persistent scene lighting, interactive shadows |

### When to Use Each

- **Additive sprites**: fast-moving, short-lived effects where shadow accuracy doesn't matter.
- **Actual 2D lights**: ambient lighting, key lights, anything that needs shadows or normal map interaction.

---

## Advanced Techniques (Visual Polish)

These techniques go beyond basic lighting setup. They combine shaders, lighting, and hand-crafted assets to achieve a cohesive, stylized 2D look — particularly suited for pixel art or games with strict color palettes.

---

### 1. Procedural Palette Generation (Posterization)

**Core idea**: Instead of relying on fixed sprite colors, use a shader that extracts the **brightness** (luminance) of each pixel and maps it to a color from a mathematically generated palette.

**How it works**:

1. The shader reads each pixel's brightness value (0.0 to 1.0).
2. That value is used as an index to sample a color from a palette (either a gradient texture or a computed hue ramp).
3. The brightness is **quantized into steps** (staircase function via `floor()` or `round()`) instead of being a smooth gradient — this is the **posterization** effect.

**Why it matters**:

- You can **dynamically change the hue of an entire scene** without touching any sprite. Swap the palette and everything shifts.
- The step-based quantization creates a retro/pixel-art look with a limited number of visible colors, even if the source textures are smooth.
- Great for mood shifts (day/night transitions, corruption effects, biome changes) driven purely by palette parameters in the shader.

**Implementation approach** (canvas_item shader):

```gdshader
shader_type canvas_item;

uniform float steps : hint_range(2.0, 16.0) = 4.0; // Number of color steps
uniform float hue_shift : hint_range(0.0, 1.0) = 0.0;

// Convert HSV to RGB
vec3 hsv2rgb(vec3 c) {
    vec3 p = abs(fract(vec3(c.x) + vec3(0.0, 2.0/3.0, 1.0/3.0)) * 6.0 - 3.0);
    return c.z * mix(vec3(1.0), clamp(p - 1.0, 0.0, 1.0), c.y);
}

void fragment() {
    vec4 tex = texture(TEXTURE, UV);
    // Extract brightness (luminance)
    float brightness = dot(tex.rgb, vec3(0.299, 0.587, 0.114));
    // Posterize: quantize brightness into discrete steps
    brightness = floor(brightness * steps) / steps;
    // Map brightness to a palette color via hue
    vec3 color = hsv2rgb(vec3(hue_shift, 0.6, brightness));
    COLOR = vec4(color, tex.a);
}
```

> **Note**: This is a simplified example. In production, the palette lookup would often use a 1D gradient texture (a `GradientTexture1D` sampled by brightness) for more artistic control over exact color choices rather than pure HSV math.

---

### 2. Noise-Based Dithering in Shaders

**Problem**: Posterization creates flat, uniform color bands. Large surfaces of the same color look static and lifeless.

**Solution**: Before the palette lookup/posterization step, **offset the brightness slightly using a noise texture**. This breaks up the hard edges between color bands and adds a textured, organic quality.

**How it works**:

1. Sample a noise texture (e.g., `FastNoiseLite` baked into a `NoiseTexture2D`, or any imported grayscale noise image).
2. Add or subtract a small noise value from the pixel's brightness **before** quantizing.
3. The result: band boundaries become irregular and dithered instead of being clean horizontal/vertical lines.

**Typical usage in the shader**:

```gdshader
uniform sampler2D noise_texture;
uniform float noise_strength : hint_range(0.0, 0.2) = 0.05;

void fragment() {
    vec4 tex = texture(TEXTURE, UV);
    float brightness = dot(tex.rgb, vec3(0.299, 0.587, 0.114));

    // Sample noise and offset brightness
    float noise = texture(noise_texture, UV).r;
    brightness += (noise - 0.5) * noise_strength;

    // Then posterize as before...
    brightness = floor(brightness * steps) / steps;
    // ... palette lookup ...
}
```

**Visual effect**: Surfaces that were flat color blocks now have a subtle grain, like wind moving across them or a hand-painted texture. This is especially effective on large background areas (floors, walls, skies).

---

### 3. Strategic PointLight2D Placement

This is about **light design intent**, not just technical setup.

#### Principle: Light as Attention Guide

Do **not** spam lights everywhere. Each light should have a purpose. In a card game or UI-heavy scene, lights should illuminate **points of interest**:

- Card hands, life counters, action buttons, interactive elements
- Key narrative objects or pickups
- The player character or current focus area

Unlit areas create contrast that naturally **draws the eye toward lit zones**. This is basic composition — dark-light contrast is one of the strongest tools for visual hierarchy.

#### Principle: Intensity Variance

Not all lights should have the same `Energy`. Match intensity to the fictional light source:

- A **small candle** → low `Energy`, small `Texture Scale`
- A **torch on a wall** → medium `Energy`, medium scale
- A **large magical effect or bonfire** → high `Energy`, large scale

Uniform light intensity across different source types flattens the scene and kills depth perception. The variance creates a believable hierarchy of light sources.

---

### 4. Custom Light Shader (`void light()`) for Palette Coherence

**Problem**: By default, Godot's 2D lights blend additively — they add white/color on top of existing pixels. This **breaks a strict color palette** because the resulting pixel colors are no longer part of the intended palette. You get smooth gradients where you wanted hard posterized steps.

**Solution**: Override the `void light()` function in the same `canvas_item` shader to force the light pass through the **same posterization/palette logic** as the base sprite rendering.

**How it works**:

```gdshader
shader_type canvas_item;

uniform float steps : hint_range(2.0, 16.0) = 4.0;
uniform float hue_shift : hint_range(0.0, 1.0) = 0.0;

vec3 hsv2rgb(vec3 c) {
    vec3 p = abs(fract(vec3(c.x) + vec3(0.0, 2.0/3.0, 1.0/3.0)) * 6.0 - 3.0);
    return c.z * mix(vec3(1.0), clamp(p - 1.0, 0.0, 1.0), c.y);
}

void fragment() {
    vec4 tex = texture(TEXTURE, UV);
    float brightness = dot(tex.rgb, vec3(0.299, 0.587, 0.114));
    brightness = floor(brightness * steps) / steps;
    COLOR = vec4(hsv2rgb(vec3(hue_shift, 0.6, brightness)), tex.a);
}

void light() {
    // LIGHT_COLOR contains the incoming light's contribution
    // Instead of additive blending, re-run palette logic on the combined brightness
    float lit_brightness = dot(LIGHT_COLOR.rgb * LIGHT_ENERGY, vec3(0.299, 0.587, 0.114));
    float base_brightness = dot(COLOR.rgb, vec3(0.299, 0.587, 0.114));

    // Combine base + light brightness, then re-posterize
    float total = clamp(base_brightness + lit_brightness, 0.0, 1.0);
    total = floor(total * steps) / steps;

    LIGHT = vec4(hsv2rgb(vec3(hue_shift, 0.6, total)) - COLOR.rgb, COLOR.a);
}
```

**Key insight**: The `void light()` function runs **per-light, per-pixel** during the light pass. `LIGHT` is the final contribution of that light to the pixel. By re-running the palette math here, every lit pixel still maps to a valid palette color — no intermediate smooth values leak through.

> **Note**: The exact implementation depends on the palette system. The above is a conceptual example. In practice, you may need to handle `LIGHT` as a delta (difference) rather than an absolute value, since Godot adds `LIGHT` to the fragment result.

---

### 5. Normal Maps for 2D Volume

Normal maps make flat 2D sprites react to light **as if they had 3D surface detail**. A pixel on the left edge of a character will darken when light comes from the right, and brighten when light comes from the left.

**How normal maps encode direction**:

- Each pixel's RGB channels encode a surface direction vector (the "normal"):
  - **R** (red) → X axis (left/right)
  - **G** (green) → Y axis (up/down)
  - **B** (blue) → Z axis (toward/away from viewer)
- A flat surface facing the camera is encoded as `(128, 128, 255)` — that distinctive purple/blue color.
- The engine uses this per-pixel direction to calculate how much light hits each point based on the light's angle.

**Impact**: Without normal maps, a 2D light uniformly brightens everything it covers. With normal maps, the light creates highlights and shadows *within* the sprite itself, simulating volume. This is especially powerful combined with `PointLight2D` — as the light moves around, the shading on the sprite shifts dynamically.

---

### 6. Hand-Painting Normal Maps (Normal Sphere Technique)

Auto-generated normal maps (from tools like Laigter or 3D software) often produce poor results on **stylized pixel art** because they interpret pixel edges as geometry. For pixel art, **hand-painting** normal maps gives far better control.

#### Method: The Normal Sphere Reference

1. **Get a normal sphere reference image**: This is a sphere rendered with correct normal map colors — a circular gradient where each point shows the color corresponding to that surface angle. These are widely available online (search "normal map sphere reference").

2. **Start with flat fill**: For all pixels of the sprite that face the viewer (flat surfaces), fill them with the **center color of the sphere** — `(128, 128, 255)`, the neutral "pointing straight at the camera" direction.

3. **Color-pick for angled surfaces**: For edges, curves, and relief details:
   - Identify the orientation of the surface at that pixel (which direction is it "facing"?).
   - On the reference sphere, find the point that faces the same direction.
   - Pick that color and paint it onto the normal map.
   - Example: A pixel on the **right edge** of a rounded object → pick from the **right side** of the sphere (more red, less blue).
   - Example: A pixel on a **top bevel** → pick from the **top** of the sphere (more green).

4. **Work zone by zone**: Don't try to be pixel-perfect everywhere. Focus on major forms first (body volumes, weapon shapes, large clothing folds), then refine details if needed.

#### Tips

- Keep it simple for small sprites — often just distinguishing "center face", "left edge", "right edge", "top", "bottom" is enough to sell volume under dynamic lighting.
- Test frequently in-engine with a moving `PointLight2D` to see how the normals respond.
- For pixel art, avoid smooth gradients between normal colors — use flat zones with hard transitions, matching the sprite's pixel aesthetic.

---

### 7. CanvasTexture for Normal Map Integration (Godot 4)

`CanvasTexture` is the Godot 4 resource that ties together a **diffuse texture** (the sprite's color) and a **normal map** (the relief information) into a single assignable resource.

#### Setup Steps

1. On the sprite or UI node, find the texture property (e.g., `Sprite2D > Texture`).
2. Instead of assigning a regular `Texture2D`, create a **New CanvasTexture**.
3. Inside the `CanvasTexture`:
   - `Diffuse > Texture` → assign the sprite's color texture.
   - `Normal Map > Texture` → assign the hand-painted or generated normal map.
   - (Optional) `Specular > Texture` → assign a specular map for reflective highlights.

#### Why CanvasTexture and Not Just the Light's Properties

`CanvasTexture` is **per-sprite**, not per-light. This means:

- Each sprite can have its own unique normal map tailored to its shape.
- The same `PointLight2D` will interact differently with each sprite based on its individual normal map.
- It works on **any node** that accepts a `Texture2D` — sprites, UI elements (`TextureRect`, `NinePatchRect`), animated sprites, etc.

> **Reminder**: After adding normal maps, increase `PointLight2D.Height` and/or `DirectionalLight2D.Height` — the default near-zero value makes normal map effects nearly invisible.

---

### Combining These Techniques

The real power comes from **stacking** these techniques:

1. **Base rendering**: Posterization shader converts sprites to a strict palette.
2. **Noise layer**: Dithering breaks color bands into organic patterns.
3. **Lighting**: `PointLight2D` / `DirectionalLight2D` placed strategically with varied intensity.
4. **`void light()` override**: Forces lit pixels back through the palette, maintaining visual coherence.
5. **Normal maps via `CanvasTexture`**: Give sprites volumetric response to lights.
6. **Shadows via `LightOccluder2D`**: Complete the depth illusion.

The result is a 2D scene that has **consistent palette discipline**, **dynamic volumetric lighting**, and **depth through shadows** — all while maintaining a stylized pixel art or painterly aesthetic.

---

## Important Notes & Gotchas

0. **2D light rendering pipeline (mental model)**: The engine (1) draws the scene, (2) overlays each `PointLight2D`/`DirectionalLight2D` texture additively, (3) checks `LightOccluder2D` polygons, (4) masks/cuts the light texture behind those polygons relative to the light's center → that's the shadow. Understanding this order clarifies why occluders without lights do nothing, and why light textures define shape.

1. **Background lighting**: The background color itself does **not** receive lighting. If you want the background lit, add a `Sprite2D` (or similar visual node) as a background. Use `Region` properties + `Texture > Repeat = Enabled` for tiling backgrounds.

2. **CanvasModulate is essential**: Without it, lights only brighten — the scene has no dark areas for lights to contrast against.

3. **Occluder polygon is mandatory**: A `LightOccluder2D` without an `OccluderPolygon2D` resource does nothing.

4. **Shadow direction**: Shadows are cast **from the light node's position**, not from the texture offset. Moving the `Offset` shifts the visual glow but doesn't change shadow angles.

5. **Directional shadows are infinitely long**: This can't be changed without a custom SDF shader.

6. **Zoom and `Max Distance`**: `DirectionalLight2D`'s `Max Distance` doesn't account for `Camera2D` zoom, so shadows may cull earlier than expected when zoomed in.

7. **Performance scaling**: Light cost scales with the number of pixels covered. `Texture Scale` directly impacts this — bigger lights = more pixels = more GPU work.

8. **Shadow filter cost**: `None` < `PCF5` < `PCF13`. Use `PCF13` only on a handful of lights.

9. **`Height` property matters for normal maps**: Default height is near-zero, making normal maps nearly invisible. Always increase it when using normal maps.