mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 01:23:51 +00:00
Add taxi system, fix WMO interior lighting, ramp collision, and /unstuck
- Implement flight path system: SMSG_SHOWTAXINODES parser, CMSG_ACTIVATETAXIEXPRESS builder, BFS multi-hop pathfinding through TaxiNodes/TaxiPath DBC, taxi destination UI, movement blocking during flight - Fix WMO interiors too dark by boosting vertex color lighting multiplier - Dim M2 objects inside WMO interiors (rugs, furniture) via per-instance interior detection - Fix ramp/stair clipping by lowering wall collision normal threshold from 0.85 to 0.55 - Restore 5-sample cardinal footprint for ground detection to fix rug slipping - Fix /unstuck command to reset player Z to WMO/terrain floor height - Handle MSG_MOVE_TELEPORT_ACK and SMSG_TRANSFER_PENDING for hearthstone teleports - Fix spawning under Stormwind with online-mode camera controller reset
This commit is contained in:
parent
c5a1fe927b
commit
3c2a728ec4
15 changed files with 691 additions and 108 deletions
|
|
@ -588,15 +588,22 @@ void CameraController::update(float deltaTime) {
|
|||
return base;
|
||||
};
|
||||
|
||||
// Use cached floor height if player hasn't moved much horizontally.
|
||||
float floorPosDist = glm::length(glm::vec2(targetPos.x, targetPos.y) - cachedFloorPos);
|
||||
// Sample center + 4 cardinal offsets so narrow M2 objects (rugs,
|
||||
// planks) are reliably detected. Take the highest result.
|
||||
std::optional<float> groundH;
|
||||
if (cachedFloorHeight && floorPosDist < 0.5f) {
|
||||
groundH = cachedFloorHeight;
|
||||
} else {
|
||||
groundH = sampleGround(targetPos.x, targetPos.y);
|
||||
cachedFloorHeight = groundH;
|
||||
cachedFloorPos = glm::vec2(targetPos.x, targetPos.y);
|
||||
{
|
||||
constexpr float FOOTPRINT = 0.4f;
|
||||
const glm::vec2 offsets[] = {
|
||||
{0.0f, 0.0f},
|
||||
{FOOTPRINT, 0.0f}, {-FOOTPRINT, 0.0f},
|
||||
{0.0f, FOOTPRINT}, {0.0f, -FOOTPRINT}
|
||||
};
|
||||
for (const auto& o : offsets) {
|
||||
auto h = sampleGround(targetPos.x + o.x, targetPos.y + o.y);
|
||||
if (h && (!groundH || *h > *groundH)) {
|
||||
groundH = h;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (groundH) {
|
||||
|
|
@ -1081,13 +1088,18 @@ void CameraController::reset() {
|
|||
};
|
||||
|
||||
// Search nearby for a stable, non-steep spawn floor to avoid waterfall/ledge spawns.
|
||||
// In online mode, use a tight search radius since the server dictates position.
|
||||
float bestScore = std::numeric_limits<float>::max();
|
||||
glm::vec3 bestPos = spawnPos;
|
||||
bool foundBest = false;
|
||||
constexpr float radii[] = {0.0f, 6.0f, 12.0f, 18.0f, 24.0f, 32.0f};
|
||||
constexpr float radiiOffline[] = {0.0f, 6.0f, 12.0f, 18.0f, 24.0f, 32.0f};
|
||||
constexpr float radiiOnline[] = {0.0f, 2.0f};
|
||||
const float* radii = onlineMode ? radiiOnline : radiiOffline;
|
||||
const int radiiCount = onlineMode ? 2 : 6;
|
||||
constexpr int ANGLES = 16;
|
||||
constexpr float PI = 3.14159265f;
|
||||
for (float r : radii) {
|
||||
for (int ri = 0; ri < radiiCount; ri++) {
|
||||
float r = radii[ri];
|
||||
int steps = (r <= 0.01f) ? 1 : ANGLES;
|
||||
for (int i = 0; i < steps; i++) {
|
||||
float a = (2.0f * PI * static_cast<float>(i)) / static_cast<float>(steps);
|
||||
|
|
@ -1128,8 +1140,9 @@ void CameraController::reset() {
|
|||
const glm::vec3 from(x, y, *h + 0.20f);
|
||||
const bool insideWMO = wmoRenderer->isInsideWMO(x, y, *h + 1.5f, nullptr);
|
||||
|
||||
// Prefer outdoors for default hearth-like spawn points.
|
||||
if (insideWMO) {
|
||||
// Prefer outdoors for default hearth-like spawn points (offline only).
|
||||
// In online mode, trust the server position even if inside a WMO.
|
||||
if (insideWMO && !onlineMode) {
|
||||
score += 120.0f;
|
||||
}
|
||||
|
||||
|
|
@ -1192,10 +1205,6 @@ void CameraController::reset() {
|
|||
lastGroundZ = spawnPos.z - 0.05f;
|
||||
}
|
||||
|
||||
// Invalidate inter-frame floor cache so the first frame probes fresh.
|
||||
cachedFloorHeight.reset();
|
||||
cachedFloorPos = glm::vec2(0.0f);
|
||||
|
||||
camera->setRotation(yaw, pitch);
|
||||
glm::vec3 forward3D = camera->getForward();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "rendering/m2_renderer.hpp"
|
||||
#include "rendering/wmo_renderer.hpp"
|
||||
#include "rendering/texture.hpp"
|
||||
#include "rendering/shader.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
|
|
@ -275,6 +276,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
|||
uniform mat4 uLightSpaceMatrix;
|
||||
uniform bool uShadowEnabled;
|
||||
uniform float uShadowStrength;
|
||||
uniform bool uInteriorDarken;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
|
|
@ -306,41 +308,48 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
|||
vec3 normal = normalize(Normal);
|
||||
vec3 lightDir = normalize(uLightDir);
|
||||
|
||||
// Two-sided lighting for foliage
|
||||
float diff = max(abs(dot(normal, lightDir)), 0.3);
|
||||
vec3 result;
|
||||
if (uInteriorDarken) {
|
||||
// Interior: dim ambient, minimal directional light
|
||||
float diff = max(abs(dot(normal, lightDir)), 0.0) * 0.15;
|
||||
result = texColor.rgb * (0.55 + diff);
|
||||
} else {
|
||||
// Two-sided lighting for foliage
|
||||
float diff = max(abs(dot(normal, lightDir)), 0.3);
|
||||
|
||||
// Blinn-Phong specular
|
||||
vec3 viewDir = normalize(uViewPos - FragPos);
|
||||
vec3 halfDir = normalize(lightDir + viewDir);
|
||||
float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
|
||||
vec3 specular = spec * uLightColor * uSpecularIntensity;
|
||||
// Blinn-Phong specular
|
||||
vec3 viewDir = normalize(uViewPos - FragPos);
|
||||
vec3 halfDir = normalize(lightDir + viewDir);
|
||||
float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
|
||||
vec3 specular = spec * uLightColor * uSpecularIntensity;
|
||||
|
||||
// Shadow mapping
|
||||
float shadow = 1.0;
|
||||
if (uShadowEnabled) {
|
||||
vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0);
|
||||
vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5;
|
||||
if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) {
|
||||
float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5));
|
||||
float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist);
|
||||
float bias = max(0.005 * (1.0 - abs(dot(normal, lightDir))), 0.001);
|
||||
shadow = 0.0;
|
||||
vec2 texelSize = vec2(1.0 / 2048.0);
|
||||
for (int sx = -1; sx <= 1; sx++) {
|
||||
for (int sy = -1; sy <= 1; sy++) {
|
||||
shadow += texture(uShadowMap, vec3(proj.xy + vec2(sx, sy) * texelSize, proj.z - bias));
|
||||
// Shadow mapping
|
||||
float shadow = 1.0;
|
||||
if (uShadowEnabled) {
|
||||
vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0);
|
||||
vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5;
|
||||
if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) {
|
||||
float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5));
|
||||
float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist);
|
||||
float bias = max(0.005 * (1.0 - abs(dot(normal, lightDir))), 0.001);
|
||||
shadow = 0.0;
|
||||
vec2 texelSize = vec2(1.0 / 2048.0);
|
||||
for (int sx = -1; sx <= 1; sx++) {
|
||||
for (int sy = -1; sy <= 1; sy++) {
|
||||
shadow += texture(uShadowMap, vec3(proj.xy + vec2(sx, sy) * texelSize, proj.z - bias));
|
||||
}
|
||||
}
|
||||
shadow /= 9.0;
|
||||
shadow = mix(1.0, shadow, coverageFade);
|
||||
}
|
||||
shadow /= 9.0;
|
||||
shadow = mix(1.0, shadow, coverageFade);
|
||||
}
|
||||
shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0));
|
||||
|
||||
vec3 ambient = uAmbientColor * texColor.rgb;
|
||||
vec3 diffuse = diff * texColor.rgb;
|
||||
|
||||
result = ambient + (diffuse + specular) * shadow;
|
||||
}
|
||||
shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0));
|
||||
|
||||
vec3 ambient = uAmbientColor * texColor.rgb;
|
||||
vec3 diffuse = diff * texColor.rgb;
|
||||
|
||||
vec3 result = ambient + (diffuse + specular) * shadow;
|
||||
|
||||
// Fog
|
||||
float fogDist = length(uViewPos - FragPos);
|
||||
|
|
@ -1487,6 +1496,14 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
|||
shader->setUniform("uModel", instance.modelMatrix);
|
||||
shader->setUniform("uFadeAlpha", fadeAlpha);
|
||||
|
||||
// Dim M2 objects inside WMO interiors
|
||||
bool interior = false;
|
||||
if (wmoRenderer && entry.distSq < 200.0f * 200.0f) {
|
||||
interior = wmoRenderer->isInsideInteriorWMO(
|
||||
instance.position.x, instance.position.y, instance.position.z);
|
||||
}
|
||||
shader->setUniform("uInteriorDarken", interior);
|
||||
|
||||
// Upload bone matrices if model has skeletal animation
|
||||
bool useBones = model.hasAnimation && !model.disableAnimation && !instance.boneMatrices.empty();
|
||||
shader->setUniform("uUseBones", useBones);
|
||||
|
|
|
|||
|
|
@ -185,6 +185,9 @@ bool Renderer::initialize(core::Window* win) {
|
|||
|
||||
// Create M2 renderer (for doodads)
|
||||
m2Renderer = std::make_unique<M2Renderer>();
|
||||
if (wmoRenderer) {
|
||||
m2Renderer->setWMORenderer(wmoRenderer.get());
|
||||
}
|
||||
// Note: M2 renderer needs asset manager, will be initialized when terrain loads
|
||||
|
||||
// Create zone manager
|
||||
|
|
|
|||
|
|
@ -109,9 +109,12 @@ bool WMORenderer::initialize(pipeline::AssetManager* assets) {
|
|||
texColor = texture(uTexture, TexCoord);
|
||||
// Alpha test only for cutout materials (lattice, grating, etc.)
|
||||
if (uAlphaTest && texColor.a < 0.5) discard;
|
||||
// Multiply vertex color (MOCV baked lighting/AO) into texture
|
||||
texColor.rgb *= VertexColor.rgb;
|
||||
alpha = texColor.a;
|
||||
// Exterior: multiply vertex color (MOCV baked AO) into texture
|
||||
// Interior: keep texture clean — vertex color is used as light below
|
||||
if (!uIsInterior) {
|
||||
texColor.rgb *= VertexColor.rgb;
|
||||
}
|
||||
} else {
|
||||
// MOCV vertex color alpha is a lighting blend factor, not transparency
|
||||
texColor = vec4(VertexColor.rgb, 1.0);
|
||||
|
|
@ -130,56 +133,56 @@ bool WMORenderer::initialize(pipeline::AssetManager* assets) {
|
|||
vec3 normal = normalize(Normal);
|
||||
vec3 lightDir = normalize(uLightDir);
|
||||
|
||||
// Interior vs exterior lighting
|
||||
vec3 ambient;
|
||||
float dirScale;
|
||||
vec3 litColor;
|
||||
if (uIsInterior) {
|
||||
ambient = vec3(0.7, 0.7, 0.7);
|
||||
dirScale = 0.3;
|
||||
// Interior: MOCV vertex colors are baked lighting.
|
||||
// Use them directly as the light multiplier on the texture.
|
||||
vec3 vertLight = VertexColor.rgb * 2.2 + 0.3;
|
||||
// Subtle directional fill so geometry reads
|
||||
float diff = max(dot(normal, lightDir), 0.0);
|
||||
vertLight += diff * 0.10;
|
||||
litColor = texColor.rgb * vertLight;
|
||||
} else {
|
||||
ambient = uAmbientColor;
|
||||
dirScale = 1.0;
|
||||
}
|
||||
// Exterior: standard diffuse + specular lighting
|
||||
vec3 ambient = uAmbientColor;
|
||||
|
||||
// Diffuse lighting
|
||||
float diff = max(dot(normal, lightDir), 0.0);
|
||||
vec3 diffuse = diff * vec3(1.0) * dirScale;
|
||||
float diff = max(dot(normal, lightDir), 0.0);
|
||||
vec3 diffuse = diff * vec3(1.0);
|
||||
|
||||
// Blinn-Phong specular
|
||||
vec3 viewDir = normalize(uViewPos - FragPos);
|
||||
vec3 halfDir = normalize(lightDir + viewDir);
|
||||
float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
|
||||
vec3 specular = spec * uLightColor * uSpecularIntensity * dirScale;
|
||||
vec3 viewDir = normalize(uViewPos - FragPos);
|
||||
vec3 halfDir = normalize(lightDir + viewDir);
|
||||
float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
|
||||
vec3 specular = spec * uLightColor * uSpecularIntensity;
|
||||
|
||||
// Shadow mapping
|
||||
float shadow = 1.0;
|
||||
if (uShadowEnabled) {
|
||||
vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0);
|
||||
vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5;
|
||||
if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) {
|
||||
float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5));
|
||||
float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist);
|
||||
float bias = max(0.005 * (1.0 - dot(normal, lightDir)), 0.001);
|
||||
shadow = 0.0;
|
||||
vec2 texelSize = vec2(1.0 / 2048.0);
|
||||
for (int sx = -1; sx <= 1; sx++) {
|
||||
for (int sy = -1; sy <= 1; sy++) {
|
||||
shadow += texture(uShadowMap, vec3(proj.xy + vec2(sx, sy) * texelSize, proj.z - bias));
|
||||
// Shadow mapping
|
||||
float shadow = 1.0;
|
||||
if (uShadowEnabled) {
|
||||
vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0);
|
||||
vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5;
|
||||
if (proj.z <= 1.0 && proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0) {
|
||||
float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5));
|
||||
float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist);
|
||||
float bias = max(0.005 * (1.0 - dot(normal, lightDir)), 0.001);
|
||||
shadow = 0.0;
|
||||
vec2 texelSize = vec2(1.0 / 2048.0);
|
||||
for (int sx = -1; sx <= 1; sx++) {
|
||||
for (int sy = -1; sy <= 1; sy++) {
|
||||
shadow += texture(uShadowMap, vec3(proj.xy + vec2(sx, sy) * texelSize, proj.z - bias));
|
||||
}
|
||||
}
|
||||
shadow /= 9.0;
|
||||
shadow = mix(1.0, shadow, coverageFade);
|
||||
}
|
||||
shadow /= 9.0;
|
||||
shadow = mix(1.0, shadow, coverageFade);
|
||||
}
|
||||
}
|
||||
shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0));
|
||||
shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0));
|
||||
|
||||
// Combine lighting with texture
|
||||
vec3 result = (ambient + (diffuse + specular) * shadow) * texColor.rgb;
|
||||
litColor = (ambient + (diffuse + specular) * shadow) * texColor.rgb;
|
||||
}
|
||||
|
||||
// Fog
|
||||
float fogDist = length(uViewPos - FragPos);
|
||||
float fogFactor = clamp((uFogEnd - fogDist) / (uFogEnd - uFogStart), 0.0, 1.0);
|
||||
result = mix(uFogColor, result, fogFactor);
|
||||
vec3 result = mix(uFogColor, litColor, fogFactor);
|
||||
|
||||
FragColor = vec4(result, alpha);
|
||||
}
|
||||
|
|
@ -1833,8 +1836,9 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
if (normalLen < 0.001f) continue;
|
||||
normal /= normalLen;
|
||||
|
||||
// Skip near-horizontal triangles (floors/ceilings).
|
||||
if (std::abs(normal.z) > 0.85f) continue;
|
||||
// Skip near-horizontal triangles (floors/ceilings/ramps).
|
||||
// Anything more horizontal than ~55° from vertical is walkable.
|
||||
if (std::abs(normal.z) > 0.55f) continue;
|
||||
|
||||
// Get triangle Z range
|
||||
float triMinZ = std::min({v0.z, v1.z, v2.z});
|
||||
|
|
@ -1852,9 +1856,6 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
// Skip low geometry that can be stepped over
|
||||
if (triMaxZ <= localFeetZ + MAX_STEP_HEIGHT) continue;
|
||||
|
||||
// Skip ramp surfaces (facing mostly upward) that are very low
|
||||
if (normal.z > 0.60f && triMaxZ <= localFeetZ + 0.8f) continue;
|
||||
|
||||
// Skip very short vertical surfaces (stair risers)
|
||||
if (triHeight < 0.6f && triMaxZ <= localFeetZ + 0.8f) continue;
|
||||
|
||||
|
|
@ -1960,6 +1961,51 @@ bool WMORenderer::isInsideWMO(float glX, float glY, float glZ, uint32_t* outMode
|
|||
return false;
|
||||
}
|
||||
|
||||
bool WMORenderer::isInsideInteriorWMO(float glX, float glY, float glZ) const {
|
||||
glm::vec3 queryMin(glX - 0.5f, glY - 0.5f, glZ - 0.5f);
|
||||
glm::vec3 queryMax(glX + 0.5f, glY + 0.5f, glZ + 0.5f);
|
||||
gatherCandidates(queryMin, queryMax, candidateScratch);
|
||||
|
||||
for (size_t idx : candidateScratch) {
|
||||
const auto& instance = instances[idx];
|
||||
if (collisionFocusEnabled &&
|
||||
pointAABBDistanceSq(collisionFocusPos, instance.worldBoundsMin, instance.worldBoundsMax) > collisionFocusRadiusSq) {
|
||||
continue;
|
||||
}
|
||||
if (glX < instance.worldBoundsMin.x || glX > instance.worldBoundsMax.x ||
|
||||
glY < instance.worldBoundsMin.y || glY > instance.worldBoundsMax.y ||
|
||||
glZ < instance.worldBoundsMin.z || glZ > instance.worldBoundsMax.z) {
|
||||
continue;
|
||||
}
|
||||
auto it = loadedModels.find(instance.modelId);
|
||||
if (it == loadedModels.end()) continue;
|
||||
const ModelData& model = it->second;
|
||||
|
||||
bool anyGroupContains = false;
|
||||
for (size_t gi = 0; gi < model.groups.size() && gi < instance.worldGroupBounds.size(); ++gi) {
|
||||
const auto& [gMin, gMax] = instance.worldGroupBounds[gi];
|
||||
if (glX >= gMin.x && glX <= gMax.x &&
|
||||
glY >= gMin.y && glY <= gMax.y &&
|
||||
glZ >= gMin.z && glZ <= gMax.z) {
|
||||
anyGroupContains = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!anyGroupContains) continue;
|
||||
|
||||
glm::vec3 localPos = glm::vec3(instance.invModelMatrix * glm::vec4(glX, glY, glZ, 1.0f));
|
||||
for (const auto& group : model.groups) {
|
||||
if (!(group.groupFlags & 0x2000)) continue; // Skip exterior groups
|
||||
if (localPos.x >= group.boundingBoxMin.x && localPos.x <= group.boundingBoxMax.x &&
|
||||
localPos.y >= group.boundingBoxMin.y && localPos.y <= group.boundingBoxMax.y &&
|
||||
localPos.z >= group.boundingBoxMin.z && localPos.z <= group.boundingBoxMax.z) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3& direction, float maxDistance) const {
|
||||
QueryTimer timer(&queryTimeMs, &queryCallCount);
|
||||
float closestHit = maxDistance;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue