M2 And Character Display
Contents
- M2 Loading
- Character State Packing
- Creation And Selection Data Flow
- DBC-Backed Outfit Data
- Component Texture Slots
- Composed Character Texture
- Skin Section IDs And Geosets
- Grounded Actor Yaw
M2 Loading
The WoW renderer loads M2 models through games/world-of-warcraft/renderer/m2/r_m2.c. DBC ownership and character-data
resolution live separately in games/world-of-warcraft/renderer/m2/r_dbc.c. The M2 loader handles:
MD20/MD21payload lookup.- M2 arrays for sequences, bones, vertices, textures, materials, attachments, and lookup tables.
- External
00.skinfiles for modern geometry. - Legacy embedded views for older models.
- Bone matrix evaluation and per-batch matrix palettes.
- Fallback model creation when data is missing or malformed.
The game side also reads M2 sequence metadata in games/world-of-warcraft/game/g_model.c so entity movement can select animation names through the game module.
m2Model_t owns the FS_ReadFile image directly and stores a typed m2File_t view plus base_offset; MD21/12DM containers point
the view into their payload without copying it. The version is read before selecting and validating the classic or modern header.
Header array descriptors remain file offsets and are resolved through bounds-checked accessors when consumed; the loader does not
copy file arrays or duplicate their counts into runtime fields. The model adds only renderer resources absent from the file: draw
batches, bounds, and flags. Batch index/vertex indirection is prevalidated once before the tight vertex upload loop. Bone matrices use module-static frame scratch like MDX;
particles and ribbons emit into the renderer's global particle pool from the shared render clock, so neither needs per-model state.
Generic renderer model registration lives in renderer/r_model.c. It is the only model cache: case-insensitive filename lookup returns
the resident model or cached missing-model placeholder, reference release leaves the image resident, and registration-sequence cleanup
reclaims unreferenced models not touched by the current map registration. Textures remain separately registered renderer resources.
Character State Packing
Appearance and equipment are packed into snapshot fields:
DWORD appearance = Wow_PackAppearance(skin, face, hair_style, hair_color,
facial_hair, class_id, flags);
DWORD equipment = Wow_PackEquipment(upper_body, lower_body, hands, feet);
appearance stores small race/gender/model-local customization IDs plus class. equipment stores local slot item indices, not raw item IDs. Index 0 means empty; nonzero indices resolve through WoW-owned equipment lists and DBC-backed ItemDisplayInfo display IDs.
The local Classic DBC maxima fit this 32-bit appearance contract. Face needs four bits (maximum 14), while facial features need
five (maximum 16), so facial-feature bit 4 uses the otherwise unused fifth face bit. Do not widen entityState_t merely to make menu
customization convenient; enumerate menu choices from DBC data and keep preview-only state outside snapshots if a future client format
exceeds the packed gameplay budget. See docs/wow-character.md.
Do not widen entity or player state just to preview more gear. A menu that needs additional equipment owns that preview state and
draws the character/equipment pieces separately; persistent menu overrides do not belong in m2Model_t or the renderer API.
Creation And Selection Data Flow
The creation preview and saved-character preview converge on the same renderer-owned DBC lookup:
ui_dbc.c selection or ~/.world-of-warcraft/characters.xml
-> packed appearance (customization + class)
-> ui_xml.c renderEntity_t
-> r_m2.c character draw
-> r_dbc.c race/gender/class starter-outfit key
-> CharStartOutfit display IDs + inventory types
-> ItemDisplayInfo components/geosets
-> composed character texture and visible M2 sections
Customization index zero is a real first variation, not a request for a bare body. Starter clothing is selected independently by the
race/class/gender key. Consequently, a valid base skin with no clothing should be investigated after the UI packing boundary: confirm
the starter record, display IDs, ItemDisplayInfo schema, and component composition in that order.
The XML save stores both class and packed appearance; keep their class values consistent. See
docs/wow-character.md for exact local values, diagnostic commands, and bounded creation/select runs.
DBC-Backed Outfit Data
r_dbc.c owns each resident WDBC image and its immutable open-addressed index. FNV-1a32
hashes the lookup key and each slot stores one int source record number, not a
copied outfit or texture. Index capacity is a power of two sized at least
twice the record count, so outfit resolution can use stack-local state without
an appearance-result cache. r_m2.c consumes only resolved appearance, item-display IDs,
outfits, and texture paths; it never accesses DBC records directly.
Character display work currently uses:
CreatureDisplayInfo.dbcCreatureDisplayInfoExtra.dbcCharStartOutfit.dbcItemDisplayInfo.dbcCharSections.dbcCharHairGeosets.dbcCharHairTextures.dbcHelmetGeosetVisData.dbc
CharStartOutfit.dbc maps race/class/gender to starter display IDs. ItemDisplayInfo.dbc carries item model names/textures, geoset groups, flags, helmet visibility, and texture component stems.
Character-model NPCs keep their AzerothCore CreatureDisplayID in the existing
snapshot class_id field. The renderer follows that ID through
CreatureDisplayInfo.ExtendedDisplayInfoID, packs the extra record's skin,
face, hair, and facial-hair fields, and applies all ten local Classic NPC
item-display slots. Creature textures follow the same direct-or-composed path
as player characters; there is no baked-texture shortcut.
Non-character creatures have no extra record and retain their ordinary M2 path.
The server CSV intentionally stops at CreatureDisplayID. CreatureDisplayInfoExtra is client-version appearance data and must not
be copied into creatures.csv; mounting client data and resolving the display's extra record keeps the model and its configuration
from the same authoritative version. In the local 19-field Classic schema, fields 8–17 are ten NPCItemDisplay values and field 18 is
the baked texture string offset, not an eleventh item.
Deputy Willem (creature 823) is a useful end-to-end oracle: CSV display 2072 selects extra record 675, whose appearance and item
displays resolve the Human Male model plus Stormwind plate component textures. If he is nude while this chain resolves, the failure is
downstream in CharSections base-skin lookup, ItemDisplayInfo field selection, or composition, not missing configuration in server
data. His nonzero skin color (4) specifically catches code that mistakes Classic's field 5 color for stock Wrath's field 9 color.
CharSections.dbc has two incompatible field orderings (Classic/TBC vs. stock Wrath); detect by sampling
field 4. Classic-era ItemDisplayInfo.dbc is 23 fields with texture components at field 14; TBC/Wrath layouts
use field 15. Full schemas and field-by-field layouts are in
dbc-reference.md.
Classic male hair is an explicit exception to the usual CharSections texture-string path. Section 3 still authoritatively selects
the race, gender, style, and color, but its texture fields are empty. Only after that exact row matches, derive replaceable texture
type 6 as Character\<Race>\Hair00_<color:02>.blp, taking the race from the model path and the color from the row. The default Human
Male color is therefore Character\Human\Hair00_00.blp (black), not the renderer's white initialization texture. Female hair rows
normally carry explicit strings. CharHairTextures.dbc describes hair-geoset flags and is not a filename lookup table.
Component Texture Slots
Item component texture stems map to eight body slots:
| Slot | Folder |
|---|---|
| Upper arm | Item\TextureComponents\ArmUpperTexture\ |
| Lower arm | Item\TextureComponents\ArmLowerTexture\ |
| Hand | Item\TextureComponents\HandTexture\ |
| Upper torso | Item\TextureComponents\TorsoUpperTexture\ |
| Lower torso | Item\TextureComponents\TorsoLowerTexture\ |
| Upper leg | Item\TextureComponents\LegUpperTexture\ |
| Lower leg | Item\TextureComponents\LegLowerTexture\ |
| Foot | Item\TextureComponents\FootTexture\ |
Component names in ItemDisplayInfo.dbc are stems, not archive paths. Resolve them under the slot folder and try gender-specific suffixes first:
Use the gender suffix that matches the character model, then universal as fallback.
Composed Character Texture
The renderer uses two paths for character body textures:
- An unmodified body renders its filename-cached source texture directly.
- A modified body is composed on the GPU into one shared temporary atlas, then
rendered immediately with that atlas. The temporary target is
M2_CHARACTER_COMPOSITE_RESOLUTION(256x256).
The temporary atlas is reused sequentially; it is valid only until the current model draw finishes. It is not a per-appearance cache and must not be retained by an entity or model.
Important constraints:
- The base body texture is not always 512x512. Classic body skins such as
Character\Orc\Male\OrcMaleSkin00_00.blpmay be 256x256. - Component rectangles are authored in 512x512 atlas space and scale to the 256x256 temporary target.
- Do not infer visible geosets from non-empty component textures. WoW keeps many default geosets visible unless item geoset groups override them.
Skin Section IDs And Geosets
M2 skin sections are grouped by hundreds: section_id = group * 100 + variant. Character renderers select one visible variant per relevant group from the current draw's stack-local outfit. Do not throw away sections at model-load time; loading all batches preserves per-entity equipment changes.
When no outfit is available the bare defaults are sections 401 (forearms), 702 (ears), and 1501 (no-cape back).
For the complete ItemDisplayInfo.dbc field layout, the slot → geoset-group mapping, and per-group variant
tables (groups 4, 5, 8, 9, 12, 13, 15), see
dbc-reference.md — ItemDisplayInfo.dbc.
Component Texture Layering
Body component textures are ordered layers. For LegLower, pants are priority 0 and boots are priority 2.
Composite pants first, then boots; transparent pixels in the boot texture reveal the pants texture below.
Collapsing the region to the last stem produces bare knees. Component texture presence never selects a geoset.
Cape Texture Resolution
Read the cape texture stem from ItemDisplayInfo.dbc field 3, try _M / _F / _U suffixes, and search
under Item\ObjectComponents\Cape\ then Item\TextureComponents\Cape\. Store separately from body component
textures. Activating the cape mesh (section 1502) requires setting geoset[15] = 1 from
outfit->cape_texture != NULL — not yet implemented (TODO).
Item Attachment Rendering
Head and shoulder items carry separate attachment M2s in ItemDisplayInfo fields 1–2. M2_DbcAddDisplayInfo stores
those stems on the outfit (helm_model, shoulder_model[0..1]); M2_RenderItemAttachments resolves them to archive
paths, loads them once per path, and renders them at the character's helm (attachment id 11) and shoulder (ids 5/6)
bones via M2_AttachmentMatrix. The parent's bone scratch is computed first, so all attachment matrices are resolved
before any recursive render overwrites it. See
dbc-reference.md — Item Attachment Models.
The renderer applies the same lifetime rule to overhead name/quest anchors. Immediately after drawing an entity, it resolves the PlayerName attachment from the current bone palette and caches the world point under the complete pose/transform key. The generic world-hover layout projection and later marker passes reuse that point instead of rebuilding the M2 pose. A bounded Human-start diagnostic recorded only cache hits after scene initialization (steady one-second windows included 20 and 148 hits, with zero misses).
Grounded Actor Yaw
Grounded WoW actors use the same one-dimensional yaw path as Warcraft III/OpenWarcraft3 entities:
- game code writes
entityState_t.anglein radians, - the client interpolates it with
LerpRotation(...), - M2 rendering consumes
renderEntity_t.angle.
Do not put player/creature yaw back into entityState_t.rotation; that vector is reserved for static object/model transforms that genuinely need three axes.