mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Compare commits
16 commits
d6de60e413
...
f374e19239
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f374e19239 | ||
|
|
88dc1f94a6 | ||
|
|
41218a3b04 | ||
|
|
a24fe4cc45 | ||
|
|
2c5b7cd368 | ||
|
|
f4c115ade9 | ||
|
|
e001aaa2b6 | ||
|
|
e4d94e5d7c | ||
|
|
ad66ef9ca6 | ||
|
|
8014dde29b | ||
|
|
4cae4bfcdc | ||
|
|
5a227c0376 | ||
|
|
7630c7aec7 | ||
|
|
8b0c2a0fa1 | ||
|
|
585d0bf50e | ||
|
|
dfc53f30a8 |
26 changed files with 938 additions and 384 deletions
48
README.md
48
README.md
|
|
@ -19,21 +19,26 @@ Protocol Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**.
|
|||
|
||||
> **Legal Disclaimer**: This is an educational/research project. It does not include any Blizzard Entertainment assets, data files, or proprietary code. World of Warcraft and all related assets are the property of Blizzard Entertainment, Inc. This project is not affiliated with or endorsed by Blizzard Entertainment. Users are responsible for supplying their own legally obtained game data files and for ensuring compliance with all applicable laws in their jurisdiction.
|
||||
|
||||
## Status & Direction (2026-02-18)
|
||||
## Status & Direction (2026-03-07)
|
||||
|
||||
- **Compatibility**: **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a** are all supported via expansion profiles and per-expansion packet parsers (`src/game/packet_parsers_classic.cpp`, `src/game/packet_parsers_tbc.cpp`). All three expansions are roughly on par — no single one is significantly more complete than the others.
|
||||
- **Tested against**: AzerothCore, TrinityCore, and Mangos.
|
||||
- **Current focus**: protocol correctness across server variants, visual accuracy (M2/WMO edge cases, equipment textures), and multi-expansion coverage.
|
||||
- **Tested against**: AzerothCore, TrinityCore, Mangos, and Turtle WoW (1.17).
|
||||
- **Current focus**: instance dungeons, visual accuracy (lava/water, shadow mapping, WMO interiors), and multi-expansion coverage.
|
||||
- **Warden**: Full module execution via Unicorn Engine CPU emulation. Decrypts (RC4→RSA→zlib), parses and relocates the PE module, executes via x86 emulation with Windows API interception. Module cache at `~/.local/share/wowee/warden_cache/`.
|
||||
- **CI**: GitHub Actions builds for Linux (x86-64, ARM64), Windows (MSYS2), and macOS (ARM64). Security scans via CodeQL, Semgrep, and sanitizers.
|
||||
|
||||
## Features
|
||||
|
||||
### Rendering Engine
|
||||
- **Terrain** -- Multi-tile streaming with async loading, texture splatting (4 layers), frustum culling
|
||||
- **Terrain** -- Multi-tile streaming with async loading, texture splatting (4 layers), frustum culling, expanded load radius during taxi flights
|
||||
- **Atmosphere** -- Procedural clouds (FBM noise), lens flare with chromatic aberration, cloud/fog star occlusion
|
||||
- **Characters** -- Skeletal animation with GPU vertex skinning (256 bones), race-aware textures
|
||||
- **Buildings** -- WMO renderer with multi-material batches, frustum culling, 160-unit distance culling
|
||||
- **Particles** -- M2 particle emitters with WotLK struct parsing, billboarded glow effects
|
||||
- **Characters** -- Skeletal animation with GPU vertex skinning (256 bones), race-aware textures, per-instance NPC hair/skin textures
|
||||
- **Buildings** -- WMO renderer with multi-material batches, frustum culling, collision (wall/floor classification, slope sliding), interior glass transparency
|
||||
- **Instances** -- WDT parser for WMO-only dungeon maps, area trigger portals with glow/spin effects, seamless zone transitions
|
||||
- **Water & Lava** -- Terrain and WMO water with refraction/reflection, magma/slime rendering with multi-octave FBM noise flow, Beer-Lambert absorption, M2 lava waterfalls with UV scroll
|
||||
- **Particles** -- M2 particle emitters with WotLK struct parsing, billboarded glow effects, lava steam/splash effects
|
||||
- **Lighting** -- Shadow mapping with PCF filtering, per-frame shadow updates, AABB-based culling, interior/exterior light modes, WMO window glass with fresnel reflections
|
||||
- **Performance** -- Binary keyframe search for animations, incremental spatial index, static doodad skip, hash-free render/shadow culling
|
||||
|
||||
### Asset Pipeline
|
||||
- Extracted loose-file **`Data/`** tree indexed by **`manifest.json`** (fast lookup + caching)
|
||||
|
|
@ -44,21 +49,27 @@ Protocol Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**.
|
|||
### Gameplay Systems
|
||||
- **Authentication** -- Full SRP6a implementation with RC4 header encryption
|
||||
- **Character System** -- Creation (with nonbinary gender option), selection, 3D preview, stats panel, race/class support
|
||||
- **Movement** -- WASD movement, camera orbit, spline path following
|
||||
- **Combat** -- Auto-attack, spell casting with cooldowns, damage calculation, death handling
|
||||
- **Targeting** -- Tab-cycling, click-to-target, faction-based hostility (using Faction.dbc)
|
||||
- **Inventory** -- 23 equipment slots, 16 backpack slots, drag-drop, auto-equip
|
||||
- **Spells** -- Spellbook with class specialty tabs, drag-drop to action bar, spell icons
|
||||
- **Movement** -- WASD movement, camera orbit, spline path following, transport riding (trams, ships, zeppelins), movement ACK responses
|
||||
- **Combat** -- Auto-attack, spell casting with cooldowns, damage calculation, death handling, spirit healer resurrection
|
||||
- **Targeting** -- Tab-cycling with hostility filtering, click-to-target, faction-based hostility (using Faction.dbc)
|
||||
- **Inventory** -- 23 equipment slots, 16 backpack slots, drag-drop, auto-equip, item tooltips with weapon damage/speed
|
||||
- **Bank** -- Full bank support for all expansions, bag slots, drag-drop, right-click deposit (non-equippable items)
|
||||
- **Spells** -- Spellbook with specialty, general, profession, mount, and companion tabs; drag-drop to action bar; item use support
|
||||
- **Talents** -- Talent tree UI with proper visuals and functionality
|
||||
- **Action Bar** -- 12 slots, drag-drop from spellbook/inventory, click-to-cast, keybindings
|
||||
- **Trainers** -- Spell trainer UI, buy spells, known/available/unavailable states
|
||||
- **Quests** -- Quest markers (! and ?) on NPCs and minimap, quest log, quest details, turn-in flow
|
||||
- **Quests** -- Quest markers (! and ?) on NPCs and minimap, quest log, quest details, turn-in flow, quest item progress tracking
|
||||
- **Auction House** -- Search with filters, pagination, sell picker with tooltips, bid/buyout
|
||||
- **Mail** -- Item attachment support for sending
|
||||
- **Vendors** -- Buy, sell, and buyback (most recent sold item), gold tracking, inventory sync
|
||||
- **Loot** -- Loot window, gold looting, item pickup
|
||||
- **Loot** -- Loot window, gold looting, item pickup, chest/gameobject looting
|
||||
- **Gossip** -- NPC interaction, dialogue options
|
||||
- **Chat** -- Tabs/channels, emotes, chat bubbles, clickable URLs, clickable item links with tooltips
|
||||
- **Party** -- Group invites, party list
|
||||
- **Party** -- Group invites, party list, out-of-range member health via SMSG_PARTY_MEMBER_STATS
|
||||
- **Pets** -- Pet tracking via SMSG_PET_SPELLS, dismiss pet button
|
||||
- **Map Exploration** -- Subzone-level fog-of-war reveal matching retail behavior
|
||||
- **Warden** -- Warden anti-cheat module execution via Unicorn Engine x86 emulation (cross-platform, no Wine)
|
||||
- **UI** -- Loading screens with progress bar, settings window, minimap with zoom/rotation/square mode, top-right minimap mute speaker, separate bag windows with compact-empty mode (aggregate view)
|
||||
- **UI** -- Loading screens with progress bar, settings window (shadow distance slider), minimap with zoom/rotation/square mode, top-right minimap mute speaker, separate bag windows with compact-empty mode (aggregate view)
|
||||
|
||||
## Building
|
||||
|
||||
|
|
@ -214,6 +225,11 @@ make -j$(nproc)
|
|||
- [Warden Quick Reference](docs/WARDEN_QUICK_REFERENCE.md) -- Warden module execution overview and testing
|
||||
- [Warden Implementation](docs/WARDEN_IMPLEMENTATION.md) -- Technical details of the implementation
|
||||
|
||||
## CI / CD
|
||||
|
||||
- GitHub Actions builds on every push: Linux (x86-64, ARM64), Windows (MSYS2), macOS (ARM64)
|
||||
- Container build via `container/build-in-container.sh` (Podman)
|
||||
|
||||
## Security
|
||||
|
||||
- GitHub Actions runs a dedicated security workflow at `.github/workflows/security.yml`.
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ void main() {
|
|||
float cs = cos(push.rotation);
|
||||
float sn = sin(push.rotation);
|
||||
vec2 rotated = vec2(center.x * cs - center.y * sn, center.x * sn + center.y * cs);
|
||||
vec2 mapUV = push.playerUV + rotated * push.zoomRadius * 2.0;
|
||||
vec2 mapUV = push.playerUV + vec2(-rotated.x, rotated.y) * push.zoomRadius * 2.0;
|
||||
|
||||
vec4 mapColor = texture(uComposite, mapUV);
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ void main() {
|
|||
float acs = cos(push.arrowRotation);
|
||||
float asn = sin(push.arrowRotation);
|
||||
vec2 ac = center;
|
||||
vec2 arrowPos = vec2(ac.x * acs - ac.y * asn, ac.x * asn + ac.y * acs);
|
||||
vec2 arrowPos = vec2(-(ac.x * acs - ac.y * asn), ac.x * asn + ac.y * acs);
|
||||
|
||||
vec2 tip = vec2(0.0, -0.04);
|
||||
vec2 left = vec2(-0.02, 0.02);
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -155,6 +155,52 @@ void main() {
|
|||
float time = fogParams.z;
|
||||
float basicType = push.liquidBasicType;
|
||||
|
||||
// ============================================================
|
||||
// Magma / Slime — self-luminous flowing surfaces, skip water path
|
||||
// ============================================================
|
||||
if (basicType > 1.5) {
|
||||
float dist = length(viewPos.xyz - FragPos);
|
||||
vec2 flowUV = FragPos.xy;
|
||||
|
||||
bool isMagma = basicType < 2.5;
|
||||
|
||||
// Multi-octave flowing noise for organic lava look
|
||||
float n1 = fbmNoise(flowUV * 0.06 + vec2(time * 0.02, time * 0.03), time * 0.4);
|
||||
float n2 = fbmNoise(flowUV * 0.10 + vec2(-time * 0.015, time * 0.025), time * 0.3);
|
||||
float n3 = noiseValue(flowUV * 0.25 + vec2(time * 0.04, -time * 0.02));
|
||||
float flow = n1 * 0.45 + n2 * 0.35 + n3 * 0.20;
|
||||
|
||||
// Dark crust vs bright molten core
|
||||
vec3 crustColor, hotColor, coreColor;
|
||||
if (isMagma) {
|
||||
crustColor = vec3(0.15, 0.04, 0.01); // dark cooled rock
|
||||
hotColor = vec3(1.0, 0.45, 0.05); // orange molten
|
||||
coreColor = vec3(1.0, 0.85, 0.3); // bright yellow-white core
|
||||
} else {
|
||||
crustColor = vec3(0.05, 0.15, 0.02);
|
||||
hotColor = vec3(0.3, 0.8, 0.15);
|
||||
coreColor = vec3(0.5, 1.0, 0.3);
|
||||
}
|
||||
|
||||
// Three-tier color: crust → molten → hot core
|
||||
float crustMask = smoothstep(0.25, 0.50, flow);
|
||||
float coreMask = smoothstep(0.60, 0.80, flow);
|
||||
vec3 color = mix(crustColor, hotColor, crustMask);
|
||||
color = mix(color, coreColor, coreMask);
|
||||
|
||||
// Subtle pulsing emissive glow
|
||||
float pulse = 1.0 + 0.15 * sin(time * 1.5 + flow * 6.0);
|
||||
color *= pulse;
|
||||
|
||||
// Emissive brightening for hot areas
|
||||
color *= 1.0 + coreMask * 0.6;
|
||||
|
||||
float fogFactor = clamp((fogParams.y - dist) / (fogParams.y - fogParams.x), 0.0, 1.0);
|
||||
color = mix(fogColor.rgb, color, fogFactor);
|
||||
outColor = vec4(color, 0.97);
|
||||
return;
|
||||
}
|
||||
|
||||
vec2 screenUV = gl_FragCoord.xy / vec2(textureSize(SceneColor, 0));
|
||||
|
||||
// --- Normal computation ---
|
||||
|
|
@ -226,11 +272,32 @@ void main() {
|
|||
float depthFade = 1.0 - exp(-verticalDepth * 0.15);
|
||||
vec3 waterBody = mix(shallowColor, deepColor, depthFade);
|
||||
|
||||
vec3 refractedColor = mix(foggedScene * absorbed, waterBody, depthFade * 0.7);
|
||||
// Detect if scene history is available (scene data captured for refraction)
|
||||
float sceneBrightness = dot(sceneRefract, vec3(0.299, 0.587, 0.114));
|
||||
bool hasSceneData = (sceneBrightness > 0.003);
|
||||
|
||||
if (verticalDepth < 0.01) {
|
||||
float opticalDepth = 1.0 - exp(-dist * 0.004);
|
||||
refractedColor = mix(foggedScene, waterBody, opticalDepth * 0.6);
|
||||
// Animated caustic shimmer — only without refraction (refraction already provides movement)
|
||||
if (!hasSceneData) {
|
||||
float caustic1 = noiseValue(FragPos.xy * 1.8 + time * vec2(0.3, 0.15));
|
||||
float caustic2 = noiseValue(FragPos.xy * 3.2 - time * vec2(0.2, 0.35));
|
||||
float causticPattern = caustic1 * 0.6 + caustic2 * 0.4;
|
||||
vec3 causticTint = vec3(0.08, 0.18, 0.28) * smoothstep(0.35, 0.75, causticPattern);
|
||||
waterBody += causticTint;
|
||||
}
|
||||
|
||||
vec3 refractedColor;
|
||||
if (hasSceneData) {
|
||||
refractedColor = mix(foggedScene * absorbed, waterBody, depthFade * 0.7);
|
||||
if (verticalDepth < 0.01) {
|
||||
float opticalDepth = 1.0 - exp(-dist * 0.004);
|
||||
refractedColor = mix(foggedScene, waterBody, opticalDepth * 0.6);
|
||||
}
|
||||
} else {
|
||||
// No refraction data — use lit water body with animated variation
|
||||
vec3 litWater = waterBody * (ambientColor.rgb * 0.8 + NdotL * lightColor.rgb * 0.6);
|
||||
float normalShift = dot(detailNorm.xy, vec2(0.5, 0.5));
|
||||
litWater += vec3(0.02, 0.06, 0.10) * normalShift;
|
||||
refractedColor = litWater;
|
||||
}
|
||||
|
||||
vec3 litBase = waterBody * (ambientColor.rgb * 0.7 + NdotL * lightColor.rgb * 0.5);
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -28,6 +28,7 @@ layout(set = 1, binding = 1) uniform WMOMaterial {
|
|||
int pomMaxSamples;
|
||||
float heightMapVariance;
|
||||
float normalMapStrength;
|
||||
int isLava;
|
||||
};
|
||||
|
||||
layout(set = 1, binding = 2) uniform sampler2D uNormalHeightMap;
|
||||
|
|
@ -120,6 +121,14 @@ void main() {
|
|||
// Compute final UV (with POM if enabled)
|
||||
vec2 finalUV = TexCoord;
|
||||
|
||||
// Lava/magma: scroll UVs for flowing effect
|
||||
if (isLava != 0) {
|
||||
float time = fogParams.z;
|
||||
// Scroll both axes — pools get horizontal flow, waterfalls get vertical flow
|
||||
// (UV orientation depends on mesh, so animate both)
|
||||
finalUV += vec2(time * 0.04, time * 0.06);
|
||||
}
|
||||
|
||||
// Build TBN matrix
|
||||
vec3 T = normalize(Tangent);
|
||||
vec3 B = normalize(Bitangent);
|
||||
|
|
@ -152,9 +161,9 @@ void main() {
|
|||
|
||||
vec3 result;
|
||||
|
||||
// Sample shadow map for all non-window WMO surfaces
|
||||
// Sample shadow map — skip for interior WMO groups (no sun indoors)
|
||||
float shadow = 1.0;
|
||||
if (shadowParams.x > 0.5) {
|
||||
if (shadowParams.x > 0.5 && isInterior == 0) {
|
||||
vec3 ldir = normalize(-lightDir.xyz);
|
||||
float normalOffset = SHADOW_TEXEL * 2.0 * (1.0 - abs(dot(norm, ldir)));
|
||||
vec3 biasedPos = FragPos + norm * normalOffset;
|
||||
|
|
@ -170,7 +179,10 @@ void main() {
|
|||
shadow = mix(1.0, shadow, shadowParams.y);
|
||||
}
|
||||
|
||||
if (unlit != 0) {
|
||||
if (isLava != 0) {
|
||||
// Lava is self-luminous — bright emissive, no shadows
|
||||
result = texColor.rgb * 1.5;
|
||||
} else if (unlit != 0) {
|
||||
result = texColor.rgb * shadow;
|
||||
} else if (isInterior != 0) {
|
||||
vec3 mocv = max(VertColor.rgb, vec3(0.5));
|
||||
|
|
@ -219,7 +231,12 @@ void main() {
|
|||
glass += specBroad * lightColor.rgb * 0.12;
|
||||
|
||||
result = glass;
|
||||
alpha = mix(0.4, 0.95, NdotV);
|
||||
if (isWindow == 2) {
|
||||
// Instance/dungeon glass: mostly transparent to see through
|
||||
alpha = mix(0.12, 0.35, fresnel);
|
||||
} else {
|
||||
alpha = mix(0.4, 0.95, NdotV);
|
||||
}
|
||||
}
|
||||
|
||||
outColor = vec4(result, alpha);
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
# Project Status
|
||||
|
||||
**Last updated**: 2026-02-19
|
||||
**Last updated**: 2026-03-07
|
||||
|
||||
## What This Repo Is
|
||||
|
||||
|
|
@ -11,23 +11,34 @@ Wowee is a native C++ World of Warcraft client experiment focused on connecting
|
|||
Implemented (working in normal use):
|
||||
|
||||
- Auth flow: SRP6a auth + realm list + world connect with header encryption
|
||||
- Rendering: terrain, WMO/M2 rendering, water, sky system, particles, minimap/world map, loading video playback
|
||||
- Character system: creation (including nonbinary gender), selection, 3D preview with equipment, character screen
|
||||
- Core gameplay: movement, targeting, combat, action bar, inventory/equipment, chat (tabs/channels, emotes, item links)
|
||||
- Quests: quest markers (! and ?) on NPCs/minimap, quest log with detail queries/retry, objective tracking, accept/complete flow, turn-in
|
||||
- Rendering: terrain, WMO/M2, water/magma/slime (FBM noise shaders), sky system, particles, shadow mapping, minimap/world map, loading video playback
|
||||
- Instances: WDT parser, WMO-only dungeon maps, area trigger portals with glow/spin effects, zone transitions
|
||||
- Character system: creation (including nonbinary gender), selection, 3D preview with equipment, character screen, per-instance NPC hair/skin textures
|
||||
- Core gameplay: movement (with ACK responses), targeting (hostility-filtered tab-cycle), combat, action bar, inventory/equipment, chat (tabs/channels, emotes, item links)
|
||||
- Quests: quest markers (! and ?) on NPCs/minimap, quest log with detail queries/retry, objective tracking, accept/complete flow, turn-in, quest item progress
|
||||
- Trainers: spell trainer UI, buy spells, known/available/unavailable states
|
||||
- Vendors, loot, gossip dialogs (including buyback for most recently sold item)
|
||||
- Spellbook with class tabs, drag-drop to action bar, spell icons
|
||||
- Vendors, loot (including chest/gameobject loot), gossip dialogs (including buyback for most recently sold item)
|
||||
- Bank: full bank support for all expansions, bag slots, drag-drop, right-click deposit
|
||||
- Auction house: search with filters, pagination, sell picker, bid/buyout, tooltips
|
||||
- Mail: item attachment support for sending
|
||||
- Spellbook with specialty/general/profession/mount/companion tabs, drag-drop to action bar, spell icons, item use
|
||||
- Talent tree UI with proper visuals and functionality
|
||||
- Pet tracking (SMSG_PET_SPELLS), dismiss pet button
|
||||
- Party: group invites, party list, out-of-range member health (SMSG_PARTY_MEMBER_STATS)
|
||||
- Map exploration: subzone-level fog-of-war reveal
|
||||
- Warden anti-cheat: full module execution via Unicorn Engine x86 emulation; module caching
|
||||
- Audio: ambient, movement, combat, spell, and UI sound systems
|
||||
- Bag UI: separate bag windows, open-bag indicator on bag bar, optional collapse-empty mode in aggregate bag view
|
||||
- Multi-expansion: Classic/Vanilla, TBC, WotLK, and Turtle WoW (1.17) protocol and asset variants
|
||||
- CI: GitHub Actions for Linux (x86-64, ARM64), Windows (MSYS2), macOS (ARM64); container builds via Podman
|
||||
|
||||
In progress / known gaps:
|
||||
|
||||
- Transports (ships, zeppelins, elevators): partial support, timing and edge cases still buggy
|
||||
- Transports: M2 transports (trams) working with position-delta riding; WMO transports (ships, zeppelins) working with path following; some edge cases remain
|
||||
- 3D positional audio: not implemented (mono/stereo only)
|
||||
- Visual edge cases: some M2/WMO rendering gaps (character shin mesh, some particle effects)
|
||||
- Interior rendering: WMO interior shadows disabled (too dark); lava steam particles sparse
|
||||
- Water refraction: implemented but disabled by default (can cause VK_ERROR_DEVICE_LOST on some GPUs)
|
||||
|
||||
## Where To Look
|
||||
|
||||
|
|
|
|||
|
|
@ -658,6 +658,9 @@ public:
|
|||
playerTransportStickyTimer_ = 8.0f;
|
||||
movementInfo.transportGuid = transportGuid;
|
||||
}
|
||||
void setPlayerTransportOffset(const glm::vec3& offset) {
|
||||
playerTransportOffset_ = offset;
|
||||
}
|
||||
void clearPlayerTransport() {
|
||||
if (playerTransportGuid_ != 0) {
|
||||
playerTransportStickyGuid_ = playerTransportGuid_;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
namespace wowee::rendering {
|
||||
class WMORenderer;
|
||||
class M2Renderer;
|
||||
}
|
||||
|
||||
namespace wowee::pipeline {
|
||||
|
|
@ -71,6 +72,7 @@ struct ActiveTransport {
|
|||
float serverAngularVelocity;
|
||||
bool hasServerVelocity;
|
||||
bool allowBootstrapVelocity; // Disable DBC bootstrap when spawn/path mismatch is clearly invalid
|
||||
bool isM2 = false; // True if rendered as M2 (not WMO), uses M2Renderer for transforms
|
||||
};
|
||||
|
||||
class TransportManager {
|
||||
|
|
@ -79,12 +81,14 @@ public:
|
|||
~TransportManager();
|
||||
|
||||
void setWMORenderer(rendering::WMORenderer* renderer) { wmoRenderer_ = renderer; }
|
||||
void setM2Renderer(rendering::M2Renderer* renderer) { m2Renderer_ = renderer; }
|
||||
|
||||
void update(float deltaTime);
|
||||
void registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId, const glm::vec3& spawnWorldPos, uint32_t entry = 0);
|
||||
void unregisterTransport(uint64_t guid);
|
||||
|
||||
ActiveTransport* getTransport(uint64_t guid);
|
||||
const std::unordered_map<uint64_t, ActiveTransport>& getTransports() const { return transports_; }
|
||||
glm::vec3 getPlayerWorldPosition(uint64_t transportGuid, const glm::vec3& localOffset);
|
||||
glm::mat4 getTransportInvTransform(uint64_t transportGuid);
|
||||
|
||||
|
|
@ -141,6 +145,7 @@ private:
|
|||
std::unordered_map<uint32_t, TransportPath> paths_; // Indexed by transportEntry (pathId from TransportAnimation.dbc)
|
||||
std::unordered_map<uint32_t, TransportPath> taxiPaths_; // Indexed by TaxiPath.dbc ID (world-coord paths for MO_TRANSPORT)
|
||||
rendering::WMORenderer* wmoRenderer_ = nullptr;
|
||||
rendering::M2Renderer* m2Renderer_ = nullptr;
|
||||
bool clientSideAnimation_ = false; // DISABLED - use server positions instead of client prediction
|
||||
float elapsedTime_ = 0.0f; // Total elapsed time (seconds)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ public:
|
|||
bool isSitting() const { return sitting; }
|
||||
bool isSwimming() const { return swimming; }
|
||||
bool isInsideWMO() const { return cachedInsideWMO; }
|
||||
void setGrounded(bool g) { grounded = g; }
|
||||
bool isOnTaxi() const { return externalFollow_; }
|
||||
const glm::vec3* getFollowTarget() const { return followTarget; }
|
||||
glm::vec3* getFollowTargetMutable() { return followTarget; }
|
||||
|
|
@ -96,6 +97,7 @@ public:
|
|||
void setExternalMoving(bool moving) { externalMoving_ = moving; }
|
||||
void setFacingYaw(float yaw) { facingYaw = yaw; } // For taxi/scripted movement
|
||||
void clearMovementInputs();
|
||||
void suppressMovementFor(float seconds) { movementSuppressTimer_ = seconds; }
|
||||
|
||||
// Trigger mount jump (applies vertical velocity for physics hop)
|
||||
void triggerMountJump();
|
||||
|
|
@ -141,6 +143,7 @@ private:
|
|||
static constexpr float MIN_DISTANCE = 0.5f; // Minimum zoom (first-person threshold)
|
||||
static constexpr float MAX_DISTANCE_NORMAL = 22.0f; // Default max zoom out
|
||||
static constexpr float MAX_DISTANCE_EXTENDED = 50.0f; // Extended max zoom out
|
||||
static constexpr float MAX_DISTANCE_INTERIOR = 12.0f; // Max zoom inside WMOs
|
||||
bool extendedZoom_ = false;
|
||||
static constexpr float ZOOM_SMOOTH_SPEED = 15.0f; // How fast zoom eases
|
||||
static constexpr float CAM_SMOOTH_SPEED = 20.0f; // How fast camera position smooths
|
||||
|
|
@ -209,6 +212,9 @@ private:
|
|||
static constexpr float SWIM_SINK_SPEED = -3.0f;
|
||||
static constexpr float WATER_SURFACE_OFFSET = 0.9f;
|
||||
|
||||
// Movement input suppression (after teleport/portal, ignore held keys)
|
||||
float movementSuppressTimer_ = 0.0f;
|
||||
|
||||
// State
|
||||
bool enabled = true;
|
||||
bool sitting = false;
|
||||
|
|
|
|||
|
|
@ -112,12 +112,14 @@ struct M2ModelGPU {
|
|||
bool hasAnimation = false; // True if any bone has keyframes
|
||||
bool isSmoke = false; // True for smoke models (UV scroll animation)
|
||||
bool isSpellEffect = false; // True for spell effect models (skip particle dampeners)
|
||||
bool isInstancePortal = false; // Instance portal model (spin + glow)
|
||||
bool disableAnimation = false; // Keep foliage/tree doodads visually stable
|
||||
bool shadowWindFoliage = false; // Apply wind sway in shadow pass for foliage/tree cards
|
||||
bool isFoliageLike = false; // Model name matches foliage/tree/bush/grass etc (precomputed)
|
||||
bool isElvenLike = false; // Model name matches elf/elven/quel (precomputed)
|
||||
bool isLanternLike = false; // Model name matches lantern/lamp/light (precomputed)
|
||||
bool isKoboldFlame = false; // Model name matches kobold+(candle/torch/mine) (precomputed)
|
||||
bool isLavaModel = false; // Model name contains lava/molten/magma (UV scroll fallback)
|
||||
bool hasTextureAnimation = false; // True if any batch has UV animation
|
||||
|
||||
// Particle emitter data (kept from M2Model)
|
||||
|
|
@ -181,9 +183,11 @@ struct M2Instance {
|
|||
bool cachedHasParticleEmitters = false;
|
||||
bool cachedIsGroundDetail = false;
|
||||
bool cachedIsInvisibleTrap = false;
|
||||
bool cachedIsInstancePortal = false;
|
||||
bool cachedIsValid = false;
|
||||
bool skipCollision = false; // WMO interior doodads — skip player wall collision
|
||||
float cachedBoundRadius = 0.0f;
|
||||
float portalSpinAngle = 0.0f; // Accumulated spin angle for portal rotation
|
||||
|
||||
// Frame-skip optimization (update distant animations less frequently)
|
||||
uint8_t frameSkipCounter = 0;
|
||||
|
|
@ -476,6 +480,7 @@ private:
|
|||
// Smoke particle system
|
||||
std::vector<SmokeParticle> smokeParticles;
|
||||
std::vector<size_t> smokeInstanceIndices_; // Indices into instances[] for smoke emitters
|
||||
std::vector<size_t> portalInstanceIndices_; // Indices into instances[] for spinning portals
|
||||
static constexpr int MAX_SMOKE_PARTICLES = 1000;
|
||||
float smokeEmitAccum = 0.0f;
|
||||
std::mt19937 smokeRng{42};
|
||||
|
|
|
|||
|
|
@ -244,6 +244,7 @@ private:
|
|||
glm::vec3 shadowCenter = glm::vec3(0.0f);
|
||||
bool shadowCenterInitialized = false;
|
||||
bool shadowsEnabled = true;
|
||||
float shadowDistance_ = 72.0f; // Shadow frustum half-extent (default: 72 units)
|
||||
uint32_t shadowFrameCounter_ = 0;
|
||||
|
||||
|
||||
|
|
@ -254,8 +255,13 @@ public:
|
|||
|
||||
void setShadowsEnabled(bool enabled) { shadowsEnabled = enabled; }
|
||||
bool areShadowsEnabled() const { return shadowsEnabled; }
|
||||
void setShadowDistance(float dist) { shadowDistance_ = glm::clamp(dist, 40.0f, 200.0f); }
|
||||
float getShadowDistance() const { return shadowDistance_; }
|
||||
void setMsaaSamples(VkSampleCountFlagBits samples);
|
||||
|
||||
void setWaterRefractionEnabled(bool enabled);
|
||||
bool isWaterRefractionEnabled() const;
|
||||
|
||||
private:
|
||||
void applyMsaaChange();
|
||||
VkSampleCountFlagBits pendingMsaaSamples_ = VK_SAMPLE_COUNT_1_BIT;
|
||||
|
|
|
|||
|
|
@ -93,12 +93,13 @@ public:
|
|||
bool hasWater1xPass() const { return water1xRenderPass != VK_NULL_HANDLE; }
|
||||
VkRenderPass getWater1xRenderPass() const { return water1xRenderPass; }
|
||||
|
||||
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera, float time, bool use1x = false);
|
||||
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera, float time, bool use1x = false, uint32_t frameIndex = 0);
|
||||
void captureSceneHistory(VkCommandBuffer cmd,
|
||||
VkImage srcColorImage,
|
||||
VkImage srcDepthImage,
|
||||
VkExtent2D srcExtent,
|
||||
bool srcDepthIsMsaa);
|
||||
bool srcDepthIsMsaa,
|
||||
uint32_t frameIndex = 0);
|
||||
|
||||
// --- Planar reflection pass ---
|
||||
// Call sequence: beginReflectionPass → [render scene] → endReflectionPass
|
||||
|
|
@ -124,6 +125,9 @@ public:
|
|||
void setEnabled(bool enabled) { renderingEnabled = enabled; }
|
||||
bool isEnabled() const { return renderingEnabled; }
|
||||
|
||||
void setRefractionEnabled(bool enabled);
|
||||
bool isRefractionEnabled() const { return refractionEnabled; }
|
||||
|
||||
std::optional<float> getWaterHeightAt(float glX, float glY) const;
|
||||
/// Like getWaterHeightAt but only returns water surfaces whose height is
|
||||
/// close to the query Z (within maxAbove units above). Avoids false
|
||||
|
|
@ -159,17 +163,22 @@ private:
|
|||
VkDescriptorPool materialDescPool = VK_NULL_HANDLE;
|
||||
VkDescriptorSetLayout sceneSetLayout = VK_NULL_HANDLE;
|
||||
VkDescriptorPool sceneDescPool = VK_NULL_HANDLE;
|
||||
VkDescriptorSet sceneSet = VK_NULL_HANDLE;
|
||||
static constexpr uint32_t MAX_WATER_SETS = 16384;
|
||||
|
||||
VkSampler sceneColorSampler = VK_NULL_HANDLE;
|
||||
VkSampler sceneDepthSampler = VK_NULL_HANDLE;
|
||||
VkImage sceneColorImage = VK_NULL_HANDLE;
|
||||
VmaAllocation sceneColorAlloc = VK_NULL_HANDLE;
|
||||
VkImageView sceneColorView = VK_NULL_HANDLE;
|
||||
VkImage sceneDepthImage = VK_NULL_HANDLE;
|
||||
VmaAllocation sceneDepthAlloc = VK_NULL_HANDLE;
|
||||
VkImageView sceneDepthView = VK_NULL_HANDLE;
|
||||
// Per-frame scene history to avoid race between frames in flight
|
||||
static constexpr uint32_t SCENE_HISTORY_FRAMES = 2;
|
||||
struct PerFrameSceneHistory {
|
||||
VkImage colorImage = VK_NULL_HANDLE;
|
||||
VmaAllocation colorAlloc = VK_NULL_HANDLE;
|
||||
VkImageView colorView = VK_NULL_HANDLE;
|
||||
VkImage depthImage = VK_NULL_HANDLE;
|
||||
VmaAllocation depthAlloc = VK_NULL_HANDLE;
|
||||
VkImageView depthView = VK_NULL_HANDLE;
|
||||
VkDescriptorSet sceneSet = VK_NULL_HANDLE;
|
||||
};
|
||||
PerFrameSceneHistory sceneHistory[SCENE_HISTORY_FRAMES];
|
||||
VkExtent2D sceneHistoryExtent = {0, 0};
|
||||
bool sceneHistoryReady = false;
|
||||
mutable uint32_t renderDiagCounter_ = 0;
|
||||
|
|
@ -200,6 +209,7 @@ private:
|
|||
|
||||
std::vector<WaterSurface> surfaces;
|
||||
bool renderingEnabled = true;
|
||||
bool refractionEnabled = false;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@ public:
|
|||
void setNormalMapStrength(float s) { normalMapStrength_ = s; materialSettingsDirty_ = true; }
|
||||
void setPOMEnabled(bool enabled) { pomEnabled_ = enabled; materialSettingsDirty_ = true; }
|
||||
void setPOMQuality(int q) { pomQuality_ = q; materialSettingsDirty_ = true; }
|
||||
void setWMOOnlyMap(bool v) { wmoOnlyMap_ = v; materialSettingsDirty_ = true; }
|
||||
bool isNormalMappingEnabled() const { return normalMappingEnabled_; }
|
||||
float getNormalMapStrength() const { return normalMapStrength_; }
|
||||
bool isPOMEnabled() const { return pomEnabled_; }
|
||||
|
|
@ -339,7 +340,9 @@ private:
|
|||
int32_t pomMaxSamples; // 36 (max ray-march steps)
|
||||
float heightMapVariance; // 40 (low variance = skip POM)
|
||||
float normalMapStrength; // 44 (0=flat, 1=full, 2=exaggerated)
|
||||
}; // 48 bytes total
|
||||
int32_t isLava; // 48 (1=lava/magma UV scroll)
|
||||
float pad[3]; // 52-60 padding to 64 bytes
|
||||
}; // 64 bytes total
|
||||
|
||||
/**
|
||||
* WMO group GPU resources
|
||||
|
|
@ -379,6 +382,7 @@ private:
|
|||
bool unlit = false;
|
||||
bool isTransparent = false; // blendMode >= 2
|
||||
bool isWindow = false; // F_SIDN or F_WINDOW material
|
||||
bool isLava = false; // lava/magma texture (UV scroll)
|
||||
// For multi-draw: store index ranges
|
||||
struct DrawRange { uint32_t firstIndex; uint32_t indexCount; };
|
||||
std::vector<DrawRange> draws;
|
||||
|
|
@ -670,6 +674,7 @@ private:
|
|||
bool pomEnabled_ = true; // on by default
|
||||
int pomQuality_ = 1; // 0=Low(16), 1=Medium(32), 2=High(64)
|
||||
bool materialSettingsDirty_ = false; // rebuild UBOs when settings change
|
||||
bool wmoOnlyMap_ = false; // true for dungeon/instance WMO-only maps
|
||||
|
||||
// Rendering state
|
||||
bool wireframeMode = false;
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ private:
|
|||
bool pendingVsync = false;
|
||||
int pendingResIndex = 0;
|
||||
bool pendingShadows = true;
|
||||
float pendingShadowDistance = 72.0f;
|
||||
bool pendingWaterRefraction = false;
|
||||
int pendingMasterVolume = 100;
|
||||
int pendingMusicVolume = 30;
|
||||
int pendingAmbientVolume = 100;
|
||||
|
|
@ -123,6 +125,7 @@ private:
|
|||
bool minimapSettingsApplied_ = false;
|
||||
bool volumeSettingsApplied_ = false; // True once saved volume settings applied to audio managers
|
||||
bool msaaSettingsApplied_ = false; // True once saved MSAA setting applied to renderer
|
||||
bool waterRefractionApplied_ = false;
|
||||
bool normalMapSettingsApplied_ = false; // True once saved normal map/POM settings applied
|
||||
|
||||
// Mute state: mute bypasses master volume without touching slider values
|
||||
|
|
|
|||
|
|
@ -968,6 +968,15 @@ void Application::update(float deltaTime) {
|
|||
gameHandler->isTaxiMountActive() ||
|
||||
gameHandler->isTaxiActivationPending());
|
||||
bool onTransportNow = gameHandler && gameHandler->isOnTransport();
|
||||
// M2 transports (trams) use position-delta approach: player keeps normal
|
||||
// movement and the transport's frame-to-frame delta is applied on top.
|
||||
// Only WMO transports (ships) use full external-driven mode.
|
||||
bool isM2Transport = false;
|
||||
if (onTransportNow && gameHandler->getTransportManager()) {
|
||||
auto* tr = gameHandler->getTransportManager()->getTransport(gameHandler->getPlayerTransportGuid());
|
||||
isM2Transport = (tr && tr->isM2);
|
||||
}
|
||||
bool onWMOTransport = onTransportNow && !isM2Transport;
|
||||
if (worldEntryMovementGraceTimer_ > 0.0f) {
|
||||
worldEntryMovementGraceTimer_ -= deltaTime;
|
||||
// Clear stale movement from before teleport each frame
|
||||
|
|
@ -976,7 +985,7 @@ void Application::update(float deltaTime) {
|
|||
renderer->getCameraController()->clearMovementInputs();
|
||||
}
|
||||
if (renderer && renderer->getCameraController()) {
|
||||
const bool externallyDrivenMotion = onTaxi || onTransportNow || chargeActive_;
|
||||
const bool externallyDrivenMotion = onTaxi || onWMOTransport || chargeActive_;
|
||||
// Keep physics frozen (externalFollow) during landing clamp when terrain
|
||||
// hasn't loaded yet — prevents gravity from pulling player through void.
|
||||
bool landingClampActive = !onTaxi && taxiLandingClampTimer_ > 0.0f &&
|
||||
|
|
@ -1057,14 +1066,18 @@ void Application::update(float deltaTime) {
|
|||
|
||||
// Sync character render position ↔ canonical WoW coords each frame
|
||||
if (renderer && gameHandler) {
|
||||
bool onTransport = gameHandler->isOnTransport();
|
||||
// For position sync branching, only WMO transports use the dedicated
|
||||
// onTransport branch. M2 transports use the normal movement else branch
|
||||
// with a position-delta correction applied on top.
|
||||
bool onTransport = onWMOTransport;
|
||||
|
||||
// Debug: Log transport state changes
|
||||
static bool wasOnTransport = false;
|
||||
if (onTransport != wasOnTransport) {
|
||||
LOG_DEBUG("Transport state changed: onTransport=", onTransport,
|
||||
bool onTransportNowDbg = gameHandler->isOnTransport();
|
||||
if (onTransportNowDbg != wasOnTransport) {
|
||||
LOG_DEBUG("Transport state changed: onTransport=", onTransportNowDbg,
|
||||
" isM2=", isM2Transport,
|
||||
" guid=0x", std::hex, gameHandler->getPlayerTransportGuid(), std::dec);
|
||||
wasOnTransport = onTransport;
|
||||
wasOnTransport = onTransportNowDbg;
|
||||
}
|
||||
|
||||
if (onTaxi) {
|
||||
|
|
@ -1092,13 +1105,11 @@ void Application::update(float deltaTime) {
|
|||
}
|
||||
}
|
||||
} else if (onTransport) {
|
||||
// Transport mode: compose world position from transport transform + local offset
|
||||
// WMO transport mode (ships): compose world position from transform + local offset
|
||||
glm::vec3 canonical = gameHandler->getComposedWorldPosition();
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
renderer->getCharacterPosition() = renderPos;
|
||||
// Keep movementInfo in lockstep with composed transport world position.
|
||||
gameHandler->setPosition(canonical.x, canonical.y, canonical.z);
|
||||
// Update camera follow target
|
||||
if (renderer->getCameraController()) {
|
||||
glm::vec3* followTarget = renderer->getCameraController()->getFollowTargetMutable();
|
||||
if (followTarget) {
|
||||
|
|
@ -1172,6 +1183,27 @@ void Application::update(float deltaTime) {
|
|||
}
|
||||
} else {
|
||||
glm::vec3 renderPos = renderer->getCharacterPosition();
|
||||
|
||||
// M2 transport riding: apply transport's frame-to-frame position delta
|
||||
// so the player moves with the tram while retaining normal movement input.
|
||||
if (isM2Transport && gameHandler->getTransportManager()) {
|
||||
auto* tr = gameHandler->getTransportManager()->getTransport(
|
||||
gameHandler->getPlayerTransportGuid());
|
||||
if (tr) {
|
||||
static glm::vec3 lastTransportCanonical(0);
|
||||
static uint64_t lastTransportGuid = 0;
|
||||
if (lastTransportGuid == gameHandler->getPlayerTransportGuid()) {
|
||||
glm::vec3 deltaCanonical = tr->position - lastTransportCanonical;
|
||||
glm::vec3 deltaRender = core::coords::canonicalToRender(deltaCanonical)
|
||||
- core::coords::canonicalToRender(glm::vec3(0));
|
||||
renderPos += deltaRender;
|
||||
renderer->getCharacterPosition() = renderPos;
|
||||
}
|
||||
lastTransportCanonical = tr->position;
|
||||
lastTransportGuid = gameHandler->getPlayerTransportGuid();
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec3 canonical = core::coords::renderToCanonical(renderPos);
|
||||
gameHandler->setPosition(canonical.x, canonical.y, canonical.z);
|
||||
|
||||
|
|
@ -1203,6 +1235,41 @@ void Application::update(float deltaTime) {
|
|||
facingSendCooldown_ = 0.1f; // max 10 Hz
|
||||
}
|
||||
}
|
||||
|
||||
// Client-side transport boarding detection (for M2 transports like trams
|
||||
// where the server doesn't send transport attachment data).
|
||||
// Use a generous AABB around each transport's current position.
|
||||
if (gameHandler->getTransportManager() && !gameHandler->isOnTransport()) {
|
||||
auto* tm = gameHandler->getTransportManager();
|
||||
glm::vec3 playerCanonical = core::coords::renderToCanonical(renderPos);
|
||||
|
||||
for (auto& [guid, transport] : tm->getTransports()) {
|
||||
if (!transport.isM2) continue;
|
||||
glm::vec3 diff = playerCanonical - transport.position;
|
||||
float horizDistSq = diff.x * diff.x + diff.y * diff.y;
|
||||
float vertDist = std::abs(diff.z);
|
||||
if (horizDistSq < 144.0f && vertDist < 15.0f) {
|
||||
gameHandler->setPlayerOnTransport(guid, playerCanonical - transport.position);
|
||||
LOG_DEBUG("M2 transport boarding: guid=0x", std::hex, guid, std::dec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// M2 transport disembark: player walked far enough from transport center
|
||||
if (isM2Transport && gameHandler->getTransportManager()) {
|
||||
auto* tm = gameHandler->getTransportManager();
|
||||
auto* tr = tm->getTransport(gameHandler->getPlayerTransportGuid());
|
||||
if (tr) {
|
||||
glm::vec3 playerCanonical = core::coords::renderToCanonical(renderPos);
|
||||
glm::vec3 diff = playerCanonical - tr->position;
|
||||
float horizDistSq = diff.x * diff.x + diff.y * diff.y;
|
||||
if (horizDistSq > 225.0f) {
|
||||
gameHandler->clearPlayerTransport();
|
||||
LOG_DEBUG("M2 transport disembark");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1555,8 +1622,10 @@ void Application::setupUICallbacks() {
|
|||
taxiLandingClampTimer_ = 0.0f;
|
||||
lastTaxiFlight_ = false;
|
||||
// Stop any movement that was active before the teleport
|
||||
if (renderer->getCameraController())
|
||||
if (renderer->getCameraController()) {
|
||||
renderer->getCameraController()->clearMovementInputs();
|
||||
renderer->getCameraController()->suppressMovementFor(0.5f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1573,8 +1642,10 @@ void Application::setupUICallbacks() {
|
|||
taxiLandingClampTimer_ = 0.0f;
|
||||
lastTaxiFlight_ = false;
|
||||
// Stop any movement that was active before the teleport
|
||||
if (renderer && renderer->getCameraController())
|
||||
if (renderer && renderer->getCameraController()) {
|
||||
renderer->getCameraController()->clearMovementInputs();
|
||||
renderer->getCameraController()->suppressMovementFor(1.0f);
|
||||
}
|
||||
loadOnlineWorldTerrain(mapId, x, y, z);
|
||||
// loadedMapId_ is set inside loadOnlineWorldTerrain (including
|
||||
// any deferred entries it processes), so we must NOT override it here.
|
||||
|
|
@ -1667,13 +1738,17 @@ void Application::setupUICallbacks() {
|
|||
}
|
||||
|
||||
// Sample floor at the DESTINATION position (after nudge).
|
||||
// Pick the highest floor so we snap up to WMO floors when fallen below.
|
||||
bool foundFloor = false;
|
||||
if (auto floor = sampleBestFloorAt(pos.x, pos.y, pos.z + 60.0f)) {
|
||||
pos.z = *floor + 0.2f;
|
||||
} else {
|
||||
pos.z += 20.0f;
|
||||
foundFloor = true;
|
||||
}
|
||||
|
||||
cc->teleportTo(pos);
|
||||
if (!foundFloor) {
|
||||
cc->setGrounded(false); // Let gravity pull player down to a surface
|
||||
}
|
||||
syncTeleportedPositionToServer(pos);
|
||||
forceServerTeleportCommand(pos);
|
||||
clearStuckMovement();
|
||||
|
|
@ -2065,7 +2140,7 @@ void Application::setupUICallbacks() {
|
|||
}
|
||||
|
||||
uint32_t wmoInstanceId = it->second.instanceId;
|
||||
LOG_DEBUG("Registering server transport: GUID=0x", std::hex, guid, std::dec,
|
||||
LOG_WARNING("Registering server transport: GUID=0x", std::hex, guid, std::dec,
|
||||
" entry=", entry, " displayId=", displayId, " wmoInstance=", wmoInstanceId,
|
||||
" pos=(", x, ", ", y, ", ", z, ")");
|
||||
|
||||
|
|
@ -2093,15 +2168,18 @@ void Application::setupUICallbacks() {
|
|||
hasUsablePath = transportManager->hasUsableMovingPathForEntry(entry, 25.0f);
|
||||
}
|
||||
|
||||
LOG_WARNING("Transport path check: entry=", entry, " hasUsablePath=", hasUsablePath,
|
||||
" preferServerData=", preferServerData, " shipOrZepDisplay=", shipOrZeppelinDisplay);
|
||||
|
||||
if (preferServerData) {
|
||||
// Strict server-authoritative mode: do not infer/remap fallback routes.
|
||||
if (!hasUsablePath) {
|
||||
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
||||
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
|
||||
LOG_DEBUG("Server-first strict registration: stationary fallback for GUID 0x",
|
||||
LOG_WARNING("Server-first strict registration: stationary fallback for GUID 0x",
|
||||
std::hex, guid, std::dec, " entry=", entry);
|
||||
} else {
|
||||
LOG_DEBUG("Server-first transport registration: using entry DBC path for entry ", entry);
|
||||
LOG_WARNING("Server-first transport registration: using entry DBC path for entry ", entry);
|
||||
}
|
||||
} else if (!hasUsablePath) {
|
||||
// Remap/infer path by spawn position when entry doesn't map 1:1 to DBC ids.
|
||||
|
|
@ -2111,12 +2189,12 @@ void Application::setupUICallbacks() {
|
|||
canonicalSpawnPos, 1200.0f, allowZOnly);
|
||||
if (inferredPath != 0) {
|
||||
pathId = inferredPath;
|
||||
LOG_DEBUG("Using inferred transport path ", pathId, " for entry ", entry);
|
||||
LOG_WARNING("Using inferred transport path ", pathId, " for entry ", entry);
|
||||
} else {
|
||||
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
|
||||
if (remappedPath != 0) {
|
||||
pathId = remappedPath;
|
||||
LOG_DEBUG("Using remapped fallback transport path ", pathId,
|
||||
LOG_WARNING("Using remapped fallback transport path ", pathId,
|
||||
" for entry ", entry, " displayId=", displayId,
|
||||
" (usableEntryPath=", transportManager->hasPathForEntry(entry), ")");
|
||||
} else {
|
||||
|
|
@ -2129,12 +2207,19 @@ void Application::setupUICallbacks() {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("Using real transport path from TransportAnimation.dbc for entry ", entry);
|
||||
LOG_WARNING("Using real transport path from TransportAnimation.dbc for entry ", entry);
|
||||
}
|
||||
|
||||
// Register the transport with spawn position (prevents rendering at origin until server update)
|
||||
transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos, entry);
|
||||
|
||||
// Mark M2 transports (e.g. Deeprun Tram cars) so TransportManager uses M2Renderer
|
||||
if (!it->second.isWmo) {
|
||||
if (auto* tr = transportManager->getTransport(guid)) {
|
||||
tr->isM2 = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Server-authoritative movement - set initial position from spawn data
|
||||
glm::vec3 canonicalPos(x, y, z);
|
||||
transportManager->updateServerTransport(guid, canonicalPos, orientation);
|
||||
|
|
@ -2163,7 +2248,7 @@ void Application::setupUICallbacks() {
|
|||
}
|
||||
|
||||
if (auto* tr = transportManager->getTransport(guid); tr) {
|
||||
LOG_DEBUG("Transport registered: guid=0x", std::hex, guid, std::dec,
|
||||
LOG_WARNING("Transport registered: guid=0x", std::hex, guid, std::dec,
|
||||
" entry=", entry, " displayId=", displayId,
|
||||
" pathId=", tr->pathId,
|
||||
" mode=", (tr->useClientAnimation ? "client" : "server"),
|
||||
|
|
@ -3428,16 +3513,22 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
glm::vec3 spawnCanonical = core::coords::serverToCanonical(glm::vec3(x, y, z));
|
||||
glm::vec3 spawnRender = core::coords::canonicalToRender(spawnCanonical);
|
||||
|
||||
// Set camera position
|
||||
// Set camera position and facing from server orientation
|
||||
if (renderer->getCameraController()) {
|
||||
float yawDeg = 0.0f;
|
||||
if (gameHandler) {
|
||||
float canonicalYaw = gameHandler->getMovementInfo().orientation;
|
||||
yawDeg = 180.0f - glm::degrees(canonicalYaw);
|
||||
}
|
||||
renderer->getCameraController()->setOnlineMode(true);
|
||||
renderer->getCameraController()->setDefaultSpawn(spawnRender, 0.0f, -15.0f);
|
||||
renderer->getCameraController()->setDefaultSpawn(spawnRender, yawDeg, -15.0f);
|
||||
renderer->getCameraController()->reset();
|
||||
}
|
||||
|
||||
// Set map name for WMO renderer
|
||||
// Set map name for WMO renderer and reset instance mode
|
||||
if (renderer->getWMORenderer()) {
|
||||
renderer->getWMORenderer()->setMapName(mapName);
|
||||
renderer->getWMORenderer()->setWMOOnlyMap(false);
|
||||
}
|
||||
|
||||
// Set map name for terrain manager
|
||||
|
|
@ -3445,11 +3536,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
renderer->getTerrainManager()->setMapName(mapName);
|
||||
}
|
||||
|
||||
// Connect TransportManager to WMORenderer (for server transports)
|
||||
if (gameHandler && gameHandler->getTransportManager() && renderer->getWMORenderer()) {
|
||||
gameHandler->getTransportManager()->setWMORenderer(renderer->getWMORenderer());
|
||||
LOG_INFO("TransportManager connected to WMORenderer for online mode");
|
||||
}
|
||||
// NOTE: TransportManager renderer connection moved to after initializeRenderers (later in this function)
|
||||
|
||||
// Connect WMORenderer to M2Renderer (for hierarchical transforms: doodads following WMO parents)
|
||||
if (renderer->getWMORenderer() && renderer->getM2Renderer()) {
|
||||
|
|
@ -3548,6 +3635,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
// Set map name on WMO renderer and disable terrain streaming (no ADT tiles for instances)
|
||||
if (renderer->getWMORenderer()) {
|
||||
renderer->getWMORenderer()->setMapName(mapName);
|
||||
renderer->getWMORenderer()->setWMOOnlyMap(true);
|
||||
}
|
||||
if (renderer->getTerrainManager()) {
|
||||
renderer->getTerrainManager()->setStreamingEnabled(false);
|
||||
|
|
@ -3707,8 +3795,8 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
}
|
||||
if (!m2Model.isValid()) continue;
|
||||
|
||||
glm::quat fixedRotation(doodad.rotation.w, doodad.rotation.y,
|
||||
doodad.rotation.x, doodad.rotation.z);
|
||||
glm::quat fixedRotation(doodad.rotation.w, doodad.rotation.x,
|
||||
doodad.rotation.y, doodad.rotation.z);
|
||||
glm::mat4 doodadLocal(1.0f);
|
||||
doodadLocal = glm::translate(doodadLocal, doodad.position);
|
||||
doodadLocal *= glm::mat4_cast(fixedRotation);
|
||||
|
|
@ -3719,7 +3807,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
|
||||
uint32_t doodadModelId = static_cast<uint32_t>(std::hash<std::string>{}(m2Path));
|
||||
m2Renderer->loadModel(m2Model, doodadModelId);
|
||||
uint32_t doodadInstId = m2Renderer->createInstance(doodadModelId, worldPos, glm::vec3(0.0f), doodad.scale);
|
||||
uint32_t doodadInstId = m2Renderer->createInstanceWithMatrix(doodadModelId, worldMatrix, worldPos);
|
||||
if (doodadInstId) m2Renderer->setSkipCollision(doodadInstId, true);
|
||||
loadedDoodads++;
|
||||
}
|
||||
|
|
@ -3918,9 +4006,18 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
renderer->getCameraController()->reset();
|
||||
}
|
||||
|
||||
// Set up test transport (development feature)
|
||||
// Test transport disabled — real transports come from server via UPDATEFLAG_TRANSPORT
|
||||
showProgress("Finalizing world...", 0.94f);
|
||||
setupTestTransport();
|
||||
// setupTestTransport();
|
||||
|
||||
// Connect TransportManager to renderers (must happen AFTER initializeRenderers)
|
||||
if (gameHandler && gameHandler->getTransportManager()) {
|
||||
auto* tm = gameHandler->getTransportManager();
|
||||
if (renderer->getWMORenderer()) tm->setWMORenderer(renderer->getWMORenderer());
|
||||
if (renderer->getM2Renderer()) tm->setM2Renderer(renderer->getM2Renderer());
|
||||
LOG_WARNING("TransportManager connected: wmoR=", (renderer->getWMORenderer() ? "yes" : "NULL"),
|
||||
" m2R=", (renderer->getM2Renderer() ? "yes" : "NULL"));
|
||||
}
|
||||
|
||||
// Set up NPC animation callbacks (for online creatures)
|
||||
showProgress("Preparing creatures...", 0.97f);
|
||||
|
|
@ -6355,6 +6452,10 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
|
|||
} else if (displayId == 2454 || displayId == 181688 || displayId == 190536) {
|
||||
modelPath = "World\\wmo\\transports\\icebreaker\\Transport_Icebreaker_ship.wmo";
|
||||
LOG_INFO("Overriding transport displayId ", displayId, " → Transport_Icebreaker_ship.wmo");
|
||||
} else if (displayId == 3831) {
|
||||
// Deeprun Tram car
|
||||
modelPath = "World\\Generic\\Gnome\\Passive Doodads\\Subway\\SubwayCar.m2";
|
||||
LOG_WARNING("Overriding transport displayId ", displayId, " → SubwayCar.m2");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6495,7 +6596,12 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
|
|||
// Transport GameObjects are not always named "transport" in their WMO path
|
||||
// (e.g. elevators/lifts). If the server marks it as a transport, always
|
||||
// notify so TransportManager can animate/carry passengers.
|
||||
if (gameHandler && gameHandler->isTransportGuid(guid)) {
|
||||
bool isTG = gameHandler && gameHandler->isTransportGuid(guid);
|
||||
LOG_WARNING("WMO GO spawned: guid=0x", std::hex, guid, std::dec,
|
||||
" entry=", entry, " displayId=", displayId,
|
||||
" isTransport=", isTG,
|
||||
" pos=(", x, ", ", y, ", ", z, ")");
|
||||
if (isTG) {
|
||||
gameHandler->notifyTransportSpawned(guid, entry, displayId, x, y, z, orientation);
|
||||
}
|
||||
|
||||
|
|
@ -6559,17 +6665,27 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
|
|||
return;
|
||||
}
|
||||
|
||||
// Freeze animation for static gameobjects, but let portals/effects animate
|
||||
// Freeze animation for static gameobjects, but let portals/effects/transports animate
|
||||
bool isTransportGO = gameHandler && gameHandler->isTransportGuid(guid);
|
||||
std::string lowerPath = modelPath;
|
||||
std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(), ::tolower);
|
||||
bool isAnimatedEffect = (lowerPath.find("instanceportal") != std::string::npos ||
|
||||
lowerPath.find("instancenewportal") != std::string::npos ||
|
||||
lowerPath.find("portalfx") != std::string::npos ||
|
||||
lowerPath.find("spellportal") != std::string::npos);
|
||||
if (!isAnimatedEffect) {
|
||||
if (!isAnimatedEffect && !isTransportGO) {
|
||||
m2Renderer->setInstanceAnimationFrozen(instanceId, true);
|
||||
}
|
||||
|
||||
gameObjectInstances_[guid] = {modelId, instanceId, false};
|
||||
|
||||
// Notify transport system for M2 transports (e.g. Deeprun Tram cars)
|
||||
if (gameHandler && gameHandler->isTransportGuid(guid)) {
|
||||
LOG_WARNING("M2 transport spawned: guid=0x", std::hex, guid, std::dec,
|
||||
" entry=", entry, " displayId=", displayId,
|
||||
" instanceId=", instanceId);
|
||||
gameHandler->notifyTransportSpawned(guid, entry, displayId, x, y, z, orientation);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG("Spawned gameobject: guid=0x", std::hex, guid, std::dec,
|
||||
|
|
|
|||
|
|
@ -4936,10 +4936,17 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
LOG_INFO("Player on transport: 0x", std::hex, playerTransportGuid_, std::dec,
|
||||
" offset=(", playerTransportOffset_.x, ", ", playerTransportOffset_.y, ", ", playerTransportOffset_.z, ")");
|
||||
} else {
|
||||
if (playerTransportGuid_ != 0) {
|
||||
LOG_INFO("Player left transport");
|
||||
// Don't clear client-side M2 transport boarding (trams) —
|
||||
// the server doesn't know about client-detected transport attachment.
|
||||
bool isClientM2Transport = false;
|
||||
if (playerTransportGuid_ != 0 && transportManager_) {
|
||||
auto* tr = transportManager_->getTransport(playerTransportGuid_);
|
||||
isClientM2Transport = (tr && tr->isM2);
|
||||
}
|
||||
if (playerTransportGuid_ != 0 && !isClientM2Transport) {
|
||||
LOG_INFO("Player left transport");
|
||||
clearPlayerTransport();
|
||||
}
|
||||
clearPlayerTransport();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5173,9 +5180,13 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
queryGameObjectInfo(itEntry->second, block.guid);
|
||||
}
|
||||
// Detect transport GameObjects via UPDATEFLAG_TRANSPORT (0x0002)
|
||||
LOG_WARNING("GameObject CREATE: guid=0x", std::hex, block.guid, std::dec,
|
||||
" entry=", go->getEntry(), " displayId=", go->getDisplayId(),
|
||||
" updateFlags=0x", std::hex, block.updateFlags, std::dec,
|
||||
" pos=(", go->getX(), ", ", go->getY(), ", ", go->getZ(), ")");
|
||||
if (block.updateFlags & 0x0002) {
|
||||
transportGuids_.insert(block.guid);
|
||||
LOG_INFO("Detected transport GameObject: 0x", std::hex, block.guid, std::dec,
|
||||
LOG_WARNING("Detected transport GameObject: 0x", std::hex, block.guid, std::dec,
|
||||
" entry=", go->getEntry(),
|
||||
" displayId=", go->getDisplayId(),
|
||||
" pos=(", go->getX(), ", ", go->getY(), ", ", go->getZ(), ")");
|
||||
|
|
@ -5691,7 +5702,13 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
movementInfo.x = pos.x;
|
||||
movementInfo.y = pos.y;
|
||||
movementInfo.z = pos.z;
|
||||
if (playerTransportGuid_ != 0) {
|
||||
// Don't clear client-side M2 transport boarding
|
||||
bool isClientM2Transport = false;
|
||||
if (playerTransportGuid_ != 0 && transportManager_) {
|
||||
auto* tr = transportManager_->getTransport(playerTransportGuid_);
|
||||
isClientM2Transport = (tr && tr->isM2);
|
||||
}
|
||||
if (playerTransportGuid_ != 0 && !isClientM2Transport) {
|
||||
LOG_INFO("Player left transport (MOVEMENT)");
|
||||
clearPlayerTransport();
|
||||
}
|
||||
|
|
@ -8814,16 +8831,16 @@ void GameHandler::checkAreaTriggers() {
|
|||
|
||||
bool inside = false;
|
||||
if (at.radius > 0.0f) {
|
||||
// Sphere trigger — small minimum so player must be near the portal
|
||||
float effectiveRadius = std::max(at.radius, 12.0f);
|
||||
// Sphere trigger — use actual radius, with small floor for very tiny triggers
|
||||
float effectiveRadius = std::max(at.radius, 3.0f);
|
||||
float dx = px - at.x;
|
||||
float dy = py - at.y;
|
||||
float dz = pz - at.z;
|
||||
float distSq = dx * dx + dy * dy + dz * dz;
|
||||
inside = (distSq <= effectiveRadius * effectiveRadius);
|
||||
} else if (at.boxLength > 0.0f || at.boxWidth > 0.0f || at.boxHeight > 0.0f) {
|
||||
// Box trigger — small minimum so player must walk into the portal area
|
||||
float boxMin = 16.0f;
|
||||
// Box trigger — use actual size, with small floor for tiny triggers
|
||||
float boxMin = 4.0f;
|
||||
float effLength = std::max(at.boxLength, boxMin);
|
||||
float effWidth = std::max(at.boxWidth, boxMin);
|
||||
float effHeight = std::max(at.boxHeight, boxMin);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "game/transport_manager.hpp"
|
||||
#include "rendering/wmo_renderer.hpp"
|
||||
#include "rendering/m2_renderer.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
|
|
@ -80,10 +81,11 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
|
|||
transport.localClockMs = 0;
|
||||
transport.hasServerClock = false;
|
||||
transport.serverClockOffsetMs = 0;
|
||||
// Default is server-authoritative movement.
|
||||
// Exception: elevator-style transports (z-only DBC paths) often do not stream continuous
|
||||
// movement updates from the server, but the client is expected to animate them.
|
||||
transport.useClientAnimation = (path.fromDBC && path.zOnly && path.durationMs > 0);
|
||||
// Start with client-side animation for all DBC paths with real movement.
|
||||
// If the server sends actual position updates, updateServerTransport() will switch
|
||||
// to server-driven mode. This ensures transports like trams (which the server doesn't
|
||||
// stream updates for) still animate, while ships/zeppelins switch to server authority.
|
||||
transport.useClientAnimation = (path.fromDBC && path.durationMs > 0);
|
||||
transport.clientAnimationReverse = false;
|
||||
transport.serverYaw = 0.0f;
|
||||
transport.hasServerYaw = false;
|
||||
|
|
@ -98,16 +100,19 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
|
|||
if (transport.useClientAnimation && path.durationMs > 0) {
|
||||
// Seed to a stable phase based on our local clock so elevators don't all start at t=0.
|
||||
transport.localClockMs = static_cast<uint32_t>(elapsedTime_ * 1000.0f) % path.durationMs;
|
||||
LOG_INFO("TransportManager: Enabled client animation for z-only transport 0x",
|
||||
LOG_INFO("TransportManager: Enabled client animation for transport 0x",
|
||||
std::hex, guid, std::dec, " path=", pathId,
|
||||
" durationMs=", path.durationMs, " seedMs=", transport.localClockMs);
|
||||
" durationMs=", path.durationMs, " seedMs=", transport.localClockMs,
|
||||
(path.worldCoords ? " [worldCoords]" : (path.zOnly ? " [z-only]" : "")));
|
||||
}
|
||||
|
||||
updateTransformMatrices(transport);
|
||||
|
||||
// CRITICAL: Update WMO renderer with initial transform
|
||||
if (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
if (transport.isM2) {
|
||||
if (m2Renderer_) m2Renderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
} else {
|
||||
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
}
|
||||
|
||||
transports_[guid] = transport;
|
||||
|
|
@ -140,6 +145,14 @@ glm::vec3 TransportManager::getPlayerWorldPosition(uint64_t transportGuid, const
|
|||
return localOffset; // Fallback
|
||||
}
|
||||
|
||||
if (transport->isM2) {
|
||||
// M2 transports (trams): localOffset is a canonical world-space delta
|
||||
// from the transport's canonical position. Just add directly.
|
||||
return transport->position + localOffset;
|
||||
}
|
||||
|
||||
// WMO transports (ships): localOffset is in transport-local space,
|
||||
// use the render-space transform matrix.
|
||||
glm::vec4 localPos(localOffset, 1.0f);
|
||||
glm::vec4 worldPos = transport->transform * localPos;
|
||||
return glm::vec3(worldPos);
|
||||
|
|
@ -284,14 +297,17 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
|
|||
glm::vec3 pathOffset = evalTimedCatmullRom(path, pathTimeMs);
|
||||
// Guard against bad fallback Z curves on some remapped transport paths (notably icebreakers),
|
||||
// where path offsets can sink far below sea level when we only have spawn-time data.
|
||||
if (transport.useClientAnimation && transport.serverUpdateCount <= 1) {
|
||||
constexpr float kMinFallbackZOffset = -2.0f;
|
||||
pathOffset.z = glm::max(pathOffset.z, kMinFallbackZOffset);
|
||||
}
|
||||
if (!transport.useClientAnimation && !transport.hasServerClock) {
|
||||
constexpr float kMinFallbackZOffset = -2.0f;
|
||||
constexpr float kMaxFallbackZOffset = 8.0f;
|
||||
pathOffset.z = glm::clamp(pathOffset.z, kMinFallbackZOffset, kMaxFallbackZOffset);
|
||||
// Skip Z clamping for world-coordinate paths (TaxiPathNode) where values are absolute positions.
|
||||
if (!path.worldCoords) {
|
||||
if (transport.useClientAnimation && transport.serverUpdateCount <= 1) {
|
||||
constexpr float kMinFallbackZOffset = -2.0f;
|
||||
pathOffset.z = glm::max(pathOffset.z, kMinFallbackZOffset);
|
||||
}
|
||||
if (!transport.useClientAnimation && !transport.hasServerClock) {
|
||||
constexpr float kMinFallbackZOffset = -2.0f;
|
||||
constexpr float kMaxFallbackZOffset = 8.0f;
|
||||
pathOffset.z = glm::clamp(pathOffset.z, kMinFallbackZOffset, kMaxFallbackZOffset);
|
||||
}
|
||||
}
|
||||
transport.position = transport.basePosition + pathOffset;
|
||||
|
||||
|
|
@ -307,24 +323,20 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
|
|||
updateTransformMatrices(transport);
|
||||
|
||||
// Update WMO instance position
|
||||
if (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
if (transport.isM2) {
|
||||
if (m2Renderer_) m2Renderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
} else {
|
||||
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
|
||||
}
|
||||
|
||||
// Debug logging every 120 frames (~2 seconds at 60fps)
|
||||
// Debug logging every 600 frames (~10 seconds at 60fps)
|
||||
static int debugFrameCount = 0;
|
||||
if (debugFrameCount++ % 120 == 0) {
|
||||
// Log canonical position AND render position to check coordinate conversion
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(transport.position);
|
||||
if (debugFrameCount++ % 600 == 0) {
|
||||
LOG_DEBUG("Transport 0x", std::hex, transport.guid, std::dec,
|
||||
" pathTime=", pathTimeMs, "ms / ", path.durationMs, "ms",
|
||||
" canonicalPos=(", transport.position.x, ", ", transport.position.y, ", ", transport.position.z, ")",
|
||||
" renderPos=(", renderPos.x, ", ", renderPos.y, ", ", renderPos.z, ")",
|
||||
" basePos=(", transport.basePosition.x, ", ", transport.basePosition.y, ", ", transport.basePosition.z, ")",
|
||||
" pathOffset=(", pathOffset.x, ", ", pathOffset.y, ", ", pathOffset.z, ")",
|
||||
" pos=(", transport.position.x, ", ", transport.position.y, ", ", transport.position.z, ")",
|
||||
" mode=", (transport.useClientAnimation ? "client" : "server"),
|
||||
" hasServerClock=", transport.hasServerClock,
|
||||
" offset=", transport.serverClockOffsetMs, "ms");
|
||||
" isM2=", transport.isM2);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -561,12 +573,24 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
|
|||
// Track server updates
|
||||
transport->serverUpdateCount++;
|
||||
transport->lastServerUpdate = elapsedTime_;
|
||||
// Server updates take precedence for moving XY transports, but z-only elevators should
|
||||
// remain client-animated (server may only send sparse state updates).
|
||||
if (!isZOnlyPath) {
|
||||
transport->useClientAnimation = false;
|
||||
} else {
|
||||
// Z-only elevators and world-coordinate paths (TaxiPathNode) always stay client-driven.
|
||||
// For other DBC paths (trams, ships): only switch to server-driven mode when the server
|
||||
// sends a position that actually differs from the current position, indicating it's
|
||||
// actively streaming movement data (not just echoing the spawn position).
|
||||
if (isZOnlyPath || isWorldCoordPath) {
|
||||
transport->useClientAnimation = true;
|
||||
} else if (transport->useClientAnimation && hasPath && pathIt->second.fromDBC) {
|
||||
float posDelta = glm::length(position - transport->position);
|
||||
if (posDelta > 1.0f) {
|
||||
// Server sent a meaningfully different position — it's actively driving this transport
|
||||
transport->useClientAnimation = false;
|
||||
LOG_INFO("Transport 0x", std::hex, guid, std::dec,
|
||||
" switching to server-driven (posDelta=", posDelta, ")");
|
||||
}
|
||||
// Otherwise keep client animation (server just echoed spawn pos or sent small jitter)
|
||||
} else if (!hasPath || !pathIt->second.fromDBC) {
|
||||
// No DBC path — purely server-driven
|
||||
transport->useClientAnimation = false;
|
||||
}
|
||||
transport->clientAnimationReverse = false;
|
||||
|
||||
|
|
@ -576,8 +600,10 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
|
|||
transport->position = position;
|
||||
transport->rotation = glm::angleAxis(orientation, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
updateTransformMatrices(*transport);
|
||||
if (wmoRenderer_) {
|
||||
wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
|
||||
if (transport->isM2) {
|
||||
if (m2Renderer_) m2Renderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
|
||||
} else {
|
||||
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -846,12 +872,23 @@ bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMg
|
|||
std::vector<TimedPoint> timedPoints;
|
||||
timedPoints.reserve(sortedWaypoints.size() + 1); // +1 for wrap point
|
||||
|
||||
// Log first few waypoints for transport 2074 to see conversion
|
||||
// Log DBC waypoints for tram entries
|
||||
if (transportEntry >= 176080 && transportEntry <= 176085) {
|
||||
size_t mid = sortedWaypoints.size() / 4; // ~quarter through
|
||||
size_t mid2 = sortedWaypoints.size() / 2; // ~halfway
|
||||
LOG_WARNING("DBC path entry=", transportEntry, " nPts=", sortedWaypoints.size(),
|
||||
" [0] t=", sortedWaypoints[0].first, " raw=(", sortedWaypoints[0].second.x, ",", sortedWaypoints[0].second.y, ",", sortedWaypoints[0].second.z, ")",
|
||||
" [", mid, "] t=", sortedWaypoints[mid].first, " raw=(", sortedWaypoints[mid].second.x, ",", sortedWaypoints[mid].second.y, ",", sortedWaypoints[mid].second.z, ")",
|
||||
" [", mid2, "] t=", sortedWaypoints[mid2].first, " raw=(", sortedWaypoints[mid2].second.x, ",", sortedWaypoints[mid2].second.y, ",", sortedWaypoints[mid2].second.z, ")");
|
||||
}
|
||||
|
||||
for (size_t idx = 0; idx < sortedWaypoints.size(); idx++) {
|
||||
const auto& [tMs, pos] = sortedWaypoints[idx];
|
||||
|
||||
// TransportAnimation.dbc uses server coordinates - convert to canonical
|
||||
glm::vec3 canonical = core::coords::serverToCanonical(pos);
|
||||
// TransportAnimation.dbc local offsets use a coordinate system where
|
||||
// the travel axis is negated relative to server world coords.
|
||||
// Negate X and Y before converting to canonical (Z=height stays the same).
|
||||
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(-pos.x, -pos.y, pos.z));
|
||||
|
||||
// CRITICAL: Detect if serverToCanonical is zeroing nonzero inputs
|
||||
if ((pos.x != 0.0f || pos.y != 0.0f || pos.z != 0.0f) &&
|
||||
|
|
@ -896,7 +933,8 @@ bool TransportManager::loadTransportAnimationDBC(pipeline::AssetManager* assetMg
|
|||
|
||||
// Add duplicate first point at end with wrap duration
|
||||
// This makes the wrap segment (last → first) have proper duration
|
||||
glm::vec3 firstCanonical = core::coords::serverToCanonical(sortedWaypoints.front().second);
|
||||
const auto& fp = sortedWaypoints.front().second;
|
||||
glm::vec3 firstCanonical = core::coords::serverToCanonical(glm::vec3(-fp.x, -fp.y, fp.z));
|
||||
timedPoints.push_back({lastTimeMs + wrapMs, firstCanonical});
|
||||
|
||||
uint32_t durationMs = lastTimeMs + wrapMs;
|
||||
|
|
|
|||
|
|
@ -200,16 +200,22 @@ void CameraController::update(float deltaTime) {
|
|||
// Don't process keyboard input when UI text input (e.g. chat box) has focus
|
||||
bool uiWantsKeyboard = ImGui::GetIO().WantTextInput;
|
||||
|
||||
// Suppress movement input after teleport/portal (keys may still be held)
|
||||
if (movementSuppressTimer_ > 0.0f) {
|
||||
movementSuppressTimer_ -= deltaTime;
|
||||
}
|
||||
bool movementSuppressed = movementSuppressTimer_ > 0.0f;
|
||||
|
||||
// Determine current key states
|
||||
bool keyW = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_W);
|
||||
bool keyS = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_S);
|
||||
bool keyA = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_A);
|
||||
bool keyD = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_D);
|
||||
bool keyQ = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_Q);
|
||||
bool keyE = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_E);
|
||||
bool keyW = !uiWantsKeyboard && !sitting && !movementSuppressed && input.isKeyPressed(SDL_SCANCODE_W);
|
||||
bool keyS = !uiWantsKeyboard && !sitting && !movementSuppressed && input.isKeyPressed(SDL_SCANCODE_S);
|
||||
bool keyA = !uiWantsKeyboard && !sitting && !movementSuppressed && input.isKeyPressed(SDL_SCANCODE_A);
|
||||
bool keyD = !uiWantsKeyboard && !sitting && !movementSuppressed && input.isKeyPressed(SDL_SCANCODE_D);
|
||||
bool keyQ = !uiWantsKeyboard && !sitting && !movementSuppressed && input.isKeyPressed(SDL_SCANCODE_Q);
|
||||
bool keyE = !uiWantsKeyboard && !sitting && !movementSuppressed && input.isKeyPressed(SDL_SCANCODE_E);
|
||||
bool shiftDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT));
|
||||
bool ctrlDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL));
|
||||
bool nowJump = !uiWantsKeyboard && !sitting && input.isKeyJustPressed(SDL_SCANCODE_SPACE);
|
||||
bool nowJump = !uiWantsKeyboard && !sitting && !movementSuppressed && input.isKeyJustPressed(SDL_SCANCODE_SPACE);
|
||||
|
||||
// Idle camera: any input resets the timer; timeout triggers a slow orbit pan
|
||||
bool anyInput = leftMouseDown || rightMouseDown || keyW || keyS || keyA || keyD || keyQ || keyE || nowJump;
|
||||
|
|
@ -626,7 +632,8 @@ void CameraController::update(float deltaTime) {
|
|||
glm::vec3 stepPos = swimFrom;
|
||||
|
||||
if (swimMoveDist > 0.01f) {
|
||||
int swimSteps = std::max(1, std::min(3, static_cast<int>(std::ceil(swimMoveDist / 0.65f))));
|
||||
float swimStepSize = cachedInsideWMO ? 0.20f : 0.35f;
|
||||
int swimSteps = std::max(1, std::min(8, static_cast<int>(std::ceil(swimMoveDist / swimStepSize))));
|
||||
glm::vec3 stepDelta = (swimTo - swimFrom) / static_cast<float>(swimSteps);
|
||||
|
||||
for (int i = 0; i < swimSteps; i++) {
|
||||
|
|
@ -634,7 +641,7 @@ void CameraController::update(float deltaTime) {
|
|||
|
||||
if (wmoRenderer) {
|
||||
glm::vec3 adjusted;
|
||||
if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) {
|
||||
if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted, cachedInsideWMO)) {
|
||||
candidate.x = adjusted.x;
|
||||
candidate.y = adjusted.y;
|
||||
candidate.z = std::max(candidate.z, adjusted.z);
|
||||
|
|
@ -1274,8 +1281,10 @@ void CameraController::update(float deltaTime) {
|
|||
lastInsideWMOCheckPos = targetPos;
|
||||
}
|
||||
|
||||
// Do not clamp zoom target by ceiling checks. First-person should always
|
||||
// be reachable; occlusion handling below will resolve camera placement safely.
|
||||
// Smoothly pull camera in when entering WMO interiors
|
||||
if (cachedInsideWMO && userTargetDistance > MAX_DISTANCE_INTERIOR) {
|
||||
userTargetDistance = MAX_DISTANCE_INTERIOR;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Camera collision (sphere sweep approximation) =====
|
||||
|
|
@ -1499,14 +1508,15 @@ void CameraController::update(float deltaTime) {
|
|||
float moveDist = glm::length(desiredFeet - startFeet);
|
||||
|
||||
if (moveDist > 0.01f) {
|
||||
int sweepSteps = std::max(1, std::min(3, static_cast<int>(std::ceil(moveDist / 0.65f))));
|
||||
float stepSize = cachedInsideWMO ? 0.20f : 0.35f;
|
||||
int sweepSteps = std::max(1, std::min(8, static_cast<int>(std::ceil(moveDist / stepSize))));
|
||||
glm::vec3 stepPos = startFeet;
|
||||
glm::vec3 stepDelta = (desiredFeet - startFeet) / static_cast<float>(sweepSteps);
|
||||
|
||||
for (int i = 0; i < sweepSteps; i++) {
|
||||
glm::vec3 candidate = stepPos + stepDelta;
|
||||
glm::vec3 adjusted;
|
||||
if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) {
|
||||
if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted, cachedInsideWMO)) {
|
||||
candidate.x = adjusted.x;
|
||||
candidate.y = adjusted.y;
|
||||
candidate.z = std::max(candidate.z, adjusted.z);
|
||||
|
|
@ -1985,6 +1995,7 @@ void CameraController::processMouseWheel(float delta) {
|
|||
float zoomSpeed = glm::max(userTargetDistance * 0.15f, 0.3f);
|
||||
userTargetDistance -= delta * zoomSpeed;
|
||||
float maxDist = extendedZoom_ ? MAX_DISTANCE_EXTENDED : MAX_DISTANCE_NORMAL;
|
||||
if (cachedInsideWMO) maxDist = std::min(maxDist, MAX_DISTANCE_INTERIOR);
|
||||
userTargetDistance = glm::clamp(userTargetDistance, MIN_DISTANCE, maxDist);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1116,9 +1116,39 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
|||
// Ground clutter (grass/pebbles/detail cards) should never block camera/movement.
|
||||
gpuModel.collisionNoBlock = true;
|
||||
}
|
||||
// Spell effect models: particle-dominated with minimal geometry (e.g. LevelUp.m2)
|
||||
gpuModel.isSpellEffect = hasParticles && model.vertices.size() <= 200 &&
|
||||
model.particleEmitters.size() >= 3;
|
||||
// Spell effect / pure-visual models: particle-dominated with minimal geometry,
|
||||
// or named effect models (light shafts, portals, emitters, spotlights)
|
||||
bool effectByName =
|
||||
(lowerName.find("lightshaft") != std::string::npos) ||
|
||||
(lowerName.find("volumetriclight") != std::string::npos) ||
|
||||
(lowerName.find("instanceportal") != std::string::npos) ||
|
||||
(lowerName.find("instancenewportal") != std::string::npos) ||
|
||||
(lowerName.find("mageportal") != std::string::npos) ||
|
||||
(lowerName.find("worldtreeportal") != std::string::npos) ||
|
||||
(lowerName.find("particleemitter") != std::string::npos) ||
|
||||
(lowerName.find("bubbles") != std::string::npos) ||
|
||||
(lowerName.find("spotlight") != std::string::npos) ||
|
||||
(lowerName.find("hazardlight") != std::string::npos) ||
|
||||
(lowerName.find("lavasplash") != std::string::npos) ||
|
||||
(lowerName.find("lavabubble") != std::string::npos) ||
|
||||
(lowerName.find("lavasteam") != std::string::npos) ||
|
||||
(lowerName.find("wisps") != std::string::npos);
|
||||
gpuModel.isSpellEffect = effectByName ||
|
||||
(hasParticles && model.vertices.size() <= 200 &&
|
||||
model.particleEmitters.size() >= 3);
|
||||
gpuModel.isLavaModel =
|
||||
(lowerName.find("forgelava") != std::string::npos) ||
|
||||
(lowerName.find("lavapot") != std::string::npos) ||
|
||||
(lowerName.find("lavaflow") != std::string::npos);
|
||||
gpuModel.isInstancePortal =
|
||||
(lowerName.find("instanceportal") != std::string::npos) ||
|
||||
(lowerName.find("instancenewportal") != std::string::npos) ||
|
||||
(lowerName.find("portalfx") != std::string::npos) ||
|
||||
(lowerName.find("spellportal") != std::string::npos);
|
||||
// Instance portals are spell effects too (additive blend, no collision)
|
||||
if (gpuModel.isInstancePortal) {
|
||||
gpuModel.isSpellEffect = true;
|
||||
}
|
||||
// Water vegetation: cattails, reeds, bulrushes, kelp, seaweed, lilypad near water
|
||||
gpuModel.isWaterVegetation =
|
||||
(lowerName.find("cattail") != std::string::npos) ||
|
||||
|
|
@ -1619,6 +1649,7 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position,
|
|||
instance.cachedBoundRadius = mdlRef.boundRadius;
|
||||
instance.cachedIsGroundDetail = mdlRef.isGroundDetail;
|
||||
instance.cachedIsInvisibleTrap = mdlRef.isInvisibleTrap;
|
||||
instance.cachedIsInstancePortal = mdlRef.isInstancePortal;
|
||||
instance.cachedIsValid = mdlRef.isValid();
|
||||
|
||||
// Initialize animation: play first sequence (usually Stand/Idle)
|
||||
|
|
@ -1637,6 +1668,9 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position,
|
|||
if (mdlRef.isSmoke) {
|
||||
smokeInstanceIndices_.push_back(idx);
|
||||
}
|
||||
if (mdlRef.isInstancePortal) {
|
||||
portalInstanceIndices_.push_back(idx);
|
||||
}
|
||||
if (!mdlRef.particleEmitters.empty()) {
|
||||
particleInstanceIndices_.push_back(idx);
|
||||
}
|
||||
|
|
@ -1926,6 +1960,18 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
|||
++i;
|
||||
}
|
||||
|
||||
// --- Spin instance portals ---
|
||||
static constexpr float PORTAL_SPIN_SPEED = 1.2f; // radians/sec
|
||||
for (size_t idx : portalInstanceIndices_) {
|
||||
if (idx >= instances.size()) continue;
|
||||
auto& inst = instances[idx];
|
||||
inst.portalSpinAngle += PORTAL_SPIN_SPEED * deltaTime;
|
||||
if (inst.portalSpinAngle > 6.2831853f)
|
||||
inst.portalSpinAngle -= 6.2831853f;
|
||||
inst.rotation.z = inst.portalSpinAngle;
|
||||
inst.updateModelMatrix();
|
||||
}
|
||||
|
||||
// --- Normal M2 animation update ---
|
||||
// Advance animTime for ALL instances (needed for texture UV animation on static doodads).
|
||||
// This is a tight loop touching only one float per instance — no hash lookups.
|
||||
|
|
@ -2236,6 +2282,22 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
if (model.isGroundDetail) {
|
||||
instanceFadeAlpha *= 0.82f;
|
||||
}
|
||||
if (model.isInstancePortal) {
|
||||
// Render mesh at low alpha + emit glow sprite at center
|
||||
instanceFadeAlpha *= 0.12f;
|
||||
if (entry.distSq < 400.0f * 400.0f) {
|
||||
glm::vec3 center = glm::vec3(instance.modelMatrix * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f));
|
||||
GlowSprite gs;
|
||||
gs.worldPos = center;
|
||||
gs.color = glm::vec4(0.35f, 0.5f, 1.0f, 1.1f);
|
||||
gs.size = instance.scale * 5.0f;
|
||||
glowSprites_.push_back(gs);
|
||||
GlowSprite halo = gs;
|
||||
halo.color.a *= 0.3f;
|
||||
halo.size *= 2.2f;
|
||||
glowSprites_.push_back(halo);
|
||||
}
|
||||
}
|
||||
|
||||
// Upload bone matrices to SSBO if model has skeletal animation
|
||||
bool useBones = model.hasAnimation && !model.disableAnimation && !instance.boneMatrices.empty();
|
||||
|
|
@ -2300,6 +2362,9 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
}
|
||||
|
||||
const bool foliageLikeModel = model.isFoliageLike;
|
||||
// Particle-dominant spell effects: mesh is emission geometry, render dim
|
||||
const bool particleDominantEffect = model.isSpellEffect &&
|
||||
!model.particleEmitters.empty() && model.batches.size() <= 2;
|
||||
for (const auto& batch : model.batches) {
|
||||
if (batch.indexCount == 0) continue;
|
||||
if (!model.isGroundDetail && batch.submeshLevel != targetLOD) continue;
|
||||
|
|
@ -2364,6 +2429,12 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
}
|
||||
}
|
||||
}
|
||||
// Lava M2 models: fallback UV scroll if no texture animation
|
||||
if (model.isLavaModel && uvOffset == glm::vec2(0.0f)) {
|
||||
static auto startTime = std::chrono::steady_clock::now();
|
||||
float t = std::chrono::duration<float>(std::chrono::steady_clock::now() - startTime).count();
|
||||
uvOffset = glm::vec2(t * 0.03f, -t * 0.08f);
|
||||
}
|
||||
|
||||
// Foliage/card-like batches render more stably as cutout (depth-write on)
|
||||
// instead of alpha-blended sorting.
|
||||
|
|
@ -2381,8 +2452,14 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
|
||||
// Select pipeline based on blend mode
|
||||
uint8_t effectiveBlendMode = batch.blendMode;
|
||||
if (model.isSpellEffect && (effectiveBlendMode == 4 || effectiveBlendMode == 5)) {
|
||||
effectiveBlendMode = 3;
|
||||
if (model.isSpellEffect) {
|
||||
// Effect models: force additive blend for opaque/cutout batches
|
||||
// so the mesh renders as a transparent glow, not a solid object
|
||||
if (effectiveBlendMode <= 1) {
|
||||
effectiveBlendMode = 3; // additive
|
||||
} else if (effectiveBlendMode == 4 || effectiveBlendMode == 5) {
|
||||
effectiveBlendMode = 3;
|
||||
}
|
||||
}
|
||||
if (forceCutout) {
|
||||
effectiveBlendMode = 1;
|
||||
|
|
@ -2435,6 +2512,10 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
pc.useBones = useBones ? 1 : 0;
|
||||
pc.isFoliage = model.shadowWindFoliage ? 1 : 0;
|
||||
pc.fadeAlpha = instanceFadeAlpha;
|
||||
// Particle-dominant effects: mesh is emission geometry, don't render
|
||||
if (particleDominantEffect && batch.blendMode <= 1) {
|
||||
continue;
|
||||
}
|
||||
vkCmdPushConstants(cmd, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(pc), &pc);
|
||||
|
||||
vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0);
|
||||
|
|
@ -3398,6 +3479,7 @@ void M2Renderer::clear() {
|
|||
instanceIndexById.clear();
|
||||
smokeParticles.clear();
|
||||
smokeInstanceIndices_.clear();
|
||||
portalInstanceIndices_.clear();
|
||||
animatedInstanceIndices_.clear();
|
||||
particleOnlyInstanceIndices_.clear();
|
||||
particleInstanceIndices_.clear();
|
||||
|
|
@ -3433,6 +3515,7 @@ void M2Renderer::rebuildSpatialIndex() {
|
|||
instanceIndexById.clear();
|
||||
instanceIndexById.reserve(instances.size());
|
||||
smokeInstanceIndices_.clear();
|
||||
portalInstanceIndices_.clear();
|
||||
animatedInstanceIndices_.clear();
|
||||
particleOnlyInstanceIndices_.clear();
|
||||
particleInstanceIndices_.clear();
|
||||
|
|
@ -3444,6 +3527,9 @@ void M2Renderer::rebuildSpatialIndex() {
|
|||
if (inst.cachedIsSmoke) {
|
||||
smokeInstanceIndices_.push_back(i);
|
||||
}
|
||||
if (inst.cachedIsInstancePortal) {
|
||||
portalInstanceIndices_.push_back(i);
|
||||
}
|
||||
if (inst.cachedHasParticleEmitters) {
|
||||
particleInstanceIndices_.push_back(i);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -855,6 +855,14 @@ void Renderer::unregisterPreview(CharacterPreview* preview) {
|
|||
}
|
||||
}
|
||||
|
||||
void Renderer::setWaterRefractionEnabled(bool enabled) {
|
||||
if (waterRenderer) waterRenderer->setRefractionEnabled(enabled);
|
||||
}
|
||||
|
||||
bool Renderer::isWaterRefractionEnabled() const {
|
||||
return waterRenderer && waterRenderer->isRefractionEnabled();
|
||||
}
|
||||
|
||||
void Renderer::setMsaaSamples(VkSampleCountFlagBits samples) {
|
||||
if (!vkCtx) return;
|
||||
|
||||
|
|
@ -1054,20 +1062,27 @@ void Renderer::endFrame() {
|
|||
|
||||
vkCmdEndRenderPass(currentCmd);
|
||||
|
||||
// Scene-history capture is disabled: with MAX_FRAMES_IN_FLIGHT=2, the single
|
||||
// sceneColorImage can race between frame N-1's water shader read and frame N's
|
||||
// transfer write, eventually causing VK_ERROR_DEVICE_LOST. Water renders
|
||||
// without refraction until per-frame scene-history images are implemented.
|
||||
// TODO: allocate per-frame sceneColor/Depth images to fix the race.
|
||||
uint32_t frame = vkCtx->getCurrentFrame();
|
||||
|
||||
// Render water in separate 1x pass (without scene refraction for now)
|
||||
// Capture scene color/depth into per-frame history images for water refraction
|
||||
if (waterRenderer && waterRenderer->isRefractionEnabled() && waterRenderer->hasSurfaces()
|
||||
&& currentImageIndex < vkCtx->getSwapchainImages().size()) {
|
||||
waterRenderer->captureSceneHistory(
|
||||
currentCmd,
|
||||
vkCtx->getSwapchainImages()[currentImageIndex],
|
||||
vkCtx->getDepthCopySourceImage(),
|
||||
vkCtx->getSwapchainExtent(),
|
||||
vkCtx->isDepthCopySourceMsaa(),
|
||||
frame);
|
||||
}
|
||||
|
||||
// Render water in separate 1x pass after MSAA resolve + scene capture
|
||||
bool waterDeferred = waterRenderer && waterRenderer->hasSurfaces() && waterRenderer->hasWater1xPass()
|
||||
&& vkCtx->getMsaaSamples() != VK_SAMPLE_COUNT_1_BIT;
|
||||
if (waterDeferred && camera) {
|
||||
VkExtent2D ext = vkCtx->getSwapchainExtent();
|
||||
uint32_t frame = vkCtx->getCurrentFrame();
|
||||
if (waterRenderer->beginWater1xPass(currentCmd, currentImageIndex, ext)) {
|
||||
waterRenderer->render(currentCmd, perFrameDescSets[frame], *camera, globalTime, true);
|
||||
waterRenderer->render(currentCmd, perFrameDescSets[frame], *camera, globalTime, true, frame);
|
||||
waterRenderer->endWater1xPass(currentCmd);
|
||||
}
|
||||
}
|
||||
|
|
@ -3268,7 +3283,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
|||
bool waterDeferred = waterRenderer && waterRenderer->hasWater1xPass()
|
||||
&& vkCtx->getMsaaSamples() != VK_SAMPLE_COUNT_1_BIT;
|
||||
if (waterRenderer && camera && !waterDeferred) {
|
||||
waterRenderer->render(currentCmd, perFrameSet, *camera, globalTime);
|
||||
waterRenderer->render(currentCmd, perFrameSet, *camera, globalTime, false, vkCtx->getCurrentFrame());
|
||||
}
|
||||
|
||||
// Weather particles
|
||||
|
|
@ -3751,10 +3766,10 @@ void Renderer::renderHUD() {
|
|||
// in createPerFrameResources() as part of the Vulkan shadow infrastructure.
|
||||
|
||||
glm::mat4 Renderer::computeLightSpaceMatrix() {
|
||||
constexpr float kShadowHalfExtent = 180.0f;
|
||||
constexpr float kShadowLightDistance = 280.0f;
|
||||
const float kShadowHalfExtent = shadowDistance_;
|
||||
const float kShadowLightDistance = shadowDistance_ * 3.0f;
|
||||
constexpr float kShadowNearPlane = 1.0f;
|
||||
constexpr float kShadowFarPlane = 600.0f;
|
||||
const float kShadowFarPlane = shadowDistance_ * 6.5f;
|
||||
|
||||
// Use active lighting direction so shadow projection matches main shading.
|
||||
// Fragment shaders derive lighting with `ldir = normalize(-lightDir.xyz)`,
|
||||
|
|
@ -3905,18 +3920,7 @@ void Renderer::renderShadowPass() {
|
|||
if (!shadowsEnabled || shadowDepthImage == VK_NULL_HANDLE) return;
|
||||
if (currentCmd == VK_NULL_HANDLE) return;
|
||||
|
||||
const int baseInterval = std::max(1, envIntOrDefault("WOWEE_SHADOW_INTERVAL", 1));
|
||||
const int denseInterval = std::max(baseInterval, envIntOrDefault("WOWEE_SHADOW_INTERVAL_DENSE", 3));
|
||||
const uint32_t denseCharThreshold = static_cast<uint32_t>(std::max(1, envIntOrDefault("WOWEE_DENSE_CHAR_THRESHOLD", 120)));
|
||||
const uint32_t denseM2Threshold = static_cast<uint32_t>(std::max(1, envIntOrDefault("WOWEE_DENSE_M2_THRESHOLD", 900)));
|
||||
const bool denseScene =
|
||||
(characterRenderer && characterRenderer->getInstanceCount() >= denseCharThreshold) ||
|
||||
(m2Renderer && m2Renderer->getInstanceCount() >= denseM2Threshold);
|
||||
const int shadowInterval = denseScene ? denseInterval : baseInterval;
|
||||
if (++shadowFrameCounter_ < static_cast<uint32_t>(shadowInterval)) {
|
||||
return;
|
||||
}
|
||||
shadowFrameCounter_ = 0;
|
||||
// Shadows render every frame — throttling causes visible flicker on player/NPCs
|
||||
|
||||
// Compute and store light space matrix; write to per-frame UBO
|
||||
lightSpaceMatrix = computeLightSpaceMatrix();
|
||||
|
|
@ -3969,9 +3973,7 @@ void Renderer::renderShadowPass() {
|
|||
vkCmdSetScissor(currentCmd, 0, 1, &sc);
|
||||
|
||||
// Phase 7/8: render shadow casters
|
||||
const float baseShadowCullRadius = static_cast<float>(std::max(40, envIntOrDefault("WOWEE_SHADOW_CULL_RADIUS", 180)));
|
||||
const float denseShadowCullRadius = static_cast<float>(std::max(30, envIntOrDefault("WOWEE_SHADOW_CULL_RADIUS_DENSE", 90)));
|
||||
const float shadowCullRadius = denseScene ? std::min(baseShadowCullRadius, denseShadowCullRadius) : baseShadowCullRadius;
|
||||
const float shadowCullRadius = shadowDistance_ * 1.35f;
|
||||
if (wmoRenderer) {
|
||||
wmoRenderer->renderShadow(currentCmd, lightSpaceMatrix, shadowCenter, shadowCullRadius);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -567,8 +567,7 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
|||
|
||||
// Build doodad's local transform (WoW coordinates)
|
||||
// WMO doodads use quaternion rotation
|
||||
// Fix: WoW quaternions need X/Y swap for correct orientation
|
||||
glm::quat fixedRotation(doodad.rotation.w, doodad.rotation.y, doodad.rotation.x, doodad.rotation.z);
|
||||
glm::quat fixedRotation(doodad.rotation.w, doodad.rotation.x, doodad.rotation.y, doodad.rotation.z);
|
||||
|
||||
glm::mat4 doodadLocal(1.0f);
|
||||
doodadLocal = glm::translate(doodadLocal, doodad.position);
|
||||
|
|
@ -838,8 +837,12 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
|
|||
modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
|
||||
for (const auto& group : wmoReady.model.groups) {
|
||||
if (!group.liquid.hasLiquid()) continue;
|
||||
// Skip interior groups — their liquid is for indoor areas
|
||||
if (group.flags & 0x2000) continue;
|
||||
// Skip interior water/ocean but keep magma/slime (e.g. Ironforge lava)
|
||||
if (group.flags & 0x2000) {
|
||||
uint16_t lt = group.liquid.materialId;
|
||||
uint8_t basicType = (lt == 0) ? 0 : ((lt - 1) % 4);
|
||||
if (basicType < 2) continue;
|
||||
}
|
||||
waterRenderer->loadFromWMO(group.liquid, modelMatrix, wmoInstId);
|
||||
loadedLiquids++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,15 +118,15 @@ bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLay
|
|||
return false;
|
||||
}
|
||||
|
||||
// Pool needs 3 combined image samplers + 1 uniform buffer
|
||||
// Pool needs 3 combined image samplers + 1 uniform buffer per frame
|
||||
std::array<VkDescriptorPoolSize, 2> scenePoolSizes{};
|
||||
scenePoolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
scenePoolSizes[0].descriptorCount = 3;
|
||||
scenePoolSizes[0].descriptorCount = 3 * SCENE_HISTORY_FRAMES;
|
||||
scenePoolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
||||
scenePoolSizes[1].descriptorCount = 1;
|
||||
scenePoolSizes[1].descriptorCount = SCENE_HISTORY_FRAMES;
|
||||
VkDescriptorPoolCreateInfo scenePoolInfo{};
|
||||
scenePoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
scenePoolInfo.maxSets = 1;
|
||||
scenePoolInfo.maxSets = SCENE_HISTORY_FRAMES;
|
||||
scenePoolInfo.poolSizeCount = static_cast<uint32_t>(scenePoolSizes.size());
|
||||
scenePoolInfo.pPoolSizes = scenePoolSizes.data();
|
||||
if (vkCreateDescriptorPool(device, &scenePoolInfo, nullptr, &sceneDescPool) != VK_SUCCESS) {
|
||||
|
|
@ -267,6 +267,47 @@ void WaterRenderer::recreatePipelines() {
|
|||
}
|
||||
}
|
||||
|
||||
void WaterRenderer::setRefractionEnabled(bool enabled) {
|
||||
if (refractionEnabled == enabled) return;
|
||||
refractionEnabled = enabled;
|
||||
|
||||
// When turning off, clear scene history images to black so the shader
|
||||
// detects "no data" and uses the non-refraction path.
|
||||
if (!enabled && vkCtx) {
|
||||
vkCtx->immediateSubmit([&](VkCommandBuffer cmd) {
|
||||
for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
|
||||
auto& sh = sceneHistory[f];
|
||||
if (!sh.colorImage) continue;
|
||||
|
||||
VkImageMemoryBarrier toTransfer{};
|
||||
toTransfer.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||
toTransfer.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
toTransfer.image = sh.colorImage;
|
||||
toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
toTransfer.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, 0, nullptr, 0, nullptr, 1, &toTransfer);
|
||||
|
||||
VkClearColorValue clearColor = {{0.0f, 0.0f, 0.0f, 0.0f}};
|
||||
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
vkCmdClearColorImage(cmd, sh.colorImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearColor, 1, &range);
|
||||
|
||||
VkImageMemoryBarrier toRead = toTransfer;
|
||||
toRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
toRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
toRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
toRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
||||
0, 0, nullptr, 0, nullptr, 1, &toRead);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void WaterRenderer::shutdown() {
|
||||
clear();
|
||||
|
||||
|
|
@ -304,13 +345,15 @@ VkDescriptorSet WaterRenderer::allocateMaterialSet() {
|
|||
void WaterRenderer::destroySceneHistoryResources() {
|
||||
if (!vkCtx) return;
|
||||
VkDevice device = vkCtx->getDevice();
|
||||
if (sceneColorView) { vkDestroyImageView(device, sceneColorView, nullptr); sceneColorView = VK_NULL_HANDLE; }
|
||||
if (sceneDepthView) { vkDestroyImageView(device, sceneDepthView, nullptr); sceneDepthView = VK_NULL_HANDLE; }
|
||||
if (sceneColorImage) { vmaDestroyImage(vkCtx->getAllocator(), sceneColorImage, sceneColorAlloc); sceneColorImage = VK_NULL_HANDLE; sceneColorAlloc = VK_NULL_HANDLE; }
|
||||
if (sceneDepthImage) { vmaDestroyImage(vkCtx->getAllocator(), sceneDepthImage, sceneDepthAlloc); sceneDepthImage = VK_NULL_HANDLE; sceneDepthAlloc = VK_NULL_HANDLE; }
|
||||
for (auto& sh : sceneHistory) {
|
||||
if (sh.colorView) { vkDestroyImageView(device, sh.colorView, nullptr); sh.colorView = VK_NULL_HANDLE; }
|
||||
if (sh.depthView) { vkDestroyImageView(device, sh.depthView, nullptr); sh.depthView = VK_NULL_HANDLE; }
|
||||
if (sh.colorImage) { vmaDestroyImage(vkCtx->getAllocator(), sh.colorImage, sh.colorAlloc); sh.colorImage = VK_NULL_HANDLE; sh.colorAlloc = VK_NULL_HANDLE; }
|
||||
if (sh.depthImage) { vmaDestroyImage(vkCtx->getAllocator(), sh.depthImage, sh.depthAlloc); sh.depthImage = VK_NULL_HANDLE; sh.depthAlloc = VK_NULL_HANDLE; }
|
||||
sh.sceneSet = VK_NULL_HANDLE;
|
||||
}
|
||||
if (sceneColorSampler) { vkDestroySampler(device, sceneColorSampler, nullptr); sceneColorSampler = VK_NULL_HANDLE; }
|
||||
if (sceneDepthSampler) { vkDestroySampler(device, sceneDepthSampler, nullptr); sceneDepthSampler = VK_NULL_HANDLE; }
|
||||
sceneSet = VK_NULL_HANDLE;
|
||||
sceneHistoryExtent = {0, 0};
|
||||
sceneHistoryReady = false;
|
||||
}
|
||||
|
|
@ -323,54 +366,7 @@ void WaterRenderer::createSceneHistoryResources(VkExtent2D extent, VkFormat colo
|
|||
vkResetDescriptorPool(device, sceneDescPool, 0);
|
||||
sceneHistoryExtent = extent;
|
||||
|
||||
VkImageCreateInfo colorImgInfo{};
|
||||
colorImgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||
colorImgInfo.imageType = VK_IMAGE_TYPE_2D;
|
||||
colorImgInfo.format = colorFormat;
|
||||
colorImgInfo.extent = {extent.width, extent.height, 1};
|
||||
colorImgInfo.mipLevels = 1;
|
||||
colorImgInfo.arrayLayers = 1;
|
||||
colorImgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
colorImgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
colorImgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
colorImgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
|
||||
VmaAllocationCreateInfo allocCI{};
|
||||
allocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY;
|
||||
if (vmaCreateImage(vkCtx->getAllocator(), &colorImgInfo, &allocCI, &sceneColorImage, &sceneColorAlloc, nullptr) != VK_SUCCESS) {
|
||||
LOG_ERROR("WaterRenderer: failed to create scene color history image");
|
||||
return;
|
||||
}
|
||||
|
||||
VkImageCreateInfo depthImgInfo = colorImgInfo;
|
||||
depthImgInfo.format = depthFormat;
|
||||
if (vmaCreateImage(vkCtx->getAllocator(), &depthImgInfo, &allocCI, &sceneDepthImage, &sceneDepthAlloc, nullptr) != VK_SUCCESS) {
|
||||
LOG_ERROR("WaterRenderer: failed to create scene depth history image");
|
||||
return;
|
||||
}
|
||||
|
||||
VkImageViewCreateInfo colorViewInfo{};
|
||||
colorViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
||||
colorViewInfo.image = sceneColorImage;
|
||||
colorViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
colorViewInfo.format = colorFormat;
|
||||
colorViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
colorViewInfo.subresourceRange.levelCount = 1;
|
||||
colorViewInfo.subresourceRange.layerCount = 1;
|
||||
if (vkCreateImageView(device, &colorViewInfo, nullptr, &sceneColorView) != VK_SUCCESS) {
|
||||
LOG_ERROR("WaterRenderer: failed to create scene color history view");
|
||||
return;
|
||||
}
|
||||
|
||||
VkImageViewCreateInfo depthViewInfo = colorViewInfo;
|
||||
depthViewInfo.image = sceneDepthImage;
|
||||
depthViewInfo.format = depthFormat;
|
||||
depthViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
if (vkCreateImageView(device, &depthViewInfo, nullptr, &sceneDepthView) != VK_SUCCESS) {
|
||||
LOG_ERROR("WaterRenderer: failed to create scene depth history view");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create shared samplers
|
||||
VkSamplerCreateInfo sampCI{};
|
||||
sampCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||
sampCI.magFilter = VK_FILTER_LINEAR;
|
||||
|
|
@ -389,99 +385,155 @@ void WaterRenderer::createSceneHistoryResources(VkExtent2D extent, VkFormat colo
|
|||
return;
|
||||
}
|
||||
|
||||
VkDescriptorSetAllocateInfo ai{};
|
||||
ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
||||
ai.descriptorPool = sceneDescPool;
|
||||
ai.descriptorSetCount = 1;
|
||||
ai.pSetLayouts = &sceneSetLayout;
|
||||
if (vkAllocateDescriptorSets(device, &ai, &sceneSet) != VK_SUCCESS) {
|
||||
LOG_ERROR("WaterRenderer: failed to allocate scene descriptor set");
|
||||
sceneSet = VK_NULL_HANDLE;
|
||||
return;
|
||||
VkImageCreateInfo colorImgInfo{};
|
||||
colorImgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||
colorImgInfo.imageType = VK_IMAGE_TYPE_2D;
|
||||
colorImgInfo.format = colorFormat;
|
||||
colorImgInfo.extent = {extent.width, extent.height, 1};
|
||||
colorImgInfo.mipLevels = 1;
|
||||
colorImgInfo.arrayLayers = 1;
|
||||
colorImgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
colorImgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
colorImgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
colorImgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
|
||||
VkImageCreateInfo depthImgInfo = colorImgInfo;
|
||||
depthImgInfo.format = depthFormat;
|
||||
|
||||
VmaAllocationCreateInfo allocCI{};
|
||||
allocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY;
|
||||
|
||||
// Create per-frame images, views, and descriptor sets
|
||||
for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
|
||||
auto& sh = sceneHistory[f];
|
||||
|
||||
if (vmaCreateImage(vkCtx->getAllocator(), &colorImgInfo, &allocCI, &sh.colorImage, &sh.colorAlloc, nullptr) != VK_SUCCESS) {
|
||||
LOG_ERROR("WaterRenderer: failed to create scene color history image [", f, "]");
|
||||
return;
|
||||
}
|
||||
if (vmaCreateImage(vkCtx->getAllocator(), &depthImgInfo, &allocCI, &sh.depthImage, &sh.depthAlloc, nullptr) != VK_SUCCESS) {
|
||||
LOG_ERROR("WaterRenderer: failed to create scene depth history image [", f, "]");
|
||||
return;
|
||||
}
|
||||
|
||||
VkImageViewCreateInfo colorViewInfo{};
|
||||
colorViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
||||
colorViewInfo.image = sh.colorImage;
|
||||
colorViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
colorViewInfo.format = colorFormat;
|
||||
colorViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
colorViewInfo.subresourceRange.levelCount = 1;
|
||||
colorViewInfo.subresourceRange.layerCount = 1;
|
||||
if (vkCreateImageView(device, &colorViewInfo, nullptr, &sh.colorView) != VK_SUCCESS) {
|
||||
LOG_ERROR("WaterRenderer: failed to create scene color history view [", f, "]");
|
||||
return;
|
||||
}
|
||||
|
||||
VkImageViewCreateInfo depthViewInfo = colorViewInfo;
|
||||
depthViewInfo.image = sh.depthImage;
|
||||
depthViewInfo.format = depthFormat;
|
||||
depthViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
if (vkCreateImageView(device, &depthViewInfo, nullptr, &sh.depthView) != VK_SUCCESS) {
|
||||
LOG_ERROR("WaterRenderer: failed to create scene depth history view [", f, "]");
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate descriptor set for this frame
|
||||
VkDescriptorSetAllocateInfo ai{};
|
||||
ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
||||
ai.descriptorPool = sceneDescPool;
|
||||
ai.descriptorSetCount = 1;
|
||||
ai.pSetLayouts = &sceneSetLayout;
|
||||
if (vkAllocateDescriptorSets(device, &ai, &sh.sceneSet) != VK_SUCCESS) {
|
||||
LOG_ERROR("WaterRenderer: failed to allocate scene descriptor set [", f, "]");
|
||||
sh.sceneSet = VK_NULL_HANDLE;
|
||||
return;
|
||||
}
|
||||
|
||||
VkDescriptorImageInfo colorInfo{};
|
||||
colorInfo.sampler = sceneColorSampler;
|
||||
colorInfo.imageView = sh.colorView;
|
||||
colorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
|
||||
VkDescriptorImageInfo depthInfo{};
|
||||
depthInfo.sampler = sceneDepthSampler;
|
||||
depthInfo.imageView = sh.depthView;
|
||||
depthInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
|
||||
VkDescriptorImageInfo reflColorInfo{};
|
||||
reflColorInfo.sampler = sceneColorSampler;
|
||||
reflColorInfo.imageView = reflectionColorView ? reflectionColorView : sh.colorView;
|
||||
reflColorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
|
||||
VkDescriptorBufferInfo reflUBOInfo{};
|
||||
reflUBOInfo.buffer = reflectionUBO;
|
||||
reflUBOInfo.offset = 0;
|
||||
reflUBOInfo.range = sizeof(ReflectionUBOData);
|
||||
|
||||
std::vector<VkWriteDescriptorSet> writes;
|
||||
|
||||
VkWriteDescriptorSet w0{};
|
||||
w0.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
w0.dstSet = sh.sceneSet;
|
||||
w0.dstBinding = 0;
|
||||
w0.descriptorCount = 1;
|
||||
w0.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
w0.pImageInfo = &colorInfo;
|
||||
writes.push_back(w0);
|
||||
|
||||
VkWriteDescriptorSet w1{};
|
||||
w1.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
w1.dstSet = sh.sceneSet;
|
||||
w1.dstBinding = 1;
|
||||
w1.descriptorCount = 1;
|
||||
w1.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
w1.pImageInfo = &depthInfo;
|
||||
writes.push_back(w1);
|
||||
|
||||
VkWriteDescriptorSet w2{};
|
||||
w2.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
w2.dstSet = sh.sceneSet;
|
||||
w2.dstBinding = 2;
|
||||
w2.descriptorCount = 1;
|
||||
w2.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
w2.pImageInfo = &reflColorInfo;
|
||||
writes.push_back(w2);
|
||||
|
||||
if (reflectionUBO) {
|
||||
VkWriteDescriptorSet w3{};
|
||||
w3.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
w3.dstSet = sh.sceneSet;
|
||||
w3.dstBinding = 3;
|
||||
w3.descriptorCount = 1;
|
||||
w3.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
||||
w3.pBufferInfo = &reflUBOInfo;
|
||||
writes.push_back(w3);
|
||||
}
|
||||
|
||||
vkUpdateDescriptorSets(device, static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
|
||||
}
|
||||
|
||||
VkDescriptorImageInfo colorInfo{};
|
||||
colorInfo.sampler = sceneColorSampler;
|
||||
colorInfo.imageView = sceneColorView;
|
||||
colorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
VkDescriptorImageInfo depthInfo{};
|
||||
depthInfo.sampler = sceneDepthSampler;
|
||||
depthInfo.imageView = sceneDepthView;
|
||||
depthInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
|
||||
// Reflection color texture (binding 2) — use scene color as placeholder until reflection is created
|
||||
VkDescriptorImageInfo reflColorInfo{};
|
||||
reflColorInfo.sampler = sceneColorSampler;
|
||||
reflColorInfo.imageView = reflectionColorView ? reflectionColorView : sceneColorView;
|
||||
reflColorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
|
||||
// Reflection UBO (binding 3)
|
||||
VkDescriptorBufferInfo reflUBOInfo{};
|
||||
reflUBOInfo.buffer = reflectionUBO;
|
||||
reflUBOInfo.offset = 0;
|
||||
reflUBOInfo.range = sizeof(ReflectionUBOData);
|
||||
|
||||
// Write bindings 0,1 always; write 2,3 only if reflection resources exist
|
||||
std::vector<VkWriteDescriptorSet> writes;
|
||||
|
||||
VkWriteDescriptorSet w0{};
|
||||
w0.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
w0.dstSet = sceneSet;
|
||||
w0.dstBinding = 0;
|
||||
w0.descriptorCount = 1;
|
||||
w0.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
w0.pImageInfo = &colorInfo;
|
||||
writes.push_back(w0);
|
||||
|
||||
VkWriteDescriptorSet w1{};
|
||||
w1.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
w1.dstSet = sceneSet;
|
||||
w1.dstBinding = 1;
|
||||
w1.descriptorCount = 1;
|
||||
w1.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
w1.pImageInfo = &depthInfo;
|
||||
writes.push_back(w1);
|
||||
|
||||
VkWriteDescriptorSet w2{};
|
||||
w2.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
w2.dstSet = sceneSet;
|
||||
w2.dstBinding = 2;
|
||||
w2.descriptorCount = 1;
|
||||
w2.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
w2.pImageInfo = &reflColorInfo;
|
||||
writes.push_back(w2);
|
||||
|
||||
if (reflectionUBO) {
|
||||
VkWriteDescriptorSet w3{};
|
||||
w3.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
w3.dstSet = sceneSet;
|
||||
w3.dstBinding = 3;
|
||||
w3.descriptorCount = 1;
|
||||
w3.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
||||
w3.pBufferInfo = &reflUBOInfo;
|
||||
writes.push_back(w3);
|
||||
}
|
||||
|
||||
vkUpdateDescriptorSets(device, static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
|
||||
|
||||
// Initialize history images to shader-read layout so first frame samples are defined.
|
||||
// Initialize all per-frame history images to shader-read layout
|
||||
vkCtx->immediateSubmit([&](VkCommandBuffer cmd) {
|
||||
VkImageMemoryBarrier barriers[2]{};
|
||||
barriers[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||
barriers[0].oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
barriers[0].newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
barriers[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barriers[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barriers[0].image = sceneColorImage;
|
||||
barriers[0].subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
barriers[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
|
||||
barriers[1] = barriers[0];
|
||||
barriers[1].image = sceneDepthImage;
|
||||
barriers[1].subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
std::vector<VkImageMemoryBarrier> barriers;
|
||||
for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
|
||||
VkImageMemoryBarrier b{};
|
||||
b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||
b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
b.image = sceneHistory[f].colorImage;
|
||||
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
barriers.push_back(b);
|
||||
|
||||
b.image = sceneHistory[f].depthImage;
|
||||
b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
barriers.push_back(b);
|
||||
}
|
||||
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
||||
0, 0, nullptr, 0, nullptr, 2, barriers);
|
||||
0, 0, nullptr, 0, nullptr, static_cast<uint32_t>(barriers.size()), barriers.data());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -492,9 +544,14 @@ void WaterRenderer::updateMaterialUBO(WaterSurface& surface) {
|
|||
// WMO liquid material override
|
||||
if (surface.wmoId != 0) {
|
||||
const uint8_t basicType = (surface.liquidType == 0) ? 0 : ((surface.liquidType - 1) % 4);
|
||||
if (basicType == 2 || basicType == 3) {
|
||||
color = glm::vec4(0.2f, 0.4f, 0.6f, 1.0f);
|
||||
alpha = 0.45f;
|
||||
if (basicType == 2) {
|
||||
// Magma — bright orange-red, opaque
|
||||
color = glm::vec4(1.0f, 0.35f, 0.05f, 1.0f);
|
||||
alpha = 0.95f;
|
||||
} else if (basicType == 3) {
|
||||
// Slime — green, semi-opaque
|
||||
color = glm::vec4(0.2f, 0.6f, 0.1f, 1.0f);
|
||||
alpha = 0.85f;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -632,12 +689,12 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap
|
|||
layer.minHeight
|
||||
);
|
||||
surface.origin = glm::vec3(
|
||||
surface.position.x - (static_cast<float>(layer.y) * TILE_SIZE),
|
||||
surface.position.y - (static_cast<float>(layer.x) * TILE_SIZE),
|
||||
surface.position.x - (static_cast<float>(layer.x) * TILE_SIZE),
|
||||
surface.position.y - (static_cast<float>(layer.y) * TILE_SIZE),
|
||||
layer.minHeight
|
||||
);
|
||||
surface.stepX = glm::vec3(0.0f, -TILE_SIZE, 0.0f);
|
||||
surface.stepY = glm::vec3(-TILE_SIZE, 0.0f, 0.0f);
|
||||
surface.stepX = glm::vec3(-TILE_SIZE, 0.0f, 0.0f);
|
||||
surface.stepY = glm::vec3(0.0f, -TILE_SIZE, 0.0f);
|
||||
|
||||
surface.minHeight = layer.minHeight;
|
||||
surface.maxHeight = layer.maxHeight;
|
||||
|
|
@ -698,8 +755,8 @@ void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool ap
|
|||
// Origin = chunk(0,0) position (NW corner of tile)
|
||||
surface.origin = glm::vec3(chunk00.position[0], chunk00.position[1], groupHeight);
|
||||
surface.position = surface.origin;
|
||||
surface.stepX = glm::vec3(0.0f, -TILE_SIZE, 0.0f);
|
||||
surface.stepY = glm::vec3(-TILE_SIZE, 0.0f, 0.0f);
|
||||
surface.stepX = glm::vec3(-TILE_SIZE, 0.0f, 0.0f);
|
||||
surface.stepY = glm::vec3(0.0f, -TILE_SIZE, 0.0f);
|
||||
|
||||
surface.minHeight = groupHeight;
|
||||
surface.maxHeight = groupHeight;
|
||||
|
|
@ -883,7 +940,7 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu
|
|||
surface.origin.z = adjustedZ;
|
||||
surface.position.z = adjustedZ;
|
||||
|
||||
if (surface.origin.z > 300.0f || surface.origin.z < -100.0f) return;
|
||||
if (surface.origin.z > 2000.0f || surface.origin.z < -500.0f) return;
|
||||
|
||||
// Build tile mask from MLIQ flags and per-vertex heights
|
||||
size_t tileCount = static_cast<size_t>(surface.width) * static_cast<size_t>(surface.height);
|
||||
|
|
@ -986,7 +1043,7 @@ void WaterRenderer::clear() {
|
|||
// ==============================================================
|
||||
|
||||
void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
|
||||
const Camera& /*camera*/, float /*time*/, bool use1x) {
|
||||
const Camera& /*camera*/, float /*time*/, bool use1x, uint32_t frameIndex) {
|
||||
VkPipeline pipeline = (use1x && water1xPipeline) ? water1xPipeline : waterPipeline;
|
||||
if (!renderingEnabled || surfaces.empty() || !pipeline) {
|
||||
if (renderDiagCounter_++ % 300 == 0 && !surfaces.empty()) {
|
||||
|
|
@ -997,7 +1054,9 @@ void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
|
|||
}
|
||||
return;
|
||||
}
|
||||
if (!sceneSet) {
|
||||
uint32_t fi = frameIndex % SCENE_HISTORY_FRAMES;
|
||||
VkDescriptorSet activeSceneSet = sceneHistory[fi].sceneSet;
|
||||
if (!activeSceneSet) {
|
||||
if (renderDiagCounter_++ % 300 == 0) {
|
||||
LOG_WARNING("Water: render skipped — sceneSet is null, surfaces=", surfaces.size());
|
||||
}
|
||||
|
|
@ -1009,7 +1068,7 @@ void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
|
|||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout,
|
||||
0, 1, &perFrameSet, 0, nullptr);
|
||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout,
|
||||
2, 1, &sceneSet, 0, nullptr);
|
||||
2, 1, &activeSceneSet, 0, nullptr);
|
||||
|
||||
for (const auto& surface : surfaces) {
|
||||
if (surface.vertexBuffer == VK_NULL_HANDLE || surface.indexCount == 0) continue;
|
||||
|
|
@ -1050,8 +1109,11 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd,
|
|||
VkImage srcColorImage,
|
||||
VkImage srcDepthImage,
|
||||
VkExtent2D srcExtent,
|
||||
bool srcDepthIsMsaa) {
|
||||
if (!vkCtx || !cmd || !sceneColorImage || !sceneDepthImage || srcExtent.width == 0 || srcExtent.height == 0) {
|
||||
bool srcDepthIsMsaa,
|
||||
uint32_t frameIndex) {
|
||||
uint32_t fi = frameIndex % SCENE_HISTORY_FRAMES;
|
||||
auto& sh = sceneHistory[fi];
|
||||
if (!vkCtx || !cmd || !sh.colorImage || !sh.depthImage || srcExtent.width == 0 || srcExtent.height == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1091,7 +1153,7 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd,
|
|||
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
0, VK_ACCESS_TRANSFER_READ_BIT,
|
||||
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
||||
barrier2(sceneColorImage, VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
barrier2(sh.colorImage, VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
||||
|
|
@ -1101,9 +1163,9 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd,
|
|||
colorCopy.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
colorCopy.extent = {copyExtent.width, copyExtent.height, 1};
|
||||
vkCmdCopyImage(cmd, srcColorImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
sceneColorImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &colorCopy);
|
||||
sh.colorImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &colorCopy);
|
||||
|
||||
barrier2(sceneColorImage, VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
barrier2(sh.colorImage, VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
|
||||
|
|
@ -1118,7 +1180,7 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd,
|
|||
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT,
|
||||
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
||||
barrier2(sceneDepthImage, VK_IMAGE_ASPECT_DEPTH_BIT,
|
||||
barrier2(sh.depthImage, VK_IMAGE_ASPECT_DEPTH_BIT,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
||||
|
|
@ -1128,9 +1190,9 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd,
|
|||
depthCopy.dstSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1};
|
||||
depthCopy.extent = {copyExtent.width, copyExtent.height, 1};
|
||||
vkCmdCopyImage(cmd, srcDepthImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
sceneDepthImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &depthCopy);
|
||||
sh.depthImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &depthCopy);
|
||||
|
||||
barrier2(sceneDepthImage, VK_IMAGE_ASPECT_DEPTH_BIT,
|
||||
barrier2(sh.depthImage, VK_IMAGE_ASPECT_DEPTH_BIT,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
|
||||
|
|
@ -1543,11 +1605,11 @@ bool WaterRenderer::isWmoWaterAt(float glX, float glY) const {
|
|||
glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const {
|
||||
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
|
||||
switch (basicType) {
|
||||
case 0: return glm::vec4(0.12f, 0.32f, 0.48f, 1.0f); // inland: blue-green
|
||||
case 1: return glm::vec4(0.04f, 0.14f, 0.30f, 1.0f); // ocean: deep blue
|
||||
case 0: return glm::vec4(0.10f, 0.28f, 0.55f, 1.0f); // inland: richer blue
|
||||
case 1: return glm::vec4(0.04f, 0.16f, 0.38f, 1.0f); // ocean: deep blue
|
||||
case 2: return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f); // magma
|
||||
case 3: return glm::vec4(0.2f, 0.6f, 0.1f, 1.0f); // slime
|
||||
default: return glm::vec4(0.12f, 0.32f, 0.48f, 1.0f);
|
||||
default: return glm::vec4(0.10f, 0.28f, 0.55f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1815,21 +1877,28 @@ void WaterRenderer::endReflectionPass(VkCommandBuffer cmd) {
|
|||
vkCmdEndRenderPass(cmd);
|
||||
reflectionColorLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
|
||||
// Update scene descriptor set with the freshly rendered reflection texture
|
||||
if (sceneSet && reflectionColorView && reflectionSampler) {
|
||||
// Update all per-frame scene descriptor sets with the freshly rendered reflection texture
|
||||
if (reflectionColorView && reflectionSampler) {
|
||||
VkDescriptorImageInfo reflInfo{};
|
||||
reflInfo.sampler = reflectionSampler;
|
||||
reflInfo.imageView = reflectionColorView;
|
||||
reflInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
|
||||
VkWriteDescriptorSet write{};
|
||||
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
write.dstSet = sceneSet;
|
||||
write.dstBinding = 2;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &reflInfo;
|
||||
vkUpdateDescriptorSets(vkCtx->getDevice(), 1, &write, 0, nullptr);
|
||||
std::vector<VkWriteDescriptorSet> writes;
|
||||
for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
|
||||
if (!sceneHistory[f].sceneSet) continue;
|
||||
VkWriteDescriptorSet write{};
|
||||
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
write.dstSet = sceneHistory[f].sceneSet;
|
||||
write.dstBinding = 2;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||
write.pImageInfo = &reflInfo;
|
||||
writes.push_back(write);
|
||||
}
|
||||
if (!writes.empty()) {
|
||||
vkUpdateDescriptorSets(vkCtx->getDevice(), static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -593,13 +593,22 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
|
||||
// Detect window/glass materials by texture name.
|
||||
// Flag 0x10 (F_SIDN) marks night-glow materials (windows AND lamps),
|
||||
// so we additionally check for "window" in the texture path to
|
||||
// so we additionally check for "window" or "glass" in the texture path to
|
||||
// distinguish actual glass from lamp post geometry.
|
||||
bool isWindow = false;
|
||||
bool isLava = false;
|
||||
if (batch.materialId < modelData.materialTextureIndices.size()) {
|
||||
uint32_t ti = modelData.materialTextureIndices[batch.materialId];
|
||||
if (ti < modelData.textureNames.size()) {
|
||||
isWindow = (modelData.textureNames[ti].find("window") != std::string::npos);
|
||||
const auto& texName = modelData.textureNames[ti];
|
||||
// Case-insensitive search for material types
|
||||
std::string texNameLower = texName;
|
||||
std::transform(texNameLower.begin(), texNameLower.end(), texNameLower.begin(), ::tolower);
|
||||
isWindow = (texNameLower.find("window") != std::string::npos ||
|
||||
texNameLower.find("glass") != std::string::npos);
|
||||
isLava = (texNameLower.find("lava") != std::string::npos ||
|
||||
texNameLower.find("molten") != std::string::npos ||
|
||||
texNameLower.find("magma") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -612,6 +621,7 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
mb.unlit = unlit;
|
||||
mb.isTransparent = (blendMode >= 2);
|
||||
mb.isWindow = isWindow;
|
||||
mb.isLava = isLava;
|
||||
// Look up normal/height map from texture cache
|
||||
if (hasTexture && tex != whiteTexture_.get()) {
|
||||
for (const auto& [cacheKey, cacheEntry] : textureCache) {
|
||||
|
|
@ -651,7 +661,7 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
matData.unlit = mb.unlit ? 1 : 0;
|
||||
matData.isInterior = isInterior ? 1 : 0;
|
||||
matData.specularIntensity = 0.5f;
|
||||
matData.isWindow = mb.isWindow ? 1 : 0;
|
||||
matData.isWindow = mb.isWindow ? (wmoOnlyMap_ ? 2 : 1) : 0;
|
||||
matData.enableNormalMap = normalMappingEnabled_ ? 1 : 0;
|
||||
matData.enablePOM = pomEnabled_ ? 1 : 0;
|
||||
matData.pomScale = 0.012f;
|
||||
|
|
@ -661,6 +671,7 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
}
|
||||
matData.heightMapVariance = mb.heightMapVariance;
|
||||
matData.normalMapStrength = normalMapStrength_;
|
||||
matData.isLava = mb.isLava ? 1 : 0;
|
||||
if (matBuf.info.pMappedData) {
|
||||
memcpy(matBuf.info.pMappedData, &matData, sizeof(matData));
|
||||
}
|
||||
|
|
@ -782,6 +793,7 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
doodadTemplate.m2Path = m2Path;
|
||||
doodadTemplate.localTransform = localTransform;
|
||||
modelData.doodadTemplates.push_back(doodadTemplate);
|
||||
|
||||
}
|
||||
|
||||
if (!modelData.doodadTemplates.empty()) {
|
||||
|
|
@ -1724,11 +1736,18 @@ void WMORenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceM
|
|||
|
||||
struct ShadowPush { glm::mat4 lightSpaceMatrix; glm::mat4 model; };
|
||||
|
||||
const float shadowRadiusSq = shadowRadius * shadowRadius;
|
||||
// WMO shadow cull uses the ortho half-extent (shadow map coverage) rather than
|
||||
// the proximity radius so that distant buildings whose shadows reach the player
|
||||
// are still rendered into the shadow map.
|
||||
const float wmoCullRadius = std::max(shadowRadius, 180.0f);
|
||||
const float wmoCullRadiusSq = wmoCullRadius * wmoCullRadius;
|
||||
|
||||
for (const auto& instance : instances) {
|
||||
// Distance cull against shadow frustum
|
||||
glm::vec3 diff = instance.position - shadowCenter;
|
||||
if (glm::dot(diff, diff) > shadowRadiusSq) continue;
|
||||
// Distance cull using world bounding box — WMO origins can be far from
|
||||
// their geometry, so point-based culling misses large buildings.
|
||||
glm::vec3 closest = glm::clamp(shadowCenter, instance.worldBoundsMin, instance.worldBoundsMax);
|
||||
glm::vec3 diff = closest - shadowCenter;
|
||||
if (glm::dot(diff, diff) > wmoCullRadiusSq) continue;
|
||||
auto modelIt = loadedModels.find(instance.modelId);
|
||||
if (modelIt == loadedModels.end()) continue;
|
||||
const ModelData& model = modelIt->second;
|
||||
|
|
@ -1737,7 +1756,8 @@ void WMORenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceM
|
|||
vkCmdPushConstants(cmd, shadowPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
|
||||
0, 128, &push);
|
||||
|
||||
for (const auto& group : model.groups) {
|
||||
for (size_t gi = 0; gi < model.groups.size(); ++gi) {
|
||||
const auto& group = model.groups[gi];
|
||||
if (group.vertexBuffer == VK_NULL_HANDLE || group.indexBuffer == VK_NULL_HANDLE) continue;
|
||||
|
||||
// Skip antiportal geometry
|
||||
|
|
@ -1746,13 +1766,18 @@ void WMORenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceM
|
|||
// Skip LOD groups in shadow pass (they overlap real geometry)
|
||||
if (group.isLOD) continue;
|
||||
|
||||
// Per-group AABB cull against shadow frustum
|
||||
if (gi < instance.worldGroupBounds.size()) {
|
||||
const auto& [gMin, gMax] = instance.worldGroupBounds[gi];
|
||||
glm::vec3 gClosest = glm::clamp(shadowCenter, gMin, gMax);
|
||||
glm::vec3 gDiff = gClosest - shadowCenter;
|
||||
if (glm::dot(gDiff, gDiff) > wmoCullRadiusSq) continue;
|
||||
}
|
||||
|
||||
VkDeviceSize offset = 0;
|
||||
vkCmdBindVertexBuffers(cmd, 0, 1, &group.vertexBuffer, &offset);
|
||||
vkCmdBindIndexBuffer(cmd, group.indexBuffer, 0, VK_INDEX_TYPE_UINT16);
|
||||
|
||||
// Draw all batches in shadow pass.
|
||||
// WMO transparency classification is not reliable enough for caster
|
||||
// selection here and was dropping major world casters.
|
||||
for (const auto& mb : group.mergedBatches) {
|
||||
for (const auto& dr : mb.draws) {
|
||||
vkCmdDrawIndexed(cmd, dr.indexCount, 1, dr.firstIndex, 0, 0);
|
||||
|
|
@ -3082,7 +3107,7 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
float rangeMinY = std::min(localFrom.y, localTo.y) - PLAYER_RADIUS - 1.5f;
|
||||
float rangeMaxX = std::max(localFrom.x, localTo.x) + PLAYER_RADIUS + 1.5f;
|
||||
float rangeMaxY = std::max(localFrom.y, localTo.y) + PLAYER_RADIUS + 1.5f;
|
||||
group.getWallTrianglesInRange(rangeMinX, rangeMinY, rangeMaxX, rangeMaxY, triScratch_);
|
||||
group.getTrianglesInRange(rangeMinX, rangeMinY, rangeMaxX, rangeMaxY, triScratch_);
|
||||
|
||||
for (uint32_t triStart : triScratch_) {
|
||||
// Use pre-computed Z bounds for fast vertical reject
|
||||
|
|
@ -3100,17 +3125,18 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
if (triHeight < 1.0f && tb.maxZ <= localFeetZ + 1.2f) continue;
|
||||
|
||||
// Use MOPY flags to filter wall collision.
|
||||
// Only RENDERED triangles (flag 0x20) with collision intent (0x01)
|
||||
// should block the player. Skip invisible collision hulls (0x08/0x48)
|
||||
// and non-collidable render-only geometry.
|
||||
// Collidable triangles (flag 0x01) block the player — including
|
||||
// invisible collision walls (0x01 without 0x20) used in tunnels.
|
||||
// Skip detail/decorative geometry (0x04) and render-only surfaces.
|
||||
uint32_t triIdx = triStart / 3;
|
||||
if (!group.triMopyFlags.empty() && triIdx < group.triMopyFlags.size()) {
|
||||
uint8_t mopy = group.triMopyFlags[triIdx];
|
||||
// Must be rendered (0x20) AND have base collision flag (0x01)
|
||||
bool rendered = (mopy & 0x20) != 0;
|
||||
bool collidable = (mopy & 0x01) != 0;
|
||||
if (mopy != 0 && !(rendered && collidable)) {
|
||||
continue;
|
||||
if (mopy != 0) {
|
||||
bool collidable = (mopy & 0x01) != 0;
|
||||
bool detail = (mopy & 0x04) != 0;
|
||||
if (!collidable || detail) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3136,13 +3162,13 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
glm::vec3 hitPoint = localFrom + (localTo - localFrom) * tHit;
|
||||
glm::vec3 hitClosest = closestPointOnTriangle(hitPoint, v0, v1, v2);
|
||||
float hitErrSq = glm::dot(hitClosest - hitPoint, hitClosest - hitPoint);
|
||||
if (hitErrSq <= 0.15f * 0.15f) {
|
||||
if (hitErrSq <= 0.25f * 0.25f) {
|
||||
float side = fromDist > 0.0f ? 1.0f : -1.0f;
|
||||
glm::vec3 safeLocal = hitPoint + normal * side * (PLAYER_RADIUS + 0.05f);
|
||||
glm::vec3 pushLocal(safeLocal.x - localTo.x, safeLocal.y - localTo.y, 0.0f);
|
||||
// Cap swept pushback so walls don't shove the player violently
|
||||
float pushLen = glm::length(glm::vec2(pushLocal.x, pushLocal.y));
|
||||
const float MAX_SWEPT_PUSH = 0.15f;
|
||||
const float MAX_SWEPT_PUSH = insideWMO ? 0.45f : 0.25f;
|
||||
if (pushLen > MAX_SWEPT_PUSH) {
|
||||
float scale = MAX_SWEPT_PUSH / pushLen;
|
||||
pushLocal.x *= scale;
|
||||
|
|
@ -3172,7 +3198,7 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
|
||||
const float SKIN = 0.005f; // small separation so we don't re-collide immediately
|
||||
// Stronger push when inside WMO for more responsive indoor collision
|
||||
const float MAX_PUSH = insideWMO ? 0.12f : 0.08f;
|
||||
const float MAX_PUSH = insideWMO ? 0.35f : 0.15f;
|
||||
float penetration = (PLAYER_RADIUS - horizDist);
|
||||
float pushDist = glm::clamp(penetration + SKIN, 0.0f, MAX_PUSH);
|
||||
glm::vec2 pushDir2;
|
||||
|
|
|
|||
|
|
@ -288,6 +288,15 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
msaaSettingsApplied_ = true;
|
||||
}
|
||||
|
||||
// Apply saved water refraction setting once when renderer is available
|
||||
if (!waterRefractionApplied_) {
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
if (renderer) {
|
||||
renderer->setWaterRefractionEnabled(pendingWaterRefraction);
|
||||
waterRefractionApplied_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply saved normal mapping / POM settings once when WMO renderer is available
|
||||
if (!normalMapSettingsApplied_) {
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
|
|
@ -1358,16 +1367,16 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
|
||||
}
|
||||
|
||||
// Slash key: focus chat input
|
||||
if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) {
|
||||
// Slash key: focus chat input — always works unless already typing in chat
|
||||
if (!chatInputActive && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) {
|
||||
refocusChatInput = true;
|
||||
chatInputBuffer[0] = '/';
|
||||
chatInputBuffer[1] = '\0';
|
||||
chatInputMoveCursorToEnd = true;
|
||||
}
|
||||
|
||||
// Enter key: focus chat input (empty)
|
||||
if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_RETURN)) {
|
||||
// Enter key: focus chat input (empty) — always works unless already typing
|
||||
if (!chatInputActive && input.isKeyJustPressed(SDL_SCANCODE_RETURN)) {
|
||||
refocusChatInput = true;
|
||||
}
|
||||
|
||||
|
|
@ -6171,6 +6180,7 @@ void GameScreen::renderSettingsWindow() {
|
|||
pendingVsync = window->isVsyncEnabled();
|
||||
if (renderer) {
|
||||
renderer->setShadowsEnabled(pendingShadows);
|
||||
renderer->setShadowDistance(pendingShadowDistance);
|
||||
// Read non-volume settings from actual state (volumes come from saved settings)
|
||||
if (auto* cameraController = renderer->getCameraController()) {
|
||||
pendingMouseSensitivity = cameraController->getMouseSensitivity();
|
||||
|
|
@ -6237,6 +6247,18 @@ void GameScreen::renderSettingsWindow() {
|
|||
if (renderer) renderer->setShadowsEnabled(pendingShadows);
|
||||
saveSettings();
|
||||
}
|
||||
if (pendingShadows) {
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(150.0f);
|
||||
if (ImGui::SliderFloat("Distance##shadow", &pendingShadowDistance, 40.0f, 200.0f, "%.0f")) {
|
||||
if (renderer) renderer->setShadowDistance(pendingShadowDistance);
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
if (ImGui::Checkbox("Water Refraction", &pendingWaterRefraction)) {
|
||||
if (renderer) renderer->setWaterRefractionEnabled(pendingWaterRefraction);
|
||||
saveSettings();
|
||||
}
|
||||
{
|
||||
const char* aaLabels[] = { "Off", "2x MSAA", "4x MSAA", "8x MSAA" };
|
||||
if (ImGui::Combo("Anti-Aliasing", &pendingAntiAliasing, aaLabels, 4)) {
|
||||
|
|
@ -6326,6 +6348,7 @@ void GameScreen::renderSettingsWindow() {
|
|||
pendingFullscreen = kDefaultFullscreen;
|
||||
pendingVsync = kDefaultVsync;
|
||||
pendingShadows = kDefaultShadows;
|
||||
pendingShadowDistance = 72.0f;
|
||||
pendingGroundClutterDensity = kDefaultGroundClutterDensity;
|
||||
pendingAntiAliasing = 0;
|
||||
pendingNormalMapping = true;
|
||||
|
|
@ -6336,7 +6359,12 @@ void GameScreen::renderSettingsWindow() {
|
|||
window->setFullscreen(pendingFullscreen);
|
||||
window->setVsync(pendingVsync);
|
||||
window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]);
|
||||
if (renderer) renderer->setShadowsEnabled(pendingShadows);
|
||||
pendingWaterRefraction = false;
|
||||
if (renderer) {
|
||||
renderer->setShadowsEnabled(pendingShadows);
|
||||
renderer->setShadowDistance(pendingShadowDistance);
|
||||
}
|
||||
if (renderer) renderer->setWaterRefractionEnabled(pendingWaterRefraction);
|
||||
if (renderer) renderer->setMsaaSamples(VK_SAMPLE_COUNT_1_BIT);
|
||||
if (renderer) {
|
||||
if (auto* tm = renderer->getTerrainManager()) {
|
||||
|
|
@ -7349,6 +7377,8 @@ void GameScreen::saveSettings() {
|
|||
out << "auto_loot=" << (pendingAutoLoot ? 1 : 0) << "\n";
|
||||
out << "ground_clutter_density=" << pendingGroundClutterDensity << "\n";
|
||||
out << "shadows=" << (pendingShadows ? 1 : 0) << "\n";
|
||||
out << "shadow_distance=" << pendingShadowDistance << "\n";
|
||||
out << "water_refraction=" << (pendingWaterRefraction ? 1 : 0) << "\n";
|
||||
out << "antialiasing=" << pendingAntiAliasing << "\n";
|
||||
out << "normal_mapping=" << (pendingNormalMapping ? 1 : 0) << "\n";
|
||||
out << "normal_map_strength=" << pendingNormalMapStrength << "\n";
|
||||
|
|
@ -7433,6 +7463,8 @@ void GameScreen::loadSettings() {
|
|||
else if (key == "auto_loot") pendingAutoLoot = (std::stoi(val) != 0);
|
||||
else if (key == "ground_clutter_density") pendingGroundClutterDensity = std::clamp(std::stoi(val), 0, 150);
|
||||
else if (key == "shadows") pendingShadows = (std::stoi(val) != 0);
|
||||
else if (key == "shadow_distance") pendingShadowDistance = std::clamp(std::stof(val), 40.0f, 200.0f);
|
||||
else if (key == "water_refraction") pendingWaterRefraction = (std::stoi(val) != 0);
|
||||
else if (key == "antialiasing") pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3);
|
||||
else if (key == "normal_mapping") pendingNormalMapping = (std::stoi(val) != 0);
|
||||
else if (key == "normal_map_strength") pendingNormalMapStrength = std::clamp(std::stof(val), 0.0f, 2.0f);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue