# Blender 5.0 Snow Layer Workflow

## Overview

This document describes the **Blender 5.0** Geometry Nodes setup for generating a procedural snow layer that:
- Only accumulates on upward-facing surfaces (slope-based filtering)
- Respects overhangs and occlusion (raycast-based filtering)
- Exports as a heightmap for runtime deformation in the game

**Requirements:**
- Blender 5.0 or later
- Terrain mesh with proper UVs
- Scene objects for occlusion testing (optional)

## Export Requirements

**Output Files:**
1. `textures/snow_depth.exr` - Single-channel R32Float heightmap (snow depth in world units, 0-0.5m range)
   - Generated by `blender/scripts/generate_snow_depth.py`
   - Or manually exported following steps below
2. `meshes/snow_mesh.gltf` - Visual snow mesh for verification (optional, not used in game)

**Resolution:** Match terrain heightmap resolution (e.g., 512×512 or 1024×1024)

**Alignment:** Snow depth map must align with terrain UVs (same coordinate space)

**File Format:**
- Format: OpenEXR (.exr)
- Bit Depth: 32-bit Float (not 16-bit Half!)
- Compression: ZIP (lossless)
- Color Space: Linear
- Expected size: ~200-500KB for 512×512 with ZIP compression

## Geometry Nodes Setup (Blender 5.0)

### Creating the Modifier

1. Select your terrain mesh
2. Go to **Modifiers** panel
3. Add → **Geometry Nodes** modifier
4. Click **New** to create a new node tree
5. Name it: `Snow Accumulation`

### Node Graph Overview

```
[Group Input]
    │
    ├─→ [Position] ──→ [Raycast to Geometry] ──→ [Is Hit] ──→ [Boolean Math (NOT)] ──→ [Occlusion Factor]
    │                       ↑                                                              │
    │                       └─ [Object Info] ← Scene Collection                            │
    │                                                                                       │
    ├─→ [Normal] ──→ [Vector Math (Dot)] ──→ [Map Range] ──→ [Normal Factor]              │
    │                     ↑                                         │                      │
    │                     └─ [Combine XYZ(0,0,1)]                  │                      │
    │                                                               │                      │
    └─→ [Math (Multiply)] ←─ [Normal Factor] ←─────────────────────┴──────────────────────┘
            │ (Snow Mask)
            ├─→ [Math (Multiply by 0.3)] ──→ [Snow Depth Value]
            │                                      │
            │                                      ├─→ [Store Named Attribute: "snow_depth"]
            │                                      │
            │                                      └─→ [Vector Math (Multiply)] → [Offset Vector]
            │                                              ↑
            │                                              └─ [Combine XYZ(0,0,1)] (Up)
            │
            └─→ [Set Position] (offset by snow_depth in Z)
                    │
                    └─→ [Group Output]
```

### Step-by-Step Setup in Blender 5.0

#### 1. Normal-Based Filtering (Slope Detection)

**Goal:** Snow only on surfaces with normals pointing upward (< 45° from vertical)

**Add Nodes (Add Menu → Search):**

1. **Normal** node (Input → Normal)
   - Outputs the surface normal vector at each vertex

2. **Combine XYZ** node (Utilities → Combine XYZ)
   - X: `0.0`
   - Y: `0.0`
   - Z: `1.0`
   - This creates the "up" vector (world Z-axis)

3. **Vector Math** node (Utilities → Vector Math)
   - Operation: **Dot Product**
   - Connect: Normal → Vector A
   - Connect: Combine XYZ → Vector B
   - Result: How much the surface faces upward (-1 to 1)

4. **Map Range** node (Utilities → Map Range)
   - Data Type: **Float**
   - Clamp: **Enabled** ✓
   - From Min: `0.7` (cos(45°) - surfaces steeper than 45° get no snow)
   - From Max: `1.0` (completely flat horizontal surfaces)
   - To Min: `0.0`
   - To Max: `1.0`
   - Connect: Vector Math (Value) → Value

**Output:** **Normal Factor** (0 = too steep for snow, 1 = perfect for snow accumulation)

#### 2. Occlusion-Based Filtering (Overhang Detection)

**Goal:** Snow doesn't accumulate under overhangs or sheltered areas

**IMPORTANT - Blender 5.0 Working Solution:**

The key issue: `Object Info` doesn't work with collections, and `Collection Info` outputs instances that raycast ignores. The solution is to use **Collection Info + Realize Instances**, and handle tree instances from other geometry nodes via the modifier stack.

**Setting Up Scene for Occlusion:**

1. In **Outliner**, create collection: `Snow Blockers`
2. Add static blocking objects (rocks, cliffs, buildings)
3. **Do NOT** put instanced trees here - we'll handle those separately
4. Keep terrain mesh OUT of this collection

**Option A: Using Collection (For Static Objects)**

**Add Nodes:**

1. **Collection Info** node (Input → Collection Info)
   - Collection: Select `Snow Blockers`
   - Separate Children: **Disabled**
   - Reset Children: **Disabled**
   - Transform Space: **Original**

2. **Realize Instances** node (Instances → Realize Instances)
   - Connect: Collection Info (Instances) → Geometry
   - **CRITICAL:** This converts instances to real geometry that raycast can see
   - Without this, raycast sees nothing!

3. **Join Geometry** node (Geometry → Join Geometry)
   - We'll use this to combine multiple blocker sources
   - Connect: Realize Instances → Geometry (socket 0)
   - Leave other sockets for additional geometry (trees, etc.)

**Option B: Including Instanced Trees from Other Geometry Nodes**

**For trees created by another Geometry Nodes modifier:**

1. On your **terrain mesh**, ensure the tree-spawning modifier comes **BEFORE** the snow modifier in the stack
2. In the **snow Geometry Nodes tree**, use the **Group Input** geometry
   - The input geometry includes all previous modifiers' results
   - This includes your instanced trees!

3. **Geometry Proximity** node (Geometry → Geometry Proximity) - *Alternative approach*
   - Target: Connect Group Input → Geometry (this includes tree instances)
   - Source Position: Connect Position
   - This gives distance to nearest geometry
   - Use threshold to determine occlusion

**Recommended: Combined Raycast Approach**

**Complete node setup:**

1. **Group Input** → Fork this into two paths:
   - Path A: Your terrain mesh (for snow calculation)
   - Path B: Extract instances for raycast targets

2. **Realize Instances** node
   - Connect: Group Input → Geometry
   - This realizes ALL instances including trees from previous modifiers
   - Output becomes our raycast target

3. **Collection Info** node (if using additional blockers)
   - Collection: `Snow Blockers`

4. **Realize Instances** node (for collection)
   - Connect: Collection Info (Instances) → Geometry

5. **Join Geometry** node
   - Socket 0: Realized instances from Group Input
   - Socket 1: Realized instances from Collection
   - Output: Combined blocking geometry

6. **Position** node (Input → Position)
   - Source position for rays

7. **Combine XYZ** node (if not already created)
   - X: `0.0`, Y: `0.0`, Z: `1.0`
   - Ray direction (up)

8. **Raycast to Geometry** node
   - Target Geometry: Connect Join Geometry → Geometry
   - Source Position: Connect Position
   - Ray Direction: Connect Combine XYZ (0,0,1)
   - Ray Length: `100.0`
   - **Output: Is Hit** (true = blocked, false = exposed)

9. **Boolean Math** node (Utilities → Boolean Math)
   - Operation: **NOT**
   - Connect: Raycast to Geometry (Is Hit) → Boolean
   - **Output: Occlusion Factor** (1.0 = exposed to sky, 0.0 = blocked)

**CRITICAL DEBUGGING TIPS:**

**Problem: Raycast doesn't detect anything**
- Check that Realize Instances is connected and enabled
- Verify Join Geometry has inputs on multiple sockets
- Test with a simple cube in `Snow Blockers` collection
- Set Ray Length to large value (100+ for testing)

**Problem: Trees don't block snow**
- Ensure tree geometry nodes modifier is ABOVE snow modifier in stack
- Add debug: Set Material node after Realize Instances to verify geometry exists
- Check tree instances are actually generating geometry (not empty)

**Problem: Terrain blocks its own snow**
- Don't include terrain in raycast target
- Use separate Group Input for terrain vs. blockers
- Filter by collection membership

**Simplified Alternative (No Occlusion):**

If raycast continues to fail, temporarily skip occlusion:
1. **Value** node (Input → Value)
   - Value: `1.0`
   - Use this as Occlusion Factor (always exposed)
2. Focus on getting normal-based snow working first
3. Debug raycast separately with simpler geometry

#### 3. Combine Factors

**Goal:** Multiply normal factor × occlusion factor to get final snow mask

**Add Nodes:**

1. **Math** node (Utilities → Math)
   - Operation: **Multiply**
   - Connect: Normal Factor (from Map Range) → Value A
   - Connect: Occlusion Factor (from Boolean Math or 1.0) → Value B
   - Result: **Snow Mask** (0.0 to 1.0, where 1.0 = full snow coverage)

2. **Math** node (Utilities → Math)
   - Operation: **Multiply**
   - Connect: Snow Mask → Value A
   - Value B: `0.3` (base snow depth in meters - **adjust this to control snow thickness**)
   - Result: **Snow Depth** (final depth value in world units, typically 0.0 to 0.3m)

**Tweaking Snow Coverage:**
- Increase second multiply value (0.3 → 0.5) for deeper snow
- Decrease (0.3 → 0.1) for light dusting
- Adjust Map Range "From Min" to control slope cutoff

#### 4. Store Snow Depth Attribute

**Goal:** Save snow depth as a named attribute for baking

**Add Node:**

