feat(animation): 452 named constants, 30-phase character animation state machine

Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.

Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.

Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.

Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.

Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.

Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
This commit is contained in:
Paul 2026-04-04 23:02:53 +03:00
parent d54e262048
commit e58f9b4b40
59 changed files with 3903 additions and 483 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,567 @@
// ============================================================================
// animation_ids.cpp — Inverse lookup & DBC validation
// Generated from animation_ids.hpp (452 constants, IDs 0451)
// ============================================================================
#include "rendering/animation_ids.hpp"
#include "pipeline/dbc_loader.hpp"
#include "core/logger.hpp"
#include <unordered_set>
namespace wowee {
namespace rendering {
namespace anim {
const char* nameFromId(uint32_t id) {
static const char* const names[ANIM_COUNT] = {
/* 0 */ "STAND",
/* 1 */ "DEATH",
/* 2 */ "SPELL",
/* 3 */ "STOP",
/* 4 */ "WALK",
/* 5 */ "RUN",
/* 6 */ "DEAD",
/* 7 */ "RISE",
/* 8 */ "STAND_WOUND",
/* 9 */ "COMBAT_WOUND",
/* 10 */ "COMBAT_CRITICAL",
/* 11 */ "SHUFFLE_LEFT",
/* 12 */ "SHUFFLE_RIGHT",
/* 13 */ "WALK_BACKWARDS",
/* 14 */ "STUN",
/* 15 */ "HANDS_CLOSED",
/* 16 */ "ATTACK_UNARMED",
/* 17 */ "ATTACK_1H",
/* 18 */ "ATTACK_2H",
/* 19 */ "ATTACK_2H_LOOSE",
/* 20 */ "PARRY_UNARMED",
/* 21 */ "PARRY_1H",
/* 22 */ "PARRY_2H",
/* 23 */ "PARRY_2H_LOOSE",
/* 24 */ "SHIELD_BLOCK",
/* 25 */ "READY_UNARMED",
/* 26 */ "READY_1H",
/* 27 */ "READY_2H",
/* 28 */ "READY_2H_LOOSE",
/* 29 */ "READY_BOW",
/* 30 */ "DODGE",
/* 31 */ "SPELL_PRECAST",
/* 32 */ "SPELL_CAST",
/* 33 */ "SPELL_CAST_AREA",
/* 34 */ "NPC_WELCOME",
/* 35 */ "NPC_GOODBYE",
/* 36 */ "BLOCK",
/* 37 */ "JUMP_START",
/* 38 */ "JUMP",
/* 39 */ "JUMP_END",
/* 40 */ "FALL",
/* 41 */ "SWIM_IDLE",
/* 42 */ "SWIM",
/* 43 */ "SWIM_LEFT",
/* 44 */ "SWIM_RIGHT",
/* 45 */ "SWIM_BACKWARDS",
/* 46 */ "ATTACK_BOW",
/* 47 */ "FIRE_BOW",
/* 48 */ "READY_RIFLE",
/* 49 */ "ATTACK_RIFLE",
/* 50 */ "LOOT",
/* 51 */ "READY_SPELL_DIRECTED",
/* 52 */ "READY_SPELL_OMNI",
/* 53 */ "SPELL_CAST_DIRECTED",
/* 54 */ "SPELL_CAST_OMNI",
/* 55 */ "BATTLE_ROAR",
/* 56 */ "READY_ABILITY",
/* 57 */ "SPECIAL_1H",
/* 58 */ "SPECIAL_2H",
/* 59 */ "SHIELD_BASH",
/* 60 */ "EMOTE_TALK",
/* 61 */ "EMOTE_EAT",
/* 62 */ "EMOTE_WORK",
/* 63 */ "EMOTE_USE_STANDING",
/* 64 */ "EMOTE_EXCLAMATION",
/* 65 */ "EMOTE_QUESTION",
/* 66 */ "EMOTE_BOW",
/* 67 */ "EMOTE_WAVE",
/* 68 */ "EMOTE_CHEER",
/* 69 */ "EMOTE_DANCE",
/* 70 */ "EMOTE_LAUGH",
/* 71 */ "EMOTE_SLEEP",
/* 72 */ "EMOTE_SIT_GROUND",
/* 73 */ "EMOTE_RUDE",
/* 74 */ "EMOTE_ROAR",
/* 75 */ "EMOTE_KNEEL",
/* 76 */ "EMOTE_KISS",
/* 77 */ "EMOTE_CRY",
/* 78 */ "EMOTE_CHICKEN",
/* 79 */ "EMOTE_BEG",
/* 80 */ "EMOTE_APPLAUD",
/* 81 */ "EMOTE_SHOUT",
/* 82 */ "EMOTE_FLEX",
/* 83 */ "EMOTE_SHY",
/* 84 */ "EMOTE_POINT",
/* 85 */ "ATTACK_1H_PIERCE",
/* 86 */ "ATTACK_2H_LOOSE_PIERCE",
/* 87 */ "ATTACK_OFF",
/* 88 */ "ATTACK_OFF_PIERCE",
/* 89 */ "SHEATHE",
/* 90 */ "HIP_SHEATHE",
/* 91 */ "MOUNT",
/* 92 */ "RUN_RIGHT",
/* 93 */ "RUN_LEFT",
/* 94 */ "MOUNT_SPECIAL",
/* 95 */ "KICK",
/* 96 */ "SIT_GROUND_DOWN",
/* 97 */ "SITTING",
/* 98 */ "SIT_GROUND_UP",
/* 99 */ "SLEEP_DOWN",
/* 100 */ "SLEEP",
/* 101 */ "SLEEP_UP",
/* 102 */ "SIT_CHAIR_LOW",
/* 103 */ "SIT_CHAIR_MED",
/* 104 */ "SIT_CHAIR_HIGH",
/* 105 */ "LOAD_BOW",
/* 106 */ "LOAD_RIFLE",
/* 107 */ "ATTACK_THROWN",
/* 108 */ "READY_THROWN",
/* 109 */ "HOLD_BOW",
/* 110 */ "HOLD_RIFLE",
/* 111 */ "HOLD_THROWN",
/* 112 */ "LOAD_THROWN",
/* 113 */ "EMOTE_SALUTE",
/* 114 */ "KNEEL_START",
/* 115 */ "KNEEL_LOOP",
/* 116 */ "KNEEL_END",
/* 117 */ "ATTACK_UNARMED_OFF",
/* 118 */ "SPECIAL_UNARMED",
/* 119 */ "STEALTH_WALK",
/* 120 */ "STEALTH_STAND",
/* 121 */ "KNOCKDOWN",
/* 122 */ "EATING_LOOP",
/* 123 */ "USE_STANDING_LOOP",
/* 124 */ "CHANNEL_CAST_DIRECTED",
/* 125 */ "CHANNEL_CAST_OMNI",
/* 126 */ "WHIRLWIND",
/* 127 */ "BIRTH",
/* 128 */ "USE_STANDING_START",
/* 129 */ "USE_STANDING_END",
/* 130 */ "CREATURE_SPECIAL",
/* 131 */ "DROWN",
/* 132 */ "DROWNED",
/* 133 */ "FISHING_CAST",
/* 134 */ "FISHING_LOOP",
/* 135 */ "FLY",
/* 136 */ "EMOTE_WORK_NO_SHEATHE",
/* 137 */ "EMOTE_STUN_NO_SHEATHE",
/* 138 */ "EMOTE_USE_STANDING_NO_SHEATHE",
/* 139 */ "SPELL_SLEEP_DOWN",
/* 140 */ "SPELL_KNEEL_START",
/* 141 */ "SPELL_KNEEL_LOOP",
/* 142 */ "SPELL_KNEEL_END",
/* 143 */ "SPRINT",
/* 144 */ "IN_FLIGHT",
/* 145 */ "SPAWN",
/* 146 */ "CLOSE",
/* 147 */ "CLOSED",
/* 148 */ "OPEN",
/* 149 */ "DESTROY",
/* 150 */ "DESTROYED",
/* 151 */ "UNSHEATHE",
/* 152 */ "SHEATHE_ALT",
/* 153 */ "ATTACK_UNARMED_NO_SHEATHE",
/* 154 */ "STEALTH_RUN",
/* 155 */ "READY_CROSSBOW",
/* 156 */ "ATTACK_CROSSBOW",
/* 157 */ "EMOTE_TALK_EXCLAMATION",
/* 158 */ "FLY_IDLE",
/* 159 */ "FLY_FORWARD",
/* 160 */ "FLY_BACKWARDS",
/* 161 */ "FLY_LEFT",
/* 162 */ "FLY_RIGHT",
/* 163 */ "FLY_UP",
/* 164 */ "FLY_DOWN",
/* 165 */ "FLY_LAND_START",
/* 166 */ "FLY_LAND_RUN",
/* 167 */ "FLY_LAND_END",
/* 168 */ "EMOTE_TALK_QUESTION",
/* 169 */ "EMOTE_READ",
/* 170 */ "EMOTE_SHIELDBLOCK",
/* 171 */ "EMOTE_CHOP",
/* 172 */ "EMOTE_HOLDRIFLE",
/* 173 */ "EMOTE_HOLDBOW",
/* 174 */ "EMOTE_HOLDTHROWN",
/* 175 */ "CUSTOM_SPELL_02",
/* 176 */ "CUSTOM_SPELL_03",
/* 177 */ "CUSTOM_SPELL_04",
/* 178 */ "CUSTOM_SPELL_05",
/* 179 */ "CUSTOM_SPELL_06",
/* 180 */ "CUSTOM_SPELL_07",
/* 181 */ "CUSTOM_SPELL_08",
/* 182 */ "CUSTOM_SPELL_09",
/* 183 */ "CUSTOM_SPELL_10",
/* 184 */ "EMOTE_STATE_DANCE",
/* 185 */ "FLY_STAND",
/* 186 */ "EMOTE_STATE_LAUGH",
/* 187 */ "EMOTE_STATE_POINT",
/* 188 */ "EMOTE_STATE_EAT",
/* 189 */ "EMOTE_STATE_WORK",
/* 190 */ "EMOTE_STATE_SIT_GROUND",
/* 191 */ "EMOTE_STATE_HOLD_BOW",
/* 192 */ "EMOTE_STATE_HOLD_RIFLE",
/* 193 */ "EMOTE_STATE_HOLD_THROWN",
/* 194 */ "FLY_COMBAT_WOUND",
/* 195 */ "FLY_COMBAT_CRITICAL",
/* 196 */ "RECLINED",
/* 197 */ "EMOTE_STATE_ROAR",
/* 198 */ "EMOTE_USE_STANDING_LOOP_2",
/* 199 */ "EMOTE_STATE_APPLAUD",
/* 200 */ "READY_FIST",
/* 201 */ "SPELL_CHANNEL_DIRECTED_OMNI",
/* 202 */ "SPECIAL_ATTACK_1H_OFF",
/* 203 */ "ATTACK_FIST_1H",
/* 204 */ "ATTACK_FIST_1H_OFF",
/* 205 */ "PARRY_FIST_1H",
/* 206 */ "READY_FIST_1H",
/* 207 */ "EMOTE_STATE_READ_AND_TALK",
/* 208 */ "EMOTE_STATE_WORK_NO_SHEATHE",
/* 209 */ "FLY_RUN",
/* 210 */ "EMOTE_STATE_KNEEL_2",
/* 211 */ "EMOTE_STATE_SPELL_KNEEL",
/* 212 */ "EMOTE_STATE_USE_STANDING",
/* 213 */ "EMOTE_STATE_STUN",
/* 214 */ "EMOTE_STATE_STUN_NO_SHEATHE",
/* 215 */ "EMOTE_TRAIN",
/* 216 */ "EMOTE_DEAD",
/* 217 */ "EMOTE_STATE_DANCE_ONCE",
/* 218 */ "FLY_DEATH",
/* 219 */ "FLY_STAND_WOUND",
/* 220 */ "FLY_SHUFFLE_LEFT",
/* 221 */ "FLY_SHUFFLE_RIGHT",
/* 222 */ "FLY_WALK_BACKWARDS",
/* 223 */ "FLY_STUN",
/* 224 */ "FLY_HANDS_CLOSED",
/* 225 */ "FLY_ATTACK_UNARMED",
/* 226 */ "FLY_ATTACK_1H",
/* 227 */ "FLY_ATTACK_2H",
/* 228 */ "FLY_ATTACK_2H_LOOSE",
/* 229 */ "FLY_SPELL",
/* 230 */ "FLY_STOP",
/* 231 */ "FLY_WALK",
/* 232 */ "FLY_DEAD",
/* 233 */ "FLY_RISE",
/* 234 */ "FLY_RUN_2",
/* 235 */ "FLY_FALL",
/* 236 */ "FLY_SWIM_IDLE",
/* 237 */ "FLY_SWIM",
/* 238 */ "FLY_SWIM_LEFT",
/* 239 */ "FLY_SWIM_RIGHT",
/* 240 */ "FLY_SWIM_BACKWARDS",
/* 241 */ "FLY_ATTACK_BOW",
/* 242 */ "FLY_FIRE_BOW",
/* 243 */ "FLY_READY_RIFLE",
/* 244 */ "FLY_ATTACK_RIFLE",
/* 245 */ "TOTEM_SMALL",
/* 246 */ "TOTEM_MEDIUM",
/* 247 */ "TOTEM_LARGE",
/* 248 */ "FLY_LOOT",
/* 249 */ "FLY_READY_SPELL_DIRECTED",
/* 250 */ "FLY_READY_SPELL_OMNI",
/* 251 */ "FLY_SPELL_CAST_DIRECTED",
/* 252 */ "FLY_SPELL_CAST_OMNI",
/* 253 */ "FLY_BATTLE_ROAR",
/* 254 */ "FLY_READY_ABILITY",
/* 255 */ "FLY_SPECIAL_1H",
/* 256 */ "FLY_SPECIAL_2H",
/* 257 */ "FLY_SHIELD_BASH",
/* 258 */ "FLY_EMOTE_TALK",
/* 259 */ "FLY_EMOTE_EAT",
/* 260 */ "FLY_EMOTE_WORK",
/* 261 */ "FLY_EMOTE_USE_STANDING",
/* 262 */ "FLY_EMOTE_BOW",
/* 263 */ "FLY_EMOTE_WAVE",
/* 264 */ "FLY_EMOTE_CHEER",
/* 265 */ "FLY_EMOTE_DANCE",
/* 266 */ "FLY_EMOTE_LAUGH",
/* 267 */ "FLY_EMOTE_SLEEP",
/* 268 */ "FLY_EMOTE_SIT_GROUND",
/* 269 */ "FLY_EMOTE_RUDE",
/* 270 */ "FLY_EMOTE_ROAR",
/* 271 */ "FLY_EMOTE_KNEEL",
/* 272 */ "FLY_EMOTE_KISS",
/* 273 */ "FLY_EMOTE_CRY",
/* 274 */ "FLY_EMOTE_CHICKEN",
/* 275 */ "FLY_EMOTE_BEG",
/* 276 */ "FLY_EMOTE_APPLAUD",
/* 277 */ "FLY_EMOTE_SHOUT",
/* 278 */ "FLY_EMOTE_FLEX",
/* 279 */ "FLY_EMOTE_SHY",
/* 280 */ "FLY_EMOTE_POINT",
/* 281 */ "FLY_ATTACK_1H_PIERCE",
/* 282 */ "FLY_ATTACK_2H_LOOSE_PIERCE",
/* 283 */ "FLY_ATTACK_OFF",
/* 284 */ "FLY_ATTACK_OFF_PIERCE",
/* 285 */ "FLY_SHEATHE",
/* 286 */ "FLY_HIP_SHEATHE",
/* 287 */ "FLY_MOUNT",
/* 288 */ "FLY_RUN_RIGHT",
/* 289 */ "FLY_RUN_LEFT",
/* 290 */ "FLY_MOUNT_SPECIAL",
/* 291 */ "FLY_KICK",
/* 292 */ "FLY_SIT_GROUND_DOWN",
/* 293 */ "FLY_SITTING",
/* 294 */ "FLY_SIT_GROUND_UP",
/* 295 */ "FLY_SLEEP_DOWN",
/* 296 */ "FLY_SLEEP",
/* 297 */ "FLY_SLEEP_UP",
/* 298 */ "FLY_SIT_CHAIR_LOW",
/* 299 */ "FLY_SIT_CHAIR_MED",
/* 300 */ "FLY_SIT_CHAIR_HIGH",
/* 301 */ "FLY_LOAD_BOW",
/* 302 */ "FLY_LOAD_RIFLE",
/* 303 */ "FLY_ATTACK_THROWN",
/* 304 */ "FLY_READY_THROWN",
/* 305 */ "FLY_HOLD_BOW",
/* 306 */ "FLY_HOLD_RIFLE",
/* 307 */ "FLY_HOLD_THROWN",
/* 308 */ "FLY_LOAD_THROWN",
/* 309 */ "FLY_EMOTE_SALUTE",
/* 310 */ "FLY_KNEEL_START",
/* 311 */ "FLY_KNEEL_LOOP",
/* 312 */ "FLY_KNEEL_END",
/* 313 */ "FLY_ATTACK_UNARMED_OFF",
/* 314 */ "FLY_SPECIAL_UNARMED",
/* 315 */ "FLY_STEALTH_WALK",
/* 316 */ "FLY_STEALTH_STAND",
/* 317 */ "FLY_KNOCKDOWN",
/* 318 */ "FLY_EATING_LOOP",
/* 319 */ "FLY_USE_STANDING_LOOP",
/* 320 */ "FLY_CHANNEL_CAST_DIRECTED",
/* 321 */ "FLY_CHANNEL_CAST_OMNI",
/* 322 */ "FLY_WHIRLWIND",
/* 323 */ "FLY_BIRTH",
/* 324 */ "FLY_USE_STANDING_START",
/* 325 */ "FLY_USE_STANDING_END",
/* 326 */ "FLY_CREATURE_SPECIAL",
/* 327 */ "FLY_DROWN",
/* 328 */ "FLY_DROWNED",
/* 329 */ "FLY_FISHING_CAST",
/* 330 */ "FLY_FISHING_LOOP",
/* 331 */ "FLY_FLY",
/* 332 */ "FLY_EMOTE_WORK_NO_SHEATHE",
/* 333 */ "FLY_EMOTE_STUN_NO_SHEATHE",
/* 334 */ "FLY_EMOTE_USE_STANDING_NO_SHEATHE",
/* 335 */ "FLY_SPELL_SLEEP_DOWN",
/* 336 */ "FLY_SPELL_KNEEL_START",
/* 337 */ "FLY_SPELL_KNEEL_LOOP",
/* 338 */ "FLY_SPELL_KNEEL_END",
/* 339 */ "FLY_SPRINT",
/* 340 */ "FLY_IN_FLIGHT",
/* 341 */ "FLY_SPAWN",
/* 342 */ "FLY_CLOSE",
/* 343 */ "FLY_CLOSED",
/* 344 */ "FLY_OPEN",
/* 345 */ "FLY_DESTROY",
/* 346 */ "FLY_DESTROYED",
/* 347 */ "FLY_UNSHEATHE",
/* 348 */ "FLY_SHEATHE_ALT",
/* 349 */ "FLY_ATTACK_UNARMED_NO_SHEATHE",
/* 350 */ "FLY_STEALTH_RUN",
/* 351 */ "FLY_READY_CROSSBOW",
/* 352 */ "FLY_ATTACK_CROSSBOW",
/* 353 */ "FLY_EMOTE_TALK_EXCLAMATION",
/* 354 */ "FLY_EMOTE_TALK_QUESTION",
/* 355 */ "FLY_EMOTE_READ",
/* 356 */ "EMOTE_HOLD_CROSSBOW",
/* 357 */ "FLY_EMOTE_HOLD_BOW",
/* 358 */ "FLY_EMOTE_HOLD_RIFLE",
/* 359 */ "FLY_EMOTE_HOLD_THROWN",
/* 360 */ "FLY_EMOTE_HOLD_CROSSBOW",
/* 361 */ "FLY_CUSTOM_SPELL_02",
/* 362 */ "FLY_CUSTOM_SPELL_03",
/* 363 */ "FLY_CUSTOM_SPELL_04",
/* 364 */ "FLY_CUSTOM_SPELL_05",
/* 365 */ "FLY_CUSTOM_SPELL_06",
/* 366 */ "FLY_CUSTOM_SPELL_07",
/* 367 */ "FLY_CUSTOM_SPELL_08",
/* 368 */ "FLY_CUSTOM_SPELL_09",
/* 369 */ "FLY_CUSTOM_SPELL_10",
/* 370 */ "FLY_EMOTE_STATE_DANCE",
/* 371 */ "EMOTE_EAT_NO_SHEATHE",
/* 372 */ "MOUNT_RUN_RIGHT",
/* 373 */ "MOUNT_RUN_LEFT",
/* 374 */ "MOUNT_WALK_BACKWARDS",
/* 375 */ "MOUNT_SWIM_IDLE",
/* 376 */ "MOUNT_SWIM",
/* 377 */ "MOUNT_SWIM_LEFT",
/* 378 */ "MOUNT_SWIM_RIGHT",
/* 379 */ "MOUNT_SWIM_BACKWARDS",
/* 380 */ "MOUNT_FLIGHT_IDLE",
/* 381 */ "MOUNT_FLIGHT_FORWARD",
/* 382 */ "MOUNT_FLIGHT_BACKWARDS",
/* 383 */ "MOUNT_FLIGHT_LEFT",
/* 384 */ "MOUNT_FLIGHT_RIGHT",
/* 385 */ "MOUNT_FLIGHT_UP",
/* 386 */ "MOUNT_FLIGHT_DOWN",
/* 387 */ "MOUNT_FLIGHT_LAND_START",
/* 388 */ "MOUNT_FLIGHT_LAND_RUN",
/* 389 */ "MOUNT_FLIGHT_LAND_END",
/* 390 */ "FLY_EMOTE_STATE_LAUGH",
/* 391 */ "FLY_EMOTE_STATE_POINT",
/* 392 */ "FLY_EMOTE_STATE_EAT",
/* 393 */ "FLY_EMOTE_STATE_WORK",
/* 394 */ "FLY_EMOTE_STATE_SIT_GROUND",
/* 395 */ "FLY_EMOTE_STATE_HOLD_BOW",
/* 396 */ "FLY_EMOTE_STATE_HOLD_RIFLE",
/* 397 */ "FLY_EMOTE_STATE_HOLD_THROWN",
/* 398 */ "FLY_EMOTE_STATE_ROAR",
/* 399 */ "FLY_RECLINED",
/* 400 */ "EMOTE_TRAIN_2",
/* 401 */ "EMOTE_DEAD_2",
/* 402 */ "FLY_EMOTE_USE_STANDING_LOOP_2",
/* 403 */ "FLY_EMOTE_STATE_APPLAUD",
/* 404 */ "FLY_READY_FIST",
/* 405 */ "FLY_SPELL_CHANNEL_DIRECTED_OMNI",
/* 406 */ "FLY_SPECIAL_ATTACK_1H_OFF",
/* 407 */ "FLY_ATTACK_FIST_1H",
/* 408 */ "FLY_ATTACK_FIST_1H_OFF",
/* 409 */ "FLY_PARRY_FIST_1H",
/* 410 */ "FLY_READY_FIST_1H",
/* 411 */ "FLY_EMOTE_STATE_READ_AND_TALK",
/* 412 */ "FLY_EMOTE_STATE_WORK_NO_SHEATHE",
/* 413 */ "FLY_EMOTE_STATE_KNEEL_2",
/* 414 */ "FLY_EMOTE_STATE_SPELL_KNEEL",
/* 415 */ "FLY_EMOTE_STATE_USE_STANDING",
/* 416 */ "FLY_EMOTE_STATE_STUN",
/* 417 */ "FLY_EMOTE_STATE_STUN_NO_SHEATHE",
/* 418 */ "FLY_EMOTE_TRAIN",
/* 419 */ "FLY_EMOTE_DEAD",
/* 420 */ "FLY_EMOTE_STATE_DANCE_ONCE",
/* 421 */ "FLY_EMOTE_EAT_NO_SHEATHE",
/* 422 */ "FLY_MOUNT_RUN_RIGHT",
/* 423 */ "FLY_MOUNT_RUN_LEFT",
/* 424 */ "FLY_MOUNT_WALK_BACKWARDS",
/* 425 */ "FLY_MOUNT_SWIM_IDLE",
/* 426 */ "FLY_MOUNT_SWIM",
/* 427 */ "FLY_MOUNT_SWIM_LEFT",
/* 428 */ "FLY_MOUNT_SWIM_RIGHT",
/* 429 */ "FLY_MOUNT_SWIM_BACKWARDS",
/* 430 */ "FLY_MOUNT_FLIGHT_IDLE",
/* 431 */ "FLY_MOUNT_FLIGHT_FORWARD",
/* 432 */ "FLY_MOUNT_FLIGHT_BACKWARDS",
/* 433 */ "FLY_MOUNT_FLIGHT_LEFT",
/* 434 */ "FLY_MOUNT_FLIGHT_RIGHT",
/* 435 */ "FLY_MOUNT_FLIGHT_UP",
/* 436 */ "FLY_MOUNT_FLIGHT_DOWN",
/* 437 */ "FLY_MOUNT_FLIGHT_LAND_START",
/* 438 */ "FLY_MOUNT_FLIGHT_LAND_RUN",
/* 439 */ "FLY_MOUNT_FLIGHT_LAND_END",
/* 440 */ "FLY_TOTEM_SMALL",
/* 441 */ "FLY_TOTEM_MEDIUM",
/* 442 */ "FLY_TOTEM_LARGE",
/* 443 */ "FLY_EMOTE_HOLD_CROSSBOW_2",
/* 444 */ "VEHICLE_GRAB",
/* 445 */ "VEHICLE_THROW",
/* 446 */ "FLY_VEHICLE_GRAB",
/* 447 */ "FLY_VEHICLE_THROW",
/* 448 */ "GUILD_CHAMPION_1",
/* 449 */ "GUILD_CHAMPION_2",
/* 450 */ "FLY_GUILD_CHAMPION_1",
/* 451 */ "FLY_GUILD_CHAMPION_2",
};
if (id < ANIM_COUNT) return names[id];
return "UNKNOWN";
}
uint32_t flyVariant(uint32_t groundId) {
// Compact lookup: ground animation ID (0451) → FLY_* variant, or 0 if none.
// Built from the 155 ground→fly pairs in animation_ids.hpp.
static const uint16_t table[] = {
// 0-9
185, 218, 229, 230, 231, 209, 232, 233, 219, 194,
// 10-19
195, 220, 221, 222, 223, 224, 225, 226, 227, 228,
// 20-29 (PARRY/READY/DODGE — no fly variants)
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// 30-39 (BLOCK/SPELL_PRECAST/NPC — no fly variants)
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// 40-49
235, 236, 237, 238, 239, 240, 241, 242, 243, 244,
// 50-59
248, 249, 250, 251, 252, 253, 254, 255, 256, 257,
// 60-69
258, 259, 260, 261, 0, 0, 262, 263, 264, 265,
// 70-79
266, 267, 268, 269, 270, 271, 272, 273, 274, 275,
// 80-89
276, 277, 278, 279, 280, 281, 282, 283, 284, 285,
// 90-99
286, 287, 288, 289, 290, 291, 292, 293, 294, 295,
// 100-109
296, 297, 298, 299, 300, 301, 302, 303, 304, 305,
// 110-119
306, 307, 308, 309, 310, 311, 312, 313, 314, 315,
// 120-129
316, 317, 318, 319, 320, 321, 322, 323, 324, 325,
// 130-139
326, 327, 328, 329, 330, 331, 332, 333, 334, 335,
// 140-149
336, 337, 338, 339, 340, 341, 342, 343, 344, 345,
// 150-159
346, 347, 348, 349, 350, 351, 352, 353, 0, 0,
// 160-169 (FLY_BACKWARDS..FLY_LAND_END are already FLY_ themselves: 0)
0, 0, 0, 0, 0, 0, 0, 0, 354, 355,
// 170-179
0, 0, 0, 0, 0, 361, 362, 363, 364, 365,
// 180-189
366, 367, 368, 369, 370, 0, 390, 391, 392, 393,
// 190-199
394, 395, 396, 397, 0, 0, 399, 398, 402, 403,
// 200-209
404, 405, 406, 407, 408, 409, 410, 411, 412, 0,
// 210-217
413, 414, 415, 416, 417, 418, 419, 420,
};
constexpr uint32_t tableSize = sizeof(table) / sizeof(table[0]);
if (groundId >= tableSize) return 0;
return table[groundId];
}
void validateAgainstDBC(const std::shared_ptr<wowee::pipeline::DBCFile>& dbc) {
if (!dbc || !dbc->isLoaded()) {
LOG_WARNING("AnimationData.dbc not available — skipping animation ID validation");
return;
}
// Collect all IDs present in the DBC (first field is the animation ID)
std::unordered_set<uint32_t> dbcIds;
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
uint32_t id = dbc->getUInt32(i, 0);
dbcIds.insert(id);
}
// Check: constants we define that are missing from DBC
uint32_t missingInDbc = 0;
for (uint32_t id = 0; id < ANIM_COUNT; ++id) {
if (dbcIds.find(id) == dbcIds.end()) {
LOG_WARNING("Animation ID ", id, " (", nameFromId(id),
") defined in constants but missing from AnimationData.dbc");
++missingInDbc;
}
}
// Check: DBC IDs beyond our constant range
uint32_t extraInDbc = 0;
for (uint32_t dbcId : dbcIds) {
if (dbcId >= ANIM_COUNT) {
++extraInDbc;
}
}
LOG_INFO("AnimationData.dbc validation: ", dbc->getRecordCount(), " DBC records, ",
ANIM_COUNT, " constants, ",
missingInDbc, " missing from DBC, ",
extraInDbc, " DBC-only IDs beyond constant range");
}
} // namespace anim
} // namespace rendering
} // namespace wowee

