World of Warcraft Grass Rendering System
Overview
This document describes the WoW ground-effect rendering architecture. OpenWarcraft3 resolves the same terrain effect and detail M2 data used by the WoW client, then reuses the normal M2 material path.
Note: authoritative M2 instancing is the active path. The camera-grid/height-atlas prototype remains disabled because it cannot yet preserve doodad geometry/material identity; see static-grass-and-height-atlas.md.
Current OpenWarcraft3 Implementation
Architecture
OpenWarcraft3 resolves ground clutter from the WoW MPQ data:
GroundEffectTexture.dbcsupplies terrain effects, doodad IDs, and amount/density; weighted layouts are version-dependent.GroundEffectDoodad.dbcresolves each doodad ID to its model name; in the bundled archive the model string is field 2, and.mdlnames are normalized to.m2.- Models are loaded from
World\\NoDXT\\Detail\\and rendered through the normal M2 path, so embedded WoW material/texture references remain authoritative. - ADT alpha coverage and height samples control whether and where each ground-effect model is placed.
- Placement randomness is deterministic per ADT chunk; renderer tuning and schema constants are named
WOW_GRASS_*defines inr_wowmap.h. - Ground-effect placements are compiled once per ADT window into persistent model groups.
- Road-like terrain layers are excluded when their sampled ADT coverage is at least
WOW_GRASS_ROAD_COVERAGE_MIN; the heuristic follows WoWee's local layer-weight check.
Instanced rendering
Ground-effect instances are rendered with GPU instancing, not one full M2_RenderModel
call per clump (which produced ~47k draw calls and dropped the frame rate by 10x):
Wow_AddGroundEffectInstanceinitially links each placement intowow_world.ground_effects; the firstWow_DrawGrassgroups it by model and builds eachMATRIX4once.- Each group uploads one
GL_STATIC_DRAWinstance VBO. The temporary matrices and linked placement nodes are then freed; ADT-window replacement releases the VBOs. R_GameRenderModelInstanced→M2_RenderInstanced(r_m2.c) renders each model batch once withR_DrawBufferInstanced, binding the persistent matrix VBO forglDrawArraysInstanced.R_ModelShaderInstanced(r_shader.c) is a copy of the model vertex shader with a per-instancemat4 i_instanceattribute (4 vec4 columns, divisor 1) folded in instead ofuModelMatrix; the fragment shader is the sharedmodel_fs. It performs distance fade and root-anchored wind.R_DrawBufferInstanced(r_buffer.c) binds a shared VAO to the batch VBO plus an instanced matrix VBO and draws all group instances without re-uploading them.
The Classic detail assets inspected in Elwynn contain no keyed bone tracks. Wind is therefore procedural GPU deformation, scaled from each model's authored Z bounds and phased from the instance's world position; roots remain fixed.
The loader supports both known 11-DWORD layouts. It detects the bundled legacy layout (ID, date stamp, continent, zone, texture ID, four doodad IDs, density, sound) versus the modern weighted layout (ID, four doodad IDs, four weights, amount/coverage, terrain type) by resolving the candidate doodad IDs through GroundEffectDoodad.dbc. Legacy rows receive uniform weights for valid slots and ignore 0xffffffff; modern rows retain their stored weights. dbctool info remains the first check when adding another client archive.
Retired procedural prototype (historical)
The blade geometry, per-chunk grass buffers, procedural coloring, and budgets below describe the removed prototype. They are retained only to explain why authoritative M2 instancing replaced it; they are not current renderer contracts. Current code uses the instanced-M2 architecture above and the implementation checklist in static-grass-and-height-atlas.md.
1. Blade Generation (r_wowmap_grass.c)
The core grass building function processes 8x8 terrain chunks:
void Wow_BuildGrassForChunk(wowAdtChunk_t *chunk,
BYTE const alpha[4][WOW_ALPHA_TEXELS],
wowLayer_t const *layers,
DWORD layer_count)
Sampling Strategy:
- Samples the 8x8 chunk grid at WOW_GRASS_CELL_STEP intervals.
- Each eligible sample selects weighted doodad models from the MPQ effect record.
- Each placement uses the ADT height map and is registered as a renderer-owned doodad instance.
Placement Algorithm:
1. Sample the alpha map at a grid position to determine terrain coverage.
2. Calculate attempts from the DBC amount/density field and alpha coverage.
3. Select a valid doodad ID uniformly from the four slots in GroundEffectTexture.dbc.
4. Resolve its model name through GroundEffectDoodad.dbc.
5. Sample height at a jittered position and place the M2 at that terrain height.
Coloring: - Base green: 105-175 (varies per clump) - Brown: 42-64 (dark brown) - Dark blue: 28-46 (shadow/depth) - Alpha: 220 (slightly transparent)
2. Blade Geometry
Each clump consists of 2 perpendicular crossed triangles:
First blade: - Base width: 0.22-0.44 units - Height: 1.05-2.30 units - Full alpha opacity
Second blade (perpendicular): - Width: 85% of first blade (0.19-0.37 units) - Height: 92% of first blade (0.97-2.12 units) - Adds visual density without doubling fill rate
Geometry Details: - Each triangle has 3 vertices (6 vertices total per clump) - Normal vectors calculated from blade direction for proper lighting - UV coordinates simple: (0,0) → (1,0) → (0.5,1) for tapered appearance
3. Shader System (r_wowmap_shader.c)
Vertex Shader:
- Wind wave animation: sin(time + dot(position.xy, vec2(0.071, 0.049)))
- Wave amplitude: 0.22 units max deflection
- Maintains blade orientation while swaying
Fragment Shader:
- Distance-based fade: smoothstep(WOW_GRASS_FADE_START_DISTANCE, WOW_GRASS_DRAW_DISTANCE)
- Lighting calculation: 0.55-1.0 range (never fully dark)
- Alpha blending with back-to-front rendering
Uniforms:
- uGrassTime: Animated time value for wind
- uGrassCameraOrigin: Camera position for distance calculations
- uGrassDrawDistance: Maximum render distance (220.0 units)
- uLightMatrix: For proper lighting direction
- uViewProjectionMatrix: Standard 3D transform
Data Structures
Layer Definition (wowLayer_t)
typedef struct {
DWORD texture_id; // Terrain texture reference
DWORD flags; // Layer flags (detail, PBR, etc.)
DWORD offset_in_mcal; // Offset into alpha map data
DWORD effect_id; // GroundEffectTexture.dbc reference
} wowLayer_t;
The effect_id field (at offset +12 bytes) is the key link to grass configuration.
Chunk State (wowAdtChunk_t)
VERTEX *grass_buffer; // Vertex array object
DWORD num_grass_vertices; // Vertex count
BOX3 grass_bounds; // Bounding box for culling
Performance
Vertex Budget per Chunk: - Max clumps: 16 (4x4 grid) × 4 (per location) = 64 - Vertices per clump: 6 - Max vertices per chunk: 384 - Memory per chunk: ~24 KB (at 64 bytes per vertex)
Draw Call Strategy:
- All ground-effect instances are batched per model: one glDrawArraysInstanced per M2 batch, regardless of how many clumps use that model.
- Culled by squared distance (WOW_GRASS_DRAW_DISTANCE) and frustum before grouping.
- Typical visible cost: a handful of instanced draw calls (one per distinct model in view).
Real World of Warcraft Implementation
Architecture
WoW's data defines a doodad-based system using actual game asset models:
- Data Source: GroundEffectTexture.dbc maps terrain types to doodad models
- Placement: MCNK carries an 8x8 two-bit layer map and a 64-bit no-effect-doodad suppression map in addition to MCLY effects, MCAL coverage, and MCVT height
- Models: M2 doodad files with full animations and LOD support
- Rendering: The public data establishes M2 inputs, not Blizzard's exact batching path
- Variety: Up to 4 different grass/plant models per terrain type with weighted selection
GroundEffectTexture.dbc / GroundEffectDoodad.dbc
The two 11-DWORD GroundEffectTexture.dbc layouts (legacy vs. modern weighted) and the GroundEffectDoodad.dbc
id/doodad-path/flags layout are documented in docs/dbc-reference.md.
Placement Algorithm
The exact original-client placement algorithm has not been established. The current OpenWarcraft3 implementation uses the following MCAL-derived approximation:
- Determine grass layer: Find layer with highest alpha coverage that has
effect_id != 0 - Look up GroundEffectTexture: Query
GroundEffectTexture.dbc[effect_id] - Select doodad: Weighted random selection from 4 doodad options
- Calculate density: Use
AmountAndCoveragefield to determine how many instances to place - Sample placement: Grid-based placement with jitter
- Sample height: Use MCVT (height map) to place doodad on terrain surface
- Instance model: Call doodad renderer with calculated transform
The redesign in static-grass-and-height-atlas.md first parses the MCNK layer/suppression maps and validates their orientation instead of claiming the approximation is original-client behavior.
ADT Chunk Format Integration
Terrain Chunks (MCNK)
Each 8x8 unit terrain chunk contains:
Layers (MCLY):
typedef struct {
DWORD texture_id; // Texture index in MMDX
DWORD flags;
DWORD offset_in_mcal; // Offset into alpha data
DWORD effect_id; // >>> GRASS REFERENCE <<<
} wowLayer_t;
Example scenario: - Layer 0: Stone texture, no effect_id (0) - Layer 1: Grass texture, effect_id=5 (look up GroundEffectTexture.dbc[5]) - Layer 2: Snow texture, effect_id=0 (no grass)
MCAL determines terrain texture coverage. Do not infer that its percentage is also the original client's ground-effect occupancy rule: MCNK has separate per-cell layer and no-effect-doodad maps that must be parsed and validated.
Height Map (MCVT)
The height map provides 9×9 height samples per chunk: - Used to position grass at exact terrain height - Interpolation within grid cells places grass on sloped terrain
Alpha Maps (MCAL)
Four 64×64 alpha textures per chunk: - Track blend amount for each layer - Used to determine grass coverage at each sample point - Format: mostly-opaque for layer 0, blend amounts for layers 1-3
Improvements in OpenWarcraft3
Phase 1: Data-Driven Integration (retired prototype)
Added in this session:
-
GroundEffectTexture.dbc Loading
-
Helper Functions
Wow_GetGroundEffectTexture(effect_id): Lookup and cacheWow_SelectDoodadFromWeights(weights, seed): Weighted random selection-
Wow_GrassEffectIdForCoverage(...): Find dominant grass layer -
Data-Driven Coloring
- The retired prototype varied procedural blade colors from terrain data.
-
This is not a fallback: current rendering requires authoritative DBC and M2 data.
-
Reason it was replaced
- Generic blade geometry could not preserve authored doodad shape or material identity.
- Treating missing DBC data as procedural grass hid asset/data failures.
Phase 2: Full Doodad System (Implemented)
Done in this session (replacing the Phase 1 blade generation):
- M2 Model Instantiation
Wow_AddGroundEffectInstance()instantiates the resolved GroundEffectDoodad M2 per clump.-
Instances are rendered through GPU instancing (
M2_RenderInstanced), not per-instance model draws. -
Animation Support
- Verified Classic doodad M2s have zero keyed bone tracks despite a nominal sequence
-
The instanced vertex shader provides world-phased, root-anchored wind
-
Appearance Variation
- Same grass placement, different visual assets
- Weighted selection from 4 doodad options per terrain type
-
Much richer visual variety
-
Material System
- Uses each M2's authoritative textures and material flags through the normal M2 path.
File Organization
Source Files
Core rendering:
- renderer/wow/r_wowmap.h - Type definitions
- renderer/wow/r_wowmap.c - Main terrain loading
- renderer/wow/r_wowmap_grass.c - Grass generation
- renderer/wow/r_wowmap_shader.c - Grass shader code
Data handling:
- common/world_wow.c - DBC parsing utilities
- common/world_wow.h - World data structures
Configuration
Runtime cvars:
- r_module: Select renderer (must be "wow" for grass)
- r_grass: Toggle grass rendering (if implemented)
- r_grass_density: Density multiplier (0.0-2.0)
- r_grass_distance: Draw distance in units
Constants in r_wowmap.h:
#define WOW_GRASS_DRAW_DISTANCE 220.0f
#define WOW_GRASS_DENSITY 1.0f
#define WOW_GRASS_CELL_STEP 2
#define WOW_GRASS_VERTICES_PER_CLUMP 6
Performance Characteristics
CPU Cost
Per Chunk Processing: - Alpha map coverage sampling: 16 samples - Height interpolation: 16 samples (3-4 float operations each) - Random number generation: 16-64 operations (seeded, deterministic) - Vertex buffer allocation and filling: Linear in clump count - Typical: <1 ms per chunk on modern CPU
Batch Cost: - Frustum culling: ~64 chunks to check - Visibility testing: 16 chunks visible typically - State setup: ~1 draw call for all grass - Typical: <5 ms per frame for visible grass
GPU Cost
Vertex Processing: - 384 vertices max per chunk (typical: 80-120) - ~16,000 total grass vertices visible typically - Wind animation: Simple sine wave, negligible cost - Typical: <1 ms vertex processing
Fragment Processing: - Simple alpha test/blend - Distance fade: One smoothstep per pixel - Lighting: Lerp between 0.55-1.0 range - Typical: <2 ms fragment processing
Memory Usage
Static: - DBC cache: ~512 entries × 44 bytes = 22 KB - Shader objects: ~5-10 KB
Per-Frame: - Vertex buffers: ~100 chunks × 384 vertices × 64 bytes = 2.4 MB - Transient allocations: Minimal (buffers freed after upload)
Testing and Validation
Required Data Files
From World of Warcraft:
- DBFilesClient\GroundEffectTexture.dbc - Terrain-to-doodad mapping
- DBFilesClient\Map.dbc - Map metadata
- DBFilesClient\WorldSafeLocs.dbc - Safe spawns
- World\Maps\<map>\<map>.wdt - WoW map tile index
- World\Maps\<map>\<map>_<x>_<y>.adt - WoW terrain chunks and ground-effect layer IDs
Validation Tools
MPQ Inspection:
build/bin/dbctool -mpq data/world-of-warcraft/dbc.MPQ info 'DBFilesClient\GroundEffectTexture.dbc'
build/bin/dbctool -mpq data/world-of-warcraft/dbc.MPQ info 'DBFilesClient\GroundEffectDoodad.dbc'
build/bin/mpqtool -mpq data/world-of-warcraft/terrain.MPQ ls 'World\Maps\Azeroth'
Rendering Inspection:
make run-wow ARGS="+set wow_playerinfo '\race\Human\sex\Male\class\1\appearance\0' +map playercreate +set r_stats 1 +com_frame_limit 300"
# At runtime, use `set r_grass 0` to isolate grass submission cost.
Known Issues and Limitations
- Required DBC data
-
GroundEffectTexture and GroundEffectDoodad are authoritative; missing records are logged and must be fixed rather than replaced with procedural colors or guessed models.
-
Placement remains an approximation
-
The public data establishes layer effects, density, suppression, models, and terrain height, but the exact original-client placement algorithm has not been confirmed.
-
Animation source
-
The inspected Classic detail M2s have no keyed bone tracks. Current movement is root-anchored GPU wind rather than nonexistent asset animation.
-
No dedicated far LOD
- All resident instances use their authored M2 geometry. Add a far representation only if GPU vertex/fill profiling—not CPU submission—shows it is necessary.
References
WoW Development Resources
- wowdev.wiki: Comprehensive ADT and DBC format documentation
- ADT specification with MCLY, MCAL, MCVT chunks
- WDBC file format and common DBC structures
-
GroundEffectTexture.dbc field layout
-
getTrinityCore: Open-source WoW server
- Reference implementations of terrain rendering
- DBC parsing examples
-
M2 model handling
-
getMaNGOS: Earlier WoW server project
- Documentation of vanilla/TBC DBC formats
- Terrain chunk processing
Related Systems
- M2 Rendering:
renderer/m2/r_m2.c - Doodad model loading and animation
- Character appearance system
-
Animation sequence handling
-
ADT Loading:
renderer/wow/r_wowmap.c - Chunk parsing and initialization
- Texture loading and management
-
Height map processing
-
DBC Utilities:
common/world_wow.c CM_WowValidDbc()- Header validationCM_WowDbcString()- String block accessCM_WowFindMapId()- Example DBC iteration
Conclusion
The current renderer resolves authoritative GroundEffect DBC records and M2 assets, compiles immutable placement matrices once per ADT window, and submits persistent instanced batches. Height placement is CPU work only during window construction; distance fade, alpha coverage, and wind are GPU work. The procedural camera-grid path remains disabled until it can preserve the same model and material identity.