1. **Store Named Attribute** node (Output → Store Named Attribute)
   - **Blender 5.0:** This replaced the old "Capture Attribute" workflow
   - Name: `snow_depth` (type this exactly - used later for baking)
   - Data Type: **Float**
   - Domain: **Point** (vertex-based storage)
   - Connect: Snow Depth (final value) → Value
   - Connect: Geometry (from Group Input) → Geometry
   - Connect output Geometry to next node

**Important:** In Blender 5.0, attributes flow through the Geometry socket, so make sure to chain Store Named Attribute's output Geometry to the next node.

#### 5. Visual Preview (CRITICAL: Simple Offset Only - No Joining!)

**Goal:** See snow coverage directly in the viewport while tweaking parameters

**IMPORTANT - Baking Requirement:**
The visual preview must NOT use Join Geometry or duplicate the mesh. The baking process reads the `snow_depth` attribute from the base terrain vertices. If you join/duplicate geometry, you'll get doubled vertices and the wrong half will have zero values.

**Correct Setup - Simple Offset (Recommended for Baking):**

1. **Store Named Attribute** node (Output → Store Named Attribute)
   - Connected from previous step (snow depth calculation)
   - Name: `snow_depth`
   - Data Type: **Float**
   - Domain: **Point**
   - **This stores the attribute on the original mesh vertices**

2. **Vector Math** node (Utilities → Vector Math)
   - Operation: **Scale**
   - Connect: Combine XYZ (0,0,1) → Vector
   - Connect: Snow Depth value → Scale
   - Result: Offset vector (0, 0, snow_depth) pointing upward

3. **Set Position** node (Geometry → Set Position)
   - Connect: Store Named Attribute (Geometry output) → Geometry
   - Connect: Vector Math (Vector output) → Offset
   - Position: Leave disconnected (uses default vertex positions)
   - **This shifts the SAME vertices upward - no duplication**

4. **Group Output** node
   - Connect: Set Position (Geometry) → Geometry
   - This is the final output of your node tree

**What NOT to Do (Common Mistake):**

❌ **DON'T do this:**
```
[Store Named Attribute]
     ├─→ [Original Mesh Path] → Join Geometry (socket 0)
     └─→ [Set Position] → [Duplicate/Copy] → Join Geometry (socket 1)
```
This creates 2× vertices and baking will read from the original mesh (zeros).

✓ **DO this instead:**
```
[Store Named Attribute]
     ↓
[Set Position] (offset the same vertices)
     ↓
[Group Output]
```

**Alternative: No Visual Preview (Simplest for Baking)**

If you want to keep visualization separate from the baking workflow:

1. **Remove** Set Position and Vector Math nodes entirely
2. Just connect: Store Named Attribute → Group Output
3. Check `snow_depth` values in **Spreadsheet Editor** instead
4. Add a separate Geometry Nodes modifier for visualization if needed
5. **This is the cleanest approach for reliable baking**

**Viewport Verification:**
- Open **Spreadsheet Editor** (Blender 5.0)
- Select terrain object
- Look for `snow_depth` column
- Values should range from 0.0 to ~0.3-0.5
- **This is more reliable than visual preview**

## Baking Snow Depth to Texture (Blender 5.0)

### Automated Method (Recommended)

**Using Python Script:**

1. Open your terrain `.blend` file in Blender
2. Open **Scripting** workspace
3. **Text → Open** → Navigate to `blender/scripts/generate_snow_depth.py`
4. **(Optional)** If you have multiple Geometry Nodes modifiers (e.g., "Trees" and "Snow"):
   - Edit the script's `__main__` section
   - Change `modifier_name = None` to `modifier_name = "Snow"` (or your exact modifier name)
5. Click **Run Script** button (▶ icon)

The script will:
- Automatically find your terrain object (looks for "TerrainPlane" or "terrain")
- Find the Geometry Nodes modifier with `snow_depth` attribute (auto-detect or use specified name)
- Create temporary bake material with Attribute node
- Create bake target image (512×512, 32-bit float)
- Clear existing materials and use bake material only
- Bake `snow_depth` attribute to texture
- Verify bake contains data (not all black)
- Restore original materials
- Export as `textures/snow_depth.exr` (ZIP compressed)
- Clean up temporary resources

**Expected Output:**
```
Baking snow depth for: TerrainPlane
Resolution: 512×512
Found snow modifier: Snow
Ensure object has UV map...
Created bake image: SnowDepth_Bake
Set up bake material with image node: Image Texture
Active node: Image Texture
Baking snow depth attribute to texture...
Bake complete!
Restored original materials
Baked image stats: max=0.3127, avg=0.1234
✓ Bake contains data (values up to 0.3127m)
Saved to: /path/to/textures/snow_depth.exr
Format: OpenEXR, 32-bit float, ZIP compression
File size: 234.5 KB
Cleaned up temporary resources
```

