Skip to content

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.dbc supplies terrain effects, doodad IDs, and amount/density; weighted layouts are version-dependent.
  • GroundEffectDoodad.dbc resolves each doodad ID to its model name; in the bundled archive the model string is field 2, and .mdl names 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 in r_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_AddGroundEffectInstance initially links each placement into wow_world.ground_effects; the first Wow_DrawGrass groups it by model and builds each MATRIX4 once.
  • Each group uploads one GL_STATIC_DRAW instance VBO. The temporary matrices and linked placement nodes are then freed; ADT-window replacement releases the VBOs.
  • R_GameRenderModelInstancedM2_RenderInstanced (r_m2.c) renders each model batch once with R_DrawBufferInstanced, binding the persistent matrix VBO for glDrawArraysInstanced.
  • R_ModelShaderInstanced (r_shader.c) is a copy of the model vertex shader with a per-instance mat4 i_instance attribute (4 vec4 columns, divisor 1) folded in instead of uModelMatrix; the fragment shader is the shared model_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:

  1. Determine grass layer: Find layer with highest alpha coverage that has effect_id != 0
  2. Look up GroundEffectTexture: Query GroundEffectTexture.dbc[effect_id]
  3. Select doodad: Weighted random selection from 4 doodad options
  4. Calculate density: Use AmountAndCoverage field to determine how many instances to place
  5. Sample placement: Grid-based placement with jitter
  6. Sample height: Use MCVT (height map) to place doodad on terrain surface
  7. 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:

  1. GroundEffectTexture.dbc Loading

    static wowGroundEffectTexture_t wow_ground_effect_textures[512];
    static BOOL Wow_LoadGroundEffectDBCs(void);
    

  2. Helper Functions

  3. Wow_GetGroundEffectTexture(effect_id): Lookup and cache
  4. Wow_SelectDoodadFromWeights(weights, seed): Weighted random selection
  5. Wow_GrassEffectIdForCoverage(...): Find dominant grass layer

  6. Data-Driven Coloring

  7. The retired prototype varied procedural blade colors from terrain data.
  8. This is not a fallback: current rendering requires authoritative DBC and M2 data.

  9. Reason it was replaced

  10. Generic blade geometry could not preserve authored doodad shape or material identity.
  11. 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):

  1. M2 Model Instantiation
  2. Wow_AddGroundEffectInstance() instantiates the resolved GroundEffectDoodad M2 per clump.
  3. Instances are rendered through GPU instancing (M2_RenderInstanced), not per-instance model draws.

  4. Animation Support

  5. Verified Classic doodad M2s have zero keyed bone tracks despite a nominal sequence
  6. The instanced vertex shader provides world-phased, root-anchored wind

  7. Appearance Variation

  8. Same grass placement, different visual assets
  9. Weighted selection from 4 doodad options per terrain type
  10. Much richer visual variety

  11. Material System

  12. 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

  1. Required DBC data
  2. GroundEffectTexture and GroundEffectDoodad are authoritative; missing records are logged and must be fixed rather than replaced with procedural colors or guessed models.

  3. Placement remains an approximation

  4. The public data establishes layer effects, density, suppression, models, and terrain height, but the exact original-client placement algorithm has not been confirmed.

  5. Animation source

  6. The inspected Classic detail M2s have no keyed bone tracks. Current movement is root-anchored GPU wind rather than nonexistent asset animation.

  7. No dedicated far LOD

  8. 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
  • 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 validation
  • CM_WowDbcString() - String block access
  • CM_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.