View file

@ -84,7 +84,7 @@ bool Celestial::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout)
.setVertexInput({binding}, {posAttr, uvAttr})
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) // test on, write off (sky layer)
.setNoDepthTest() // Sky layer: celestials always render (skybox doesn't write depth)
.setColorBlendAttachment(PipelineBuilder::blendAdditive())
.setMultisample(vkCtx_->getMsaaSamples())
.setLayout(pipelineLayout_)
@ -411,6 +411,12 @@ float Celestial::calculateCelestialAngle(float timeOfDay, float riseTime, float
void Celestial::update(float deltaTime) {
sunHazeTimer_ += deltaTime;
// Keep timer in a range where GPU sin() precision is reliable (< ~10000).
// The noise period repeats at multiples of 1.0 on each axis, so fmod by a
// large integer preserves visual continuity.
if (sunHazeTimer_ > 10000.0f) {
sunHazeTimer_ = std::fmod(sunHazeTimer_, 10000.0f);
}
if (!moonPhaseCycling_) {
return;

View file

@ -1,5 +1,6 @@
#include "rendering/character_preview.hpp"
#include "rendering/character_renderer.hpp"
#include "rendering/animation_ids.hpp"
#include "rendering/vk_render_target.hpp"
#include "rendering/vk_texture.hpp"
#include "rendering/vk_context.hpp"
@ -584,7 +585,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
charRenderer_->setActiveGeosets(instanceId_, activeGeosets);
// Play idle animation (Stand = animation ID 0)
charRenderer_->playAnimation(instanceId_, 0, true);
charRenderer_->playAnimation(instanceId_, rendering::anim::STAND, true);
// Cache core appearance for later equipment geosets.
race_ = race;

View file

@ -15,6 +15,7 @@
* the original WoW Model Viewer (charcontrol.h, REGION_FAC=2).
*/
#include "rendering/character_renderer.hpp"
#include "rendering/animation_ids.hpp"
#include "rendering/vk_context.hpp"
#include "rendering/vk_texture.hpp"
#include "rendering/vk_pipeline.hpp"
@ -34,6 +35,7 @@
#include <cmath>
#include <filesystem>
#include <future>
#include <numeric>
#include <thread>
#include <functional>
#include <unordered_map>
@ -261,7 +263,8 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram
.setVertexInput({charBinding}, charAttrs)
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL)
.setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS)
.setDepthBias(0.0f, 0.0f)
.setColorBlendAttachment(blendState)
.setMultisample(samples);
if (alphaToCoverage)
@ -269,7 +272,7 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram
return builder
.setLayout(pipelineLayout_)
.setRenderPass(mainPass)
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, VK_DYNAMIC_STATE_DEPTH_BIAS})
.build(device, vkCtx_->getPipelineCache());
};
@ -1733,9 +1736,9 @@ void CharacterRenderer::update(float deltaTime, const glm::vec3& cameraPos) {
inst.animationTime -= static_cast<float>(seq.duration);
}
} else {
// One-shot animation finished: return to Stand (0) unless dead
if (inst.currentAnimationId != 1 /*Death*/) {
playAnimation(pair.first, 0, true);
// One-shot animation finished: return to Stand unless dead
if (inst.currentAnimationId != anim::DEATH) {
playAnimation(pair.first, anim::STAND, true);
} else {
// Stay on last frame of death
inst.animationTime = static_cast<float>(seq.duration);
@ -2380,8 +2383,24 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
return gpuModel.data.materials[b.materialIndex].blendMode;
return 0;
};
// Sort batches by (priorityPlane, materialLayer) so equipment layers
// render in the order the M2 format intends. priorityPlane separates
// overlay effects; materialLayer orders coplanar body parts.
std::vector<size_t> sortedBatchIndices(gpuModel.data.batches.size());
std::iota(sortedBatchIndices.begin(), sortedBatchIndices.end(), 0);
std::stable_sort(sortedBatchIndices.begin(), sortedBatchIndices.end(),
[&](size_t a, size_t b) {
const auto& ba = gpuModel.data.batches[a];
const auto& bb = gpuModel.data.batches[b];
if (ba.priorityPlane != bb.priorityPlane)
return ba.priorityPlane < bb.priorityPlane;
return ba.materialLayer < bb.materialLayer;
});
for (int pass = 0; pass < 2; pass++) {
for (const auto& batch : gpuModel.data.batches) {
for (size_t bi : sortedBatchIndices) {
const auto& batch = gpuModel.data.batches[bi];
uint16_t bm = getBatchBlendMode(batch);
if (pass == 0 && bm != 0) continue; // pass 0: opaque only
if (pass == 1 && bm == 0) continue; // pass 1: non-opaque only
@ -2599,6 +2618,10 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
pipelineLayout_, 1, 1, &materialSet, 0, nullptr);
// Per-batch depth bias from materialLayer to separate coplanar
// armor pieces (chest/legs/gloves) that share identical depth.
vkCmdSetDepthBias(cmd, static_cast<float>(batch.materialLayer) * 0.5f, 0.0f, 0.0f);
vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0);
}
} // end pass loop
@ -3030,8 +3053,8 @@ void CharacterRenderer::moveInstanceTo(uint32_t instanceId, const glm::vec3& des
// Stop at current location.
inst.position = destination;
inst.isMoving = false;
if (inst.currentAnimationId == 4 || inst.currentAnimationId == 5) {
playAnimation(instanceId, 0, true);
if (inst.currentAnimationId == anim::WALK || inst.currentAnimationId == anim::RUN) {
playAnimation(instanceId, anim::STAND, true);
}
return;
}
@ -3509,7 +3532,8 @@ void CharacterRenderer::recreatePipelines() {
.setVertexInput({charBinding}, charAttrs)
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL)
.setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS)
.setDepthBias(0.0f, 0.0f)
.setColorBlendAttachment(blendState)
.setMultisample(samples);
if (alphaToCoverage)
@ -3517,7 +3541,7 @@ void CharacterRenderer::recreatePipelines() {
return builder
.setLayout(pipelineLayout_)
.setRenderPass(mainPass)
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, VK_DYNAMIC_STATE_DEPTH_BIAS})
.build(device, vkCtx_->getPipelineCache());
};