**If You See "WARNING: Baked image appears to be all black":**
1. Check **Spreadsheet Editor** - does `snow_depth` attribute column exist?
2. Verify **Store Named Attribute** node is connected in your Geometry Nodes tree
3. Check modifier is enabled (eye icon in modifier stack)
4. Try specifying the exact modifier name in the script
5. Test with simple values - temporarily set snow depth to constant 1.0

**Customization:**
Edit the script's `__main__` section to adjust:
- `resolution=512` → Change to 1024 for higher detail
- Terrain object name matching
- Output path

### Manual Method (For Understanding)

If you prefer manual control or need to debug:

### 1. UV Unwrap Terrain

**Ensure terrain mesh has proper UVs:**

1. Select terrain mesh
2. Enter **Edit Mode** (Tab)
3. Select all vertices (A)
4. **UV → Unwrap** or use **Smart UV Project**
   - For simple terrain planes: **Unwrap** works well
   - For complex terrain: **Smart UV Project** with low Island Margin
5. In **UV Editor**, verify UVs fill the 0-1 space without major distortion
6. Return to **Object Mode**

**Requirements:**
- UVs should span 0-1 range (check in UV Editor)
- Minimal distortion (for accurate snow placement)
- No overlapping UV islands
- Same UV layout as terrain heightmap (if exported from same mesh)

### 2. Create Bake Target Image

**In Shader Editor (with terrain selected):**

1. Open **Shader Editor** (if not already open)
2. **Add → Texture → Image Texture** node
3. Click **+ New Image** in the node
   - Name: `snow_depth`
   - Width: `512` (match your terrain heightmap resolution)
   - Height: `512`
   - Color: **Black** (default)
   - **Alpha: Unchecked** (single channel)
   - **32-bit Float: CHECKED** ✓ (critical for EXR export)
   - **Type:** Blank
4. **Select the Image Texture node** (click it - should have white outline)
   - **This selection is CRITICAL** - Blender bakes to the selected Image Texture node

**Don't connect this node to anything** - it's just a bake target.

### 3. Set Up Bake Shader (Temporary)

**Create a simple emission shader to bake the attribute:**

1. In Shader Editor, **temporarily modify** your terrain material:
   - Add **Attribute** node (Input → Attribute)
   - Name: `snow_depth` (must match stored attribute name exactly)
   - Connect **Color** output → **Emission** shader → **Material Output**

**Blender 5.0 Note:** You can also use **AOV Output** workflow, but Emission baking is simpler for single-channel data.

### 4. Bake the Attribute

1. With terrain mesh selected, open **Render Properties** panel (camera icon)
2. Scroll to **Bake** section
3. **Bake Settings:**
   - Bake Type: **Emit**
   - Influence → Contributions → **Only Emit** (enabled)
   - Margin: **0 px** (no padding needed for terrain UVs)
   - Target: **Image Textures** (should be selected by default)
   - Clear Image: **Enabled** ✓ (start fresh)
4. Verify Image Texture node is still selected (white outline)
5. Click **Bake** button

**Baking Progress:**
- Blender will show progress in the status bar
- For 512×512: Should take < 1 second
- For 1024×1024: May take a few seconds

**Verify Result:**
- Switch Image Texture node to show `snow_depth` image
- You should see white areas where snow accumulates
- Black areas have no snow
- Gray values = partial snow (slopes or partial occlusion)

### 5. Export as EXR (Blender 5.0)

**Steps:**

1. **Switch to UV Editor or Image Editor** (any editor showing the image)
2. Ensure `snow_depth` image is active
3. **Image Menu → Save As** (or Alt+S)
4. **File Browser Settings:**
   - Navigate to your project: `meshes/` directory
   - Filename: `snow_depth.exr`
   - **File Format: OpenEXR** (should auto-select based on .exr extension)

5. **OpenEXR Settings (Right Panel):**
   - **Color Depth: 32-bit Float** (FULL, not HALF)
   - **Codec: ZIP** (lossless compression - good balance of size/quality)
     - Alternative: **None** (no compression, larger file)
     - **Don't use:** DWAA/DWAB (lossy, may lose precision)
   - **Color Space: Linear** (not sRGB)
   - **Z Buffer: Disabled** (we only need color/depth channel)

6. Click **Save As Image** button

**Blender 5.0 Specific Notes:**
- EXR settings may be in a collapsible panel on the right side of the file browser
- If you don't see format options, ensure the filename ends with `.exr`
- Blender 5.0 defaults to 16-bit Half for EXR - **change to 32-bit Float**

**Verify Export:**

After saving, re-open the file to verify:
1. **File → External Data → Edit Externally** (opens in system viewer if available)
2. Check file properties:
   - Format: OpenEXR
   - Bit depth: 32-bit float
   - Channels: RGB (or R only - either works, Rust code reads first channel)
   - Values: 0.0 to ~0.3-0.5 range (world space meters)

