From 1ab254273ebf92b1892f2a874fc764dd89d624e1 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 30 Mar 2026 17:28:47 -0700 Subject: [PATCH] docs: add M2 format why-comments to character preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Explain M2 version 264 threshold (WotLK stores submesh/bone data in external .skin files; Classic/TBC embed it in the M2) - Explain M2 texture types 1 and 6 (skin and hair/scalp; empty filenames resolved via CharSections.dbc at runtime) - Explain 0x20 anim flag (embedded data; when clear, keyframes live in external {Model}{SeqID}-{Var}.anim files) - Explain geoset ID encoding (group × 100 + variant from ItemDisplayInfo.dbc; e.g. 801 = sleeves variant 1) --- src/rendering/character_preview.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index 86b8eea2..c3895eb4 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -301,7 +301,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, auto model = pipeline::M2Loader::load(m2Data); - // Load skin file (only for WotLK M2s - vanilla has embedded skin) + // M2 version 264+ (WotLK) stores submesh/bone data in external .skin files. + // Earlier versions (Classic ≤256, TBC ≤263) have skin data embedded in the M2. std::string skinPath = modelDir + baseName + "00.skin"; auto skinData = assetManager_->readFile(skinPath); if (!skinData.empty() && model.version >= 264) { @@ -398,6 +399,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, auto& tex = model.textures[ti]; LOG_INFO(" Model texture[", ti, "]: type=", tex.type, " filename='", tex.filename, "'"); + // M2 texture types: 1=character skin, 6=hair/scalp. Empty filename means + // the texture is resolved at runtime via CharSections.dbc lookup. if (tex.type == 1 && tex.filename.empty() && !bodySkinPath_.empty()) { tex.filename = bodySkinPath_; } else if (tex.type == 6 && tex.filename.empty() && !hairScalpPath.empty()) { @@ -405,7 +408,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, } } - // Load external .anim files + // Load external .anim files for sequences that store keyframes outside the M2. + // Flag 0x20 = embedded data; when clear, animation lives in {ModelName}{SeqID}-{Var}.anim for (uint32_t si = 0; si < model.sequences.size(); si++) { if (!(model.sequences[si].flags & 0x20)) { char animFileName[256]; @@ -582,6 +586,9 @@ bool CharacterPreview::applyEquipment(const std::vector& eq }; // --- Geosets --- + // M2 geoset IDs encode body part group × 100 + variant (e.g., 801 = group 8 + // (sleeves) variant 1, 1301 = group 13 (pants) variant 1). ItemDisplayInfo.dbc + // provides the variant offset per equipped item; base IDs are per-group constants. std::unordered_set geosets; for (uint16_t i = 0; i <= 99; i++) geosets.insert(i); geosets.insert(static_cast(100 + hairStyle_ + 1)); // Hair style