View file

@ -4212,6 +4212,37 @@ void M2Renderer::setInstanceAnimationFrozen(uint32_t instanceId, bool frozen) {
}
}
void M2Renderer::setInstanceAnimation(uint32_t instanceId, uint32_t animationId, bool loop) {
auto idxIt = instanceIndexById.find(instanceId);
if (idxIt == instanceIndexById.end()) return;
auto& inst = instances[idxIt->second];
if (!inst.cachedModel) return;
const auto& seqs = inst.cachedModel->sequences;
// Find the first sequence matching the requested animation ID
for (int i = 0; i < static_cast<int>(seqs.size()); ++i) {
if (seqs[i].id == animationId) {
inst.currentSequenceIndex = i;
inst.animDuration = static_cast<float>(seqs[i].duration);
inst.animTime = 0.0f;
inst.animSpeed = 1.0f;
// Use playingVariation=true for one-shot (returns to idle when done)
inst.playingVariation = !loop;
return;
}
}
}
bool M2Renderer::hasAnimation(uint32_t instanceId, uint32_t animationId) const {
auto idxIt = instanceIndexById.find(instanceId);
if (idxIt == instanceIndexById.end()) return false;
const auto& inst = instances[idxIt->second];
if (!inst.cachedModel) return false;
for (const auto& seq : inst.cachedModel->sequences) {
if (seq.id == animationId) return true;
}
return false;
}
float M2Renderer::getInstanceAnimDuration(uint32_t instanceId) const {
auto idxIt = instanceIndexById.find(instanceId);
if (idxIt == instanceIndexById.end()) return 0.0f;

View file

@ -250,13 +250,13 @@ bool Renderer::createPerFrameResources() {
// --- Create descriptor pool for UBO + image sampler (normal frames + reflection) ---
VkDescriptorPoolSize poolSizes[2]{};
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSizes[0].descriptorCount = MAX_FRAMES + 1; // +1 for reflection perFrame UBO
poolSizes[0].descriptorCount = MAX_FRAMES * 2; // normal frames + reflection frames
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
poolSizes[1].descriptorCount = MAX_FRAMES + 1;
poolSizes[1].descriptorCount = MAX_FRAMES * 2;
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.maxSets = MAX_FRAMES + 1; // +1 for reflection descriptor set
poolInfo.maxSets = MAX_FRAMES * 2; // normal frames + reflection frames
poolInfo.poolSizeCount = 2;
poolInfo.pPoolSizes = poolSizes;
@ -344,42 +344,48 @@ bool Renderer::createPerFrameResources() {
}
reflPerFrameUBOMapped = mapInfo.pMappedData;
VkDescriptorSetLayout layouts[MAX_FRAMES];
for (uint32_t i = 0; i < MAX_FRAMES; i++) layouts[i] = perFrameSetLayout;
VkDescriptorSetAllocateInfo setAlloc{};
setAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
setAlloc.descriptorPool = sceneDescriptorPool;
setAlloc.descriptorSetCount = 1;
setAlloc.pSetLayouts = &perFrameSetLayout;
setAlloc.descriptorSetCount = MAX_FRAMES;
setAlloc.pSetLayouts = layouts;
if (vkAllocateDescriptorSets(device, &setAlloc, &reflPerFrameDescSet) != VK_SUCCESS) {
LOG_ERROR("Failed to allocate reflection per-frame descriptor set");
if (vkAllocateDescriptorSets(device, &setAlloc, reflPerFrameDescSet) != VK_SUCCESS) {
LOG_ERROR("Failed to allocate reflection per-frame descriptor sets");
return false;
}
VkDescriptorBufferInfo descBuf{};
descBuf.buffer = reflPerFrameUBO;
descBuf.offset = 0;
descBuf.range = sizeof(GPUPerFrameData);
// Bind each reflection descriptor to the same UBO but its own frame's shadow view
for (uint32_t i = 0; i < MAX_FRAMES; i++) {
VkDescriptorBufferInfo descBuf{};
descBuf.buffer = reflPerFrameUBO;
descBuf.offset = 0;
descBuf.range = sizeof(GPUPerFrameData);
VkDescriptorImageInfo shadowImgInfo{};
shadowImgInfo.sampler = shadowSampler;
shadowImgInfo.imageView = shadowDepthView[0]; // reflection uses frame 0 shadow view
shadowImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDescriptorImageInfo shadowImgInfo{};
shadowImgInfo.sampler = shadowSampler;
shadowImgInfo.imageView = shadowDepthView[i];
shadowImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkWriteDescriptorSet writes[2]{};
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[0].dstSet = reflPerFrameDescSet;
writes[0].dstBinding = 0;
writes[0].descriptorCount = 1;
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
writes[0].pBufferInfo = &descBuf;
writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[1].dstSet = reflPerFrameDescSet;
writes[1].dstBinding = 1;
writes[1].descriptorCount = 1;
writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
writes[1].pImageInfo = &shadowImgInfo;
VkWriteDescriptorSet writes[2]{};
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[0].dstSet = reflPerFrameDescSet[i];
writes[0].dstBinding = 0;
writes[0].descriptorCount = 1;
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
writes[0].pBufferInfo = &descBuf;
writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[1].dstSet = reflPerFrameDescSet[i];
writes[1].dstBinding = 1;
writes[1].descriptorCount = 1;
writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
writes[1].pImageInfo = &shadowImgInfo;
vkUpdateDescriptorSets(device, 2, writes, 0, nullptr);
vkUpdateDescriptorSets(device, 2, writes, 0, nullptr);
}
}
LOG_INFO("Per-frame Vulkan resources created (shadow map ", SHADOW_MAP_SIZE, "x", SHADOW_MAP_SIZE, ")");
@ -460,7 +466,7 @@ void Renderer::updatePerFrameUBO() {
currentFrameData.lightSpaceMatrix = lightSpaceMatrix;
// Scale shadow bias proportionally to ortho extent to avoid acne at close range / gaps at far range
float shadowBias = 0.8f * (shadowDistance_ / 300.0f);
float shadowBias = glm::clamp(0.8f * (shadowDistance_ / 300.0f), 0.0f, 1.0f);
currentFrameData.shadowParams = glm::vec4(shadowsEnabled ? 1.0f : 0.0f, shadowBias, 0.0f, 0.0f);
// Player water ripple data: pack player XY into shadowParams.zw, ripple strength into fogParams.w
@ -566,7 +572,7 @@ bool Renderer::initialize(core::Window* win) {
postProcessPipeline_ = std::make_unique<PostProcessPipeline>();
postProcessPipeline_->initialize(vkCtx);
// Phase 2.5: Create render graph and register virtual resources
// Create render graph and register virtual resources
renderGraph_ = std::make_unique<RenderGraph>();
renderGraph_->registerResource("shadow_depth");
renderGraph_->registerResource("reflection_texture");
@ -687,7 +693,7 @@ void Renderer::shutdown() {
postProcessPipeline_.reset();
}
// Phase 2.5: Destroy render graph
// Destroy render graph
renderGraph_.reset();
destroyPerFrameResources();
@ -1018,8 +1024,26 @@ void Renderer::setInCombat(bool combat) {
if (animationController_) animationController_->setInCombat(combat);
}
void Renderer::setEquippedWeaponType(uint32_t inventoryType) {
if (animationController_) animationController_->setEquippedWeaponType(inventoryType);
void Renderer::setEquippedWeaponType(uint32_t inventoryType, bool is2HLoose, bool isFist,
bool isDagger, bool hasOffHand, bool hasShield) {
if (animationController_) animationController_->setEquippedWeaponType(inventoryType, is2HLoose, isFist, isDagger, hasOffHand, hasShield);
}
void Renderer::triggerSpecialAttack(uint32_t spellId) {
if (animationController_) animationController_->triggerSpecialAttack(spellId);
}
void Renderer::setEquippedRangedType(RangedWeaponType type) {
if (animationController_) animationController_->setEquippedRangedType(type);
}
void Renderer::triggerRangedShot() {
if (animationController_) animationController_->triggerRangedShot();
}
RangedWeaponType Renderer::getEquippedRangedType() const {
return animationController_ ? animationController_->getEquippedRangedType()
: RangedWeaponType::NONE;
}
void Renderer::setCharging(bool c) {
@ -2797,8 +2821,8 @@ glm::mat4 Renderer::computeLightSpaceMatrix() {
sunDir = -sunDir;
}
// Keep a minimum downward component so the frustum doesn't collapse at grazing angles.
if (sunDir.z > -0.08f) {
sunDir.z = -0.08f;
if (sunDir.z > -0.15f) {
sunDir.z = -0.15f;
sunDir = glm::normalize(sunDir);
}
@ -2986,6 +3010,11 @@ void Renderer::renderReflectionPass() {
if (!waterRenderer || !camera || !waterRenderer->hasReflectionPass() || !waterRenderer->hasSurfaces()) return;
if (currentCmd == VK_NULL_HANDLE || !reflPerFrameUBOMapped) return;
// Select the current frame's pre-bound reflection descriptor set
// (each frame's set was bound to its own shadow depth view at init).
uint32_t frame = vkCtx->getCurrentFrame();
VkDescriptorSet reflDescSet = reflPerFrameDescSet[frame];
// Reflection pass uses 1x MSAA. Scene pipelines must be render-pass-compatible,
// which requires matching sample counts. Only render scene into reflection when MSAA is off.
bool canRenderScene = (vkCtx->getMsaaSamples() == VK_SAMPLE_COUNT_1_BIT);
@ -3040,13 +3069,13 @@ void Renderer::renderReflectionPass() {
skyParams.horizonGlow = lp.horizonGlow;
}
// weatherIntensity left at default 0 for reflection pass (no game handler in scope)
skySystem->render(currentCmd, reflPerFrameDescSet, *camera, skyParams);
skySystem->render(currentCmd, reflDescSet, *camera, skyParams);
}
if (terrainRenderer && terrainEnabled) {
terrainRenderer->render(currentCmd, reflPerFrameDescSet, *camera);
terrainRenderer->render(currentCmd, reflDescSet, *camera);
}
if (wmoRenderer) {
wmoRenderer->render(currentCmd, reflPerFrameDescSet, *camera);
wmoRenderer->render(currentCmd, reflDescSet, *camera);
}
}
@ -3139,7 +3168,7 @@ void Renderer::renderShadowPass() {
shadowDepthLayout_[frame] = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
}
// Phase 2.5: Build the per-frame render graph for off-screen pre-passes.
// Build the per-frame render graph for off-screen pre-passes.
// Declares passes as graph nodes with input/output dependencies.
// compile() performs topological sort; execute() runs them with auto barriers.
void Renderer::buildFrameGraph(game::GameHandler* gameHandler) {

View file

@ -193,7 +193,7 @@ bool TerrainRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameL
envSizeMBOrDefault("WOWEE_TERRAIN_TEX_CACHE_MB", 4096) * 1024ull * 1024ull;
LOG_INFO("Terrain texture cache budget: ", textureCacheBudgetBytes_ / (1024 * 1024), " MB");
// Phase 2.2: Allocate mega vertex/index buffers and indirect draw buffer.
// Allocate mega vertex/index buffers and indirect draw buffer.
// All terrain chunks share these buffers, eliminating per-chunk VB/IB rebinds.
{
VmaAllocator allocator = vkCtx->getAllocator();
@ -375,7 +375,7 @@ void TerrainRenderer::shutdown() {
if (shadowParamsLayout_) { vkDestroyDescriptorSetLayout(device, shadowParamsLayout_, nullptr); shadowParamsLayout_ = VK_NULL_HANDLE; }
if (shadowParamsUBO_) { vmaDestroyBuffer(allocator, shadowParamsUBO_, shadowParamsAlloc_); shadowParamsUBO_ = VK_NULL_HANDLE; shadowParamsAlloc_ = VK_NULL_HANDLE; }
// Phase 2.2: Destroy mega buffers and indirect draw buffer
// Destroy mega buffers and indirect draw buffer
if (megaVB_) { vmaDestroyBuffer(allocator, megaVB_, megaVBAlloc_); megaVB_ = VK_NULL_HANDLE; megaVBAlloc_ = VK_NULL_HANDLE; megaVBMapped_ = nullptr; }
if (megaIB_) { vmaDestroyBuffer(allocator, megaIB_, megaIBAlloc_); megaIB_ = VK_NULL_HANDLE; megaIBAlloc_ = VK_NULL_HANDLE; megaIBMapped_ = nullptr; }
if (indirectBuffer_) { vmaDestroyBuffer(allocator, indirectBuffer_, indirectAlloc_); indirectBuffer_ = VK_NULL_HANDLE; indirectAlloc_ = VK_NULL_HANDLE; indirectMapped_ = nullptr; }
@ -622,7 +622,7 @@ TerrainChunkGPU TerrainRenderer::uploadChunk(const pipeline::ChunkMesh& chunk) {
gpuChunk.indexBuffer = ib.buffer;
gpuChunk.indexAlloc = ib.allocation;
// Phase 2.2: Also copy into mega buffers for indirect drawing
// Also copy into mega buffers for indirect drawing
uint32_t vertCount = static_cast<uint32_t>(chunk.vertices.size());
uint32_t idxCount = static_cast<uint32_t>(chunk.indices.size());
if (megaVBMapped_ && megaIBMapped_ &&
@ -880,7 +880,7 @@ void TerrainRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, c
renderedChunks = 0;
culledChunks = 0;
// Phase 2.2: Use mega VB + IB when available.
// Use mega VB + IB when available.
// Bind mega buffers once, then use direct draws with base vertex/index offsets.
const bool useMegaBuffers = (megaVB_ && megaIB_);
if (useMegaBuffers) {
@ -1092,7 +1092,7 @@ void TerrainRenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSp
vkCmdPushConstants(cmd, shadowPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
0, 128, &push);
// Phase 2.2: Bind mega buffers once for shadow pass (same as opaque)
// Bind mega buffers once for shadow pass (same as opaque)
const bool useMegaShadow = (megaVB_ && megaIB_);
if (useMegaShadow) {
VkDeviceSize megaOffset = 0;