**Common Issues:**
- **Image looks pure black when saved:** You may need to adjust Image Editor → View → Exposure to see low values
- **File is huge (>10MB for 512×512):** Check codec is set to ZIP, not None
- **Values are clamped 0-1:** Ensure Color Space is Linear, not sRGB

## Testing the Export (Blender 5.0)

### In Blender - Visual Verification:

1. **Geometry Nodes Preview:**
   - Ensure Geometry Nodes modifier is enabled (eye icon)
   - Switch viewport to **Material Preview** or **Rendered** shading
   - Snow should appear on appropriate surfaces (flat, exposed areas)
   - Verify snow respects slope limits and overhangs

2. **Check Attribute Values:**
   - Open **Spreadsheet Editor** (new in Blender 5.0 - top menu bar)
   - With terrain selected, spreadsheet shows geometry data
   - Look for `snow_depth` column in the attribute list
   - Values should range from 0.0 (no snow) to ~0.3-0.5 (full snow)
   - **Blender 5.0:** Attributes are displayed in a sortable table format

3. **Verify Coverage:**
   - **Flat surfaces:** Should have maximum snow (bright white/high values)
   - **Steep slopes (>45°):** Should have minimal/no snow (dark/zero values)
   - **Under overhangs:** Should have no snow (zero values)
   - **Exposed slopes:** Should have graduated snow based on angle

4. **Check Baked Texture:**
   - In Image Editor, view `snow_depth` image
   - **View → Display Settings → Exposure:** Increase if image looks too dark
   - White pixels = snow coverage
   - Black pixels = no snow
   - Gray = partial coverage

### Quick Test in Blender (Before Game Integration):

**Method 1: UV Editor Visualization**
1. Split viewport → **UV Editor**
2. Select terrain mesh
3. In UV Editor, select `snow_depth` image from dropdown
4. UVs should align with snow coverage

**Method 2: Re-import and Verify**
1. Create a test plane
2. Add Image Texture node in Shader Editor
3. Load `meshes/snow_depth.exr`
4. Connect to Emission → Material Output
5. View in Rendered mode - should see snow pattern

### In Game (After Rust Implementation):

1. Place `meshes/snow_depth.exr` in project directory
2. Verify file loads without errors:
   ```rust
   let snow_config = SnowConfig::new(
       "meshes/snow_depth.exr",
       Vec2::new(1000.0, 1000.0),
       (512, 512)
   );
   let snow_layer = SnowLayer::load(&mut world, &snow_config)?;
   ```
3. Snow mesh should appear as white layer on terrain
4. Test deformation with player movement
5. Verify trails appear where player walks

**Troubleshooting:**
- **No snow appears in game:** Check EXR file path and resolution match terrain
- **Snow in wrong location:** Verify terrain UVs match between Blender and game
- **Crashes on load:** Check EXR format is 32-bit float, not 16-bit half

## Troubleshooting Baking Issues

### Problem: Baked EXR is Completely Black or Has Wrong Values (SOLVED)

**Symptoms:**
- Baking script reports data exists (e.g., "max=1.0"), but game loads all zeros
- OR: Baking reports "max=1.0" but Spreadsheet shows "max=5.0" (clamped values)
- File size is suspiciously small (< 100KB for 1000×1000)
- Blender console shows: "No active and selected image texture node found in material X"

**Root Cause:**
The terrain object has **multiple material slots** (e.g., "heightmap", "terrain", "snow"). The baking operation requires the active image node to be present in **all** material slots, not just the first one. If any slot is missing the setup, the bake fails silently or produces incorrect data.

**Solution:**
The updated `generate_snow_depth.py` script now:
1. Saves all original materials
2. Temporarily replaces **all material slots** with a single bake material
3. Performs the bake
4. Restores all original materials

**Verification:**
After running the script, you should see:
```
Object has 3 material slot(s): ['heightmap', 'terrain', 'snow']
Temporarily replaced all materials with bake material
Baking with Cycles (EMIT)...
Bake complete!
Baked image stats: max=5.0000, avg=2.5275
Non-zero pixels: 3469321 (86.7%)
✓ Bake contains data (values up to 5.0000m)
File size: 1863.5 KB
```

**If you still get issues:**
- Ensure you're using the latest version of `generate_snow_depth.py`
- Check that the script explicitly mentions "Temporarily replaced all materials"
- Verify file size is > 1MB for 1000×1000 resolution

### Problem: Baked EXR is Completely Black (All Zero Values)

**Symptoms:**
- Python script reports: "WARNING: Baked image appears to be all black"
- Spreadsheet Editor shows `snow_depth` attribute with valid values (e.g., max=5.0)
- But exported EXR has all zero values
- Debug output may show: "First 10,404 vertices: all zeros, Second 10,404 vertices: actual values"

**Root Cause:**
Your Geometry Nodes setup is joining/duplicating geometry, creating 2× vertices. The baking process reads from the base mesh vertices (which have `snow_depth = 0`), not the offset/duplicated vertices (which have the actual values).

