- name
- forge-grass-rendering
- description
- Add instanced grass rendering with segmented blades, wind animation, LOD density rings, and terrain LOD with geomorphing to an SDL GPU project
Add grass rendering to an SDL GPU project using `forge_scene.h`. Implements
instanced segmented grass blades with wind animation, density-based LOD rings,
and tile-based terrain LOD with vertex shader geomorphing.
## When to use
- Adding vegetation to a 3D scene
- Implementing instanced rendering with per-instance attributes
- Adding LOD systems for both terrain and foliage
- Split-screen or multi-viewport rendering
## Key API calls
- `forge_shapes_plane()` — generate tile meshes at different resolutions
- `forge_scene_upload_buffer_deferred()` — per-frame instance buffer upload
- `forge_scene_create_shader()` — create custom shaders alongside forge_scene
- `SDL_SetGPUViewport()` / `SDL_SetGPUScissor()` — split-screen viewports
- `SDL_DrawGPUIndexedPrimitives()` — instanced draw with per-instance buffer
- `forge_noise_fbm2d()` — procedural heightmap generation
- `forge_hash_wang()` / `forge_hash_to_float()` — deterministic placement variation
## Correct order
1. Generate procedural heightmap via fBm noise
2. Upload heightmap as R32_FLOAT GPU texture
3. Create tile meshes at multiple LOD resolutions (both triangle and line IBs)
4. Create grass blade meshes at multiple segment counts
5. Create pipelines: terrain fill/line/shadow + grass fill/line/shadow
6. Per frame:
a. Select terrain tile LODs based on camera distance
b. Place grass instances with distance/frustum/slope culling
c. Upload instance buffers via deferred upload
d. Shadow pass: draw terrain tiles + near grass into shadow map
e. Main pass: set left viewport → draw terrain + grass filled;
set right viewport → draw terrain + grass wireframe
f. Reset viewport for UI
## Grass blade geometry
Each blade is a vertical segmented strip with `2*(N+1)` vertices and `2*N`
triangles. The `height_t` attribute (0 at base, 1 at tip) controls wind
weighting and tapering.
```c
for (int i = 0; i <= segments; i++) {
float t = (float)i / (float)segments;
float half_w = 0.5f * (1.0f - t * GRASS_BLADE_TIP_TAPER);
verts[2*i+0] = (GrassVertex){ {-half_w, t, 0}, t };
verts[2*i+1] = (GrassVertex){ { half_w, t, 0}, t };
}
```
## Per-instance data
```c
typedef struct {
float position[3]; /* world-space base position */
float rotation; /* Y-axis rotation (radians) */
float scale[2]; /* width, height */
float color[3]; /* RGB tint */
float _pad; /* alignment to 40 bytes */
} GrassInstance;
```
## Wind animation (vertex shader)
```hlsl
float wind_weight = height_t * height_t; /* quadratic: tip bends most */
float phase = time * wind_speed + world.x * 0.7 + world.z * 0.3;
float wind_offset = sin(phase) * wind_strength * wind_weight;
```
## Terrain geomorphing (vertex shader)
```hlsl
float2 snapped_uv = round(global_uv / coarse_cell_size) * coarse_cell_size;
float2 morphed_uv = lerp(global_uv, snapped_uv, morph_factor);
float morphed_x = lerp(world_x, snapped_x, morph_factor);
float h = height_tex.SampleLevel(smp, morphed_uv, 0).r;
```
## Common mistakes
- **Forgetting half-aspect projection** — split-screen viewports are half the
screen width, so the projection matrix must use `half_w / height` aspect
- **Using TRIANGLELIST pipeline for wireframe** — the right viewport needs a
LINELIST pipeline with a separate line index buffer (each triangle edge as
two indices)
- **Grass shadow without wind** — the shadow vertex shader must apply the same
wind animation as the main shader, or shadows will not match blade positions
- **Missing morph factor** — without geomorphing, terrain tile LOD switches
cause visible popping; morph_factor must ramp 0→1 in the last 20% of each
LOD distance band
- **Forgetting to reset viewport for UI** — after right-viewport draws, reset
viewport and scissor to full screen before forge_scene_end_main_pass
## Reference
See [Lesson 50 — Grass Rendering](../../../lessons/gpu/50-grass-rendering/)
for the full implementation.
在 GitHub 查看