**Diagnosis:**
1. Select terrain object
2. Check vertex count in **Spreadsheet Editor**
3. If vertex count is exactly 2× your base mesh count → you're joining geometry
4. Check your Geometry Nodes for **Join Geometry** or mesh duplication nodes

**Solution:**

**Option 1: Remove Visual Offset (Simplest - Recommended)**
1. Open your "Snow Accumulation" Geometry Nodes tree
2. Find and **delete** these nodes:
   - Any **Join Geometry** nodes that combine original + offset mesh
   - Any mesh duplication/copy nodes
   - (Keep Set Position if it's just offsetting, not duplicating)
3. Final node chain should be:
   ```
   [Calculate Snow Depth]
   ↓
   [Store Named Attribute: "snow_depth"]
   ↓
   [Group Output]  (no offset, no join)
   ```
4. Re-run the baking script
5. Verify bake in Spreadsheet Editor instead of visual preview

**Option 2: Fix the Node Structure**
If you have:
```
[Store Named Attribute]
     ├─→ [Original Path] → Join Geometry
     └─→ [Set Position Offset] → Join Geometry
```

Change to:
```
[Store Named Attribute]
     ↓
[Set Position] (offset the SAME vertices, don't duplicate)
     ↓
[Group Output]
```

**Option 3: Separate Modifiers**
1. Create TWO Geometry Nodes modifiers on terrain:
   - Modifier 1: "Snow Data" - Just calculates and stores `snow_depth` attribute
   - Modifier 2: "Snow Visualization" - Reads attribute and creates visual offset
2. **Disable Modifier 2** before baking
3. Bake with only Modifier 1 enabled
4. Re-enable Modifier 2 after export

**Verification After Fix:**
1. Select terrain in Blender
2. Open **Spreadsheet Editor**
3. Check vertex count - should match your base terrain (not doubled)
4. Check `snow_depth` column - should have non-zero values
5. Re-run `generate_snow_depth.py` script
6. Should see: "✓ Bake contains data (values up to X.XXXXm)"

### Problem: Some Areas Have Snow Values, Others Are Zero

**Symptoms:**
- Baked EXR has partial data
- Some regions are black (zero) unexpectedly
- Values are present but in wrong locations

**Possible Causes:**
1. **UV Layout Issue:**
   - UVs are overlapping or outside 0-1 range
   - Solution: Re-unwrap terrain UVs, ensure no overlap

2. **Modifier Stack Order:**
   - Other modifiers before snow are transforming geometry
   - Solution: Move snow modifier to top of stack for testing

3. **Partial Geometry Selection:**
   - Store Named Attribute has a Selection input that's filtering vertices
   - Solution: Ensure Selection input is empty (affects all vertices)

## Troubleshooting Raycast/Occlusion Issues

### Problem: Raycast Doesn't Detect Any Geometry

**Diagnosis:**
1. Add **Viewer** node after your Join Geometry
2. Check **Spreadsheet Editor** - does it show geometry?
3. If Spreadsheet is empty → Realize Instances didn't work

**Solutions:**
- Ensure **Realize Instances** is connected between Collection Info and Join Geometry
- Check Collection Info is set to correct collection
- Verify collection actually has objects in it (check Outliner)
- Try realizing instances twice (sometimes needed for nested instances)

### Problem: Instanced Trees Don't Block Snow

**Diagnosis:**
Your tree instances are in a separate geometry nodes modifier.

**Solution - Modifier Stack Order:**
```
Terrain Mesh
├─ 1. Tree Scatter (Geometry Nodes) ← MUST BE FIRST
└─ 2. Snow Accumulation (Geometry Nodes) ← SECOND
```

**In Snow node tree:**
1. The **Group Input** geometry already includes realized trees from previous modifier
2. **DON'T** realize Group Input again (causes issues)
3. Instead, use **Geometry to Instance** on Group Input, then **Realize Instances**

**Alternative - Separate Input for Blockers:**

Add a **Geometry Nodes Modifier Input** for blocker geometry:
1. In modifier properties, add **Input** → **Geometry**
2. Name it: `Blocker_Geometry`
3. Drag your tree-spawning object into this input
4. In node tree, use this input for raycast target

### Problem: Everything Gets Blocked (No Snow Anywhere)

**Diagnosis:**
Raycast target includes the terrain itself, or ray direction is wrong.

**Solutions:**
- Verify Ray Direction is (0, 0, 1) - straight up
- Don't include terrain mesh in blocker geometry
- Check Ray Length isn't too short
- Test with **Boolean Math → NOT** on the Is Hit output

### Alternative Approach: Vertex Paint Method

**If raycast continues to fail, use manual vertex painting:**

1. Skip all raycast nodes
2. Add **Vertex Color** input node (Input → Color Attribute)
   - Name: `occlusion_mask`
3. Use as Occlusion Factor (white = exposed, black = blocked)
4. In Edit Mode, use **Vertex Paint** mode to paint occlusion manually
5. More control, but less automatic

### Alternative Approach: Use Z-Depth

**For simple overhang detection without raycast:**

1. **Separate XYZ** node
   - Connect: Position → Vector
   - Use Z output
2. **Compare** node
   - A: Z position
   - B: Threshold value (height above which = no overhang)
   - Greater Than
3. Use as simple occlusion approximation
4. Works for simple overhangs, not complex geometry

## Iteration Workflow

**To adjust snow coverage:**
1. Modify normal threshold (Map Range "From Min" value - lower = more snow on slopes)
2. Adjust base snow depth multiplier (final Math → Multiply value)
3. Re-bake attribute to texture (Render → Bake)
4. Re-export EXR (Image → Save As)
5. Restart game to reload (hot-reload not implemented yet)

**For occluding objects:**
- Add/remove objects from `Snow Blockers` collection
- Move tree modifier above snow modifier if needed
- Re-bake to update occlusion
- Don't forget **Realize Instances** step!

**Performance Optimization:**
- Lower bake resolution for faster iteration (256×256 for testing)
- Disable raycast (use Value = 1.0) while tweaking slopes
- Only enable occlusion for final bake
- Use simpler blocker geometry during authoring

## Advanced: Working with Instanced Trees

### Complete Example: Trees from Scatter Modifier

**Scenario:** You have a "Tree Scatter" geometry nodes modifier that instances trees, and you want those trees to block snow.

**Step 1: Modifier Stack Setup**
```
Terrain Object
├─ Geometry Nodes: "Tree Scatter" ← Position 1 (FIRST)
└─ Geometry Nodes: "Snow Accumulation" ← Position 2 (SECOND)
```

**Step 2: In Tree Scatter Node Tree**
- Outputs instanced trees on the terrain
- No special setup needed
- Just ensure instances are actually being created

**Step 3: In Snow Accumulation Node Tree**

**Method A: Use Incoming Geometry (Simpler)**
```
[Group Input] (contains terrain + tree instances from previous modifier)
      ├─→ [Mesh to Points] → [Points to Extract Trees]
      │
      ├─→ [Delete Geometry] → [Keep Only Terrain for Snow]
      │     ↑
      │     └─ Selection: Invert (keeps terrain, removes instances)
      │
      └─→ [Realize Instances] → [Tree Geometry for Raycast]
            └─→ [Join Geometry] ← (blocker geometry)
```

**Method B: Reference Tree Object Directly**
1. Put actual tree mesh in `Snow Blockers` collection
2. Use Collection Info → Realize Instances → Join Geometry
3. Simpler but doesn't respect scattering variations

**Step 4: Connect to Raycast**
```
[Join Geometry] (all blockers)
      ├─→ [Raycast to Geometry] (Target Geometry)

[Position] ───→ [Raycast to Geometry] (Source Position)
[Combine XYZ(0,0,1)] ───→ [Raycast to Geometry] (Ray Direction)

[Raycast to Geometry] (Is Hit) ───→ [Boolean Math: NOT] ───→ Occlusion Factor
```

### Node Tree Layout Example

**For terrain with separate tree instances:**

```
┌─────────────────────────────────────────────────────┐
│ BLOCKER PREPARATION (Top of tree)                  │
├─────────────────────────────────────────────────────┤
│ [Collection Info: Snow Blockers]                   │
│         ↓                                           │
│ [Realize Instances]                                 │
│         ↓                                           │
│ [Join Geometry] ← (socket 0)                        │
│         ↓                                           │
│    (blocker_geo) ──────────────┐                    │
└────────────────────────────────┼────────────────────┘
                                 │
┌────────────────────────────────┼────────────────────┐
│ TERRAIN PROCESSING              ↓                   │
├─────────────────────────────────────────────────────┤
│ [Group Input: Geometry]                             │
│         ├─→ [Position] ─────────┐                   │
│         │                       ↓                   │
│         └─→ [Normal] ──→ [Dot Product] → ...        │
│                                 ↓                   │
│                         [Raycast to Geometry]       │
│                           ↑                         │
│                    (blocker_geo)                    │
│                                 ↓                   │
│                         [Boolean Math: NOT]         │
│                                 ↓                   │
│                         (occlusion_factor)          │
└─────────────────────────────────────────────────────┘
```

## Advanced: Directional Snow Accumulation

**Optional enhancement for wind-driven snow:**

1. Replace raycast up direction (0,0,1) with angled vector
   - Example: `(0.3, 0, 1)` for wind from +X direction
   - Normalize this vector for consistent results
2. **Vector Math** node: Normalize
   - Input: Combine XYZ (0.3, 0, 1)
   - Output → Ray Direction
3. Creates asymmetric accumulation (more snow on lee side)
4. Combine with normal factor to prevent snow on windward slopes

**Advanced Normal Calculation:**
```
Wind Direction: (-0.3, 0, -1) normalized
↓
[Vector Math: Dot Product]
├─ Normal (surface normal)
└─ Wind Direction
↓
[Map Range]
├─ From Min: -0.5 (windward side)
├─ From Max: 0.5 (lee side)
└─ Result: More snow on protected slopes
```

## Quick Start: Minimal Working Setup (No Occlusion)

**If you're having trouble with raycast, start with this simple version first:**

### Phase 1: Normal-Based Snow Only

1. **Create Geometry Nodes modifier** on terrain
2. **Add these nodes only:**
   ```
   [Group Input]
   ↓
   [Normal] → [Vector Math: Dot] ← [Combine XYZ: 0,0,1]
   ↓
   [Map Range: 0.7→1.0 to 0→1]
   ↓
   [Math: Multiply by 0.3] → (snow_depth value)
   ↓
   [Store Named Attribute: "snow_depth"]
   ↓
   [Group Output]
   ```

3. **Test:** Switch to Spreadsheet Editor, verify `snow_depth` column exists
4. **Bake and export** following steps above
5. **Verify in game** - snow should appear on flat surfaces

### Phase 2: Add Occlusion Later

Once Phase 1 works:

1. **Add Collection Info → Realize Instances → Join Geometry**
2. **Insert Raycast** between Map Range and final Multiply
3. **Multiply:** normal_factor × occlusion_factor × 0.3
4. **Debug:** Use Viewer node to verify blocker geometry exists
5. **Re-bake and test**

## Debugging Raycast Step-by-Step

**Test 1: Verify Blocker Geometry Exists**
1. Add **Viewer** node after **Realize Instances**
2. Select terrain object
3. Open **Spreadsheet Editor**
4. Check "Viewer" data source
5. Should show vertices/faces from blocker objects
   - **If empty:** Realize Instances isn't working
   - **If has data:** Geometry exists, raycast should work

**Test 2: Visualize Raycast Results**
1. Add **Store Named Attribute** node
   - Name: `is_occluded`
   - Value: Connect "Is Hit" output from Raycast
   - Data Type: Boolean → Float (convert with Math node if needed)
2. Bake this attribute to see occlusion map
3. White = blocked, Black = exposed
4. If all black/white uniformly → raycast isn't working

**Test 3: Simple Cube Test**
1. Add a cube above your terrain
2. Add cube to `Snow Blockers` collection
3. Position cube to cast obvious shadow
4. Re-calculate geometry nodes
5. Should see circular area of no snow under cube
   - **If no effect:** Raycast isn't seeing the cube
   - **If works:** Your collection setup is correct

**Test 4: Check Ray Parameters**
```
Position: Should be terrain vertex positions (auto from Position node)
Direction: Must be (0, 0, 1) exactly
Length: Try 1000.0 (very large) to rule out length issues
Target Geo: Must have realized geometry (not instances)
```

## Export Checklist

### Blender Setup:
- [ ] Terrain has proper UVs (checked in UV Editor)
- [ ] Geometry Nodes modifier applied and enabled (eye icon)
- [ ] `snow_depth` attribute visible in Spreadsheet Editor
- [ ] Values in reasonable range (0.0 to ~0.3-0.5)
- [ ] Tested in Blender viewport (visual preview with Set Position offset)

### Baking (Choose One Method):

**Automated (Recommended):**
- [ ] Run `blender/scripts/generate_snow_depth.py` in Blender
- [ ] Verify console output shows successful bake
- [ ] Check `textures/snow_depth.exr` exists

**Manual:**
- [ ] Baked to image texture (512×512 or 1024×1024, 32-bit float)
- [ ] Image shows expected snow pattern (white on flat areas)
- [ ] Saved as `textures/snow_depth.exr` (OpenEXR format, ZIP codec)
- [ ] Re-opened file to verify export settings (32-bit float, not 16-bit half)

### Verification:
- [ ] File size reasonable (512×512 should be 200-500KB with ZIP)
- [ ] File path: `textures/snow_depth.exr` relative to project root
- [ ] Ready for game integration

### Game Integration:
- [ ] Update `SnowConfig` path if different from default
- [ ] Load snow layer: `SnowLayer::load(&mut world, &config)?`
- [ ] Hook up deformation to player movement system
- [ ] Test snow rendering and trail deformation

## Common Blender 5.0 Gotchas

1. **Realize Instances is REQUIRED** - Collection Info outputs instances by default
2. **Modifier stack order matters** - Tree scatter must be BEFORE snow
3. **32-bit Float must be set manually** - Blender defaults to 16-bit Half for EXR
4. **Store Named Attribute replaces Capture Attribute** - Old tutorials won't work
5. **Object Info doesn't take collections** - Use Collection Info instead
6. **Instances are invisible to raycast** - Must realize first
7. **Group Input includes previous modifiers** - Can be used for tree instances
