2026-02-02 12:24:50 -08:00
|
|
|
|
#include "rendering/wmo_renderer.hpp"
|
2026-02-11 02:23:37 -08:00
|
|
|
|
#include "rendering/m2_renderer.hpp"
|
2026-02-21 19:41:21 -08:00
|
|
|
|
#include "rendering/vk_context.hpp"
|
|
|
|
|
|
#include "rendering/vk_texture.hpp"
|
|
|
|
|
|
#include "rendering/vk_buffer.hpp"
|
|
|
|
|
|
#include "rendering/vk_pipeline.hpp"
|
|
|
|
|
|
#include "rendering/vk_shader.hpp"
|
|
|
|
|
|
#include "rendering/vk_utils.hpp"
|
|
|
|
|
|
#include "rendering/vk_frame_data.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
|
#include "rendering/camera.hpp"
|
2026-02-02 23:03:45 -08:00
|
|
|
|
#include "rendering/frustum.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
|
#include "pipeline/wmo_loader.hpp"
|
|
|
|
|
|
#include "pipeline/asset_manager.hpp"
|
|
|
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
|
#include <glm/gtc/matrix_transform.hpp>
|
|
|
|
|
|
#include <glm/gtc/type_ptr.hpp>
|
|
|
|
|
|
#include <algorithm>
|
2026-02-03 16:21:48 -08:00
|
|
|
|
#include <chrono>
|
2026-02-03 16:04:21 -08:00
|
|
|
|
#include <cmath>
|
2026-02-22 07:26:54 -08:00
|
|
|
|
#include <cstdlib>
|
2026-02-05 17:20:30 -08:00
|
|
|
|
#include <filesystem>
|
|
|
|
|
|
#include <fstream>
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
#include <future>
|
2026-02-03 16:04:21 -08:00
|
|
|
|
#include <limits>
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
#include <thread>
|
2026-02-02 23:03:45 -08:00
|
|
|
|
#include <unordered_set>
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
namespace wowee {
|
|
|
|
|
|
namespace rendering {
|
|
|
|
|
|
|
2026-02-22 07:26:54 -08:00
|
|
|
|
namespace {
|
|
|
|
|
|
size_t envSizeMBOrDefault(const char* name, size_t defMb) {
|
|
|
|
|
|
const char* raw = std::getenv(name);
|
|
|
|
|
|
if (!raw || !*raw) return defMb;
|
|
|
|
|
|
char* end = nullptr;
|
|
|
|
|
|
unsigned long long mb = std::strtoull(raw, &end, 10);
|
|
|
|
|
|
if (end == raw || mb == 0) return defMb;
|
|
|
|
|
|
return static_cast<size_t>(mb);
|
|
|
|
|
|
}
|
2026-02-22 08:12:08 -08:00
|
|
|
|
|
|
|
|
|
|
size_t envSizeOrDefault(const char* name, size_t defValue) {
|
|
|
|
|
|
const char* raw = std::getenv(name);
|
|
|
|
|
|
if (!raw || !*raw) return defValue;
|
|
|
|
|
|
char* end = nullptr;
|
|
|
|
|
|
unsigned long long v = std::strtoull(raw, &end, 10);
|
|
|
|
|
|
if (end == raw || v == 0) return defValue;
|
|
|
|
|
|
return static_cast<size_t>(v);
|
|
|
|
|
|
}
|
2026-02-22 07:26:54 -08:00
|
|
|
|
} // namespace
|
|
|
|
|
|
|
2026-02-03 16:04:21 -08:00
|
|
|
|
static void transformAABB(const glm::mat4& modelMatrix,
|
|
|
|
|
|
const glm::vec3& localMin,
|
|
|
|
|
|
const glm::vec3& localMax,
|
|
|
|
|
|
glm::vec3& outMin,
|
|
|
|
|
|
glm::vec3& outMax);
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
WMORenderer::WMORenderer() {
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
WMORenderer::~WMORenderer() {
|
|
|
|
|
|
shutdown();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout,
|
|
|
|
|
|
pipeline::AssetManager* assets) {
|
2026-02-18 22:41:05 -08:00
|
|
|
|
if (initialized_) { assetManager = assets; return true; }
|
2026-02-21 19:41:21 -08:00
|
|
|
|
core::Logger::getInstance().info("Initializing WMO renderer (Vulkan)...");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
vkCtx_ = ctx;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
assetManager = assets;
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (!vkCtx_) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer: null VkContext");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 08:12:08 -08:00
|
|
|
|
const unsigned hc = std::thread::hardware_concurrency();
|
|
|
|
|
|
const size_t availableCores = (hc > 1u) ? static_cast<size_t>(hc - 1u) : 1ull;
|
|
|
|
|
|
// WMO culling is lighter than animation; keep defaults conservative to reduce spikes.
|
|
|
|
|
|
const size_t defaultCullThreads = std::max<size_t>(1, availableCores / 4);
|
|
|
|
|
|
numCullThreads_ = static_cast<uint32_t>(std::max<size_t>(
|
|
|
|
|
|
1, envSizeOrDefault("WOWEE_WMO_CULL_THREADS", defaultCullThreads)));
|
|
|
|
|
|
core::Logger::getInstance().info("WMO cull threads: ", numCullThreads_);
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkDevice device = vkCtx_->getDevice();
|
|
|
|
|
|
|
|
|
|
|
|
// --- Create material descriptor set layout (set 1) ---
|
|
|
|
|
|
// binding 0: sampler2D (diffuse texture)
|
|
|
|
|
|
// binding 1: uniform buffer (WMOMaterial)
|
2026-02-23 01:10:58 -08:00
|
|
|
|
// binding 2: sampler2D (normal+height map)
|
|
|
|
|
|
std::vector<VkDescriptorSetLayoutBinding> materialBindings(3);
|
2026-02-21 19:41:21 -08:00
|
|
|
|
materialBindings[0] = {};
|
|
|
|
|
|
materialBindings[0].binding = 0;
|
|
|
|
|
|
materialBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
materialBindings[0].descriptorCount = 1;
|
|
|
|
|
|
materialBindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
|
|
|
|
materialBindings[1] = {};
|
|
|
|
|
|
materialBindings[1].binding = 1;
|
|
|
|
|
|
materialBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
|
|
|
|
materialBindings[1].descriptorCount = 1;
|
|
|
|
|
|
materialBindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
2026-02-23 01:10:58 -08:00
|
|
|
|
materialBindings[2] = {};
|
|
|
|
|
|
materialBindings[2].binding = 2;
|
|
|
|
|
|
materialBindings[2].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
materialBindings[2].descriptorCount = 1;
|
|
|
|
|
|
materialBindings[2].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
|
|
|
|
|
|
materialSetLayout_ = createDescriptorSetLayout(device, materialBindings);
|
|
|
|
|
|
if (!materialSetLayout_) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer: failed to create material set layout");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// --- Create descriptor pool ---
|
|
|
|
|
|
VkDescriptorPoolSize poolSizes[] = {
|
2026-02-23 01:10:58 -08:00
|
|
|
|
{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, MAX_MATERIAL_SETS * 2 }, // diffuse + normal/height
|
2026-02-21 19:41:21 -08:00
|
|
|
|
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, MAX_MATERIAL_SETS },
|
|
|
|
|
|
};
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkDescriptorPoolCreateInfo poolInfo{};
|
|
|
|
|
|
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
|
|
|
|
|
poolInfo.maxSets = MAX_MATERIAL_SETS;
|
|
|
|
|
|
poolInfo.poolSizeCount = 2;
|
|
|
|
|
|
poolInfo.pPoolSizes = poolSizes;
|
2026-02-04 15:05:46 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &materialDescPool_) != VK_SUCCESS) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer: failed to create descriptor pool");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-02-09 18:13:05 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// --- Create pipeline layout ---
|
|
|
|
|
|
VkPushConstantRange pushRange{};
|
|
|
|
|
|
pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
|
|
|
|
|
|
pushRange.offset = 0;
|
|
|
|
|
|
pushRange.size = sizeof(GPUPushConstants);
|
2026-02-04 15:05:46 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
std::vector<VkDescriptorSetLayout> setLayouts = { perFrameLayout, materialSetLayout_ };
|
|
|
|
|
|
pipelineLayout_ = createPipelineLayout(device, setLayouts, { pushRange });
|
|
|
|
|
|
if (!pipelineLayout_) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer: failed to create pipeline layout");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-02-04 15:05:46 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// --- Load shaders ---
|
|
|
|
|
|
VkShaderModule vertShader, fragShader;
|
|
|
|
|
|
if (!vertShader.loadFromFile(device, "assets/shaders/wmo.vert.spv")) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer: failed to load vertex shader");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!fragShader.loadFromFile(device, "assets/shaders/wmo.frag.spv")) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer: failed to load fragment shader");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Vertex input ---
|
2026-02-23 01:10:58 -08:00
|
|
|
|
// WMO vertex: pos3 + normal3 + texCoord2 + color4 + tangent4 = 64 bytes
|
2026-02-21 19:41:21 -08:00
|
|
|
|
struct WMOVertexData {
|
|
|
|
|
|
glm::vec3 position;
|
|
|
|
|
|
glm::vec3 normal;
|
|
|
|
|
|
glm::vec2 texCoord;
|
|
|
|
|
|
glm::vec4 color;
|
2026-02-23 01:10:58 -08:00
|
|
|
|
glm::vec4 tangent; // xyz=tangent dir, w=handedness ±1
|
2026-02-21 19:41:21 -08:00
|
|
|
|
};
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkVertexInputBindingDescription vertexBinding{};
|
|
|
|
|
|
vertexBinding.binding = 0;
|
|
|
|
|
|
vertexBinding.stride = sizeof(WMOVertexData);
|
|
|
|
|
|
vertexBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
|
|
|
|
|
|
2026-02-23 01:10:58 -08:00
|
|
|
|
std::vector<VkVertexInputAttributeDescription> vertexAttribs(5);
|
2026-02-21 19:41:21 -08:00
|
|
|
|
vertexAttribs[0] = { 0, 0, VK_FORMAT_R32G32B32_SFLOAT,
|
|
|
|
|
|
static_cast<uint32_t>(offsetof(WMOVertexData, position)) };
|
|
|
|
|
|
vertexAttribs[1] = { 1, 0, VK_FORMAT_R32G32B32_SFLOAT,
|
|
|
|
|
|
static_cast<uint32_t>(offsetof(WMOVertexData, normal)) };
|
|
|
|
|
|
vertexAttribs[2] = { 2, 0, VK_FORMAT_R32G32_SFLOAT,
|
|
|
|
|
|
static_cast<uint32_t>(offsetof(WMOVertexData, texCoord)) };
|
|
|
|
|
|
vertexAttribs[3] = { 3, 0, VK_FORMAT_R32G32B32A32_SFLOAT,
|
|
|
|
|
|
static_cast<uint32_t>(offsetof(WMOVertexData, color)) };
|
2026-02-23 01:10:58 -08:00
|
|
|
|
vertexAttribs[4] = { 4, 0, VK_FORMAT_R32G32B32A32_SFLOAT,
|
|
|
|
|
|
static_cast<uint32_t>(offsetof(WMOVertexData, tangent)) };
|
2026-02-21 19:41:21 -08:00
|
|
|
|
|
|
|
|
|
|
// --- Build opaque pipeline ---
|
|
|
|
|
|
VkRenderPass mainPass = vkCtx_->getImGuiRenderPass();
|
|
|
|
|
|
|
|
|
|
|
|
opaquePipeline_ = PipelineBuilder()
|
|
|
|
|
|
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
|
|
|
|
|
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
|
|
|
|
|
.setVertexInput({ vertexBinding }, vertexAttribs)
|
|
|
|
|
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
|
|
|
|
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
|
|
|
|
|
.setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL)
|
|
|
|
|
|
.setColorBlendAttachment(PipelineBuilder::blendDisabled())
|
2026-02-22 02:59:24 -08:00
|
|
|
|
.setMultisample(vkCtx_->getMsaaSamples())
|
2026-02-21 19:41:21 -08:00
|
|
|
|
.setLayout(pipelineLayout_)
|
|
|
|
|
|
.setRenderPass(mainPass)
|
|
|
|
|
|
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
|
|
|
|
|
.build(device);
|
|
|
|
|
|
|
|
|
|
|
|
if (!opaquePipeline_) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer: failed to create opaque pipeline");
|
|
|
|
|
|
vertShader.destroy();
|
|
|
|
|
|
fragShader.destroy();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// --- Build transparent pipeline ---
|
|
|
|
|
|
transparentPipeline_ = PipelineBuilder()
|
|
|
|
|
|
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
|
|
|
|
|
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
|
|
|
|
|
.setVertexInput({ vertexBinding }, vertexAttribs)
|
|
|
|
|
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
|
|
|
|
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
|
|
|
|
|
.setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL)
|
|
|
|
|
|
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
|
2026-02-22 02:59:24 -08:00
|
|
|
|
.setMultisample(vkCtx_->getMsaaSamples())
|
2026-02-21 19:41:21 -08:00
|
|
|
|
.setLayout(pipelineLayout_)
|
|
|
|
|
|
.setRenderPass(mainPass)
|
|
|
|
|
|
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
|
|
|
|
|
.build(device);
|
|
|
|
|
|
|
|
|
|
|
|
if (!transparentPipeline_) {
|
|
|
|
|
|
core::Logger::getInstance().warning("WMORenderer: transparent pipeline not available");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 00:43:14 -08:00
|
|
|
|
// --- Build glass pipeline (alpha blend WITH depth write for windows) ---
|
|
|
|
|
|
glassPipeline_ = PipelineBuilder()
|
|
|
|
|
|
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
|
|
|
|
|
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
|
|
|
|
|
.setVertexInput({ vertexBinding }, vertexAttribs)
|
|
|
|
|
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
|
|
|
|
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
|
|
|
|
|
.setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL)
|
|
|
|
|
|
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
|
|
|
|
|
|
.setMultisample(vkCtx_->getMsaaSamples())
|
|
|
|
|
|
.setLayout(pipelineLayout_)
|
|
|
|
|
|
.setRenderPass(mainPass)
|
|
|
|
|
|
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
|
|
|
|
|
.build(device);
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// --- Build wireframe pipeline ---
|
|
|
|
|
|
wireframePipeline_ = PipelineBuilder()
|
|
|
|
|
|
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
|
|
|
|
|
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
|
|
|
|
|
.setVertexInput({ vertexBinding }, vertexAttribs)
|
|
|
|
|
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
|
|
|
|
|
.setRasterization(VK_POLYGON_MODE_LINE, VK_CULL_MODE_NONE)
|
|
|
|
|
|
.setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL)
|
|
|
|
|
|
.setColorBlendAttachment(PipelineBuilder::blendDisabled())
|
2026-02-22 02:59:24 -08:00
|
|
|
|
.setMultisample(vkCtx_->getMsaaSamples())
|
2026-02-21 19:41:21 -08:00
|
|
|
|
.setLayout(pipelineLayout_)
|
|
|
|
|
|
.setRenderPass(mainPass)
|
|
|
|
|
|
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
|
|
|
|
|
.build(device);
|
|
|
|
|
|
|
|
|
|
|
|
if (!wireframePipeline_) {
|
|
|
|
|
|
core::Logger::getInstance().warning("WMORenderer: wireframe pipeline not available");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
vertShader.destroy();
|
|
|
|
|
|
fragShader.destroy();
|
|
|
|
|
|
|
|
|
|
|
|
// --- Create fallback white texture ---
|
|
|
|
|
|
whiteTexture_ = std::make_unique<VkTexture>();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
uint8_t whitePixel[4] = {255, 255, 255, 255};
|
2026-02-21 19:41:21 -08:00
|
|
|
|
whiteTexture_->upload(*vkCtx_, whitePixel, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false);
|
|
|
|
|
|
whiteTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
|
|
|
|
|
VK_SAMPLER_ADDRESS_MODE_REPEAT);
|
2026-02-23 01:10:58 -08:00
|
|
|
|
|
|
|
|
|
|
// --- Create flat normal placeholder texture ---
|
|
|
|
|
|
// (128,128,255,128) = flat normal pointing up (0,0,1), mid-height
|
|
|
|
|
|
flatNormalTexture_ = std::make_unique<VkTexture>();
|
|
|
|
|
|
uint8_t flatNormalPixel[4] = {128, 128, 255, 128};
|
|
|
|
|
|
flatNormalTexture_->upload(*vkCtx_, flatNormalPixel, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false);
|
|
|
|
|
|
flatNormalTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
|
|
|
|
|
VK_SAMPLER_ADDRESS_MODE_REPEAT);
|
2026-02-22 07:26:54 -08:00
|
|
|
|
textureCacheBudgetBytes_ =
|
2026-02-27 04:59:12 -08:00
|
|
|
|
envSizeMBOrDefault("WOWEE_WMO_TEX_CACHE_MB", 8192) * 1024ull * 1024ull;
|
2026-02-22 07:26:54 -08:00
|
|
|
|
modelCacheLimit_ = envSizeMBOrDefault("WOWEE_WMO_MODEL_LIMIT", 4000);
|
|
|
|
|
|
core::Logger::getInstance().info("WMO texture cache budget: ",
|
|
|
|
|
|
textureCacheBudgetBytes_ / (1024 * 1024), " MB");
|
|
|
|
|
|
core::Logger::getInstance().info("WMO model cache limit: ", modelCacheLimit_);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
core::Logger::getInstance().info("WMO renderer initialized (Vulkan)");
|
2026-02-18 22:41:05 -08:00
|
|
|
|
initialized_ = true;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WMORenderer::shutdown() {
|
|
|
|
|
|
core::Logger::getInstance().info("Shutting down WMO renderer...");
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (!vkCtx_) {
|
|
|
|
|
|
loadedModels.clear();
|
|
|
|
|
|
instances.clear();
|
|
|
|
|
|
spatialGrid.clear();
|
|
|
|
|
|
instanceIndexById.clear();
|
|
|
|
|
|
initialized_ = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
VkDevice device = vkCtx_->getDevice();
|
|
|
|
|
|
VmaAllocator allocator = vkCtx_->getAllocator();
|
|
|
|
|
|
|
|
|
|
|
|
vkDeviceWaitIdle(device);
|
|
|
|
|
|
|
|
|
|
|
|
// Free all GPU resources for loaded models
|
2026-02-02 12:24:50 -08:00
|
|
|
|
for (auto& [id, model] : loadedModels) {
|
|
|
|
|
|
for (auto& group : model.groups) {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
destroyGroupGPU(group);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Free cached textures
|
2026-02-12 16:29:36 -08:00
|
|
|
|
for (auto& [path, entry] : textureCache) {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (entry.texture) entry.texture->destroy(device, allocator);
|
2026-02-23 01:10:58 -08:00
|
|
|
|
if (entry.normalHeightMap) entry.normalHeightMap->destroy(device, allocator);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
textureCache.clear();
|
2026-02-12 16:29:36 -08:00
|
|
|
|
textureCacheBytes_ = 0;
|
|
|
|
|
|
textureCacheCounter_ = 0;
|
2026-02-22 07:26:54 -08:00
|
|
|
|
failedTextureCache_.clear();
|
|
|
|
|
|
loggedTextureLoadFails_.clear();
|
|
|
|
|
|
textureBudgetRejectWarnings_ = 0;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-23 01:10:58 -08:00
|
|
|
|
// Free white texture and flat normal texture
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (whiteTexture_) { whiteTexture_->destroy(device, allocator); whiteTexture_.reset(); }
|
2026-02-23 01:10:58 -08:00
|
|
|
|
if (flatNormalTexture_) { flatNormalTexture_->destroy(device, allocator); flatNormalTexture_.reset(); }
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
loadedModels.clear();
|
|
|
|
|
|
instances.clear();
|
2026-02-03 16:21:48 -08:00
|
|
|
|
spatialGrid.clear();
|
|
|
|
|
|
instanceIndexById.clear();
|
2026-02-05 15:51:08 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Destroy pipelines
|
|
|
|
|
|
if (opaquePipeline_) { vkDestroyPipeline(device, opaquePipeline_, nullptr); opaquePipeline_ = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (transparentPipeline_) { vkDestroyPipeline(device, transparentPipeline_, nullptr); transparentPipeline_ = VK_NULL_HANDLE; }
|
2026-02-23 00:43:14 -08:00
|
|
|
|
if (glassPipeline_) { vkDestroyPipeline(device, glassPipeline_, nullptr); glassPipeline_ = VK_NULL_HANDLE; }
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (wireframePipeline_) { vkDestroyPipeline(device, wireframePipeline_, nullptr); wireframePipeline_ = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (pipelineLayout_) { vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); pipelineLayout_ = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (materialDescPool_) { vkDestroyDescriptorPool(device, materialDescPool_, nullptr); materialDescPool_ = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (materialSetLayout_) { vkDestroyDescriptorSetLayout(device, materialSetLayout_, nullptr); materialSetLayout_ = VK_NULL_HANDLE; }
|
|
|
|
|
|
|
|
|
|
|
|
// Destroy shadow resources
|
|
|
|
|
|
if (shadowPipeline_) { vkDestroyPipeline(device, shadowPipeline_, nullptr); shadowPipeline_ = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (shadowPipelineLayout_) { vkDestroyPipelineLayout(device, shadowPipelineLayout_, nullptr); shadowPipelineLayout_ = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (shadowParamsPool_) { vkDestroyDescriptorPool(device, shadowParamsPool_, nullptr); shadowParamsPool_ = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (shadowParamsLayout_) { vkDestroyDescriptorSetLayout(device, shadowParamsLayout_, nullptr); shadowParamsLayout_ = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (shadowParamsUBO_) { vmaDestroyBuffer(allocator, shadowParamsUBO_, shadowParamsAlloc_); shadowParamsUBO_ = VK_NULL_HANDLE; }
|
|
|
|
|
|
|
|
|
|
|
|
vkCtx_ = nullptr;
|
|
|
|
|
|
initialized_ = false;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|
|
|
|
|
if (!model.isValid()) {
|
|
|
|
|
|
core::Logger::getInstance().error("Cannot load invalid WMO model");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if already loaded
|
2026-02-18 23:02:59 -08:00
|
|
|
|
auto existingIt = loadedModels.find(id);
|
|
|
|
|
|
if (existingIt != loadedModels.end()) {
|
|
|
|
|
|
// If a model was first loaded while texture resolution failed (or before
|
|
|
|
|
|
// assets were fully available), it can remain permanently white because
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// merged batches cache texture pointers at load time. Do a one-time reload for
|
2026-02-18 23:02:59 -08:00
|
|
|
|
// models that have texture paths but no resolved non-white textures.
|
|
|
|
|
|
if (assetManager && !model.textures.empty()) {
|
|
|
|
|
|
bool hasResolvedTexture = false;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
for (VkTexture* tex : existingIt->second.textures) {
|
|
|
|
|
|
if (tex != nullptr && tex != whiteTexture_.get()) {
|
2026-02-18 23:02:59 -08:00
|
|
|
|
hasResolvedTexture = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
static std::unordered_set<uint32_t> retryReloadedModels;
|
2026-02-22 07:26:54 -08:00
|
|
|
|
static bool retryReloadedModelsCapped = false;
|
|
|
|
|
|
if (retryReloadedModels.size() > 8192) {
|
|
|
|
|
|
retryReloadedModels.clear();
|
|
|
|
|
|
if (!retryReloadedModelsCapped) {
|
|
|
|
|
|
core::Logger::getInstance().warning("WMO fallback-retry set exceeded 8192 entries; reset");
|
|
|
|
|
|
retryReloadedModelsCapped = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-18 23:02:59 -08:00
|
|
|
|
if (!hasResolvedTexture && retryReloadedModels.insert(id).second) {
|
|
|
|
|
|
core::Logger::getInstance().warning(
|
|
|
|
|
|
"WMO model ", id,
|
|
|
|
|
|
" has only fallback textures; forcing one-time reload");
|
|
|
|
|
|
unloadModel(id);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
2026-02-22 07:26:54 -08:00
|
|
|
|
if (loadedModels.size() >= modelCacheLimit_) {
|
2026-02-23 04:32:58 -08:00
|
|
|
|
if (modelLimitRejectWarnings_ < 3) {
|
2026-02-22 07:26:54 -08:00
|
|
|
|
core::Logger::getInstance().warning("WMO model cache full (",
|
|
|
|
|
|
loadedModels.size(), "/", modelCacheLimit_,
|
|
|
|
|
|
"), skipping model load: id=", id);
|
|
|
|
|
|
}
|
|
|
|
|
|
++modelLimitRejectWarnings_;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-11 22:27:02 -08:00
|
|
|
|
core::Logger::getInstance().debug("Loading WMO model ", id, " with ", model.groups.size(), " groups, ",
|
|
|
|
|
|
model.textures.size(), " textures...");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
ModelData modelData;
|
|
|
|
|
|
modelData.id = id;
|
|
|
|
|
|
modelData.boundingBoxMin = model.boundingBoxMin;
|
|
|
|
|
|
modelData.boundingBoxMax = model.boundingBoxMax;
|
2026-02-07 20:51:53 -08:00
|
|
|
|
{
|
|
|
|
|
|
glm::vec3 ext = model.boundingBoxMax - model.boundingBoxMin;
|
|
|
|
|
|
float horiz = std::max(ext.x, ext.y);
|
|
|
|
|
|
float vert = ext.z;
|
|
|
|
|
|
modelData.isLowPlatform = (vert < 6.0f && horiz > 20.0f);
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-11 22:27:02 -08:00
|
|
|
|
core::Logger::getInstance().debug(" WMO bounds: min=(", model.boundingBoxMin.x, ", ", model.boundingBoxMin.y, ", ", model.boundingBoxMin.z,
|
|
|
|
|
|
") max=(", model.boundingBoxMax.x, ", ", model.boundingBoxMax.y, ", ", model.boundingBoxMax.z, ")");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Load textures for this model
|
2026-02-11 22:27:02 -08:00
|
|
|
|
core::Logger::getInstance().debug(" WMO has ", model.textures.size(), " texture paths, ", model.materials.size(), " materials");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
if (assetManager && !model.textures.empty()) {
|
|
|
|
|
|
for (size_t i = 0; i < model.textures.size(); i++) {
|
|
|
|
|
|
const auto& texPath = model.textures[i];
|
|
|
|
|
|
core::Logger::getInstance().debug(" Loading texture ", i, ": ", texPath);
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkTexture* tex = loadTexture(texPath);
|
|
|
|
|
|
modelData.textures.push_back(tex);
|
2026-02-23 03:29:07 -08:00
|
|
|
|
// Store lowercase texture name for material detection
|
|
|
|
|
|
std::string lowerPath = texPath;
|
|
|
|
|
|
std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(),
|
|
|
|
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
|
|
|
|
modelData.textureNames.push_back(lowerPath);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
2026-02-11 22:27:02 -08:00
|
|
|
|
core::Logger::getInstance().debug(" Loaded ", modelData.textures.size(), " textures for WMO");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Store material -> texture index mapping
|
|
|
|
|
|
// IMPORTANT: mat.texture1 is a byte offset into MOTX, not an array index!
|
|
|
|
|
|
// We need to convert it using the textureOffsetToIndex map
|
2026-02-11 22:27:02 -08:00
|
|
|
|
core::Logger::getInstance().debug(" textureOffsetToIndex map has ", model.textureOffsetToIndex.size(), " entries");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
static int matLogCount = 0;
|
2026-02-18 23:00:46 -08:00
|
|
|
|
auto resolveTextureIndex = [&](uint32_t textureField) -> uint32_t {
|
|
|
|
|
|
auto it = model.textureOffsetToIndex.find(textureField);
|
|
|
|
|
|
if (it != model.textureOffsetToIndex.end()) {
|
|
|
|
|
|
return it->second;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Some files may store direct index instead of MOTX byte offset.
|
|
|
|
|
|
if (textureField < model.textures.size()) {
|
|
|
|
|
|
return textureField;
|
|
|
|
|
|
}
|
|
|
|
|
|
return std::numeric_limits<uint32_t>::max();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
for (size_t i = 0; i < model.materials.size(); i++) {
|
|
|
|
|
|
const auto& mat = model.materials[i];
|
|
|
|
|
|
uint32_t texIndex = 0; // Default to first texture
|
2026-02-18 23:00:46 -08:00
|
|
|
|
const uint32_t t1 = resolveTextureIndex(mat.texture1);
|
|
|
|
|
|
const uint32_t t2 = resolveTextureIndex(mat.texture2);
|
|
|
|
|
|
const uint32_t t3 = resolveTextureIndex(mat.texture3);
|
|
|
|
|
|
|
|
|
|
|
|
// Prefer first valid non-empty texture among texture1/2/3.
|
|
|
|
|
|
auto pickValid = [&](uint32_t idx) -> bool {
|
|
|
|
|
|
if (idx == std::numeric_limits<uint32_t>::max()) return false;
|
|
|
|
|
|
if (idx >= model.textures.size()) return false;
|
|
|
|
|
|
if (model.textures[idx].empty()) return false;
|
|
|
|
|
|
texIndex = idx;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
if (!pickValid(t1)) {
|
|
|
|
|
|
if (!pickValid(t2)) {
|
|
|
|
|
|
pickValid(t3);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-18 23:00:46 -08:00
|
|
|
|
if (matLogCount < 20) {
|
|
|
|
|
|
core::Logger::getInstance().debug(" Material ", i,
|
|
|
|
|
|
": tex1=", mat.texture1, "->", t1,
|
|
|
|
|
|
" tex2=", mat.texture2, "->", t2,
|
|
|
|
|
|
" tex3=", mat.texture3, "->", t3,
|
|
|
|
|
|
" chosen=", texIndex);
|
|
|
|
|
|
matLogCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
modelData.materialTextureIndices.push_back(texIndex);
|
|
|
|
|
|
modelData.materialBlendModes.push_back(mat.blendMode);
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
modelData.materialFlags.push_back(mat.flags);
|
2026-02-23 03:23:18 -08:00
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 01:54:05 -08:00
|
|
|
|
// Helper: look up group name from MOGN raw data via MOGI nameOffset
|
|
|
|
|
|
auto getGroupName = [&](uint32_t groupIdx) -> std::string {
|
|
|
|
|
|
if (groupIdx < model.groupInfo.size()) {
|
|
|
|
|
|
int32_t nameOff = model.groupInfo[groupIdx].nameOffset;
|
|
|
|
|
|
if (nameOff >= 0 && static_cast<size_t>(nameOff) < model.groupNameRaw.size()) {
|
|
|
|
|
|
const char* str = reinterpret_cast<const char*>(model.groupNameRaw.data() + nameOff);
|
|
|
|
|
|
size_t maxLen = model.groupNameRaw.size() - nameOff;
|
|
|
|
|
|
return std::string(str, strnlen(str, maxLen));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return {};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
// Create GPU resources for each group
|
|
|
|
|
|
uint32_t loadedGroups = 0;
|
2026-02-23 01:54:05 -08:00
|
|
|
|
for (size_t gi = 0; gi < model.groups.size(); gi++) {
|
|
|
|
|
|
const auto& wmoGroup = model.groups[gi];
|
2026-02-02 12:24:50 -08:00
|
|
|
|
// Skip empty groups
|
|
|
|
|
|
if (wmoGroup.vertices.empty() || wmoGroup.indices.empty()) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
GroupResources resources;
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
if (createGroupResources(wmoGroup, resources, wmoGroup.flags)) {
|
2026-02-23 03:23:18 -08:00
|
|
|
|
// Detect distance-only LOD/exterior shell groups:
|
|
|
|
|
|
// 1. Very low vertex count (<100) — portal connectors, tiny shells
|
|
|
|
|
|
// 2. ALWAYS_DRAW (0x10000) with low verts — distant LOD stand-ins
|
|
|
|
|
|
// 3. Pure OUTDOOR groups (0x8 set, 0x2000 not set) in large WMOs —
|
|
|
|
|
|
// exterior cityscape shells (e.g. "city01" in Stormwind)
|
|
|
|
|
|
bool alwaysDraw = (wmoGroup.flags & 0x10000) != 0;
|
|
|
|
|
|
size_t nVerts = wmoGroup.vertices.size();
|
|
|
|
|
|
bool isLargeWmo = model.nGroups > 50;
|
|
|
|
|
|
// Detect facade groups by name (exterior face of buildings)
|
2026-02-23 01:54:05 -08:00
|
|
|
|
std::string gname = getGroupName(static_cast<uint32_t>(gi));
|
2026-02-23 03:23:18 -08:00
|
|
|
|
bool isFacade = false;
|
|
|
|
|
|
bool isCityShell = false;
|
2026-02-23 01:54:05 -08:00
|
|
|
|
if (!gname.empty()) {
|
|
|
|
|
|
std::string lower = gname;
|
|
|
|
|
|
std::transform(lower.begin(), lower.end(), lower.begin(),
|
|
|
|
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
2026-02-23 03:23:18 -08:00
|
|
|
|
isFacade = lower.find("facade") != std::string::npos;
|
|
|
|
|
|
// "city01" etc are exterior cityscape shells in large WMOs
|
|
|
|
|
|
isCityShell = (lower.find("city") == 0 && lower.size() <= 8);
|
|
|
|
|
|
}
|
|
|
|
|
|
bool isIndoor = (wmoGroup.flags & 0x2000) != 0;
|
2026-03-04 19:47:01 -08:00
|
|
|
|
if ((nVerts < 100 && isLargeWmo && !isIndoor) || (alwaysDraw && nVerts < 5000 && isLargeWmo && !isIndoor) || (isFacade && isLargeWmo && !isIndoor) || (isCityShell && !isIndoor && isLargeWmo)) {
|
2026-02-23 03:23:18 -08:00
|
|
|
|
resources.isLOD = true;
|
2026-02-23 01:54:05 -08:00
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
modelData.groups.push_back(resources);
|
|
|
|
|
|
loadedGroups++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (loadedGroups == 0) {
|
|
|
|
|
|
core::Logger::getInstance().warning("No valid groups loaded for WMO ", id);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 17:20:30 -08:00
|
|
|
|
// Build pre-merged batches for each group (texture-sorted for efficient rendering)
|
|
|
|
|
|
for (auto& groupRes : modelData.groups) {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Use pointer value as key for batching
|
|
|
|
|
|
struct BatchKey {
|
|
|
|
|
|
uintptr_t texPtr;
|
|
|
|
|
|
bool alphaTest;
|
|
|
|
|
|
bool unlit;
|
2026-02-23 01:54:05 -08:00
|
|
|
|
bool isWindow;
|
|
|
|
|
|
bool operator==(const BatchKey& o) const { return texPtr == o.texPtr && alphaTest == o.alphaTest && unlit == o.unlit && isWindow == o.isWindow; }
|
2026-02-21 19:41:21 -08:00
|
|
|
|
};
|
|
|
|
|
|
struct BatchKeyHash {
|
|
|
|
|
|
size_t operator()(const BatchKey& k) const {
|
2026-02-23 01:54:05 -08:00
|
|
|
|
return std::hash<uintptr_t>()(k.texPtr) ^ (std::hash<bool>()(k.alphaTest) << 1) ^ (std::hash<bool>()(k.unlit) << 2) ^ (std::hash<bool>()(k.isWindow) << 3);
|
2026-02-21 19:41:21 -08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
std::unordered_map<BatchKey, GroupResources::MergedBatch, BatchKeyHash> batchMap;
|
2026-02-05 17:20:30 -08:00
|
|
|
|
|
|
|
|
|
|
for (const auto& batch : groupRes.batches) {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkTexture* tex = whiteTexture_.get();
|
2026-02-05 17:20:30 -08:00
|
|
|
|
bool hasTexture = false;
|
|
|
|
|
|
|
|
|
|
|
|
if (batch.materialId < modelData.materialTextureIndices.size()) {
|
|
|
|
|
|
uint32_t texIndex = modelData.materialTextureIndices[batch.materialId];
|
|
|
|
|
|
if (texIndex < modelData.textures.size()) {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
tex = modelData.textures[texIndex];
|
|
|
|
|
|
hasTexture = (tex != nullptr && tex != whiteTexture_.get());
|
|
|
|
|
|
if (!tex) tex = whiteTexture_.get();
|
2026-02-05 17:20:30 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool alphaTest = false;
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
uint32_t blendMode = 0;
|
2026-02-05 17:20:30 -08:00
|
|
|
|
if (batch.materialId < modelData.materialBlendModes.size()) {
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
blendMode = modelData.materialBlendModes[batch.materialId];
|
|
|
|
|
|
alphaTest = (blendMode == 1);
|
2026-02-05 17:20:30 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
bool unlit = false;
|
2026-02-14 21:15:28 -08:00
|
|
|
|
uint32_t matFlags = 0;
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
if (batch.materialId < modelData.materialFlags.size()) {
|
2026-02-14 21:15:28 -08:00
|
|
|
|
matFlags = modelData.materialFlags[batch.materialId];
|
|
|
|
|
|
unlit = (matFlags & 0x01) != 0;
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 03:29:07 -08:00
|
|
|
|
// 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
|
|
|
|
|
|
// distinguish actual glass from lamp post geometry.
|
|
|
|
|
|
bool isWindow = 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-14 21:15:28 -08:00
|
|
|
|
|
2026-02-23 01:54:05 -08:00
|
|
|
|
BatchKey key{ reinterpret_cast<uintptr_t>(tex), alphaTest, unlit, isWindow };
|
2026-02-05 17:20:30 -08:00
|
|
|
|
auto& mb = batchMap[key];
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (mb.draws.empty()) {
|
|
|
|
|
|
mb.texture = tex;
|
2026-02-05 17:20:30 -08:00
|
|
|
|
mb.hasTexture = hasTexture;
|
|
|
|
|
|
mb.alphaTest = alphaTest;
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
mb.unlit = unlit;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
mb.isTransparent = (blendMode >= 2);
|
2026-02-23 00:43:14 -08:00
|
|
|
|
mb.isWindow = isWindow;
|
2026-02-23 01:10:58 -08:00
|
|
|
|
// Look up normal/height map from texture cache
|
|
|
|
|
|
if (hasTexture && tex != whiteTexture_.get()) {
|
|
|
|
|
|
for (const auto& [cacheKey, cacheEntry] : textureCache) {
|
|
|
|
|
|
if (cacheEntry.texture.get() == tex) {
|
|
|
|
|
|
mb.normalHeightMap = cacheEntry.normalHeightMap.get();
|
|
|
|
|
|
mb.heightMapVariance = cacheEntry.heightMapVariance;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-05 17:20:30 -08:00
|
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
|
GroupResources::MergedBatch::DrawRange dr;
|
|
|
|
|
|
dr.firstIndex = batch.startIndex;
|
|
|
|
|
|
dr.indexCount = batch.indexCount;
|
|
|
|
|
|
mb.draws.push_back(dr);
|
2026-02-05 17:20:30 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Allocate descriptor sets and UBOs for each merged batch
|
2026-02-05 17:20:30 -08:00
|
|
|
|
groupRes.mergedBatches.reserve(batchMap.size());
|
Fix mount sounds, grey WMO meshes, taxi landing, tree animations, and classic dismount
- Per-family mount sounds (kodo, tallstrider, mechanostrider, etc.) detected from M2 model path
- Skip WMO groups with SHOW_SKYBOX flag or all-untextured batches (grey mesh in Orgrimmar)
- Freeze physics during taxi landing until terrain loads to prevent falling through void
- Disable bone animations on tropical vegetation (palm, bamboo, banana, etc.) to fix wiggling
- Snap player to final taxi waypoint on flight completion
- Extract mount aura spell ID from classic UNIT_FIELD_AURAS for CMSG_CANCEL_AURA dismount
- Increase /unstuck forward nudge to 5 units
2026-02-14 21:04:20 -08:00
|
|
|
|
bool anyTextured = false;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
bool isInterior = (groupRes.groupFlags & 0x2000) != 0;
|
2026-02-05 17:20:30 -08:00
|
|
|
|
for (auto& [key, mb] : batchMap) {
|
Fix mount sounds, grey WMO meshes, taxi landing, tree animations, and classic dismount
- Per-family mount sounds (kodo, tallstrider, mechanostrider, etc.) detected from M2 model path
- Skip WMO groups with SHOW_SKYBOX flag or all-untextured batches (grey mesh in Orgrimmar)
- Freeze physics during taxi landing until terrain loads to prevent falling through void
- Disable bone animations on tropical vegetation (palm, bamboo, banana, etc.) to fix wiggling
- Snap player to final taxi waypoint on flight completion
- Extract mount aura spell ID from classic UNIT_FIELD_AURAS for CMSG_CANCEL_AURA dismount
- Increase /unstuck forward nudge to 5 units
2026-02-14 21:04:20 -08:00
|
|
|
|
if (mb.hasTexture) anyTextured = true;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
|
|
|
|
|
|
// Create material UBO
|
|
|
|
|
|
VmaAllocator allocator = vkCtx_->getAllocator();
|
|
|
|
|
|
AllocatedBuffer matBuf = createBuffer(allocator, sizeof(WMOMaterialUBO),
|
|
|
|
|
|
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
|
|
|
|
|
|
VMA_MEMORY_USAGE_CPU_TO_GPU);
|
|
|
|
|
|
mb.materialUBO = matBuf.buffer;
|
|
|
|
|
|
mb.materialUBOAlloc = matBuf.allocation;
|
|
|
|
|
|
|
|
|
|
|
|
// Write material params
|
|
|
|
|
|
WMOMaterialUBO matData{};
|
|
|
|
|
|
matData.hasTexture = mb.hasTexture ? 1 : 0;
|
|
|
|
|
|
matData.alphaTest = mb.alphaTest ? 1 : 0;
|
|
|
|
|
|
matData.unlit = mb.unlit ? 1 : 0;
|
|
|
|
|
|
matData.isInterior = isInterior ? 1 : 0;
|
|
|
|
|
|
matData.specularIntensity = 0.5f;
|
2026-02-23 00:43:14 -08:00
|
|
|
|
matData.isWindow = mb.isWindow ? 1 : 0;
|
2026-02-23 01:10:58 -08:00
|
|
|
|
matData.enableNormalMap = normalMappingEnabled_ ? 1 : 0;
|
|
|
|
|
|
matData.enablePOM = pomEnabled_ ? 1 : 0;
|
2026-02-23 01:18:42 -08:00
|
|
|
|
matData.pomScale = 0.012f;
|
2026-02-23 01:10:58 -08:00
|
|
|
|
{
|
|
|
|
|
|
static const int pomSampleTable[] = { 16, 32, 64 };
|
|
|
|
|
|
matData.pomMaxSamples = pomSampleTable[std::clamp(pomQuality_, 0, 2)];
|
|
|
|
|
|
}
|
|
|
|
|
|
matData.heightMapVariance = mb.heightMapVariance;
|
2026-02-23 01:18:42 -08:00
|
|
|
|
matData.normalMapStrength = normalMapStrength_;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (matBuf.info.pMappedData) {
|
|
|
|
|
|
memcpy(matBuf.info.pMappedData, &matData, sizeof(matData));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Allocate and write descriptor set
|
|
|
|
|
|
mb.materialSet = allocateMaterialSet();
|
|
|
|
|
|
if (mb.materialSet) {
|
|
|
|
|
|
VkTexture* texToUse = mb.texture ? mb.texture : whiteTexture_.get();
|
|
|
|
|
|
VkDescriptorImageInfo imgInfo = texToUse->descriptorInfo();
|
|
|
|
|
|
|
|
|
|
|
|
VkDescriptorBufferInfo bufInfo{};
|
|
|
|
|
|
bufInfo.buffer = mb.materialUBO;
|
|
|
|
|
|
bufInfo.offset = 0;
|
|
|
|
|
|
bufInfo.range = sizeof(WMOMaterialUBO);
|
|
|
|
|
|
|
2026-02-23 01:10:58 -08:00
|
|
|
|
VkTexture* nhMap = mb.normalHeightMap ? mb.normalHeightMap : flatNormalTexture_.get();
|
|
|
|
|
|
VkDescriptorImageInfo nhImgInfo = nhMap->descriptorInfo();
|
|
|
|
|
|
|
|
|
|
|
|
VkWriteDescriptorSet writes[3] = {};
|
2026-02-21 19:41:21 -08:00
|
|
|
|
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
|
|
|
|
writes[0].dstSet = mb.materialSet;
|
|
|
|
|
|
writes[0].dstBinding = 0;
|
|
|
|
|
|
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
writes[0].descriptorCount = 1;
|
|
|
|
|
|
writes[0].pImageInfo = &imgInfo;
|
|
|
|
|
|
|
|
|
|
|
|
writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
|
|
|
|
writes[1].dstSet = mb.materialSet;
|
|
|
|
|
|
writes[1].dstBinding = 1;
|
|
|
|
|
|
writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
|
|
|
|
writes[1].descriptorCount = 1;
|
|
|
|
|
|
writes[1].pBufferInfo = &bufInfo;
|
|
|
|
|
|
|
2026-02-23 01:10:58 -08:00
|
|
|
|
writes[2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
|
|
|
|
writes[2].dstSet = mb.materialSet;
|
|
|
|
|
|
writes[2].dstBinding = 2;
|
|
|
|
|
|
writes[2].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
writes[2].descriptorCount = 1;
|
|
|
|
|
|
writes[2].pImageInfo = &nhImgInfo;
|
|
|
|
|
|
|
|
|
|
|
|
vkUpdateDescriptorSets(vkCtx_->getDevice(), 3, writes, 0, nullptr);
|
2026-02-21 19:41:21 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 17:20:30 -08:00
|
|
|
|
groupRes.mergedBatches.push_back(std::move(mb));
|
|
|
|
|
|
}
|
Fix mount sounds, grey WMO meshes, taxi landing, tree animations, and classic dismount
- Per-family mount sounds (kodo, tallstrider, mechanostrider, etc.) detected from M2 model path
- Skip WMO groups with SHOW_SKYBOX flag or all-untextured batches (grey mesh in Orgrimmar)
- Freeze physics during taxi landing until terrain loads to prevent falling through void
- Disable bone animations on tropical vegetation (palm, bamboo, banana, etc.) to fix wiggling
- Snap player to final taxi waypoint on flight completion
- Extract mount aura spell ID from classic UNIT_FIELD_AURAS for CMSG_CANCEL_AURA dismount
- Increase /unstuck forward nudge to 5 units
2026-02-14 21:04:20 -08:00
|
|
|
|
groupRes.allUntextured = !anyTextured && !groupRes.mergedBatches.empty();
|
2026-02-05 17:20:30 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 15:31:00 -08:00
|
|
|
|
// Copy portal data for visibility culling
|
|
|
|
|
|
modelData.portalVertices = model.portalVertices;
|
|
|
|
|
|
for (const auto& portal : model.portals) {
|
|
|
|
|
|
PortalData pd;
|
|
|
|
|
|
pd.startVertex = portal.startVertex;
|
|
|
|
|
|
pd.vertexCount = portal.vertexCount;
|
|
|
|
|
|
// Compute portal plane from vertices if we have them
|
|
|
|
|
|
if (portal.vertexCount >= 3 && portal.startVertex + portal.vertexCount <= model.portalVertices.size()) {
|
|
|
|
|
|
glm::vec3 v0 = model.portalVertices[portal.startVertex];
|
|
|
|
|
|
glm::vec3 v1 = model.portalVertices[portal.startVertex + 1];
|
|
|
|
|
|
glm::vec3 v2 = model.portalVertices[portal.startVertex + 2];
|
|
|
|
|
|
pd.normal = glm::normalize(glm::cross(v1 - v0, v2 - v0));
|
|
|
|
|
|
pd.distance = glm::dot(pd.normal, v0);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pd.normal = glm::vec3(0.0f, 0.0f, 1.0f);
|
|
|
|
|
|
pd.distance = 0.0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
modelData.portals.push_back(pd);
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const auto& ref : model.portalRefs) {
|
|
|
|
|
|
PortalRef pr;
|
|
|
|
|
|
pr.portalIndex = ref.portalIndex;
|
|
|
|
|
|
pr.groupIndex = ref.groupIndex;
|
|
|
|
|
|
pr.side = ref.side;
|
|
|
|
|
|
modelData.portalRefs.push_back(pr);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Build per-group portal ref ranges from WMOGroup data
|
|
|
|
|
|
modelData.groupPortalRefs.resize(model.groups.size(), {0, 0});
|
|
|
|
|
|
for (size_t gi = 0; gi < model.groups.size(); gi++) {
|
|
|
|
|
|
modelData.groupPortalRefs[gi] = {model.groups[gi].portalStart, model.groups[gi].portalCount};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!modelData.portals.empty()) {
|
2026-02-11 22:27:02 -08:00
|
|
|
|
core::Logger::getInstance().debug("WMO portals: ", modelData.portals.size(),
|
|
|
|
|
|
" refs: ", modelData.portalRefs.size());
|
2026-02-05 15:31:00 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 02:23:37 -08:00
|
|
|
|
// Store doodad templates (M2 models placed in WMO) for instancing later
|
|
|
|
|
|
if (!model.doodadSets.empty() && !model.doodads.empty()) {
|
|
|
|
|
|
const auto& doodadSet = model.doodadSets[0]; // Use first doodad set
|
|
|
|
|
|
for (uint32_t di = 0; di < doodadSet.count; di++) {
|
|
|
|
|
|
uint32_t doodadIdx = doodadSet.startIndex + di;
|
|
|
|
|
|
if (doodadIdx >= model.doodads.size()) break;
|
|
|
|
|
|
|
|
|
|
|
|
const auto& doodad = model.doodads[doodadIdx];
|
|
|
|
|
|
auto nameIt = model.doodadNames.find(doodad.nameIndex);
|
|
|
|
|
|
if (nameIt == model.doodadNames.end()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
std::string m2Path = nameIt->second;
|
|
|
|
|
|
if (m2Path.empty()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Convert .mdx/.mdl to .m2
|
|
|
|
|
|
if (m2Path.size() > 4) {
|
|
|
|
|
|
std::string ext = m2Path.substr(m2Path.size() - 4);
|
|
|
|
|
|
for (char& c : ext) c = std::tolower(c);
|
|
|
|
|
|
if (ext == ".mdx" || ext == ".mdl") {
|
|
|
|
|
|
m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Build doodad's local transform (WoW coordinates)
|
|
|
|
|
|
// WMO doodads use quaternion rotation (X/Y swapped for correct orientation)
|
|
|
|
|
|
glm::quat fixedRotation(doodad.rotation.w, doodad.rotation.y, doodad.rotation.x, doodad.rotation.z);
|
|
|
|
|
|
|
|
|
|
|
|
glm::mat4 localTransform(1.0f);
|
|
|
|
|
|
localTransform = glm::translate(localTransform, doodad.position);
|
|
|
|
|
|
localTransform *= glm::mat4_cast(fixedRotation);
|
|
|
|
|
|
localTransform = glm::scale(localTransform, glm::vec3(doodad.scale));
|
|
|
|
|
|
|
|
|
|
|
|
DoodadTemplate doodadTemplate;
|
|
|
|
|
|
doodadTemplate.m2Path = m2Path;
|
|
|
|
|
|
doodadTemplate.localTransform = localTransform;
|
|
|
|
|
|
modelData.doodadTemplates.push_back(doodadTemplate);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!modelData.doodadTemplates.empty()) {
|
2026-02-11 22:27:02 -08:00
|
|
|
|
core::Logger::getInstance().debug("WMO has ", modelData.doodadTemplates.size(), " doodad templates");
|
2026-02-11 02:23:37 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
loadedModels[id] = std::move(modelData);
|
2026-02-11 22:27:02 -08:00
|
|
|
|
core::Logger::getInstance().debug("WMO model ", id, " loaded successfully (", loadedGroups, " groups)");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WMORenderer::unloadModel(uint32_t id) {
|
|
|
|
|
|
auto it = loadedModels.find(id);
|
|
|
|
|
|
if (it == loadedModels.end()) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Free GPU resources
|
|
|
|
|
|
for (auto& group : it->second.groups) {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
destroyGroupGPU(group);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
loadedModels.erase(it);
|
|
|
|
|
|
core::Logger::getInstance().info("WMO model ", id, " unloaded");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 23:03:45 -08:00
|
|
|
|
void WMORenderer::cleanupUnusedModels() {
|
|
|
|
|
|
// Build set of model IDs that are still referenced by instances
|
|
|
|
|
|
std::unordered_set<uint32_t> usedModelIds;
|
|
|
|
|
|
for (const auto& instance : instances) {
|
|
|
|
|
|
usedModelIds.insert(instance.modelId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Find and remove models with no instances
|
|
|
|
|
|
std::vector<uint32_t> toRemove;
|
|
|
|
|
|
for (const auto& [id, model] : loadedModels) {
|
|
|
|
|
|
if (usedModelIds.find(id) == usedModelIds.end()) {
|
|
|
|
|
|
toRemove.push_back(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Delete GPU resources and remove from map
|
|
|
|
|
|
for (uint32_t id : toRemove) {
|
|
|
|
|
|
unloadModel(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!toRemove.empty()) {
|
|
|
|
|
|
core::Logger::getInstance().info("WMO cleanup: removed ", toRemove.size(), " unused models, ", loadedModels.size(), " remaining");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
uint32_t WMORenderer::createInstance(uint32_t modelId, const glm::vec3& position,
|
|
|
|
|
|
const glm::vec3& rotation, float scale) {
|
|
|
|
|
|
// Check if model is loaded
|
|
|
|
|
|
if (loadedModels.find(modelId) == loadedModels.end()) {
|
|
|
|
|
|
core::Logger::getInstance().error("Cannot create instance of unloaded WMO model ", modelId);
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
WMOInstance instance;
|
|
|
|
|
|
instance.id = nextInstanceId++;
|
|
|
|
|
|
instance.modelId = modelId;
|
|
|
|
|
|
instance.position = position;
|
|
|
|
|
|
instance.rotation = rotation;
|
|
|
|
|
|
instance.scale = scale;
|
|
|
|
|
|
instance.updateModelMatrix();
|
2026-02-03 16:21:48 -08:00
|
|
|
|
const ModelData& model = loadedModels[modelId];
|
|
|
|
|
|
transformAABB(instance.modelMatrix, model.boundingBoxMin, model.boundingBoxMax,
|
|
|
|
|
|
instance.worldBoundsMin, instance.worldBoundsMax);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-04 11:31:08 -08:00
|
|
|
|
// Pre-compute world-space group bounds to avoid per-frame transformAABB
|
|
|
|
|
|
instance.worldGroupBounds.reserve(model.groups.size());
|
|
|
|
|
|
for (const auto& group : model.groups) {
|
|
|
|
|
|
glm::vec3 gMin, gMax;
|
|
|
|
|
|
transformAABB(instance.modelMatrix, group.boundingBoxMin, group.boundingBoxMax, gMin, gMax);
|
|
|
|
|
|
gMin -= glm::vec3(0.5f);
|
|
|
|
|
|
gMax += glm::vec3(0.5f);
|
|
|
|
|
|
instance.worldGroupBounds.emplace_back(gMin, gMax);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
instances.push_back(instance);
|
2026-02-03 16:21:48 -08:00
|
|
|
|
size_t idx = instances.size() - 1;
|
|
|
|
|
|
instanceIndexById[instance.id] = idx;
|
|
|
|
|
|
GridCell minCell = toCell(instance.worldBoundsMin);
|
|
|
|
|
|
GridCell maxCell = toCell(instance.worldBoundsMax);
|
|
|
|
|
|
for (int z = minCell.z; z <= maxCell.z; z++) {
|
|
|
|
|
|
for (int y = minCell.y; y <= maxCell.y; y++) {
|
|
|
|
|
|
for (int x = minCell.x; x <= maxCell.x; x++) {
|
|
|
|
|
|
spatialGrid[GridCell{x, y, z}].push_back(instance.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-11 22:27:02 -08:00
|
|
|
|
core::Logger::getInstance().debug("Created WMO instance ", instance.id, " (model ", modelId, ")");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
return instance.id;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 00:59:40 -08:00
|
|
|
|
void WMORenderer::setInstancePosition(uint32_t instanceId, const glm::vec3& position) {
|
|
|
|
|
|
auto idxIt = instanceIndexById.find(instanceId);
|
|
|
|
|
|
if (idxIt == instanceIndexById.end()) return;
|
|
|
|
|
|
auto& inst = instances[idxIt->second];
|
|
|
|
|
|
inst.position = position;
|
|
|
|
|
|
inst.updateModelMatrix();
|
|
|
|
|
|
auto modelIt = loadedModels.find(inst.modelId);
|
|
|
|
|
|
if (modelIt != loadedModels.end()) {
|
|
|
|
|
|
const ModelData& model = modelIt->second;
|
|
|
|
|
|
transformAABB(inst.modelMatrix, model.boundingBoxMin, model.boundingBoxMax,
|
|
|
|
|
|
inst.worldBoundsMin, inst.worldBoundsMax);
|
|
|
|
|
|
inst.worldGroupBounds.clear();
|
|
|
|
|
|
inst.worldGroupBounds.reserve(model.groups.size());
|
|
|
|
|
|
for (const auto& group : model.groups) {
|
|
|
|
|
|
glm::vec3 gMin, gMax;
|
|
|
|
|
|
transformAABB(inst.modelMatrix, group.boundingBoxMin, group.boundingBoxMax, gMin, gMax);
|
|
|
|
|
|
gMin -= glm::vec3(0.5f);
|
|
|
|
|
|
gMax += glm::vec3(0.5f);
|
|
|
|
|
|
inst.worldGroupBounds.emplace_back(gMin, gMax);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
rebuildSpatialIndex();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 21:29:10 -08:00
|
|
|
|
void WMORenderer::setInstanceTransform(uint32_t instanceId, const glm::mat4& transform) {
|
|
|
|
|
|
auto idxIt = instanceIndexById.find(instanceId);
|
|
|
|
|
|
if (idxIt == instanceIndexById.end()) return;
|
|
|
|
|
|
auto& inst = instances[idxIt->second];
|
|
|
|
|
|
|
|
|
|
|
|
// Decompose transform to position/rotation/scale
|
|
|
|
|
|
inst.position = glm::vec3(transform[3]);
|
|
|
|
|
|
|
|
|
|
|
|
// Extract rotation (assuming uniform scale)
|
|
|
|
|
|
glm::mat3 rotationMatrix(transform);
|
|
|
|
|
|
float scaleX = glm::length(glm::vec3(transform[0]));
|
|
|
|
|
|
float scaleY = glm::length(glm::vec3(transform[1]));
|
|
|
|
|
|
float scaleZ = glm::length(glm::vec3(transform[2]));
|
|
|
|
|
|
inst.scale = scaleX; // Assume uniform scale
|
|
|
|
|
|
|
|
|
|
|
|
if (scaleX > 0.0001f) rotationMatrix[0] /= scaleX;
|
|
|
|
|
|
if (scaleY > 0.0001f) rotationMatrix[1] /= scaleY;
|
|
|
|
|
|
if (scaleZ > 0.0001f) rotationMatrix[2] /= scaleZ;
|
|
|
|
|
|
|
|
|
|
|
|
inst.rotation = glm::vec3(0.0f); // Euler angles not directly used, so zero them
|
|
|
|
|
|
|
|
|
|
|
|
// Update model matrix and bounds
|
|
|
|
|
|
inst.modelMatrix = transform;
|
|
|
|
|
|
inst.invModelMatrix = glm::inverse(transform);
|
|
|
|
|
|
|
|
|
|
|
|
auto modelIt = loadedModels.find(inst.modelId);
|
|
|
|
|
|
if (modelIt != loadedModels.end()) {
|
|
|
|
|
|
const ModelData& model = modelIt->second;
|
|
|
|
|
|
transformAABB(inst.modelMatrix, model.boundingBoxMin, model.boundingBoxMax,
|
|
|
|
|
|
inst.worldBoundsMin, inst.worldBoundsMax);
|
|
|
|
|
|
inst.worldGroupBounds.clear();
|
|
|
|
|
|
inst.worldGroupBounds.reserve(model.groups.size());
|
|
|
|
|
|
for (const auto& group : model.groups) {
|
|
|
|
|
|
glm::vec3 gMin, gMax;
|
|
|
|
|
|
transformAABB(inst.modelMatrix, group.boundingBoxMin, group.boundingBoxMax, gMin, gMax);
|
|
|
|
|
|
gMin -= glm::vec3(0.5f);
|
|
|
|
|
|
gMax += glm::vec3(0.5f);
|
|
|
|
|
|
inst.worldGroupBounds.emplace_back(gMin, gMax);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-11 02:23:37 -08:00
|
|
|
|
|
|
|
|
|
|
// Propagate transform to child M2 doodads (chairs, furniture on transports)
|
|
|
|
|
|
if (m2Renderer_ && !inst.doodads.empty()) {
|
|
|
|
|
|
for (const auto& doodad : inst.doodads) {
|
|
|
|
|
|
glm::mat4 worldTransform = inst.modelMatrix * doodad.localTransform;
|
|
|
|
|
|
m2Renderer_->setInstanceTransform(doodad.m2InstanceId, worldTransform);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 19:47:01 -08:00
|
|
|
|
rebuildSpatialIndex();
|
2026-02-11 02:23:37 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WMORenderer::addDoodadToInstance(uint32_t instanceId, uint32_t m2InstanceId, const glm::mat4& localTransform) {
|
|
|
|
|
|
auto it = std::find_if(instances.begin(), instances.end(),
|
|
|
|
|
|
[instanceId](const WMOInstance& inst) { return inst.id == instanceId; });
|
|
|
|
|
|
if (it != instances.end()) {
|
|
|
|
|
|
WMOInstance::DoodadInfo doodad;
|
|
|
|
|
|
doodad.m2InstanceId = m2InstanceId;
|
|
|
|
|
|
doodad.localTransform = localTransform;
|
|
|
|
|
|
it->doodads.push_back(doodad);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::vector<WMORenderer::DoodadTemplate>* WMORenderer::getDoodadTemplates(uint32_t modelId) const {
|
|
|
|
|
|
auto it = loadedModels.find(modelId);
|
|
|
|
|
|
if (it != loadedModels.end() && !it->second.doodadTemplates.empty()) {
|
|
|
|
|
|
return &it->second.doodadTemplates;
|
|
|
|
|
|
}
|
|
|
|
|
|
return nullptr;
|
2026-02-10 21:29:10 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
void WMORenderer::removeInstance(uint32_t instanceId) {
|
|
|
|
|
|
auto it = std::find_if(instances.begin(), instances.end(),
|
|
|
|
|
|
[instanceId](const WMOInstance& inst) { return inst.id == instanceId; });
|
|
|
|
|
|
if (it != instances.end()) {
|
2026-02-12 00:04:53 -08:00
|
|
|
|
if (m2Renderer_) {
|
|
|
|
|
|
for (const auto& doodad : it->doodads) {
|
|
|
|
|
|
m2Renderer_->removeInstance(doodad.m2InstanceId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
instances.erase(it);
|
2026-02-03 16:21:48 -08:00
|
|
|
|
rebuildSpatialIndex();
|
2026-02-11 22:27:02 -08:00
|
|
|
|
core::Logger::getInstance().debug("Removed WMO instance ", instanceId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WMORenderer::removeInstances(const std::vector<uint32_t>& instanceIds) {
|
|
|
|
|
|
if (instanceIds.empty() || instances.empty()) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::unordered_set<uint32_t> toRemove(instanceIds.begin(), instanceIds.end());
|
2026-02-12 00:04:53 -08:00
|
|
|
|
if (m2Renderer_) {
|
|
|
|
|
|
for (const auto& inst : instances) {
|
|
|
|
|
|
if (toRemove.find(inst.id) == toRemove.end()) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const auto& doodad : inst.doodads) {
|
|
|
|
|
|
m2Renderer_->removeInstance(doodad.m2InstanceId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 22:27:02 -08:00
|
|
|
|
const size_t oldSize = instances.size();
|
|
|
|
|
|
instances.erase(std::remove_if(instances.begin(), instances.end(),
|
|
|
|
|
|
[&toRemove](const WMOInstance& inst) {
|
|
|
|
|
|
return toRemove.find(inst.id) != toRemove.end();
|
|
|
|
|
|
}),
|
|
|
|
|
|
instances.end());
|
|
|
|
|
|
|
|
|
|
|
|
if (instances.size() != oldSize) {
|
|
|
|
|
|
rebuildSpatialIndex();
|
|
|
|
|
|
core::Logger::getInstance().debug("Removed ", (oldSize - instances.size()),
|
|
|
|
|
|
" WMO instances (batched)");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WMORenderer::clearInstances() {
|
2026-02-12 00:04:53 -08:00
|
|
|
|
if (m2Renderer_) {
|
|
|
|
|
|
for (const auto& inst : instances) {
|
|
|
|
|
|
for (const auto& doodad : inst.doodads) {
|
|
|
|
|
|
m2Renderer_->removeInstance(doodad.m2InstanceId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
instances.clear();
|
2026-02-03 16:21:48 -08:00
|
|
|
|
spatialGrid.clear();
|
|
|
|
|
|
instanceIndexById.clear();
|
2026-02-05 17:20:30 -08:00
|
|
|
|
precomputedFloorGrid.clear(); // Invalidate floor cache when instances change
|
2026-02-02 12:24:50 -08:00
|
|
|
|
core::Logger::getInstance().info("Cleared all WMO instances");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 04:59:12 -08:00
|
|
|
|
void WMORenderer::clearAll() {
|
|
|
|
|
|
clearInstances();
|
|
|
|
|
|
|
|
|
|
|
|
if (vkCtx_) {
|
|
|
|
|
|
VkDevice device = vkCtx_->getDevice();
|
|
|
|
|
|
VmaAllocator allocator = vkCtx_->getAllocator();
|
|
|
|
|
|
vkDeviceWaitIdle(device);
|
|
|
|
|
|
|
|
|
|
|
|
// Free GPU resources for loaded models
|
|
|
|
|
|
for (auto& [id, model] : loadedModels) {
|
|
|
|
|
|
for (auto& group : model.groups) {
|
|
|
|
|
|
destroyGroupGPU(group);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Free cached textures
|
|
|
|
|
|
for (auto& [path, entry] : textureCache) {
|
|
|
|
|
|
if (entry.texture) entry.texture->destroy(device, allocator);
|
|
|
|
|
|
if (entry.normalHeightMap) entry.normalHeightMap->destroy(device, allocator);
|
|
|
|
|
|
}
|
2026-03-02 08:06:35 -08:00
|
|
|
|
|
|
|
|
|
|
// Reset descriptor pool so new allocations succeed after reload
|
|
|
|
|
|
if (materialDescPool_) {
|
|
|
|
|
|
vkResetDescriptorPool(device, materialDescPool_, 0);
|
|
|
|
|
|
}
|
2026-02-27 04:59:12 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
loadedModels.clear();
|
|
|
|
|
|
textureCache.clear();
|
|
|
|
|
|
textureCacheBytes_ = 0;
|
|
|
|
|
|
textureCacheCounter_ = 0;
|
|
|
|
|
|
failedTextureCache_.clear();
|
|
|
|
|
|
loggedTextureLoadFails_.clear();
|
|
|
|
|
|
textureBudgetRejectWarnings_ = 0;
|
|
|
|
|
|
precomputedFloorGrid.clear();
|
|
|
|
|
|
|
|
|
|
|
|
LOG_WARNING("Cleared all WMO models, instances, and texture cache");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 16:21:48 -08:00
|
|
|
|
void WMORenderer::setCollisionFocus(const glm::vec3& worldPos, float radius) {
|
|
|
|
|
|
collisionFocusEnabled = (radius > 0.0f);
|
|
|
|
|
|
collisionFocusPos = worldPos;
|
|
|
|
|
|
collisionFocusRadius = std::max(0.0f, radius);
|
|
|
|
|
|
collisionFocusRadiusSq = collisionFocusRadius * collisionFocusRadius;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WMORenderer::clearCollisionFocus() {
|
|
|
|
|
|
collisionFocusEnabled = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// setLighting is now a no-op (lighting is in the per-frame UBO)
|
Implement WoW-accurate DBC-driven sky system with lore-faithful celestial bodies
Add SkySystem coordinator that follows WoW's actual architecture where skyboxes
are authoritative and procedural elements serve as fallbacks. Integrate lighting
system across all renderers (terrain, WMO, M2, character) with unified parameters.
Sky System:
- SkySystem coordinator manages skybox, celestial bodies, stars, clouds, lens flare
- Skybox is authoritative (baked stars from M2 models, procedural fallback only)
- skyboxHasStars flag gates procedural star rendering (prevents double-star bug)
Celestial Bodies (Lore-Accurate):
- Two moons: White Lady (30-day cycle, pale white) + Blue Child (27-day cycle, pale blue)
- Deterministic moon phases from server gameTime (not deltaTime toys)
- Sun positioning driven by LightingManager directionalDir (DBC-sourced)
- Camera-locked sky dome (translation ignored, rotation applied)
Lighting Integration:
- Apply LightingManager params to WMO, M2, character renderers
- Unified lighting: directional light, diffuse color, ambient color, fog
- Star occlusion by cloud density (70% weight) and fog density (30% weight)
Documentation:
- Add comprehensive SKY_SYSTEM.md technical guide
- Update MEMORY.md with sky system architecture and anti-patterns
- Update README.md with WoW-accurate descriptions
Critical design decisions:
- NO latitude-based star rotation (Azeroth not modeled as spherical planet)
- NO always-on procedural stars (skybox authority prevents zone identity loss)
- NO universal dual-moon setup (map-specific celestial configurations)
2026-02-10 14:36:17 -08:00
|
|
|
|
|
2026-02-03 16:21:48 -08:00
|
|
|
|
void WMORenderer::resetQueryStats() {
|
|
|
|
|
|
queryTimeMs = 0.0;
|
|
|
|
|
|
queryCallCount = 0;
|
2026-02-05 17:20:30 -08:00
|
|
|
|
currentFrameId++;
|
|
|
|
|
|
// Note: precomputedFloorGrid is persistent and not cleared per-frame
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 17:35:17 -08:00
|
|
|
|
bool WMORenderer::saveFloorCache() const {
|
|
|
|
|
|
if (mapName_.empty()) {
|
|
|
|
|
|
core::Logger::getInstance().warning("Cannot save floor cache: no map name set");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string filepath = "cache/wmo_floor_" + mapName_ + ".bin";
|
|
|
|
|
|
|
2026-02-05 17:20:30 -08:00
|
|
|
|
// Create directory if needed
|
|
|
|
|
|
std::filesystem::path path(filepath);
|
2026-02-05 17:26:18 -08:00
|
|
|
|
std::filesystem::path absPath = std::filesystem::absolute(path);
|
|
|
|
|
|
core::Logger::getInstance().info("Saving floor cache to: ", absPath.string());
|
|
|
|
|
|
|
2026-02-05 17:20:30 -08:00
|
|
|
|
if (path.has_parent_path()) {
|
2026-02-05 17:26:18 -08:00
|
|
|
|
std::error_code ec;
|
|
|
|
|
|
std::filesystem::create_directories(path.parent_path(), ec);
|
|
|
|
|
|
if (ec) {
|
|
|
|
|
|
core::Logger::getInstance().error("Failed to create cache directory: ", ec.message());
|
|
|
|
|
|
}
|
2026-02-05 17:20:30 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::ofstream file(filepath, std::ios::binary);
|
|
|
|
|
|
if (!file) {
|
|
|
|
|
|
core::Logger::getInstance().error("Failed to open floor cache file for writing: ", filepath);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Write header: magic + version + count
|
|
|
|
|
|
const uint32_t magic = 0x574D4F46; // "WMOF"
|
|
|
|
|
|
const uint32_t version = 1;
|
|
|
|
|
|
const uint64_t count = precomputedFloorGrid.size();
|
|
|
|
|
|
|
|
|
|
|
|
file.write(reinterpret_cast<const char*>(&magic), sizeof(magic));
|
|
|
|
|
|
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
|
|
|
|
|
|
file.write(reinterpret_cast<const char*>(&count), sizeof(count));
|
|
|
|
|
|
|
|
|
|
|
|
// Write each entry: key (uint64) + height (float)
|
|
|
|
|
|
for (const auto& [key, height] : precomputedFloorGrid) {
|
|
|
|
|
|
file.write(reinterpret_cast<const char*>(&key), sizeof(key));
|
|
|
|
|
|
file.write(reinterpret_cast<const char*>(&height), sizeof(height));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 17:35:17 -08:00
|
|
|
|
core::Logger::getInstance().info("Saved WMO floor cache (", mapName_, "): ", count, " entries");
|
2026-02-05 17:20:30 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 17:35:17 -08:00
|
|
|
|
bool WMORenderer::loadFloorCache() {
|
|
|
|
|
|
if (mapName_.empty()) {
|
|
|
|
|
|
core::Logger::getInstance().warning("Cannot load floor cache: no map name set");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string filepath = "cache/wmo_floor_" + mapName_ + ".bin";
|
|
|
|
|
|
|
2026-02-05 17:20:30 -08:00
|
|
|
|
std::ifstream file(filepath, std::ios::binary);
|
|
|
|
|
|
if (!file) {
|
2026-02-05 17:35:17 -08:00
|
|
|
|
core::Logger::getInstance().info("No existing floor cache for map: ", mapName_);
|
2026-02-05 17:20:30 -08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Read and validate header
|
|
|
|
|
|
uint32_t magic = 0, version = 0;
|
|
|
|
|
|
uint64_t count = 0;
|
|
|
|
|
|
|
|
|
|
|
|
file.read(reinterpret_cast<char*>(&magic), sizeof(magic));
|
|
|
|
|
|
file.read(reinterpret_cast<char*>(&version), sizeof(version));
|
|
|
|
|
|
file.read(reinterpret_cast<char*>(&count), sizeof(count));
|
|
|
|
|
|
|
|
|
|
|
|
if (magic != 0x574D4F46 || version != 1) {
|
|
|
|
|
|
core::Logger::getInstance().warning("Invalid floor cache file format: ", filepath);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Read entries
|
|
|
|
|
|
precomputedFloorGrid.clear();
|
|
|
|
|
|
precomputedFloorGrid.reserve(count);
|
|
|
|
|
|
|
|
|
|
|
|
for (uint64_t i = 0; i < count; i++) {
|
|
|
|
|
|
uint64_t key;
|
|
|
|
|
|
float height;
|
|
|
|
|
|
file.read(reinterpret_cast<char*>(&key), sizeof(key));
|
|
|
|
|
|
file.read(reinterpret_cast<char*>(&height), sizeof(height));
|
|
|
|
|
|
precomputedFloorGrid[key] = height;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 17:35:17 -08:00
|
|
|
|
core::Logger::getInstance().info("Loaded WMO floor cache (", mapName_, "): ", precomputedFloorGrid.size(), " entries");
|
2026-02-05 17:20:30 -08:00
|
|
|
|
return true;
|
2026-02-03 16:21:48 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 17:30:08 -08:00
|
|
|
|
void WMORenderer::precomputeFloorCache() {
|
|
|
|
|
|
if (instances.empty()) {
|
|
|
|
|
|
core::Logger::getInstance().info("precomputeFloorCache: no instances to precompute");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
size_t startSize = precomputedFloorGrid.size();
|
|
|
|
|
|
size_t samplesChecked = 0;
|
|
|
|
|
|
|
|
|
|
|
|
core::Logger::getInstance().info("Pre-computing floor cache for ", instances.size(), " WMO instances...");
|
|
|
|
|
|
|
|
|
|
|
|
for (const auto& instance : instances) {
|
|
|
|
|
|
// Get world bounds for this instance
|
|
|
|
|
|
const glm::vec3& boundsMin = instance.worldBoundsMin;
|
|
|
|
|
|
const glm::vec3& boundsMax = instance.worldBoundsMax;
|
|
|
|
|
|
|
|
|
|
|
|
// Sample reference Z is above the structure
|
|
|
|
|
|
float refZ = boundsMax.z + 10.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Iterate over grid points within the bounds
|
|
|
|
|
|
float startX = std::floor(boundsMin.x / FLOOR_GRID_CELL_SIZE) * FLOOR_GRID_CELL_SIZE;
|
|
|
|
|
|
float startY = std::floor(boundsMin.y / FLOOR_GRID_CELL_SIZE) * FLOOR_GRID_CELL_SIZE;
|
|
|
|
|
|
|
2026-02-18 20:02:12 -08:00
|
|
|
|
int stepsX = static_cast<int>((boundsMax.x - startX) / FLOOR_GRID_CELL_SIZE) + 1;
|
|
|
|
|
|
int stepsY = static_cast<int>((boundsMax.y - startY) / FLOOR_GRID_CELL_SIZE) + 1;
|
|
|
|
|
|
for (int ix = 0; ix < stepsX; ++ix) {
|
|
|
|
|
|
float x = startX + ix * FLOOR_GRID_CELL_SIZE;
|
|
|
|
|
|
for (int iy = 0; iy < stepsY; ++iy) {
|
|
|
|
|
|
float y = startY + iy * FLOOR_GRID_CELL_SIZE;
|
2026-02-05 17:30:08 -08:00
|
|
|
|
// Sample at grid cell center
|
|
|
|
|
|
float sampleX = x + FLOOR_GRID_CELL_SIZE * 0.5f;
|
|
|
|
|
|
float sampleY = y + FLOOR_GRID_CELL_SIZE * 0.5f;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if already cached
|
|
|
|
|
|
uint64_t key = floorGridKey(sampleX, sampleY);
|
|
|
|
|
|
if (precomputedFloorGrid.find(key) != precomputedFloorGrid.end()) {
|
|
|
|
|
|
continue; // Already computed
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
samplesChecked++;
|
|
|
|
|
|
|
2026-03-04 19:47:01 -08:00
|
|
|
|
// Query floor height and store result in the precomputed grid
|
|
|
|
|
|
auto h = getFloorHeight(sampleX, sampleY, refZ);
|
|
|
|
|
|
if (h) {
|
|
|
|
|
|
precomputedFloorGrid[key] = *h;
|
|
|
|
|
|
}
|
2026-02-05 17:30:08 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
size_t newEntries = precomputedFloorGrid.size() - startSize;
|
|
|
|
|
|
core::Logger::getInstance().info("Floor cache precompute complete: ", samplesChecked, " samples checked, ",
|
|
|
|
|
|
newEntries, " new entries, total ", precomputedFloorGrid.size());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 16:21:48 -08:00
|
|
|
|
WMORenderer::GridCell WMORenderer::toCell(const glm::vec3& p) const {
|
|
|
|
|
|
return GridCell{
|
|
|
|
|
|
static_cast<int>(std::floor(p.x / SPATIAL_CELL_SIZE)),
|
|
|
|
|
|
static_cast<int>(std::floor(p.y / SPATIAL_CELL_SIZE)),
|
|
|
|
|
|
static_cast<int>(std::floor(p.z / SPATIAL_CELL_SIZE))
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WMORenderer::rebuildSpatialIndex() {
|
|
|
|
|
|
spatialGrid.clear();
|
|
|
|
|
|
instanceIndexById.clear();
|
|
|
|
|
|
instanceIndexById.reserve(instances.size());
|
|
|
|
|
|
|
|
|
|
|
|
for (size_t i = 0; i < instances.size(); i++) {
|
|
|
|
|
|
const auto& inst = instances[i];
|
|
|
|
|
|
instanceIndexById[inst.id] = i;
|
|
|
|
|
|
|
|
|
|
|
|
GridCell minCell = toCell(inst.worldBoundsMin);
|
|
|
|
|
|
GridCell maxCell = toCell(inst.worldBoundsMax);
|
|
|
|
|
|
for (int z = minCell.z; z <= maxCell.z; z++) {
|
|
|
|
|
|
for (int y = minCell.y; y <= maxCell.y; y++) {
|
|
|
|
|
|
for (int x = minCell.x; x <= maxCell.x; x++) {
|
|
|
|
|
|
spatialGrid[GridCell{x, y, z}].push_back(inst.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WMORenderer::gatherCandidates(const glm::vec3& queryMin, const glm::vec3& queryMax,
|
|
|
|
|
|
std::vector<size_t>& outIndices) const {
|
|
|
|
|
|
outIndices.clear();
|
|
|
|
|
|
candidateIdScratch.clear();
|
|
|
|
|
|
|
|
|
|
|
|
GridCell minCell = toCell(queryMin);
|
|
|
|
|
|
GridCell maxCell = toCell(queryMax);
|
|
|
|
|
|
for (int z = minCell.z; z <= maxCell.z; z++) {
|
|
|
|
|
|
for (int y = minCell.y; y <= maxCell.y; y++) {
|
|
|
|
|
|
for (int x = minCell.x; x <= maxCell.x; x++) {
|
|
|
|
|
|
auto it = spatialGrid.find(GridCell{x, y, z});
|
|
|
|
|
|
if (it == spatialGrid.end()) continue;
|
|
|
|
|
|
for (uint32_t id : it->second) {
|
|
|
|
|
|
if (!candidateIdScratch.insert(id).second) continue;
|
|
|
|
|
|
auto idxIt = instanceIndexById.find(id);
|
|
|
|
|
|
if (idxIt != instanceIndexById.end()) {
|
|
|
|
|
|
outIndices.push_back(idxIt->second);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Safety fallback: if the grid misses due streaming/index drift, avoid
|
|
|
|
|
|
// tunneling by scanning all instances instead of returning no candidates.
|
|
|
|
|
|
if (outIndices.empty() && !instances.empty()) {
|
|
|
|
|
|
outIndices.reserve(instances.size());
|
|
|
|
|
|
for (size_t i = 0; i < instances.size(); i++) {
|
|
|
|
|
|
outIndices.push_back(i);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) {
|
2026-02-25 11:56:58 -08:00
|
|
|
|
++currentFrameId;
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (!opaquePipeline_ || instances.empty()) {
|
2026-02-02 12:24:50 -08:00
|
|
|
|
lastDrawCalls = 0;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 01:10:58 -08:00
|
|
|
|
// Update material UBOs if settings changed
|
|
|
|
|
|
if (materialSettingsDirty_) {
|
|
|
|
|
|
materialSettingsDirty_ = false;
|
|
|
|
|
|
static const int pomSampleTable[] = { 16, 32, 64 };
|
|
|
|
|
|
int maxSamples = pomSampleTable[std::clamp(pomQuality_, 0, 2)];
|
|
|
|
|
|
for (auto& [modelId, model] : loadedModels) {
|
|
|
|
|
|
for (auto& group : model.groups) {
|
|
|
|
|
|
for (auto& mb : group.mergedBatches) {
|
|
|
|
|
|
if (!mb.materialUBO) continue;
|
|
|
|
|
|
// Read existing UBO data, update normal/POM fields
|
|
|
|
|
|
VmaAllocationInfo allocInfo{};
|
|
|
|
|
|
vmaGetAllocationInfo(vkCtx_->getAllocator(), mb.materialUBOAlloc, &allocInfo);
|
|
|
|
|
|
if (allocInfo.pMappedData) {
|
|
|
|
|
|
auto* ubo = reinterpret_cast<WMOMaterialUBO*>(allocInfo.pMappedData);
|
|
|
|
|
|
ubo->enableNormalMap = normalMappingEnabled_ ? 1 : 0;
|
|
|
|
|
|
ubo->enablePOM = pomEnabled_ ? 1 : 0;
|
2026-02-23 01:18:42 -08:00
|
|
|
|
ubo->pomScale = 0.012f;
|
2026-02-23 01:10:58 -08:00
|
|
|
|
ubo->pomMaxSamples = maxSamples;
|
|
|
|
|
|
ubo->heightMapVariance = mb.heightMapVariance;
|
2026-02-23 01:18:42 -08:00
|
|
|
|
ubo->normalMapStrength = normalMapStrength_;
|
2026-02-23 01:10:58 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
lastDrawCalls = 0;
|
|
|
|
|
|
|
2026-02-02 23:03:45 -08:00
|
|
|
|
// Extract frustum planes for proper culling
|
2026-02-21 19:41:21 -08:00
|
|
|
|
glm::mat4 viewProj = camera.getProjectionMatrix() * camera.getViewMatrix();
|
2026-02-02 23:03:45 -08:00
|
|
|
|
Frustum frustum;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
frustum.extractFromMatrix(viewProj);
|
2026-02-02 23:03:45 -08:00
|
|
|
|
|
2026-02-05 15:31:00 -08:00
|
|
|
|
lastPortalCulledGroups = 0;
|
2026-02-05 15:51:08 -08:00
|
|
|
|
lastDistanceCulledGroups = 0;
|
2026-02-05 15:31:00 -08:00
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
// ── Phase 1: Parallel visibility culling ──────────────────────────
|
|
|
|
|
|
std::vector<size_t> visibleInstances;
|
|
|
|
|
|
visibleInstances.reserve(instances.size());
|
|
|
|
|
|
for (size_t i = 0; i < instances.size(); ++i) {
|
|
|
|
|
|
const auto& instance = instances[i];
|
|
|
|
|
|
if (loadedModels.find(instance.modelId) == loadedModels.end())
|
2026-02-02 12:24:50 -08:00
|
|
|
|
continue;
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
visibleInstances.push_back(i);
|
|
|
|
|
|
}
|
2026-02-03 16:21:48 -08:00
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
glm::vec3 camPos = camera.getPosition();
|
|
|
|
|
|
bool doPortalCull = portalCulling;
|
2026-02-18 23:00:46 -08:00
|
|
|
|
bool doFrustumCull = false; // Temporarily disabled: can over-cull world WMOs
|
|
|
|
|
|
bool doDistanceCull = distanceCulling;
|
2026-02-05 15:51:08 -08:00
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
auto cullInstance = [&](size_t instIdx) -> InstanceDrawList {
|
2026-02-07 18:57:34 -08:00
|
|
|
|
if (instIdx >= instances.size()) return InstanceDrawList{};
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
const auto& instance = instances[instIdx];
|
2026-02-07 18:57:34 -08:00
|
|
|
|
auto mdlIt = loadedModels.find(instance.modelId);
|
|
|
|
|
|
if (mdlIt == loadedModels.end()) return InstanceDrawList{};
|
|
|
|
|
|
const ModelData& model = mdlIt->second;
|
2026-02-05 15:51:08 -08:00
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
InstanceDrawList result;
|
|
|
|
|
|
result.instanceIndex = instIdx;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
// Portal-based visibility
|
2026-02-05 15:31:00 -08:00
|
|
|
|
std::unordered_set<uint32_t> portalVisibleGroups;
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
bool usePortalCulling = doPortalCull && !model.portals.empty() && !model.portalRefs.empty();
|
2026-02-05 15:31:00 -08:00
|
|
|
|
if (usePortalCulling) {
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
glm::vec4 localCamPos = instance.invModelMatrix * glm::vec4(camPos, 1.0f);
|
|
|
|
|
|
getVisibleGroupsViaPortals(model, glm::vec3(localCamPos), frustum,
|
|
|
|
|
|
instance.modelMatrix, portalVisibleGroups);
|
2026-02-05 15:31:00 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 11:31:08 -08:00
|
|
|
|
for (size_t gi = 0; gi < model.groups.size(); ++gi) {
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
if (usePortalCulling &&
|
|
|
|
|
|
portalVisibleGroups.find(static_cast<uint32_t>(gi)) == portalVisibleGroups.end()) {
|
|
|
|
|
|
result.portalCulled++;
|
2026-02-05 15:31:00 -08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 15:51:08 -08:00
|
|
|
|
if (gi < instance.worldGroupBounds.size()) {
|
2026-02-04 11:31:08 -08:00
|
|
|
|
const auto& [gMin, gMax] = instance.worldGroupBounds[gi];
|
2026-02-05 15:51:08 -08:00
|
|
|
|
|
2026-02-18 23:00:46 -08:00
|
|
|
|
if (doDistanceCull) {
|
2026-02-25 03:06:06 -08:00
|
|
|
|
glm::vec3 closestPoint = glm::clamp(camPos, gMin, gMax);
|
|
|
|
|
|
float distSq = glm::dot(closestPoint - camPos, closestPoint - camPos);
|
|
|
|
|
|
if (distSq > 250000.0f) {
|
|
|
|
|
|
result.distanceCulled++;
|
|
|
|
|
|
continue;
|
2026-02-18 23:00:46 -08:00
|
|
|
|
}
|
2026-02-05 15:51:08 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
if (doFrustumCull && !frustum.intersectsAABB(gMin, gMax))
|
2026-02-02 23:03:45 -08:00
|
|
|
|
continue;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
result.visibleGroups.push_back(static_cast<uint32_t>(gi));
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Dispatch culling — parallel when enough instances, sequential otherwise.
|
|
|
|
|
|
std::vector<InstanceDrawList> drawLists;
|
|
|
|
|
|
drawLists.reserve(visibleInstances.size());
|
|
|
|
|
|
|
2026-02-22 08:12:08 -08:00
|
|
|
|
static const size_t minParallelCullInstances = std::max<size_t>(
|
|
|
|
|
|
4, envSizeOrDefault("WOWEE_WMO_CULL_MT_MIN", 128));
|
|
|
|
|
|
if (visibleInstances.size() >= minParallelCullInstances && numCullThreads_ > 1) {
|
|
|
|
|
|
static const size_t minCullWorkPerThread = std::max<size_t>(
|
|
|
|
|
|
16, envSizeOrDefault("WOWEE_WMO_CULL_WORK_PER_THREAD", 64));
|
|
|
|
|
|
const size_t maxUsefulThreads = std::max<size_t>(
|
|
|
|
|
|
1, (visibleInstances.size() + minCullWorkPerThread - 1) / minCullWorkPerThread);
|
|
|
|
|
|
const size_t numThreads = std::min(static_cast<size_t>(numCullThreads_), maxUsefulThreads);
|
|
|
|
|
|
if (numThreads <= 1) {
|
|
|
|
|
|
for (size_t idx : visibleInstances) {
|
|
|
|
|
|
drawLists.push_back(cullInstance(idx));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const size_t chunkSize = visibleInstances.size() / numThreads;
|
|
|
|
|
|
const size_t remainder = visibleInstances.size() % numThreads;
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
|
2026-02-22 08:12:08 -08:00
|
|
|
|
drawLists.resize(visibleInstances.size());
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
|
2026-02-22 08:12:08 -08:00
|
|
|
|
cullFutures_.clear();
|
|
|
|
|
|
if (cullFutures_.capacity() < numThreads) {
|
|
|
|
|
|
cullFutures_.reserve(numThreads);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
size_t start = 0;
|
|
|
|
|
|
for (size_t t = 0; t < numThreads; ++t) {
|
|
|
|
|
|
const size_t end = start + chunkSize + (t < remainder ? 1 : 0);
|
|
|
|
|
|
cullFutures_.push_back(std::async(std::launch::async,
|
|
|
|
|
|
[&, start, end]() {
|
|
|
|
|
|
for (size_t j = start; j < end; ++j) {
|
|
|
|
|
|
drawLists[j] = cullInstance(visibleInstances[j]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}));
|
|
|
|
|
|
start = end;
|
|
|
|
|
|
}
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
|
2026-02-22 08:12:08 -08:00
|
|
|
|
for (auto& f : cullFutures_) {
|
|
|
|
|
|
f.get();
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
} else {
|
|
|
|
|
|
for (size_t idx : visibleInstances)
|
|
|
|
|
|
drawLists.push_back(cullInstance(idx));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// ── Phase 2: Vulkan draw ────────────────────────────────
|
|
|
|
|
|
// Select pipeline based on wireframe mode
|
|
|
|
|
|
VkPipeline activePipeline = (wireframeMode && wireframePipeline_) ? wireframePipeline_ : opaquePipeline_;
|
|
|
|
|
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, activePipeline);
|
|
|
|
|
|
|
|
|
|
|
|
// Bind per-frame descriptor set (set 0)
|
|
|
|
|
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_,
|
|
|
|
|
|
0, 1, &perFrameSet, 0, nullptr);
|
|
|
|
|
|
|
2026-02-23 00:43:14 -08:00
|
|
|
|
// Track which pipeline is currently bound: 0=opaque, 1=transparent, 2=glass
|
|
|
|
|
|
int currentPipelineKind = 0;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
for (const auto& dl : drawLists) {
|
2026-02-07 18:57:34 -08:00
|
|
|
|
if (dl.instanceIndex >= instances.size()) continue;
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
const auto& instance = instances[dl.instanceIndex];
|
2026-02-07 18:57:34 -08:00
|
|
|
|
auto modelIt = loadedModels.find(instance.modelId);
|
|
|
|
|
|
if (modelIt == loadedModels.end()) continue;
|
|
|
|
|
|
const ModelData& model = modelIt->second;
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
|
2026-02-23 03:23:18 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Push model matrix
|
|
|
|
|
|
GPUPushConstants push{};
|
|
|
|
|
|
push.model = instance.modelMatrix;
|
|
|
|
|
|
vkCmdPushConstants(cmd, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
|
|
|
|
|
|
0, sizeof(GPUPushConstants), &push);
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
|
2026-02-23 03:23:18 -08:00
|
|
|
|
// LOD shell groups render only beyond this distance squared (190 units)
|
|
|
|
|
|
static constexpr float LOD_SHELL_DIST_SQ = 196.0f * 196.0f;
|
2026-02-23 01:54:05 -08:00
|
|
|
|
|
2026-02-18 23:00:46 -08:00
|
|
|
|
// Render visible groups
|
2026-02-09 18:51:28 -08:00
|
|
|
|
for (uint32_t gi : dl.visibleGroups) {
|
2026-02-09 19:03:02 -08:00
|
|
|
|
const auto& group = model.groups[gi];
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Only skip antiportal geometry
|
|
|
|
|
|
if (group.groupFlags & 0x4000000) continue;
|
|
|
|
|
|
|
2026-02-23 03:23:18 -08:00
|
|
|
|
// Skip distance-only LOD shell groups when camera is close to the group
|
|
|
|
|
|
if (group.isLOD) {
|
|
|
|
|
|
glm::vec3 groupCenter = instance.modelMatrix * glm::vec4(
|
|
|
|
|
|
(group.boundingBoxMin + group.boundingBoxMax) * 0.5f, 1.0f);
|
|
|
|
|
|
float groupDistSq = glm::dot(camPos - groupCenter, camPos - groupCenter);
|
|
|
|
|
|
if (groupDistSq < LOD_SHELL_DIST_SQ) continue;
|
|
|
|
|
|
}
|
2026-02-23 01:54:05 -08:00
|
|
|
|
|
2026-03-02 09:00:56 -08:00
|
|
|
|
// Skip groups with invalid GPU resources
|
|
|
|
|
|
if (group.vertexBuffer == VK_NULL_HANDLE || group.indexBuffer == VK_NULL_HANDLE) continue;
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Bind vertex + index buffers
|
|
|
|
|
|
VkDeviceSize offset = 0;
|
|
|
|
|
|
vkCmdBindVertexBuffers(cmd, 0, 1, &group.vertexBuffer, &offset);
|
|
|
|
|
|
vkCmdBindIndexBuffer(cmd, group.indexBuffer, 0, VK_INDEX_TYPE_UINT16);
|
|
|
|
|
|
|
|
|
|
|
|
// Render each merged batch
|
|
|
|
|
|
for (const auto& mb : group.mergedBatches) {
|
|
|
|
|
|
if (!mb.materialSet) continue;
|
|
|
|
|
|
|
2026-02-23 00:43:14 -08:00
|
|
|
|
// Determine which pipeline this batch needs
|
|
|
|
|
|
int neededPipeline = 0; // opaque
|
|
|
|
|
|
if (mb.isWindow && glassPipeline_) {
|
|
|
|
|
|
neededPipeline = 2; // glass (alpha blend + depth write)
|
|
|
|
|
|
} else if (mb.isTransparent && transparentPipeline_) {
|
|
|
|
|
|
neededPipeline = 1; // transparent (alpha blend, no depth write)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 06:06:24 -08:00
|
|
|
|
// Switch pipeline if needed (descriptor sets and push constants
|
|
|
|
|
|
// are preserved across compatible pipeline layout switches)
|
2026-02-23 00:43:14 -08:00
|
|
|
|
if (neededPipeline != currentPipelineKind) {
|
|
|
|
|
|
VkPipeline targetPipeline = activePipeline;
|
|
|
|
|
|
if (neededPipeline == 1) targetPipeline = transparentPipeline_;
|
|
|
|
|
|
else if (neededPipeline == 2) targetPipeline = glassPipeline_;
|
2026-03-02 09:00:56 -08:00
|
|
|
|
if (targetPipeline == VK_NULL_HANDLE) continue;
|
2026-02-23 00:43:14 -08:00
|
|
|
|
|
|
|
|
|
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, targetPipeline);
|
|
|
|
|
|
currentPipelineKind = neededPipeline;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
}
|
Fix mount sounds, grey WMO meshes, taxi landing, tree animations, and classic dismount
- Per-family mount sounds (kodo, tallstrider, mechanostrider, etc.) detected from M2 model path
- Skip WMO groups with SHOW_SKYBOX flag or all-untextured batches (grey mesh in Orgrimmar)
- Freeze physics during taxi landing until terrain loads to prevent falling through void
- Disable bone animations on tropical vegetation (palm, bamboo, banana, etc.) to fix wiggling
- Snap player to final taxi waypoint on flight completion
- Extract mount aura spell ID from classic UNIT_FIELD_AURAS for CMSG_CANCEL_AURA dismount
- Increase /unstuck forward nudge to 5 units
2026-02-14 21:04:20 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Bind material descriptor set (set 1)
|
|
|
|
|
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_,
|
|
|
|
|
|
1, 1, &mb.materialSet, 0, nullptr);
|
2026-02-16 19:50:35 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Issue draw calls for each range in this merged batch
|
|
|
|
|
|
for (const auto& dr : mb.draws) {
|
2026-03-02 09:00:56 -08:00
|
|
|
|
if (dr.indexCount == 0) continue;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
vkCmdDrawIndexed(cmd, dr.indexCount, 1, dr.firstIndex, 0, 0);
|
|
|
|
|
|
lastDrawCalls++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-09 18:51:28 -08:00
|
|
|
|
}
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
|
|
|
|
|
|
lastPortalCulledGroups += dl.portalCulled;
|
|
|
|
|
|
lastDistanceCulledGroups += dl.distanceCulled;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool WMORenderer::initializeShadow(VkRenderPass shadowRenderPass) {
|
|
|
|
|
|
if (!vkCtx_ || shadowRenderPass == VK_NULL_HANDLE) return false;
|
|
|
|
|
|
VkDevice device = vkCtx_->getDevice();
|
|
|
|
|
|
|
|
|
|
|
|
// ShadowParams UBO: useBones, useTexture, alphaTest, foliageSway, windTime, foliageMotionDamp
|
|
|
|
|
|
struct ShadowParamsUBO {
|
|
|
|
|
|
int32_t useBones = 0;
|
|
|
|
|
|
int32_t useTexture = 0;
|
|
|
|
|
|
int32_t alphaTest = 0;
|
|
|
|
|
|
int32_t foliageSway = 0;
|
|
|
|
|
|
float windTime = 0.0f;
|
|
|
|
|
|
float foliageMotionDamp = 1.0f;
|
|
|
|
|
|
};
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Create ShadowParams UBO
|
|
|
|
|
|
VkBufferCreateInfo bufCI{};
|
|
|
|
|
|
bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
|
|
|
|
|
bufCI.size = sizeof(ShadowParamsUBO);
|
|
|
|
|
|
bufCI.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
|
|
|
|
|
|
VmaAllocationCreateInfo allocCI{};
|
|
|
|
|
|
allocCI.usage = VMA_MEMORY_USAGE_CPU_TO_GPU;
|
|
|
|
|
|
allocCI.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT;
|
|
|
|
|
|
VmaAllocationInfo allocInfo{};
|
|
|
|
|
|
if (vmaCreateBuffer(vkCtx_->getAllocator(), &bufCI, &allocCI,
|
|
|
|
|
|
&shadowParamsUBO_, &shadowParamsAlloc_, &allocInfo) != VK_SUCCESS) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer: failed to create shadow params UBO");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
ShadowParamsUBO defaultParams{};
|
|
|
|
|
|
std::memcpy(allocInfo.pMappedData, &defaultParams, sizeof(defaultParams));
|
|
|
|
|
|
|
|
|
|
|
|
// Create descriptor set layout: binding 0 = sampler2D (texture), binding 1 = ShadowParams UBO
|
|
|
|
|
|
VkDescriptorSetLayoutBinding layoutBindings[2]{};
|
|
|
|
|
|
layoutBindings[0].binding = 0;
|
|
|
|
|
|
layoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
layoutBindings[0].descriptorCount = 1;
|
|
|
|
|
|
layoutBindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
|
|
|
|
layoutBindings[1].binding = 1;
|
|
|
|
|
|
layoutBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
|
|
|
|
layoutBindings[1].descriptorCount = 1;
|
|
|
|
|
|
layoutBindings[1].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
|
|
|
|
VkDescriptorSetLayoutCreateInfo layoutCI{};
|
|
|
|
|
|
layoutCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
|
|
|
|
|
|
layoutCI.bindingCount = 2;
|
|
|
|
|
|
layoutCI.pBindings = layoutBindings;
|
|
|
|
|
|
if (vkCreateDescriptorSetLayout(device, &layoutCI, nullptr, &shadowParamsLayout_) != VK_SUCCESS) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer: failed to create shadow params layout");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create descriptor pool
|
|
|
|
|
|
VkDescriptorPoolSize poolSizes[2]{};
|
|
|
|
|
|
poolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
poolSizes[0].descriptorCount = 1;
|
|
|
|
|
|
poolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
|
|
|
|
poolSizes[1].descriptorCount = 1;
|
|
|
|
|
|
VkDescriptorPoolCreateInfo poolCI{};
|
|
|
|
|
|
poolCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
|
|
|
|
|
poolCI.maxSets = 1;
|
|
|
|
|
|
poolCI.poolSizeCount = 2;
|
|
|
|
|
|
poolCI.pPoolSizes = poolSizes;
|
|
|
|
|
|
if (vkCreateDescriptorPool(device, &poolCI, nullptr, &shadowParamsPool_) != VK_SUCCESS) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer: failed to create shadow params pool");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Allocate descriptor set
|
|
|
|
|
|
VkDescriptorSetAllocateInfo setAlloc{};
|
|
|
|
|
|
setAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
|
|
|
|
|
setAlloc.descriptorPool = shadowParamsPool_;
|
|
|
|
|
|
setAlloc.descriptorSetCount = 1;
|
|
|
|
|
|
setAlloc.pSetLayouts = &shadowParamsLayout_;
|
|
|
|
|
|
if (vkAllocateDescriptorSets(device, &setAlloc, &shadowParamsSet_) != VK_SUCCESS) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer: failed to allocate shadow params set");
|
|
|
|
|
|
return false;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Write descriptors
|
|
|
|
|
|
VkDescriptorBufferInfo bufInfo{};
|
|
|
|
|
|
bufInfo.buffer = shadowParamsUBO_;
|
|
|
|
|
|
bufInfo.offset = 0;
|
|
|
|
|
|
bufInfo.range = sizeof(ShadowParamsUBO);
|
|
|
|
|
|
|
|
|
|
|
|
VkWriteDescriptorSet writes[2]{};
|
|
|
|
|
|
// binding 0: texture (use white fallback so binding is valid; useTexture=0 so it's not sampled)
|
|
|
|
|
|
VkDescriptorImageInfo imgInfo{};
|
|
|
|
|
|
imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
|
|
|
|
imgInfo.imageView = whiteTexture_->getImageView();
|
|
|
|
|
|
imgInfo.sampler = whiteTexture_->getSampler();
|
|
|
|
|
|
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
|
|
|
|
writes[0].dstSet = shadowParamsSet_;
|
|
|
|
|
|
writes[0].dstBinding = 0;
|
|
|
|
|
|
writes[0].descriptorCount = 1;
|
|
|
|
|
|
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
writes[0].pImageInfo = &imgInfo;
|
|
|
|
|
|
// binding 1: params UBO
|
|
|
|
|
|
writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
|
|
|
|
writes[1].dstSet = shadowParamsSet_;
|
|
|
|
|
|
writes[1].dstBinding = 1;
|
|
|
|
|
|
writes[1].descriptorCount = 1;
|
|
|
|
|
|
writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
|
|
|
|
writes[1].pBufferInfo = &bufInfo;
|
|
|
|
|
|
vkUpdateDescriptorSets(device, 2, writes, 0, nullptr);
|
|
|
|
|
|
|
|
|
|
|
|
// Create shadow pipeline layout: set 1 = shadowParamsLayout_, push constants = 128 bytes
|
|
|
|
|
|
VkPushConstantRange pc{};
|
|
|
|
|
|
pc.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
|
|
|
|
|
|
pc.offset = 0;
|
|
|
|
|
|
pc.size = 128; // lightSpaceMatrix (64) + model (64)
|
|
|
|
|
|
shadowPipelineLayout_ = createPipelineLayout(device, {shadowParamsLayout_}, {pc});
|
|
|
|
|
|
if (!shadowPipelineLayout_) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer: failed to create shadow pipeline layout");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Load shadow shaders
|
|
|
|
|
|
VkShaderModule vertShader, fragShader;
|
|
|
|
|
|
if (!vertShader.loadFromFile(device, "assets/shaders/shadow.vert.spv")) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer: failed to load shadow vertex shader");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!fragShader.loadFromFile(device, "assets/shaders/shadow.frag.spv")) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer: failed to load shadow fragment shader");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 01:10:58 -08:00
|
|
|
|
// WMO vertex layout: pos(loc0,off0) normal(loc1,off12) texCoord(loc2,off24) color(loc3,off32) tangent(loc4,off48), stride=64
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Shadow shader locations: 0=aPos, 1=aTexCoord, 2=aBoneWeights, 3=aBoneIndicesF
|
|
|
|
|
|
// useBones=0 so locations 2,3 are never read; we alias them to existing data offsets
|
|
|
|
|
|
VkVertexInputBindingDescription vertBind{};
|
|
|
|
|
|
vertBind.binding = 0;
|
2026-02-23 01:10:58 -08:00
|
|
|
|
vertBind.stride = 64;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
vertBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
|
|
|
|
|
std::vector<VkVertexInputAttributeDescription> vertAttrs = {
|
|
|
|
|
|
{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // aPos -> position
|
|
|
|
|
|
{1, 0, VK_FORMAT_R32G32_SFLOAT, 24}, // aTexCoord -> texCoord
|
|
|
|
|
|
{2, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 32}, // aBoneWeights (aliased to color, not used)
|
|
|
|
|
|
{3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 32}, // aBoneIndicesF (aliased to color, not used)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
shadowPipeline_ = PipelineBuilder()
|
|
|
|
|
|
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
|
|
|
|
|
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
|
|
|
|
|
.setVertexInput({vertBind}, vertAttrs)
|
|
|
|
|
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
2026-02-22 10:23:20 -08:00
|
|
|
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
2026-02-21 19:41:21 -08:00
|
|
|
|
.setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL)
|
2026-02-22 10:23:20 -08:00
|
|
|
|
.setDepthBias(0.05f, 0.20f)
|
2026-02-21 19:41:21 -08:00
|
|
|
|
.setNoColorAttachment()
|
|
|
|
|
|
.setLayout(shadowPipelineLayout_)
|
|
|
|
|
|
.setRenderPass(shadowRenderPass)
|
|
|
|
|
|
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
|
|
|
|
|
|
.build(device);
|
|
|
|
|
|
|
|
|
|
|
|
vertShader.destroy();
|
|
|
|
|
|
fragShader.destroy();
|
|
|
|
|
|
|
|
|
|
|
|
if (!shadowPipeline_) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer: failed to create shadow pipeline");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
core::Logger::getInstance().info("WMORenderer shadow pipeline initialized");
|
|
|
|
|
|
return true;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 04:48:26 -08:00
|
|
|
|
void WMORenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix,
|
|
|
|
|
|
const glm::vec3& shadowCenter, float shadowRadius) {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (!shadowPipeline_ || !shadowParamsSet_) return;
|
|
|
|
|
|
if (instances.empty() || loadedModels.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipeline_);
|
|
|
|
|
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipelineLayout_,
|
|
|
|
|
|
0, 1, &shadowParamsSet_, 0, nullptr);
|
|
|
|
|
|
|
|
|
|
|
|
struct ShadowPush { glm::mat4 lightSpaceMatrix; glm::mat4 model; };
|
|
|
|
|
|
|
2026-02-23 04:48:26 -08:00
|
|
|
|
const float shadowRadiusSq = shadowRadius * shadowRadius;
|
2026-02-04 15:05:46 -08:00
|
|
|
|
for (const auto& instance : instances) {
|
2026-02-23 04:48:26 -08:00
|
|
|
|
// Distance cull against shadow frustum
|
|
|
|
|
|
glm::vec3 diff = instance.position - shadowCenter;
|
|
|
|
|
|
if (glm::dot(diff, diff) > shadowRadiusSq) continue;
|
2026-02-04 15:05:46 -08:00
|
|
|
|
auto modelIt = loadedModels.find(instance.modelId);
|
|
|
|
|
|
if (modelIt == loadedModels.end()) continue;
|
|
|
|
|
|
const ModelData& model = modelIt->second;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
|
|
|
|
|
|
ShadowPush push{lightSpaceMatrix, instance.modelMatrix};
|
|
|
|
|
|
vkCmdPushConstants(cmd, shadowPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
|
|
|
|
|
|
0, 128, &push);
|
|
|
|
|
|
|
2026-02-04 15:05:46 -08:00
|
|
|
|
for (const auto& group : model.groups) {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (group.vertexBuffer == VK_NULL_HANDLE || group.indexBuffer == VK_NULL_HANDLE) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Skip antiportal geometry
|
|
|
|
|
|
if (group.groupFlags & 0x4000000) continue;
|
|
|
|
|
|
|
2026-02-23 01:54:05 -08:00
|
|
|
|
// Skip LOD groups in shadow pass (they overlap real geometry)
|
|
|
|
|
|
if (group.isLOD) continue;
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkDeviceSize offset = 0;
|
|
|
|
|
|
vkCmdBindVertexBuffers(cmd, 0, 1, &group.vertexBuffer, &offset);
|
|
|
|
|
|
vkCmdBindIndexBuffer(cmd, group.indexBuffer, 0, VK_INDEX_TYPE_UINT16);
|
|
|
|
|
|
|
2026-02-22 10:23:20 -08:00
|
|
|
|
// Draw all batches in shadow pass.
|
|
|
|
|
|
// WMO transparency classification is not reliable enough for caster
|
|
|
|
|
|
// selection here and was dropping major world casters.
|
2026-02-21 19:41:21 -08:00
|
|
|
|
for (const auto& mb : group.mergedBatches) {
|
|
|
|
|
|
for (const auto& dr : mb.draws) {
|
|
|
|
|
|
vkCmdDrawIndexed(cmd, dr.indexCount, 1, dr.firstIndex, 0, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-04 15:05:46 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
uint32_t WMORenderer::getTotalTriangleCount() const {
|
|
|
|
|
|
uint32_t total = 0;
|
|
|
|
|
|
for (const auto& instance : instances) {
|
|
|
|
|
|
auto modelIt = loadedModels.find(instance.modelId);
|
|
|
|
|
|
if (modelIt != loadedModels.end()) {
|
|
|
|
|
|
total += modelIt->second.getTotalTriangles();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return total;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
bool WMORenderer::createGroupResources(const pipeline::WMOGroup& group, GroupResources& resources, uint32_t groupFlags) {
|
2026-02-02 12:24:50 -08:00
|
|
|
|
if (group.vertices.empty() || group.indices.empty()) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
resources.groupFlags = groupFlags;
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
resources.vertexCount = group.vertices.size();
|
|
|
|
|
|
resources.indexCount = group.indices.size();
|
|
|
|
|
|
resources.boundingBoxMin = group.boundingBoxMin;
|
|
|
|
|
|
resources.boundingBoxMax = group.boundingBoxMax;
|
|
|
|
|
|
|
2026-02-23 01:10:58 -08:00
|
|
|
|
// Create vertex data (position, normal, texcoord, color, tangent)
|
2026-02-02 12:24:50 -08:00
|
|
|
|
struct VertexData {
|
|
|
|
|
|
glm::vec3 position;
|
|
|
|
|
|
glm::vec3 normal;
|
|
|
|
|
|
glm::vec2 texCoord;
|
|
|
|
|
|
glm::vec4 color;
|
2026-02-23 01:10:58 -08:00
|
|
|
|
glm::vec4 tangent; // xyz=tangent dir, w=handedness ±1
|
2026-02-02 12:24:50 -08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<VertexData> vertices;
|
|
|
|
|
|
vertices.reserve(group.vertices.size());
|
|
|
|
|
|
|
|
|
|
|
|
for (const auto& v : group.vertices) {
|
|
|
|
|
|
VertexData vd;
|
|
|
|
|
|
vd.position = v.position;
|
|
|
|
|
|
vd.normal = v.normal;
|
|
|
|
|
|
vd.texCoord = v.texCoord;
|
|
|
|
|
|
vd.color = v.color;
|
2026-02-23 01:10:58 -08:00
|
|
|
|
vd.tangent = glm::vec4(0.0f);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
vertices.push_back(vd);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 01:10:58 -08:00
|
|
|
|
// Compute tangents using Lengyel's method
|
|
|
|
|
|
{
|
|
|
|
|
|
std::vector<glm::vec3> tan1(vertices.size(), glm::vec3(0.0f));
|
|
|
|
|
|
std::vector<glm::vec3> tan2(vertices.size(), glm::vec3(0.0f));
|
|
|
|
|
|
|
|
|
|
|
|
const auto& indices = group.indices;
|
|
|
|
|
|
for (size_t i = 0; i + 2 < indices.size(); i += 3) {
|
|
|
|
|
|
uint16_t i0 = indices[i], i1 = indices[i + 1], i2 = indices[i + 2];
|
|
|
|
|
|
if (i0 >= vertices.size() || i1 >= vertices.size() || i2 >= vertices.size()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
const glm::vec3& p0 = vertices[i0].position;
|
|
|
|
|
|
const glm::vec3& p1 = vertices[i1].position;
|
|
|
|
|
|
const glm::vec3& p2 = vertices[i2].position;
|
|
|
|
|
|
const glm::vec2& uv0 = vertices[i0].texCoord;
|
|
|
|
|
|
const glm::vec2& uv1 = vertices[i1].texCoord;
|
|
|
|
|
|
const glm::vec2& uv2 = vertices[i2].texCoord;
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 dp1 = p1 - p0;
|
|
|
|
|
|
glm::vec3 dp2 = p2 - p0;
|
|
|
|
|
|
glm::vec2 duv1 = uv1 - uv0;
|
|
|
|
|
|
glm::vec2 duv2 = uv2 - uv0;
|
|
|
|
|
|
|
|
|
|
|
|
float det = duv1.x * duv2.y - duv1.y * duv2.x;
|
|
|
|
|
|
if (std::abs(det) < 1e-8f) continue; // degenerate UVs
|
|
|
|
|
|
float r = 1.0f / det;
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 sdir = (dp1 * duv2.y - dp2 * duv1.y) * r;
|
|
|
|
|
|
glm::vec3 tdir = (dp2 * duv1.x - dp1 * duv2.x) * r;
|
|
|
|
|
|
|
|
|
|
|
|
tan1[i0] += sdir; tan1[i1] += sdir; tan1[i2] += sdir;
|
|
|
|
|
|
tan2[i0] += tdir; tan2[i1] += tdir; tan2[i2] += tdir;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (size_t i = 0; i < vertices.size(); i++) {
|
|
|
|
|
|
glm::vec3 n = glm::normalize(vertices[i].normal);
|
|
|
|
|
|
glm::vec3 t = tan1[i];
|
|
|
|
|
|
|
|
|
|
|
|
if (glm::dot(t, t) < 1e-8f) {
|
|
|
|
|
|
// Fallback: generate tangent perpendicular to normal
|
|
|
|
|
|
glm::vec3 up = (std::abs(n.y) < 0.999f) ? glm::vec3(0, 1, 0) : glm::vec3(1, 0, 0);
|
|
|
|
|
|
t = glm::normalize(glm::cross(n, up));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Gram-Schmidt orthogonalize
|
|
|
|
|
|
t = glm::normalize(t - n * glm::dot(n, t));
|
|
|
|
|
|
float w = (glm::dot(glm::cross(n, t), tan2[i]) < 0.0f) ? -1.0f : 1.0f;
|
|
|
|
|
|
vertices[i].tangent = glm::vec4(t, w);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Upload vertex buffer to GPU
|
|
|
|
|
|
AllocatedBuffer vertBuf = uploadBuffer(*vkCtx_, vertices.data(),
|
|
|
|
|
|
vertices.size() * sizeof(VertexData),
|
|
|
|
|
|
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
|
|
|
|
|
resources.vertexBuffer = vertBuf.buffer;
|
|
|
|
|
|
resources.vertexAlloc = vertBuf.allocation;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Upload index buffer to GPU
|
|
|
|
|
|
AllocatedBuffer idxBuf = uploadBuffer(*vkCtx_, group.indices.data(),
|
|
|
|
|
|
group.indices.size() * sizeof(uint16_t),
|
|
|
|
|
|
VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
|
|
|
|
|
|
resources.indexBuffer = idxBuf.buffer;
|
|
|
|
|
|
resources.indexAlloc = idxBuf.allocation;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Store collision geometry for floor raycasting
|
|
|
|
|
|
resources.collisionVertices.reserve(group.vertices.size());
|
|
|
|
|
|
for (const auto& v : group.vertices) {
|
|
|
|
|
|
resources.collisionVertices.push_back(v.position);
|
|
|
|
|
|
}
|
|
|
|
|
|
resources.collisionIndices = group.indices;
|
|
|
|
|
|
|
|
|
|
|
|
// Compute actual bounding box from vertices (WMO header bboxes can be unreliable)
|
|
|
|
|
|
if (!resources.collisionVertices.empty()) {
|
|
|
|
|
|
resources.boundingBoxMin = resources.collisionVertices[0];
|
|
|
|
|
|
resources.boundingBoxMax = resources.collisionVertices[0];
|
|
|
|
|
|
for (const auto& v : resources.collisionVertices) {
|
|
|
|
|
|
resources.boundingBoxMin = glm::min(resources.boundingBoxMin, v);
|
|
|
|
|
|
resources.boundingBoxMax = glm::max(resources.boundingBoxMax, v);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 15:47:43 -08:00
|
|
|
|
// Build 2D spatial grid for fast collision triangle lookup
|
|
|
|
|
|
resources.buildCollisionGrid();
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
// Create batches
|
|
|
|
|
|
if (!group.batches.empty()) {
|
|
|
|
|
|
for (const auto& batch : group.batches) {
|
|
|
|
|
|
GroupResources::Batch resBatch;
|
|
|
|
|
|
resBatch.startIndex = batch.startIndex;
|
|
|
|
|
|
resBatch.indexCount = batch.indexCount;
|
|
|
|
|
|
resBatch.materialId = batch.materialId;
|
|
|
|
|
|
resources.batches.push_back(resBatch);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// No batches defined - render entire group as one batch
|
|
|
|
|
|
GroupResources::Batch batch;
|
|
|
|
|
|
batch.startIndex = 0;
|
|
|
|
|
|
batch.indexCount = resources.indexCount;
|
|
|
|
|
|
batch.materialId = 0;
|
|
|
|
|
|
resources.batches.push_back(batch);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// renderGroup removed — draw calls are inlined in render()
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
void WMORenderer::destroyGroupGPU(GroupResources& group) {
|
|
|
|
|
|
if (!vkCtx_) return;
|
|
|
|
|
|
VmaAllocator allocator = vkCtx_->getAllocator();
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (group.vertexBuffer) {
|
|
|
|
|
|
vmaDestroyBuffer(allocator, group.vertexBuffer, group.vertexAlloc);
|
|
|
|
|
|
group.vertexBuffer = VK_NULL_HANDLE;
|
|
|
|
|
|
group.vertexAlloc = VK_NULL_HANDLE;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (group.indexBuffer) {
|
|
|
|
|
|
vmaDestroyBuffer(allocator, group.indexBuffer, group.indexAlloc);
|
|
|
|
|
|
group.indexBuffer = VK_NULL_HANDLE;
|
|
|
|
|
|
group.indexAlloc = VK_NULL_HANDLE;
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Destroy material UBOs (descriptor sets are freed when pool is reset/destroyed)
|
|
|
|
|
|
for (auto& mb : group.mergedBatches) {
|
|
|
|
|
|
if (mb.materialUBO) {
|
|
|
|
|
|
vmaDestroyBuffer(allocator, mb.materialUBO, mb.materialUBOAlloc);
|
|
|
|
|
|
mb.materialUBO = VK_NULL_HANDLE;
|
|
|
|
|
|
mb.materialUBOAlloc = VK_NULL_HANDLE;
|
2026-02-05 17:20:30 -08:00
|
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkDescriptorSet WMORenderer::allocateMaterialSet() {
|
|
|
|
|
|
if (!materialDescPool_ || !materialSetLayout_) return VK_NULL_HANDLE;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkDescriptorSetAllocateInfo allocInfo{};
|
|
|
|
|
|
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
|
|
|
|
|
allocInfo.descriptorPool = materialDescPool_;
|
|
|
|
|
|
allocInfo.descriptorSetCount = 1;
|
|
|
|
|
|
allocInfo.pSetLayouts = &materialSetLayout_;
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkDescriptorSet set = VK_NULL_HANDLE;
|
|
|
|
|
|
if (vkAllocateDescriptorSets(vkCtx_->getDevice(), &allocInfo, &set) != VK_SUCCESS) {
|
|
|
|
|
|
core::Logger::getInstance().warning("WMORenderer: failed to allocate material descriptor set");
|
|
|
|
|
|
return VK_NULL_HANDLE;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
|
return set;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool WMORenderer::isGroupVisible(const GroupResources& group, const glm::mat4& modelMatrix,
|
|
|
|
|
|
const Camera& camera) const {
|
|
|
|
|
|
// Simple frustum culling using bounding box
|
|
|
|
|
|
// Transform bounding box corners to world space
|
|
|
|
|
|
glm::vec3 corners[8] = {
|
|
|
|
|
|
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMin.y, group.boundingBoxMin.z),
|
|
|
|
|
|
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMin.y, group.boundingBoxMin.z),
|
|
|
|
|
|
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMax.y, group.boundingBoxMin.z),
|
|
|
|
|
|
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMax.y, group.boundingBoxMin.z),
|
|
|
|
|
|
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMin.y, group.boundingBoxMax.z),
|
|
|
|
|
|
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMin.y, group.boundingBoxMax.z),
|
|
|
|
|
|
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMax.y, group.boundingBoxMax.z),
|
|
|
|
|
|
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMax.y, group.boundingBoxMax.z)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Transform corners to world space
|
|
|
|
|
|
for (int i = 0; i < 8; i++) {
|
|
|
|
|
|
glm::vec4 worldPos = modelMatrix * glm::vec4(corners[i], 1.0f);
|
|
|
|
|
|
corners[i] = glm::vec3(worldPos);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Simple check: if all corners are behind camera, cull
|
|
|
|
|
|
// (This is a very basic culling implementation - a full frustum test would be better)
|
|
|
|
|
|
glm::vec3 forward = camera.getForward();
|
|
|
|
|
|
glm::vec3 camPos = camera.getPosition();
|
|
|
|
|
|
|
|
|
|
|
|
int behindCount = 0;
|
|
|
|
|
|
for (int i = 0; i < 8; i++) {
|
|
|
|
|
|
glm::vec3 toCorner = corners[i] - camPos;
|
|
|
|
|
|
if (glm::dot(toCorner, forward) < 0.0f) {
|
|
|
|
|
|
behindCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If all corners are behind camera, cull
|
|
|
|
|
|
return behindCount < 8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 15:31:00 -08:00
|
|
|
|
int WMORenderer::findContainingGroup(const ModelData& model, const glm::vec3& localPos) const {
|
|
|
|
|
|
// Find which group's bounding box contains the position
|
|
|
|
|
|
// Prefer interior groups (smaller volume) when multiple match
|
|
|
|
|
|
int bestGroup = -1;
|
|
|
|
|
|
float bestVolume = std::numeric_limits<float>::max();
|
|
|
|
|
|
|
|
|
|
|
|
for (size_t gi = 0; gi < model.groups.size(); gi++) {
|
|
|
|
|
|
const auto& group = model.groups[gi];
|
|
|
|
|
|
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) {
|
|
|
|
|
|
glm::vec3 size = group.boundingBoxMax - group.boundingBoxMin;
|
|
|
|
|
|
float volume = size.x * size.y * size.z;
|
|
|
|
|
|
if (volume < bestVolume) {
|
|
|
|
|
|
bestVolume = volume;
|
|
|
|
|
|
bestGroup = static_cast<int>(gi);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return bestGroup;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool WMORenderer::isPortalVisible(const ModelData& model, uint16_t portalIndex,
|
|
|
|
|
|
[[maybe_unused]] const glm::vec3& cameraLocalPos,
|
|
|
|
|
|
const Frustum& frustum,
|
|
|
|
|
|
const glm::mat4& modelMatrix) const {
|
|
|
|
|
|
if (portalIndex >= model.portals.size()) return false;
|
|
|
|
|
|
|
|
|
|
|
|
const auto& portal = model.portals[portalIndex];
|
|
|
|
|
|
if (portal.vertexCount < 3) return false;
|
|
|
|
|
|
if (portal.startVertex + portal.vertexCount > model.portalVertices.size()) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Get portal polygon center and bounds for frustum test
|
|
|
|
|
|
glm::vec3 center(0.0f);
|
|
|
|
|
|
glm::vec3 pMin = model.portalVertices[portal.startVertex];
|
|
|
|
|
|
glm::vec3 pMax = pMin;
|
|
|
|
|
|
for (uint16_t i = 0; i < portal.vertexCount; i++) {
|
|
|
|
|
|
const auto& v = model.portalVertices[portal.startVertex + i];
|
|
|
|
|
|
center += v;
|
|
|
|
|
|
pMin = glm::min(pMin, v);
|
|
|
|
|
|
pMax = glm::max(pMax, v);
|
|
|
|
|
|
}
|
|
|
|
|
|
center /= static_cast<float>(portal.vertexCount);
|
|
|
|
|
|
|
|
|
|
|
|
// Transform bounds to world space for frustum test
|
|
|
|
|
|
glm::vec4 worldMin = modelMatrix * glm::vec4(pMin, 1.0f);
|
|
|
|
|
|
glm::vec4 worldMax = modelMatrix * glm::vec4(pMax, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Check if portal AABB intersects frustum (more robust than point test)
|
|
|
|
|
|
return frustum.intersectsAABB(glm::vec3(worldMin), glm::vec3(worldMax));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WMORenderer::getVisibleGroupsViaPortals(const ModelData& model,
|
|
|
|
|
|
const glm::vec3& cameraLocalPos,
|
|
|
|
|
|
const Frustum& frustum,
|
|
|
|
|
|
const glm::mat4& modelMatrix,
|
|
|
|
|
|
std::unordered_set<uint32_t>& outVisibleGroups) const {
|
|
|
|
|
|
// Find camera's containing group
|
|
|
|
|
|
int cameraGroup = findContainingGroup(model, cameraLocalPos);
|
|
|
|
|
|
|
|
|
|
|
|
// If camera is outside all groups, fall back to frustum culling only
|
|
|
|
|
|
if (cameraGroup < 0) {
|
|
|
|
|
|
// Camera outside WMO - mark all groups as potentially visible
|
|
|
|
|
|
// (will still be frustum culled in render)
|
|
|
|
|
|
for (size_t gi = 0; gi < model.groups.size(); gi++) {
|
|
|
|
|
|
outVisibleGroups.insert(static_cast<uint32_t>(gi));
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BFS through portals from camera's group
|
|
|
|
|
|
std::vector<bool> visited(model.groups.size(), false);
|
|
|
|
|
|
std::vector<uint32_t> queue;
|
|
|
|
|
|
queue.push_back(static_cast<uint32_t>(cameraGroup));
|
|
|
|
|
|
visited[cameraGroup] = true;
|
|
|
|
|
|
outVisibleGroups.insert(static_cast<uint32_t>(cameraGroup));
|
|
|
|
|
|
|
|
|
|
|
|
size_t queueIdx = 0;
|
|
|
|
|
|
while (queueIdx < queue.size()) {
|
|
|
|
|
|
uint32_t currentGroup = queue[queueIdx++];
|
|
|
|
|
|
|
|
|
|
|
|
// Get portal refs for this group
|
|
|
|
|
|
if (currentGroup >= model.groupPortalRefs.size()) continue;
|
|
|
|
|
|
auto [portalStart, portalCount] = model.groupPortalRefs[currentGroup];
|
|
|
|
|
|
|
|
|
|
|
|
for (uint16_t pi = 0; pi < portalCount; pi++) {
|
|
|
|
|
|
uint16_t refIdx = portalStart + pi;
|
|
|
|
|
|
if (refIdx >= model.portalRefs.size()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
const auto& ref = model.portalRefs[refIdx];
|
|
|
|
|
|
uint32_t targetGroup = ref.groupIndex;
|
|
|
|
|
|
|
|
|
|
|
|
if (targetGroup >= model.groups.size()) continue;
|
|
|
|
|
|
if (visited[targetGroup]) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if portal is visible from camera
|
|
|
|
|
|
if (isPortalVisible(model, ref.portalIndex, cameraLocalPos, frustum, modelMatrix)) {
|
|
|
|
|
|
visited[targetGroup] = true;
|
|
|
|
|
|
outVisibleGroups.insert(targetGroup);
|
|
|
|
|
|
queue.push_back(targetGroup);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
void WMORenderer::WMOInstance::updateModelMatrix() {
|
|
|
|
|
|
modelMatrix = glm::mat4(1.0f);
|
|
|
|
|
|
modelMatrix = glm::translate(modelMatrix, position);
|
|
|
|
|
|
|
|
|
|
|
|
// Apply MODF placement rotation (WoW-to-GL coordinate transform)
|
|
|
|
|
|
// WoW Ry(B)*Rx(A)*Rz(C) becomes GL Rz(B)*Ry(-A)*Rx(-C)
|
|
|
|
|
|
// rotation stored as (-C, -A, B) in radians by caller
|
|
|
|
|
|
// Apply in Z, Y, X order to get Rz(B) * Ry(-A) * Rx(-C)
|
|
|
|
|
|
modelMatrix = glm::rotate(modelMatrix, rotation.z, glm::vec3(0.0f, 0.0f, 1.0f));
|
|
|
|
|
|
modelMatrix = glm::rotate(modelMatrix, rotation.y, glm::vec3(0.0f, 1.0f, 0.0f));
|
|
|
|
|
|
modelMatrix = glm::rotate(modelMatrix, rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
|
|
|
|
|
|
|
|
|
|
|
|
modelMatrix = glm::scale(modelMatrix, glm::vec3(scale));
|
2026-02-03 13:33:31 -08:00
|
|
|
|
|
|
|
|
|
|
// Cache inverse for collision detection
|
|
|
|
|
|
invModelMatrix = glm::inverse(modelMatrix);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 01:10:58 -08:00
|
|
|
|
std::unique_ptr<VkTexture> WMORenderer::generateNormalHeightMap(
|
|
|
|
|
|
const uint8_t* pixels, uint32_t width, uint32_t height, float& outVariance) {
|
|
|
|
|
|
if (!vkCtx_ || width == 0 || height == 0) return nullptr;
|
|
|
|
|
|
|
|
|
|
|
|
const uint32_t totalPixels = width * height;
|
|
|
|
|
|
|
|
|
|
|
|
// Step 1: Compute height from luminance
|
|
|
|
|
|
std::vector<float> heightMap(totalPixels);
|
|
|
|
|
|
double sumH = 0.0, sumH2 = 0.0;
|
|
|
|
|
|
for (uint32_t i = 0; i < totalPixels; i++) {
|
|
|
|
|
|
float r = pixels[i * 4 + 0] / 255.0f;
|
|
|
|
|
|
float g = pixels[i * 4 + 1] / 255.0f;
|
|
|
|
|
|
float b = pixels[i * 4 + 2] / 255.0f;
|
|
|
|
|
|
float h = 0.299f * r + 0.587f * g + 0.114f * b;
|
|
|
|
|
|
heightMap[i] = h;
|
|
|
|
|
|
sumH += h;
|
|
|
|
|
|
sumH2 += h * h;
|
|
|
|
|
|
}
|
|
|
|
|
|
double mean = sumH / totalPixels;
|
|
|
|
|
|
outVariance = static_cast<float>(sumH2 / totalPixels - mean * mean);
|
|
|
|
|
|
|
2026-02-23 01:18:42 -08:00
|
|
|
|
// Step 1.5: Box blur the height map to reduce noise from diffuse textures
|
|
|
|
|
|
auto wrapSample = [&](const std::vector<float>& map, int x, int y) -> float {
|
|
|
|
|
|
x = ((x % (int)width) + (int)width) % (int)width;
|
|
|
|
|
|
y = ((y % (int)height) + (int)height) % (int)height;
|
|
|
|
|
|
return map[y * width + x];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<float> blurredHeight(totalPixels);
|
|
|
|
|
|
for (uint32_t y = 0; y < height; y++) {
|
|
|
|
|
|
for (uint32_t x = 0; x < width; x++) {
|
|
|
|
|
|
int ix = static_cast<int>(x), iy = static_cast<int>(y);
|
|
|
|
|
|
float sum = 0.0f;
|
|
|
|
|
|
for (int dy = -1; dy <= 1; dy++)
|
|
|
|
|
|
for (int dx = -1; dx <= 1; dx++)
|
|
|
|
|
|
sum += wrapSample(heightMap, ix + dx, iy + dy);
|
|
|
|
|
|
blurredHeight[y * width + x] = sum / 9.0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Step 2: Sobel 3x3 → normal map
|
|
|
|
|
|
// Use ORIGINAL height for normals (crisp detail), blurred height for POM alpha only
|
2026-02-23 01:10:58 -08:00
|
|
|
|
const float strength = 2.0f;
|
|
|
|
|
|
std::vector<uint8_t> output(totalPixels * 4);
|
|
|
|
|
|
|
|
|
|
|
|
auto sampleH = [&](int x, int y) -> float {
|
|
|
|
|
|
x = ((x % (int)width) + (int)width) % (int)width;
|
|
|
|
|
|
y = ((y % (int)height) + (int)height) % (int)height;
|
|
|
|
|
|
return heightMap[y * width + x];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
for (uint32_t y = 0; y < height; y++) {
|
|
|
|
|
|
for (uint32_t x = 0; x < width; x++) {
|
|
|
|
|
|
int ix = static_cast<int>(x);
|
|
|
|
|
|
int iy = static_cast<int>(y);
|
|
|
|
|
|
// Sobel X
|
|
|
|
|
|
float gx = -sampleH(ix-1, iy-1) - 2.0f*sampleH(ix-1, iy) - sampleH(ix-1, iy+1)
|
|
|
|
|
|
+ sampleH(ix+1, iy-1) + 2.0f*sampleH(ix+1, iy) + sampleH(ix+1, iy+1);
|
|
|
|
|
|
// Sobel Y
|
|
|
|
|
|
float gy = -sampleH(ix-1, iy-1) - 2.0f*sampleH(ix, iy-1) - sampleH(ix+1, iy-1)
|
|
|
|
|
|
+ sampleH(ix-1, iy+1) + 2.0f*sampleH(ix, iy+1) + sampleH(ix+1, iy+1);
|
|
|
|
|
|
|
|
|
|
|
|
float nx = -gx * strength;
|
|
|
|
|
|
float ny = -gy * strength;
|
|
|
|
|
|
float nz = 1.0f;
|
|
|
|
|
|
float len = std::sqrt(nx*nx + ny*ny + nz*nz);
|
|
|
|
|
|
if (len > 0.0f) { nx /= len; ny /= len; nz /= len; }
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t idx = (y * width + x) * 4;
|
|
|
|
|
|
output[idx + 0] = static_cast<uint8_t>(std::clamp((nx * 0.5f + 0.5f) * 255.0f, 0.0f, 255.0f));
|
|
|
|
|
|
output[idx + 1] = static_cast<uint8_t>(std::clamp((ny * 0.5f + 0.5f) * 255.0f, 0.0f, 255.0f));
|
|
|
|
|
|
output[idx + 2] = static_cast<uint8_t>(std::clamp((nz * 0.5f + 0.5f) * 255.0f, 0.0f, 255.0f));
|
2026-02-23 01:18:42 -08:00
|
|
|
|
output[idx + 3] = static_cast<uint8_t>(std::clamp(blurredHeight[y * width + x] * 255.0f, 0.0f, 255.0f));
|
2026-02-23 01:10:58 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Step 3: Upload to GPU with mipmaps
|
|
|
|
|
|
auto tex = std::make_unique<VkTexture>();
|
|
|
|
|
|
if (!tex->upload(*vkCtx_, output.data(), width, height, VK_FORMAT_R8G8B8A8_UNORM, true)) {
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
tex->createSampler(vkCtx_->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
|
|
|
|
|
VK_SAMPLER_ADDRESS_MODE_REPEAT);
|
|
|
|
|
|
return tex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
|
|
|
|
|
if (!assetManager || !vkCtx_) {
|
|
|
|
|
|
return whiteTexture_.get();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 16:15:25 -08:00
|
|
|
|
auto normalizeKey = [](std::string key) {
|
|
|
|
|
|
std::replace(key.begin(), key.end(), '/', '\\');
|
|
|
|
|
|
std::transform(key.begin(), key.end(), key.begin(),
|
|
|
|
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
|
|
|
|
return key;
|
|
|
|
|
|
};
|
2026-02-18 23:00:46 -08:00
|
|
|
|
std::string key = path;
|
|
|
|
|
|
// Some assets contain stray bytes after a NUL in path chunks.
|
|
|
|
|
|
size_t nul = key.find('\0');
|
|
|
|
|
|
if (nul != std::string::npos) key.resize(nul);
|
|
|
|
|
|
key = normalizeKey(key);
|
|
|
|
|
|
if (key.rfind(".\\", 0) == 0) key = key.substr(2);
|
|
|
|
|
|
while (!key.empty() && key.front() == '\\') key.erase(key.begin());
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (key.empty()) return whiteTexture_.get();
|
2026-02-18 23:00:46 -08:00
|
|
|
|
|
|
|
|
|
|
auto hasKnownExt = [](const std::string& p) {
|
|
|
|
|
|
if (p.size() < 4) return false;
|
|
|
|
|
|
std::string ext = p.substr(p.size() - 4);
|
|
|
|
|
|
std::transform(ext.begin(), ext.end(), ext.begin(),
|
|
|
|
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
|
|
|
|
return (ext == ".blp" || ext == ".tga" || ext == ".dds");
|
|
|
|
|
|
};
|
|
|
|
|
|
auto toBlp = [](std::string p) {
|
|
|
|
|
|
if (p.size() >= 4) {
|
|
|
|
|
|
std::string ext = p.substr(p.size() - 4);
|
|
|
|
|
|
std::transform(ext.begin(), ext.end(), ext.begin(),
|
|
|
|
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
|
|
|
|
if (ext == ".tga" || ext == ".dds") {
|
|
|
|
|
|
p = p.substr(0, p.size() - 4) + ".blp";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return p;
|
|
|
|
|
|
};
|
2026-02-12 16:15:25 -08:00
|
|
|
|
|
2026-02-18 23:00:46 -08:00
|
|
|
|
std::vector<std::string> candidates;
|
|
|
|
|
|
auto addCandidate = [&](const std::string& raw) {
|
|
|
|
|
|
std::string c = normalizeKey(raw);
|
|
|
|
|
|
if (c.rfind(".\\", 0) == 0) c = c.substr(2);
|
|
|
|
|
|
while (!c.empty() && c.front() == '\\') c.erase(c.begin());
|
|
|
|
|
|
if (!c.empty()) candidates.push_back(c);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
addCandidate(toBlp(key));
|
|
|
|
|
|
if (!hasKnownExt(key)) addCandidate(key + ".blp");
|
|
|
|
|
|
|
|
|
|
|
|
// Common WMO references omit folder prefix; manifest often stores these under textures\...
|
|
|
|
|
|
std::string keyWithExt = hasKnownExt(key) ? toBlp(key) : (key + ".blp");
|
|
|
|
|
|
if (key.find('\\') == std::string::npos) {
|
|
|
|
|
|
addCandidate(std::string("textures\\") + keyWithExt);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (key.rfind("texture\\", 0) == 0) {
|
|
|
|
|
|
addCandidate(std::string("textures\\") + key.substr(8));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-18 23:00:46 -08:00
|
|
|
|
// De-duplicate while preserving order.
|
|
|
|
|
|
std::vector<std::string> uniqueCandidates;
|
|
|
|
|
|
uniqueCandidates.reserve(candidates.size());
|
|
|
|
|
|
std::unordered_set<std::string> seen;
|
|
|
|
|
|
for (const auto& c : candidates) {
|
|
|
|
|
|
if (seen.insert(c).second) uniqueCandidates.push_back(c);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Cache lookup across all candidate keys
|
|
|
|
|
|
for (const auto& c : uniqueCandidates) {
|
|
|
|
|
|
auto it = textureCache.find(c);
|
|
|
|
|
|
if (it != textureCache.end()) {
|
|
|
|
|
|
it->second.lastUse = ++textureCacheCounter_;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
return it->second.texture.get();
|
2026-02-18 23:00:46 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 08:12:08 -08:00
|
|
|
|
const auto& attemptedCandidates = uniqueCandidates;
|
2026-02-22 07:26:54 -08:00
|
|
|
|
|
2026-02-18 23:00:46 -08:00
|
|
|
|
// Try loading all candidates until one succeeds
|
|
|
|
|
|
pipeline::BLPImage blp;
|
|
|
|
|
|
std::string resolvedKey;
|
2026-02-22 07:26:54 -08:00
|
|
|
|
for (const auto& c : attemptedCandidates) {
|
2026-02-18 23:00:46 -08:00
|
|
|
|
blp = assetManager->loadTexture(c);
|
|
|
|
|
|
if (blp.isValid()) {
|
|
|
|
|
|
resolvedKey = c;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
if (!blp.isValid()) {
|
2026-02-22 07:26:54 -08:00
|
|
|
|
if (loggedTextureLoadFails_.insert(key).second) {
|
|
|
|
|
|
core::Logger::getInstance().warning("WMO: Failed to load texture: ", path);
|
|
|
|
|
|
}
|
2026-02-11 22:27:02 -08:00
|
|
|
|
// Do not cache failures as white. MPQ reads can fail transiently
|
|
|
|
|
|
// during streaming/contention, and caching white here permanently
|
|
|
|
|
|
// poisons the texture for this session.
|
2026-02-21 19:41:21 -08:00
|
|
|
|
return whiteTexture_.get();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
core::Logger::getInstance().debug("WMO texture: ", path, " size=", blp.width, "x", blp.height);
|
|
|
|
|
|
|
2026-02-22 07:26:54 -08:00
|
|
|
|
size_t base = static_cast<size_t>(blp.width) * static_cast<size_t>(blp.height) * 4ull;
|
|
|
|
|
|
size_t approxBytes = base + (base / 3);
|
|
|
|
|
|
if (textureCacheBytes_ + approxBytes > textureCacheBudgetBytes_) {
|
2026-02-23 04:32:58 -08:00
|
|
|
|
if (textureBudgetRejectWarnings_ < 3) {
|
2026-02-22 07:26:54 -08:00
|
|
|
|
core::Logger::getInstance().warning(
|
|
|
|
|
|
"WMO texture cache full (", textureCacheBytes_ / (1024 * 1024),
|
|
|
|
|
|
" MB / ", textureCacheBudgetBytes_ / (1024 * 1024),
|
|
|
|
|
|
" MB), rejecting texture: ", path);
|
|
|
|
|
|
}
|
|
|
|
|
|
++textureBudgetRejectWarnings_;
|
|
|
|
|
|
return whiteTexture_.get();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Create Vulkan texture
|
|
|
|
|
|
auto texture = std::make_unique<VkTexture>();
|
|
|
|
|
|
if (!texture->upload(*vkCtx_, blp.data.data(), blp.width, blp.height,
|
|
|
|
|
|
VK_FORMAT_R8G8B8A8_UNORM, true)) {
|
|
|
|
|
|
core::Logger::getInstance().warning("WMO: Failed to upload texture to GPU: ", path);
|
|
|
|
|
|
return whiteTexture_.get();
|
|
|
|
|
|
}
|
|
|
|
|
|
texture->createSampler(vkCtx_->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
|
|
|
|
|
VK_SAMPLER_ADDRESS_MODE_REPEAT);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-23 01:10:58 -08:00
|
|
|
|
// Generate normal+height map from diffuse pixels
|
|
|
|
|
|
float nhVariance = 0.0f;
|
|
|
|
|
|
std::unique_ptr<VkTexture> nhMap;
|
|
|
|
|
|
if (normalMappingEnabled_ || pomEnabled_) {
|
|
|
|
|
|
nhMap = generateNormalHeightMap(blp.data.data(), blp.width, blp.height, nhVariance);
|
|
|
|
|
|
if (nhMap) {
|
|
|
|
|
|
approxBytes *= 2; // account for normal map in budget
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
// Cache it
|
2026-02-12 16:29:36 -08:00
|
|
|
|
TextureCacheEntry e;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkTexture* rawPtr = texture.get();
|
2026-02-22 07:26:54 -08:00
|
|
|
|
e.approxBytes = approxBytes;
|
2026-02-12 16:29:36 -08:00
|
|
|
|
e.lastUse = ++textureCacheCounter_;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
e.texture = std::move(texture);
|
2026-02-23 01:10:58 -08:00
|
|
|
|
e.normalHeightMap = std::move(nhMap);
|
|
|
|
|
|
e.heightMapVariance = nhVariance;
|
2026-02-12 16:29:36 -08:00
|
|
|
|
textureCacheBytes_ += e.approxBytes;
|
2026-02-18 23:00:46 -08:00
|
|
|
|
if (!resolvedKey.empty()) {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
textureCache[resolvedKey] = std::move(e);
|
2026-02-18 23:00:46 -08:00
|
|
|
|
} else {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
textureCache[key] = std::move(e);
|
2026-02-18 23:00:46 -08:00
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
core::Logger::getInstance().debug("WMO: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
return rawPtr;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ray-AABB intersection (slab method)
|
|
|
|
|
|
// Returns true if the ray intersects the axis-aligned bounding box
|
|
|
|
|
|
static bool rayIntersectsAABB(const glm::vec3& origin, const glm::vec3& dir,
|
|
|
|
|
|
const glm::vec3& bmin, const glm::vec3& bmax) {
|
|
|
|
|
|
float tmin = -1e30f, tmax = 1e30f;
|
|
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
|
|
|
|
if (std::abs(dir[i]) < 1e-8f) {
|
|
|
|
|
|
// Ray is parallel to this slab — check if origin is inside
|
|
|
|
|
|
if (origin[i] < bmin[i] || origin[i] > bmax[i]) return false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
float invD = 1.0f / dir[i];
|
|
|
|
|
|
float t0 = (bmin[i] - origin[i]) * invD;
|
|
|
|
|
|
float t1 = (bmax[i] - origin[i]) * invD;
|
|
|
|
|
|
if (t0 > t1) std::swap(t0, t1);
|
|
|
|
|
|
tmin = std::max(tmin, t0);
|
|
|
|
|
|
tmax = std::min(tmax, t1);
|
|
|
|
|
|
if (tmin > tmax) return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return tmax >= 0.0f; // At least part of the ray is forward
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 16:04:21 -08:00
|
|
|
|
static void transformAABB(const glm::mat4& modelMatrix,
|
|
|
|
|
|
const glm::vec3& localMin,
|
|
|
|
|
|
const glm::vec3& localMax,
|
|
|
|
|
|
glm::vec3& outMin,
|
|
|
|
|
|
glm::vec3& outMax) {
|
|
|
|
|
|
const glm::vec3 corners[8] = {
|
|
|
|
|
|
{localMin.x, localMin.y, localMin.z},
|
|
|
|
|
|
{localMin.x, localMin.y, localMax.z},
|
|
|
|
|
|
{localMin.x, localMax.y, localMin.z},
|
|
|
|
|
|
{localMin.x, localMax.y, localMax.z},
|
|
|
|
|
|
{localMax.x, localMin.y, localMin.z},
|
|
|
|
|
|
{localMax.x, localMin.y, localMax.z},
|
|
|
|
|
|
{localMax.x, localMax.y, localMin.z},
|
|
|
|
|
|
{localMax.x, localMax.y, localMax.z}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
outMin = glm::vec3(std::numeric_limits<float>::max());
|
|
|
|
|
|
outMax = glm::vec3(-std::numeric_limits<float>::max());
|
|
|
|
|
|
for (const glm::vec3& corner : corners) {
|
|
|
|
|
|
glm::vec3 world = glm::vec3(modelMatrix * glm::vec4(corner, 1.0f));
|
|
|
|
|
|
outMin = glm::min(outMin, world);
|
|
|
|
|
|
outMax = glm::max(outMax, world);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 16:21:48 -08:00
|
|
|
|
static float pointAABBDistanceSq(const glm::vec3& p, const glm::vec3& bmin, const glm::vec3& bmax) {
|
|
|
|
|
|
glm::vec3 q = glm::clamp(p, bmin, bmax);
|
|
|
|
|
|
glm::vec3 d = p - q;
|
|
|
|
|
|
return glm::dot(d, d);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
struct QueryTimer {
|
|
|
|
|
|
double* totalMs = nullptr;
|
|
|
|
|
|
uint32_t* callCount = nullptr;
|
|
|
|
|
|
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
|
|
|
|
|
|
QueryTimer(double* total, uint32_t* calls) : totalMs(total), callCount(calls) {}
|
|
|
|
|
|
~QueryTimer() {
|
|
|
|
|
|
if (callCount) {
|
|
|
|
|
|
(*callCount)++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (totalMs) {
|
|
|
|
|
|
auto end = std::chrono::steady_clock::now();
|
|
|
|
|
|
*totalMs += std::chrono::duration<double, std::milli>(end - start).count();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
// Möller–Trumbore ray-triangle intersection
|
|
|
|
|
|
// Returns distance along ray if hit, or negative if miss
|
|
|
|
|
|
static float rayTriangleIntersect(const glm::vec3& origin, const glm::vec3& dir,
|
|
|
|
|
|
const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2) {
|
|
|
|
|
|
const float EPSILON = 1e-6f;
|
|
|
|
|
|
glm::vec3 e1 = v1 - v0;
|
|
|
|
|
|
glm::vec3 e2 = v2 - v0;
|
|
|
|
|
|
glm::vec3 h = glm::cross(dir, e2);
|
|
|
|
|
|
float a = glm::dot(e1, h);
|
|
|
|
|
|
if (a > -EPSILON && a < EPSILON) return -1.0f;
|
|
|
|
|
|
|
|
|
|
|
|
float f = 1.0f / a;
|
|
|
|
|
|
glm::vec3 s = origin - v0;
|
|
|
|
|
|
float u = f * glm::dot(s, h);
|
|
|
|
|
|
if (u < 0.0f || u > 1.0f) return -1.0f;
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 q = glm::cross(s, e1);
|
|
|
|
|
|
float v = f * glm::dot(dir, q);
|
|
|
|
|
|
if (v < 0.0f || u + v > 1.0f) return -1.0f;
|
|
|
|
|
|
|
|
|
|
|
|
float t = f * glm::dot(e2, q);
|
|
|
|
|
|
return t > EPSILON ? t : -1.0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 16:04:21 -08:00
|
|
|
|
// Closest point on triangle (from Real-Time Collision Detection).
|
|
|
|
|
|
static glm::vec3 closestPointOnTriangle(const glm::vec3& p, const glm::vec3& a,
|
|
|
|
|
|
const glm::vec3& b, const glm::vec3& c) {
|
|
|
|
|
|
glm::vec3 ab = b - a;
|
|
|
|
|
|
glm::vec3 ac = c - a;
|
|
|
|
|
|
glm::vec3 ap = p - a;
|
|
|
|
|
|
float d1 = glm::dot(ab, ap);
|
|
|
|
|
|
float d2 = glm::dot(ac, ap);
|
|
|
|
|
|
if (d1 <= 0.0f && d2 <= 0.0f) return a;
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 bp = p - b;
|
|
|
|
|
|
float d3 = glm::dot(ab, bp);
|
|
|
|
|
|
float d4 = glm::dot(ac, bp);
|
|
|
|
|
|
if (d3 >= 0.0f && d4 <= d3) return b;
|
|
|
|
|
|
|
|
|
|
|
|
float vc = d1 * d4 - d3 * d2;
|
|
|
|
|
|
if (vc <= 0.0f && d1 >= 0.0f && d3 <= 0.0f) {
|
|
|
|
|
|
float v = d1 / (d1 - d3);
|
|
|
|
|
|
return a + v * ab;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 cp = p - c;
|
|
|
|
|
|
float d5 = glm::dot(ab, cp);
|
|
|
|
|
|
float d6 = glm::dot(ac, cp);
|
|
|
|
|
|
if (d6 >= 0.0f && d5 <= d6) return c;
|
|
|
|
|
|
|
|
|
|
|
|
float vb = d5 * d2 - d1 * d6;
|
|
|
|
|
|
if (vb <= 0.0f && d2 >= 0.0f && d6 <= 0.0f) {
|
|
|
|
|
|
float w = d2 / (d2 - d6);
|
|
|
|
|
|
return a + w * ac;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
float va = d3 * d6 - d5 * d4;
|
|
|
|
|
|
if (va <= 0.0f && (d4 - d3) >= 0.0f && (d5 - d6) >= 0.0f) {
|
|
|
|
|
|
float w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
|
|
|
|
|
|
return b + w * (c - b);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
float denom = 1.0f / (va + vb + vc);
|
|
|
|
|
|
float v = vb * denom;
|
|
|
|
|
|
float w = vc * denom;
|
|
|
|
|
|
return a + ab * v + ac * w;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 15:47:43 -08:00
|
|
|
|
// ---- Per-group 2D collision grid ----
|
|
|
|
|
|
|
|
|
|
|
|
void WMORenderer::GroupResources::buildCollisionGrid() {
|
|
|
|
|
|
if (collisionVertices.empty() || collisionIndices.size() < 3) {
|
|
|
|
|
|
gridCellsX = 0;
|
|
|
|
|
|
gridCellsY = 0;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
gridOrigin = glm::vec2(boundingBoxMin.x, boundingBoxMin.y);
|
|
|
|
|
|
float extentX = boundingBoxMax.x - boundingBoxMin.x;
|
|
|
|
|
|
float extentY = boundingBoxMax.y - boundingBoxMin.y;
|
|
|
|
|
|
|
|
|
|
|
|
gridCellsX = std::max(1, static_cast<int>(std::ceil(extentX / COLLISION_CELL_SIZE)));
|
|
|
|
|
|
gridCellsY = std::max(1, static_cast<int>(std::ceil(extentY / COLLISION_CELL_SIZE)));
|
|
|
|
|
|
|
|
|
|
|
|
// Cap grid size to avoid excessive memory for huge groups
|
|
|
|
|
|
if (gridCellsX > 64) gridCellsX = 64;
|
|
|
|
|
|
if (gridCellsY > 64) gridCellsY = 64;
|
|
|
|
|
|
|
2026-02-08 17:38:30 -08:00
|
|
|
|
size_t totalCells = gridCellsX * gridCellsY;
|
|
|
|
|
|
cellTriangles.resize(totalCells);
|
|
|
|
|
|
cellFloorTriangles.resize(totalCells);
|
|
|
|
|
|
cellWallTriangles.resize(totalCells);
|
|
|
|
|
|
|
|
|
|
|
|
size_t numTriangles = collisionIndices.size() / 3;
|
|
|
|
|
|
triBounds.resize(numTriangles);
|
2026-02-25 11:56:58 -08:00
|
|
|
|
triNormals.resize(numTriangles);
|
|
|
|
|
|
triVisited.resize(numTriangles, 0);
|
2026-02-07 15:47:43 -08:00
|
|
|
|
|
|
|
|
|
|
float invCellW = gridCellsX / std::max(0.01f, extentX);
|
|
|
|
|
|
float invCellH = gridCellsY / std::max(0.01f, extentY);
|
|
|
|
|
|
|
|
|
|
|
|
for (size_t i = 0; i + 2 < collisionIndices.size(); i += 3) {
|
|
|
|
|
|
const glm::vec3& v0 = collisionVertices[collisionIndices[i]];
|
|
|
|
|
|
const glm::vec3& v1 = collisionVertices[collisionIndices[i + 1]];
|
|
|
|
|
|
const glm::vec3& v2 = collisionVertices[collisionIndices[i + 2]];
|
|
|
|
|
|
|
|
|
|
|
|
// Triangle XY bounding box
|
|
|
|
|
|
float triMinX = std::min({v0.x, v1.x, v2.x});
|
|
|
|
|
|
float triMinY = std::min({v0.y, v1.y, v2.y});
|
|
|
|
|
|
float triMaxX = std::max({v0.x, v1.x, v2.x});
|
|
|
|
|
|
float triMaxY = std::max({v0.y, v1.y, v2.y});
|
|
|
|
|
|
|
2026-02-08 17:38:30 -08:00
|
|
|
|
// Per-triangle Z bounds
|
|
|
|
|
|
float triMinZ = std::min({v0.z, v1.z, v2.z});
|
|
|
|
|
|
float triMaxZ = std::max({v0.z, v1.z, v2.z});
|
|
|
|
|
|
triBounds[i / 3] = { triMinZ, triMaxZ };
|
|
|
|
|
|
|
2026-02-25 11:56:58 -08:00
|
|
|
|
// Precompute and store unit normal
|
2026-02-08 17:38:30 -08:00
|
|
|
|
glm::vec3 edge1 = v1 - v0;
|
|
|
|
|
|
glm::vec3 edge2 = v2 - v0;
|
|
|
|
|
|
glm::vec3 normal = glm::cross(edge1, edge2);
|
|
|
|
|
|
float normalLen = glm::length(normal);
|
2026-02-25 11:56:58 -08:00
|
|
|
|
if (normalLen > 0.001f) {
|
|
|
|
|
|
normal /= normalLen;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
normal = glm::vec3(0.0f, 0.0f, 1.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
triNormals[i / 3] = normal;
|
|
|
|
|
|
|
|
|
|
|
|
// Classify floor vs wall by normal.
|
|
|
|
|
|
// Wall threshold matches the runtime skip in checkWallCollision (absNz >= 0.35).
|
|
|
|
|
|
float absNz = std::abs(normal.z);
|
2026-02-08 20:31:00 -08:00
|
|
|
|
bool isFloor = (absNz >= 0.35f); // ~70° max slope (relaxed for steep stairs)
|
2026-02-25 11:56:58 -08:00
|
|
|
|
bool isWall = (absNz < 0.35f); // Matches checkWallCollision skip threshold
|
2026-02-08 17:38:30 -08:00
|
|
|
|
|
2026-02-07 15:47:43 -08:00
|
|
|
|
int cellMinX = std::max(0, static_cast<int>((triMinX - gridOrigin.x) * invCellW));
|
|
|
|
|
|
int cellMinY = std::max(0, static_cast<int>((triMinY - gridOrigin.y) * invCellH));
|
|
|
|
|
|
int cellMaxX = std::min(gridCellsX - 1, static_cast<int>((triMaxX - gridOrigin.x) * invCellW));
|
|
|
|
|
|
int cellMaxY = std::min(gridCellsY - 1, static_cast<int>((triMaxY - gridOrigin.y) * invCellH));
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t triIdx = static_cast<uint32_t>(i);
|
|
|
|
|
|
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
|
|
|
|
|
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
2026-02-08 17:38:30 -08:00
|
|
|
|
int cellIdx = cy * gridCellsX + cx;
|
|
|
|
|
|
cellTriangles[cellIdx].push_back(triIdx);
|
|
|
|
|
|
if (isFloor) cellFloorTriangles[cellIdx].push_back(triIdx);
|
|
|
|
|
|
if (isWall) cellWallTriangles[cellIdx].push_back(triIdx);
|
2026-02-07 15:47:43 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::vector<uint32_t>* WMORenderer::GroupResources::getTrianglesAtLocal(float localX, float localY) const {
|
|
|
|
|
|
if (gridCellsX == 0 || gridCellsY == 0) return nullptr;
|
|
|
|
|
|
|
|
|
|
|
|
float extentX = boundingBoxMax.x - boundingBoxMin.x;
|
|
|
|
|
|
float extentY = boundingBoxMax.y - boundingBoxMin.y;
|
|
|
|
|
|
float invCellW = gridCellsX / std::max(0.01f, extentX);
|
|
|
|
|
|
float invCellH = gridCellsY / std::max(0.01f, extentY);
|
|
|
|
|
|
|
|
|
|
|
|
int cx = static_cast<int>((localX - gridOrigin.x) * invCellW);
|
|
|
|
|
|
int cy = static_cast<int>((localY - gridOrigin.y) * invCellH);
|
|
|
|
|
|
|
|
|
|
|
|
if (cx < 0 || cx >= gridCellsX || cy < 0 || cy >= gridCellsY) return nullptr;
|
|
|
|
|
|
|
|
|
|
|
|
return &cellTriangles[cy * gridCellsX + cx];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WMORenderer::GroupResources::getTrianglesInRange(
|
|
|
|
|
|
float minX, float minY, float maxX, float maxY,
|
|
|
|
|
|
std::vector<uint32_t>& out) const {
|
|
|
|
|
|
out.clear();
|
|
|
|
|
|
if (gridCellsX == 0 || gridCellsY == 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
float extentX = boundingBoxMax.x - boundingBoxMin.x;
|
|
|
|
|
|
float extentY = boundingBoxMax.y - boundingBoxMin.y;
|
|
|
|
|
|
float invCellW = gridCellsX / std::max(0.01f, extentX);
|
|
|
|
|
|
float invCellH = gridCellsY / std::max(0.01f, extentY);
|
|
|
|
|
|
|
|
|
|
|
|
int cellMinX = std::max(0, static_cast<int>((minX - gridOrigin.x) * invCellW));
|
|
|
|
|
|
int cellMinY = std::max(0, static_cast<int>((minY - gridOrigin.y) * invCellH));
|
|
|
|
|
|
int cellMaxX = std::min(gridCellsX - 1, static_cast<int>((maxX - gridOrigin.x) * invCellW));
|
|
|
|
|
|
int cellMaxY = std::min(gridCellsY - 1, static_cast<int>((maxY - gridOrigin.y) * invCellH));
|
|
|
|
|
|
|
|
|
|
|
|
if (cellMinX > cellMaxX || cellMinY > cellMaxY) return;
|
|
|
|
|
|
|
2026-02-25 11:56:58 -08:00
|
|
|
|
// Collect unique triangle indices using visited bitset (O(n) dedup)
|
|
|
|
|
|
bool multiCell = (cellMinX != cellMaxX || cellMinY != cellMaxY);
|
|
|
|
|
|
if (multiCell && !triVisited.empty()) {
|
|
|
|
|
|
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
|
|
|
|
|
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
|
|
|
|
|
const auto& cell = cellTriangles[cy * gridCellsX + cx];
|
|
|
|
|
|
for (uint32_t tri : cell) {
|
|
|
|
|
|
uint32_t idx = tri / 3;
|
|
|
|
|
|
if (!triVisited[idx]) {
|
|
|
|
|
|
triVisited[idx] = 1;
|
|
|
|
|
|
out.push_back(tri);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Clear visited bits
|
|
|
|
|
|
for (uint32_t tri : out) triVisited[tri / 3] = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
|
|
|
|
|
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
|
|
|
|
|
const auto& cell = cellTriangles[cy * gridCellsX + cx];
|
|
|
|
|
|
out.insert(out.end(), cell.begin(), cell.end());
|
|
|
|
|
|
}
|
2026-02-07 15:47:43 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 17:38:30 -08:00
|
|
|
|
void WMORenderer::GroupResources::getFloorTrianglesInRange(
|
|
|
|
|
|
float minX, float minY, float maxX, float maxY,
|
|
|
|
|
|
std::vector<uint32_t>& out) const {
|
|
|
|
|
|
out.clear();
|
|
|
|
|
|
if (gridCellsX == 0 || gridCellsY == 0 || cellFloorTriangles.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
float extentX = boundingBoxMax.x - boundingBoxMin.x;
|
|
|
|
|
|
float extentY = boundingBoxMax.y - boundingBoxMin.y;
|
|
|
|
|
|
float invCellW = gridCellsX / std::max(0.01f, extentX);
|
|
|
|
|
|
float invCellH = gridCellsY / std::max(0.01f, extentY);
|
|
|
|
|
|
|
|
|
|
|
|
int cellMinX = std::max(0, static_cast<int>((minX - gridOrigin.x) * invCellW));
|
|
|
|
|
|
int cellMinY = std::max(0, static_cast<int>((minY - gridOrigin.y) * invCellH));
|
|
|
|
|
|
int cellMaxX = std::min(gridCellsX - 1, static_cast<int>((maxX - gridOrigin.x) * invCellW));
|
|
|
|
|
|
int cellMaxY = std::min(gridCellsY - 1, static_cast<int>((maxY - gridOrigin.y) * invCellH));
|
|
|
|
|
|
|
|
|
|
|
|
if (cellMinX > cellMaxX || cellMinY > cellMaxY) return;
|
|
|
|
|
|
|
2026-02-25 11:56:58 -08:00
|
|
|
|
bool multiCell = (cellMinX != cellMaxX || cellMinY != cellMaxY);
|
|
|
|
|
|
if (multiCell && !triVisited.empty()) {
|
|
|
|
|
|
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
|
|
|
|
|
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
|
|
|
|
|
const auto& cell = cellFloorTriangles[cy * gridCellsX + cx];
|
|
|
|
|
|
for (uint32_t tri : cell) {
|
|
|
|
|
|
uint32_t idx = tri / 3;
|
|
|
|
|
|
if (!triVisited[idx]) {
|
|
|
|
|
|
triVisited[idx] = 1;
|
|
|
|
|
|
out.push_back(tri);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for (uint32_t tri : out) triVisited[tri / 3] = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
|
|
|
|
|
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
|
|
|
|
|
const auto& cell = cellFloorTriangles[cy * gridCellsX + cx];
|
|
|
|
|
|
out.insert(out.end(), cell.begin(), cell.end());
|
|
|
|
|
|
}
|
2026-02-08 17:38:30 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WMORenderer::GroupResources::getWallTrianglesInRange(
|
|
|
|
|
|
float minX, float minY, float maxX, float maxY,
|
|
|
|
|
|
std::vector<uint32_t>& out) const {
|
|
|
|
|
|
out.clear();
|
|
|
|
|
|
if (gridCellsX == 0 || gridCellsY == 0 || cellWallTriangles.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
float extentX = boundingBoxMax.x - boundingBoxMin.x;
|
|
|
|
|
|
float extentY = boundingBoxMax.y - boundingBoxMin.y;
|
|
|
|
|
|
float invCellW = gridCellsX / std::max(0.01f, extentX);
|
|
|
|
|
|
float invCellH = gridCellsY / std::max(0.01f, extentY);
|
|
|
|
|
|
|
|
|
|
|
|
int cellMinX = std::max(0, static_cast<int>((minX - gridOrigin.x) * invCellW));
|
|
|
|
|
|
int cellMinY = std::max(0, static_cast<int>((minY - gridOrigin.y) * invCellH));
|
|
|
|
|
|
int cellMaxX = std::min(gridCellsX - 1, static_cast<int>((maxX - gridOrigin.x) * invCellW));
|
|
|
|
|
|
int cellMaxY = std::min(gridCellsY - 1, static_cast<int>((maxY - gridOrigin.y) * invCellH));
|
|
|
|
|
|
|
|
|
|
|
|
if (cellMinX > cellMaxX || cellMinY > cellMaxY) return;
|
|
|
|
|
|
|
2026-02-25 11:56:58 -08:00
|
|
|
|
bool multiCell = (cellMinX != cellMaxX || cellMinY != cellMaxY);
|
|
|
|
|
|
if (multiCell && !triVisited.empty()) {
|
|
|
|
|
|
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
|
|
|
|
|
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
|
|
|
|
|
const auto& cell = cellWallTriangles[cy * gridCellsX + cx];
|
|
|
|
|
|
for (uint32_t tri : cell) {
|
|
|
|
|
|
uint32_t idx = tri / 3;
|
|
|
|
|
|
if (!triVisited[idx]) {
|
|
|
|
|
|
triVisited[idx] = 1;
|
|
|
|
|
|
out.push_back(tri);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for (uint32_t tri : out) triVisited[tri / 3] = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
|
|
|
|
|
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
|
|
|
|
|
const auto& cell = cellWallTriangles[cy * gridCellsX + cx];
|
|
|
|
|
|
out.insert(out.end(), cell.begin(), cell.end());
|
|
|
|
|
|
}
|
2026-02-08 17:38:30 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ, float* outNormalZ) const {
|
2026-02-25 11:56:58 -08:00
|
|
|
|
// Per-frame cache disabled: camera and player query the same (x,y) at
|
|
|
|
|
|
// different Z within a single frame. The allowAbove filter depends on glZ,
|
|
|
|
|
|
// so caching by (x,y) alone returns wrong floors across Z contexts.
|
2026-02-05 17:20:30 -08:00
|
|
|
|
|
2026-02-03 16:21:48 -08:00
|
|
|
|
QueryTimer timer(&queryTimeMs, &queryCallCount);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
std::optional<float> bestFloor;
|
2026-02-08 17:38:30 -08:00
|
|
|
|
float bestNormalZ = 1.0f;
|
2026-02-07 20:51:53 -08:00
|
|
|
|
bool bestFromLowPlatform = false;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
// World-space ray: from high above, pointing straight down
|
|
|
|
|
|
glm::vec3 worldOrigin(glX, glY, glZ + 500.0f);
|
|
|
|
|
|
glm::vec3 worldDir(0.0f, 0.0f, -1.0f);
|
|
|
|
|
|
|
2026-02-08 17:38:30 -08:00
|
|
|
|
// Lambda to test a single group for floor hits
|
|
|
|
|
|
auto testGroupFloor = [&](const WMOInstance& instance, const ModelData& model,
|
|
|
|
|
|
const GroupResources& group,
|
|
|
|
|
|
const glm::vec3& localOrigin, const glm::vec3& localDir) {
|
|
|
|
|
|
const auto& verts = group.collisionVertices;
|
|
|
|
|
|
const auto& indices = group.collisionIndices;
|
|
|
|
|
|
|
|
|
|
|
|
// Use unfiltered triangle list: a vertical ray naturally misses vertical
|
|
|
|
|
|
// geometry via ray-triangle intersection, so pre-filtering by normal is
|
|
|
|
|
|
// unnecessary and risks excluding legitimate floor geometry (steep ramps,
|
|
|
|
|
|
// stair treads with non-trivial normals).
|
|
|
|
|
|
group.getTrianglesInRange(
|
|
|
|
|
|
localOrigin.x - 1.0f, localOrigin.y - 1.0f,
|
|
|
|
|
|
localOrigin.x + 1.0f, localOrigin.y + 1.0f,
|
2026-02-25 11:56:58 -08:00
|
|
|
|
triScratch_);
|
2026-02-08 17:38:30 -08:00
|
|
|
|
|
2026-02-25 11:56:58 -08:00
|
|
|
|
for (uint32_t triStart : triScratch_) {
|
2026-02-08 17:38:30 -08:00
|
|
|
|
const glm::vec3& v0 = verts[indices[triStart]];
|
|
|
|
|
|
const glm::vec3& v1 = verts[indices[triStart + 1]];
|
|
|
|
|
|
const glm::vec3& v2 = verts[indices[triStart + 2]];
|
|
|
|
|
|
|
|
|
|
|
|
float t = rayTriangleIntersect(localOrigin, localDir, v0, v1, v2);
|
|
|
|
|
|
if (t <= 0.0f) {
|
|
|
|
|
|
t = rayTriangleIntersect(localOrigin, localDir, v0, v2, v1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (t > 0.0f) {
|
|
|
|
|
|
glm::vec3 hitLocal = localOrigin + localDir * t;
|
|
|
|
|
|
glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f));
|
|
|
|
|
|
|
2026-02-25 11:56:58 -08:00
|
|
|
|
// Accept floors at or below glZ (the caller already elevates
|
|
|
|
|
|
// glZ by stepUpBudget to handle step-up range). Among those,
|
|
|
|
|
|
// pick the highest (closest to feet).
|
|
|
|
|
|
if (hitWorld.z <= glZ) {
|
2026-02-08 17:38:30 -08:00
|
|
|
|
if (!bestFloor || hitWorld.z > *bestFloor) {
|
|
|
|
|
|
bestFloor = hitWorld.z;
|
|
|
|
|
|
bestFromLowPlatform = model.isLowPlatform;
|
|
|
|
|
|
|
2026-02-25 11:56:58 -08:00
|
|
|
|
// Use precomputed normal, ensure upward, transform to world
|
|
|
|
|
|
glm::vec3 localNormal = group.triNormals[triStart / 3];
|
|
|
|
|
|
if (localNormal.z < 0.0f) localNormal = -localNormal;
|
|
|
|
|
|
glm::vec3 worldNormal = glm::normalize(
|
|
|
|
|
|
glm::vec3(instance.modelMatrix * glm::vec4(localNormal, 0.0f)));
|
|
|
|
|
|
bestNormalZ = std::abs(worldNormal.z);
|
2026-02-08 17:38:30 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-25 10:22:05 -08:00
|
|
|
|
// Fast path: current active interior group and its neighbors are usually
|
|
|
|
|
|
// the right answer for player-floor queries while moving in cities/buildings.
|
|
|
|
|
|
if (activeGroup_.isValid() && activeGroup_.instanceIdx < instances.size()) {
|
|
|
|
|
|
const auto& instance = instances[activeGroup_.instanceIdx];
|
|
|
|
|
|
auto it = loadedModels.find(instance.modelId);
|
|
|
|
|
|
if (it != loadedModels.end() && instance.modelId == activeGroup_.modelId) {
|
|
|
|
|
|
const ModelData& model = it->second;
|
|
|
|
|
|
glm::vec3 localOrigin = glm::vec3(instance.invModelMatrix * glm::vec4(worldOrigin, 1.0f));
|
|
|
|
|
|
glm::vec3 localDir = glm::normalize(glm::vec3(instance.invModelMatrix * glm::vec4(worldDir, 0.0f)));
|
|
|
|
|
|
|
|
|
|
|
|
auto testGroupIdx = [&](uint32_t gi) {
|
|
|
|
|
|
if (gi >= model.groups.size()) return;
|
|
|
|
|
|
if (gi < instance.worldGroupBounds.size()) {
|
|
|
|
|
|
const auto& [gMin, gMax] = instance.worldGroupBounds[gi];
|
|
|
|
|
|
if (glX < gMin.x || glX > gMax.x ||
|
|
|
|
|
|
glY < gMin.y || glY > gMax.y ||
|
|
|
|
|
|
glZ - 4.0f > gMax.z) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const auto& group = model.groups[gi];
|
|
|
|
|
|
if (!rayIntersectsAABB(localOrigin, localDir, group.boundingBoxMin, group.boundingBoxMax)) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
testGroupFloor(instance, model, group, localOrigin, localDir);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (activeGroup_.groupIdx >= 0) {
|
|
|
|
|
|
testGroupIdx(static_cast<uint32_t>(activeGroup_.groupIdx));
|
|
|
|
|
|
}
|
|
|
|
|
|
for (uint32_t ngi : activeGroup_.neighborGroups) {
|
|
|
|
|
|
testGroupIdx(ngi);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 11:56:58 -08:00
|
|
|
|
// Full scan: test all instances (active group result above is not
|
|
|
|
|
|
// early-returned because overlapping WMO instances need full coverage).
|
2026-02-03 16:21:48 -08:00
|
|
|
|
glm::vec3 queryMin(glX - 2.0f, glY - 2.0f, glZ - 8.0f);
|
|
|
|
|
|
glm::vec3 queryMax(glX + 2.0f, glY + 2.0f, glZ + 10.0f);
|
|
|
|
|
|
gatherCandidates(queryMin, queryMax, candidateScratch);
|
|
|
|
|
|
|
|
|
|
|
|
for (size_t idx : candidateScratch) {
|
|
|
|
|
|
const auto& instance = instances[idx];
|
|
|
|
|
|
if (collisionFocusEnabled &&
|
|
|
|
|
|
pointAABBDistanceSq(collisionFocusPos, instance.worldBoundsMin, instance.worldBoundsMax) > collisionFocusRadiusSq) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 20:51:53 -08:00
|
|
|
|
auto it = loadedModels.find(instance.modelId);
|
|
|
|
|
|
if (it == loadedModels.end()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
const ModelData& model = it->second;
|
|
|
|
|
|
float zMarginDown = model.isLowPlatform ? 20.0f : 2.0f;
|
|
|
|
|
|
float zMarginUp = model.isLowPlatform ? 20.0f : 4.0f;
|
|
|
|
|
|
|
2026-02-03 16:21:48 -08:00
|
|
|
|
// Broad-phase reject in world space to avoid expensive matrix transforms.
|
2026-02-25 10:22:05 -08:00
|
|
|
|
if (bestFloor && instance.worldBoundsMax.z <= (*bestFloor + 0.05f)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-02-03 16:21:48 -08:00
|
|
|
|
if (glX < instance.worldBoundsMin.x || glX > instance.worldBoundsMax.x ||
|
|
|
|
|
|
glY < instance.worldBoundsMin.y || glY > instance.worldBoundsMax.y ||
|
2026-02-07 20:51:53 -08:00
|
|
|
|
glZ < instance.worldBoundsMin.z - zMarginDown || glZ > instance.worldBoundsMax.z + zMarginUp) {
|
2026-02-03 16:21:48 -08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
// World-space pre-pass: check which groups' world XY bounds contain
|
|
|
|
|
|
// the query point. For a vertical ray this eliminates most groups
|
|
|
|
|
|
// before any local-space math.
|
|
|
|
|
|
bool anyGroupOverlaps = 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 - 4.0f <= gMax.z) {
|
|
|
|
|
|
anyGroupOverlaps = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!anyGroupOverlaps) continue;
|
|
|
|
|
|
|
2026-02-03 13:33:31 -08:00
|
|
|
|
// Use cached inverse matrix
|
|
|
|
|
|
glm::vec3 localOrigin = glm::vec3(instance.invModelMatrix * glm::vec4(worldOrigin, 1.0f));
|
|
|
|
|
|
glm::vec3 localDir = glm::normalize(glm::vec3(instance.invModelMatrix * glm::vec4(worldDir, 0.0f)));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
for (size_t gi = 0; gi < model.groups.size(); ++gi) {
|
|
|
|
|
|
// World-space group cull — vertical ray at (glX, glY)
|
|
|
|
|
|
if (gi < instance.worldGroupBounds.size()) {
|
|
|
|
|
|
const auto& [gMin, gMax] = instance.worldGroupBounds[gi];
|
|
|
|
|
|
if (glX < gMin.x || glX > gMax.x ||
|
|
|
|
|
|
glY < gMin.y || glY > gMax.y ||
|
|
|
|
|
|
glZ - 4.0f > gMax.z) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-03 14:26:08 -08:00
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
const auto& group = model.groups[gi];
|
2026-02-02 12:24:50 -08:00
|
|
|
|
if (!rayIntersectsAABB(localOrigin, localDir, group.boundingBoxMin, group.boundingBoxMax)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 17:38:30 -08:00
|
|
|
|
testGroupFloor(instance, model, group, localOrigin, localDir);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 20:34:27 -08:00
|
|
|
|
// Persistent grid cache disabled (see above comment about stairs fall-through)
|
2026-02-05 17:20:30 -08:00
|
|
|
|
|
2026-02-08 20:37:04 -08:00
|
|
|
|
if (bestFloor && outNormalZ) {
|
|
|
|
|
|
*outNormalZ = bestNormalZ;
|
2026-02-08 17:38:30 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
return bestFloor;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 19:47:01 -08:00
|
|
|
|
void WMORenderer::debugDumpGroupsAtPosition(float glX, float glY, float glZ) const {
|
|
|
|
|
|
LOG_WARNING("=== WMO Floor Debug at render(", glX, ", ", glY, ", ", glZ, ") ===");
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 worldOrigin(glX, glY, glZ + 500.0f);
|
|
|
|
|
|
glm::vec3 worldDir(0.0f, 0.0f, -1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
int totalInstancesChecked = 0;
|
|
|
|
|
|
int totalGroupsOverlapping = 0;
|
|
|
|
|
|
int totalFloorHits = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (const auto& instance : instances) {
|
|
|
|
|
|
auto it = loadedModels.find(instance.modelId);
|
|
|
|
|
|
if (it == loadedModels.end()) continue;
|
|
|
|
|
|
const ModelData& model = it->second;
|
|
|
|
|
|
|
|
|
|
|
|
// Check instance world bounds
|
|
|
|
|
|
if (glX < instance.worldBoundsMin.x || glX > instance.worldBoundsMax.x ||
|
|
|
|
|
|
glY < instance.worldBoundsMin.y || glY > instance.worldBoundsMax.y ||
|
|
|
|
|
|
glZ < instance.worldBoundsMin.z - 20.0f || glZ > instance.worldBoundsMax.z + 20.0f) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
totalInstancesChecked++;
|
|
|
|
|
|
LOG_WARNING(" Instance modelId=", instance.modelId,
|
|
|
|
|
|
" worldBounds=(", instance.worldBoundsMin.x, ",", instance.worldBoundsMin.y, ",", instance.worldBoundsMin.z,
|
|
|
|
|
|
")-(", instance.worldBoundsMax.x, ",", instance.worldBoundsMax.y, ",", instance.worldBoundsMax.z,
|
|
|
|
|
|
") groups=", model.groups.size());
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 localOrigin = glm::vec3(instance.invModelMatrix * glm::vec4(worldOrigin, 1.0f));
|
|
|
|
|
|
glm::vec3 localDir = glm::normalize(glm::vec3(instance.invModelMatrix * glm::vec4(worldDir, 0.0f)));
|
|
|
|
|
|
|
|
|
|
|
|
for (size_t gi = 0; gi < model.groups.size(); ++gi) {
|
|
|
|
|
|
// Check world-space group bounds
|
|
|
|
|
|
if (gi < instance.worldGroupBounds.size()) {
|
|
|
|
|
|
const auto& [gMin, gMax] = instance.worldGroupBounds[gi];
|
|
|
|
|
|
if (glX < gMin.x || glX > gMax.x ||
|
|
|
|
|
|
glY < gMin.y || glY > gMax.y) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
totalGroupsOverlapping++;
|
|
|
|
|
|
const auto& group = model.groups[gi];
|
|
|
|
|
|
|
|
|
|
|
|
// Count floor triangles in this group under the player
|
|
|
|
|
|
int floorTris = 0;
|
|
|
|
|
|
float bestHitZ = -999999.0f;
|
|
|
|
|
|
const auto& verts = group.collisionVertices;
|
|
|
|
|
|
const auto& indices = group.collisionIndices;
|
|
|
|
|
|
for (size_t ti = 0; ti + 2 < indices.size(); ti += 3) {
|
|
|
|
|
|
const glm::vec3& v0 = verts[indices[ti]];
|
|
|
|
|
|
const glm::vec3& v1 = verts[indices[ti + 1]];
|
|
|
|
|
|
const glm::vec3& v2 = verts[indices[ti + 2]];
|
|
|
|
|
|
float t = rayTriangleIntersect(localOrigin, localDir, v0, v1, v2);
|
|
|
|
|
|
if (t <= 0.0f) t = rayTriangleIntersect(localOrigin, localDir, v0, v2, v1);
|
|
|
|
|
|
if (t > 0.0f) {
|
|
|
|
|
|
glm::vec3 hitLocal = localOrigin + localDir * t;
|
|
|
|
|
|
glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f));
|
|
|
|
|
|
floorTris++;
|
|
|
|
|
|
totalFloorHits++;
|
|
|
|
|
|
if (hitWorld.z > bestHitZ) bestHitZ = hitWorld.z;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 gWorldMin(0), gWorldMax(0);
|
|
|
|
|
|
if (gi < instance.worldGroupBounds.size()) {
|
|
|
|
|
|
gWorldMin = instance.worldGroupBounds[gi].first;
|
|
|
|
|
|
gWorldMax = instance.worldGroupBounds[gi].second;
|
|
|
|
|
|
}
|
|
|
|
|
|
LOG_WARNING(" Group[", gi, "] flags=0x", std::hex, group.groupFlags, std::dec,
|
|
|
|
|
|
" verts=", group.collisionVertices.size(),
|
|
|
|
|
|
" tris=", group.collisionIndices.size()/3,
|
|
|
|
|
|
" batches=", group.mergedBatches.size(),
|
|
|
|
|
|
" isLOD=", group.isLOD,
|
|
|
|
|
|
" floorHits=", floorTris,
|
|
|
|
|
|
" bestHitZ=", bestHitZ,
|
|
|
|
|
|
" wBounds=(", gWorldMin.x, ",", gWorldMin.y, ",", gWorldMin.z,
|
|
|
|
|
|
")-(", gWorldMax.x, ",", gWorldMax.y, ",", gWorldMax.z, ")");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LOG_WARNING("=== Total: ", totalInstancesChecked, " instances, ",
|
|
|
|
|
|
totalGroupsOverlapping, " overlapping groups, ",
|
|
|
|
|
|
totalFloorHits, " floor hits ===");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 20:20:37 -08:00
|
|
|
|
bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, glm::vec3& adjustedPos, bool insideWMO) const {
|
2026-02-03 16:21:48 -08:00
|
|
|
|
QueryTimer timer(&queryTimeMs, &queryCallCount);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
adjustedPos = to;
|
|
|
|
|
|
bool blocked = false;
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 moveDir = to - from;
|
2026-02-08 17:38:30 -08:00
|
|
|
|
float moveDist = glm::length(moveDir);
|
|
|
|
|
|
if (moveDist < 0.001f) return false;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-08 17:38:30 -08:00
|
|
|
|
// Player collision parameters — WoW-style horizontal cylinder
|
2026-02-08 20:20:37 -08:00
|
|
|
|
// Tighter radius when inside for more responsive indoor collision
|
|
|
|
|
|
const float PLAYER_RADIUS = insideWMO ? 0.45f : 0.50f;
|
2026-02-08 17:38:30 -08:00
|
|
|
|
const float PLAYER_HEIGHT = 2.0f; // Cylinder height for Z bounds
|
|
|
|
|
|
const float MAX_STEP_HEIGHT = 1.0f; // Step-up threshold
|
2026-02-03 14:26:08 -08:00
|
|
|
|
|
2026-02-03 16:21:48 -08:00
|
|
|
|
glm::vec3 queryMin = glm::min(from, to) - glm::vec3(8.0f, 8.0f, 5.0f);
|
|
|
|
|
|
glm::vec3 queryMax = glm::max(from, to) + glm::vec3(8.0f, 8.0f, 5.0f);
|
|
|
|
|
|
gatherCandidates(queryMin, queryMax, candidateScratch);
|
|
|
|
|
|
|
|
|
|
|
|
for (size_t idx : candidateScratch) {
|
|
|
|
|
|
const auto& instance = instances[idx];
|
|
|
|
|
|
if (collisionFocusEnabled &&
|
|
|
|
|
|
pointAABBDistanceSq(collisionFocusPos, instance.worldBoundsMin, instance.worldBoundsMax) > collisionFocusRadiusSq) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const float broadMargin = PLAYER_RADIUS + 1.5f;
|
|
|
|
|
|
if (from.x < instance.worldBoundsMin.x - broadMargin && to.x < instance.worldBoundsMin.x - broadMargin) continue;
|
|
|
|
|
|
if (from.x > instance.worldBoundsMax.x + broadMargin && to.x > instance.worldBoundsMax.x + broadMargin) continue;
|
|
|
|
|
|
if (from.y < instance.worldBoundsMin.y - broadMargin && to.y < instance.worldBoundsMin.y - broadMargin) continue;
|
|
|
|
|
|
if (from.y > instance.worldBoundsMax.y + broadMargin && to.y > instance.worldBoundsMax.y + broadMargin) continue;
|
|
|
|
|
|
if (from.z > instance.worldBoundsMax.z + PLAYER_HEIGHT && to.z > instance.worldBoundsMax.z + PLAYER_HEIGHT) continue;
|
|
|
|
|
|
if (from.z + PLAYER_HEIGHT < instance.worldBoundsMin.z && to.z + PLAYER_HEIGHT < instance.worldBoundsMin.z) continue;
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
auto it = loadedModels.find(instance.modelId);
|
|
|
|
|
|
if (it == loadedModels.end()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
const ModelData& model = it->second;
|
|
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
// World-space pre-pass: skip instances where no groups are near the movement
|
|
|
|
|
|
const float wallMargin = PLAYER_RADIUS + 2.0f;
|
|
|
|
|
|
bool anyGroupNear = false;
|
|
|
|
|
|
for (size_t gi = 0; gi < model.groups.size() && gi < instance.worldGroupBounds.size(); ++gi) {
|
|
|
|
|
|
const auto& [gMin, gMax] = instance.worldGroupBounds[gi];
|
|
|
|
|
|
if (to.x >= gMin.x - wallMargin && to.x <= gMax.x + wallMargin &&
|
|
|
|
|
|
to.y >= gMin.y - wallMargin && to.y <= gMax.y + wallMargin &&
|
|
|
|
|
|
to.z + PLAYER_HEIGHT >= gMin.z && to.z <= gMax.z + wallMargin) {
|
|
|
|
|
|
anyGroupNear = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!anyGroupNear) continue;
|
|
|
|
|
|
|
2026-02-03 13:33:31 -08:00
|
|
|
|
// Transform positions into local space using cached inverse
|
2026-02-03 16:04:21 -08:00
|
|
|
|
glm::vec3 localFrom = glm::vec3(instance.invModelMatrix * glm::vec4(from, 1.0f));
|
2026-02-03 13:33:31 -08:00
|
|
|
|
glm::vec3 localTo = glm::vec3(instance.invModelMatrix * glm::vec4(to, 1.0f));
|
2026-02-03 14:26:08 -08:00
|
|
|
|
float localFeetZ = localTo.z;
|
2026-02-07 17:13:09 -08:00
|
|
|
|
for (size_t gi = 0; gi < model.groups.size(); ++gi) {
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
// World-space group cull
|
|
|
|
|
|
if (gi < instance.worldGroupBounds.size()) {
|
|
|
|
|
|
const auto& [gMin, gMax] = instance.worldGroupBounds[gi];
|
|
|
|
|
|
if (to.x < gMin.x - wallMargin || to.x > gMax.x + wallMargin ||
|
|
|
|
|
|
to.y < gMin.y - wallMargin || to.y > gMax.y + wallMargin ||
|
|
|
|
|
|
to.z > gMax.z + PLAYER_HEIGHT || to.z + PLAYER_HEIGHT < gMin.z) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const auto& group = model.groups[gi];
|
|
|
|
|
|
// Local-space AABB check
|
2026-02-03 14:26:08 -08:00
|
|
|
|
float margin = PLAYER_RADIUS + 2.0f;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
if (localTo.x < group.boundingBoxMin.x - margin || localTo.x > group.boundingBoxMax.x + margin ||
|
|
|
|
|
|
localTo.y < group.boundingBoxMin.y - margin || localTo.y > group.boundingBoxMax.y + margin ||
|
|
|
|
|
|
localTo.z < group.boundingBoxMin.z - margin || localTo.z > group.boundingBoxMax.z + margin) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const auto& verts = group.collisionVertices;
|
|
|
|
|
|
const auto& indices = group.collisionIndices;
|
|
|
|
|
|
|
2026-02-07 15:47:43 -08:00
|
|
|
|
// Use spatial grid: query range covering the movement segment + player radius
|
2026-02-08 14:17:04 -08:00
|
|
|
|
float rangeMinX = std::min(localFrom.x, localTo.x) - PLAYER_RADIUS - 1.5f;
|
|
|
|
|
|
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;
|
2026-02-25 11:56:58 -08:00
|
|
|
|
group.getWallTrianglesInRange(rangeMinX, rangeMinY, rangeMaxX, rangeMaxY, triScratch_);
|
2026-02-07 15:47:43 -08:00
|
|
|
|
|
2026-02-25 11:56:58 -08:00
|
|
|
|
for (uint32_t triStart : triScratch_) {
|
2026-02-08 17:38:30 -08:00
|
|
|
|
// Use pre-computed Z bounds for fast vertical reject
|
|
|
|
|
|
const auto& tb = group.triBounds[triStart / 3];
|
|
|
|
|
|
|
|
|
|
|
|
// Only collide with walls in player's vertical range
|
|
|
|
|
|
if (tb.maxZ < localFeetZ + 0.3f) continue;
|
|
|
|
|
|
if (tb.minZ > localFeetZ + PLAYER_HEIGHT) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Skip low geometry that can be stepped over
|
|
|
|
|
|
if (tb.maxZ <= localFeetZ + MAX_STEP_HEIGHT) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Skip very short vertical surfaces (stair risers)
|
|
|
|
|
|
float triHeight = tb.maxZ - tb.minZ;
|
|
|
|
|
|
if (triHeight < 1.0f && tb.maxZ <= localFeetZ + 1.2f) continue;
|
|
|
|
|
|
|
2026-02-07 15:47:43 -08:00
|
|
|
|
const glm::vec3& v0 = verts[indices[triStart]];
|
|
|
|
|
|
const glm::vec3& v1 = verts[indices[triStart + 1]];
|
|
|
|
|
|
const glm::vec3& v2 = verts[indices[triStart + 2]];
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-25 11:56:58 -08:00
|
|
|
|
// Use precomputed normal for swept test and push fallback
|
|
|
|
|
|
glm::vec3 normal = group.triNormals[triStart / 3];
|
|
|
|
|
|
if (glm::dot(normal, normal) < 0.5f) continue; // degenerate
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-08 17:38:30 -08:00
|
|
|
|
// Recompute plane distances with current (possibly pushed) localTo
|
2026-02-07 17:13:09 -08:00
|
|
|
|
float fromDist = glm::dot(localFrom - v0, normal);
|
|
|
|
|
|
float toDist = glm::dot(localTo - v0, normal);
|
|
|
|
|
|
|
2026-02-08 17:38:30 -08:00
|
|
|
|
// Swept test: prevent tunneling when crossing a wall between frames
|
2026-02-03 16:04:21 -08:00
|
|
|
|
if ((fromDist > PLAYER_RADIUS && toDist < -PLAYER_RADIUS) ||
|
|
|
|
|
|
(fromDist < -PLAYER_RADIUS && toDist > PLAYER_RADIUS)) {
|
|
|
|
|
|
float denom = (fromDist - toDist);
|
|
|
|
|
|
if (std::abs(denom) > 1e-6f) {
|
2026-02-05 18:12:27 -08:00
|
|
|
|
float tHit = fromDist / denom;
|
2026-02-03 16:04:21 -08:00
|
|
|
|
if (tHit >= 0.0f && tHit <= 1.0f) {
|
|
|
|
|
|
glm::vec3 hitPoint = localFrom + (localTo - localFrom) * tHit;
|
|
|
|
|
|
glm::vec3 hitClosest = closestPointOnTriangle(hitPoint, v0, v1, v2);
|
|
|
|
|
|
float hitErrSq = glm::dot(hitClosest - hitPoint, hitClosest - hitPoint);
|
2026-02-08 14:17:04 -08:00
|
|
|
|
if (hitErrSq <= 0.15f * 0.15f) {
|
2026-02-03 16:04:21 -08:00
|
|
|
|
float side = fromDist > 0.0f ? 1.0f : -1.0f;
|
2026-02-05 18:12:27 -08:00
|
|
|
|
glm::vec3 safeLocal = hitPoint + normal * side * (PLAYER_RADIUS + 0.05f);
|
2026-02-08 17:38:30 -08:00
|
|
|
|
glm::vec3 pushLocal(safeLocal.x - localTo.x, safeLocal.y - localTo.y, 0.0f);
|
2026-02-08 18:53:56 -08:00
|
|
|
|
// 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;
|
|
|
|
|
|
if (pushLen > MAX_SWEPT_PUSH) {
|
|
|
|
|
|
float scale = MAX_SWEPT_PUSH / pushLen;
|
|
|
|
|
|
pushLocal.x *= scale;
|
|
|
|
|
|
pushLocal.y *= scale;
|
|
|
|
|
|
}
|
|
|
|
|
|
localTo.x += pushLocal.x;
|
|
|
|
|
|
localTo.y += pushLocal.y;
|
2026-02-08 17:38:30 -08:00
|
|
|
|
glm::vec3 pushWorld = glm::vec3(instance.modelMatrix * glm::vec4(pushLocal, 0.0f));
|
|
|
|
|
|
adjustedPos.x += pushWorld.x;
|
|
|
|
|
|
adjustedPos.y += pushWorld.y;
|
2026-02-03 16:04:21 -08:00
|
|
|
|
blocked = true;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-08 17:38:30 -08:00
|
|
|
|
// Horizontal cylinder collision: closest point + horizontal distance
|
2026-02-03 16:04:21 -08:00
|
|
|
|
glm::vec3 closest = closestPointOnTriangle(localTo, v0, v1, v2);
|
|
|
|
|
|
glm::vec3 delta = localTo - closest;
|
|
|
|
|
|
float horizDist = glm::length(glm::vec2(delta.x, delta.y));
|
2026-02-08 17:38:30 -08:00
|
|
|
|
|
2026-02-03 16:04:21 -08:00
|
|
|
|
if (horizDist <= PLAYER_RADIUS) {
|
2026-02-08 17:38:30 -08:00
|
|
|
|
// Skip floor-like surfaces — grounding handles them, not wall collision
|
|
|
|
|
|
float absNz = std::abs(normal.z);
|
2026-02-08 20:31:00 -08:00
|
|
|
|
if (absNz >= 0.35f) continue;
|
2026-02-08 17:38:30 -08:00
|
|
|
|
|
2026-02-08 20:20:37 -08:00
|
|
|
|
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;
|
2026-02-08 18:12:52 -08:00
|
|
|
|
float penetration = (PLAYER_RADIUS - horizDist);
|
|
|
|
|
|
float pushDist = glm::clamp(penetration + SKIN, 0.0f, MAX_PUSH);
|
2026-02-05 18:12:27 -08:00
|
|
|
|
glm::vec2 pushDir2;
|
|
|
|
|
|
if (horizDist > 1e-4f) {
|
|
|
|
|
|
pushDir2 = glm::normalize(glm::vec2(delta.x, delta.y));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
glm::vec2 n2(normal.x, normal.y);
|
2026-02-08 17:38:30 -08:00
|
|
|
|
float n2Len = glm::length(n2);
|
|
|
|
|
|
if (n2Len < 1e-4f) continue;
|
|
|
|
|
|
pushDir2 = n2 / n2Len;
|
2026-02-05 18:12:27 -08:00
|
|
|
|
}
|
|
|
|
|
|
glm::vec3 pushLocal(pushDir2.x * pushDist, pushDir2.y * pushDist, 0.0f);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-07 17:13:09 -08:00
|
|
|
|
localTo.x += pushLocal.x;
|
|
|
|
|
|
localTo.y += pushLocal.y;
|
2026-02-05 18:12:27 -08:00
|
|
|
|
glm::vec3 pushWorld = glm::vec3(instance.modelMatrix * glm::vec4(pushLocal, 0.0f));
|
|
|
|
|
|
adjustedPos.x += pushWorld.x;
|
|
|
|
|
|
adjustedPos.y += pushWorld.y;
|
|
|
|
|
|
blocked = true;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return blocked;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 17:38:30 -08:00
|
|
|
|
void WMORenderer::updateActiveGroup(float glX, float glY, float glZ) {
|
|
|
|
|
|
// If active group is still valid, check if player is still inside it
|
|
|
|
|
|
if (activeGroup_.isValid() && activeGroup_.instanceIdx < instances.size()) {
|
|
|
|
|
|
const auto& instance = instances[activeGroup_.instanceIdx];
|
|
|
|
|
|
if (instance.modelId == activeGroup_.modelId) {
|
|
|
|
|
|
auto it = loadedModels.find(instance.modelId);
|
|
|
|
|
|
if (it != loadedModels.end()) {
|
|
|
|
|
|
const ModelData& model = it->second;
|
|
|
|
|
|
glm::vec3 localPos = glm::vec3(instance.invModelMatrix * glm::vec4(glX, glY, glZ, 1.0f));
|
|
|
|
|
|
|
|
|
|
|
|
// Still inside active group?
|
|
|
|
|
|
if (activeGroup_.groupIdx >= 0 && static_cast<size_t>(activeGroup_.groupIdx) < model.groups.size()) {
|
|
|
|
|
|
const auto& group = model.groups[activeGroup_.groupIdx];
|
|
|
|
|
|
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; // Still in same group
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check portal-neighbor groups
|
|
|
|
|
|
for (uint32_t ngi : activeGroup_.neighborGroups) {
|
|
|
|
|
|
if (ngi < model.groups.size()) {
|
|
|
|
|
|
const auto& group = model.groups[ngi];
|
|
|
|
|
|
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) {
|
|
|
|
|
|
// Moved to a neighbor group — update
|
|
|
|
|
|
activeGroup_.groupIdx = static_cast<int32_t>(ngi);
|
|
|
|
|
|
// Rebuild neighbors for new group
|
|
|
|
|
|
activeGroup_.neighborGroups.clear();
|
|
|
|
|
|
if (ngi < model.groupPortalRefs.size()) {
|
|
|
|
|
|
auto [portalStart, portalCount] = model.groupPortalRefs[ngi];
|
|
|
|
|
|
for (uint16_t pi = 0; pi < portalCount; pi++) {
|
|
|
|
|
|
uint16_t refIdx = portalStart + pi;
|
|
|
|
|
|
if (refIdx < model.portalRefs.size()) {
|
|
|
|
|
|
uint32_t tgt = model.portalRefs[refIdx].groupIndex;
|
|
|
|
|
|
if (tgt < model.groups.size()) {
|
|
|
|
|
|
activeGroup_.neighborGroups.push_back(tgt);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Full scan: find which instance/group contains the player
|
|
|
|
|
|
activeGroup_.invalidate();
|
|
|
|
|
|
|
|
|
|
|
|
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 (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;
|
|
|
|
|
|
glm::vec3 localPos = glm::vec3(instance.invModelMatrix * glm::vec4(glX, glY, glZ, 1.0f));
|
|
|
|
|
|
|
|
|
|
|
|
int gi = findContainingGroup(model, localPos);
|
|
|
|
|
|
if (gi >= 0) {
|
|
|
|
|
|
activeGroup_.instanceIdx = static_cast<uint32_t>(idx);
|
|
|
|
|
|
activeGroup_.modelId = instance.modelId;
|
|
|
|
|
|
activeGroup_.groupIdx = gi;
|
|
|
|
|
|
|
|
|
|
|
|
// Build neighbor list from portal refs
|
|
|
|
|
|
activeGroup_.neighborGroups.clear();
|
|
|
|
|
|
uint32_t groupIdx = static_cast<uint32_t>(gi);
|
|
|
|
|
|
if (groupIdx < model.groupPortalRefs.size()) {
|
|
|
|
|
|
auto [portalStart, portalCount] = model.groupPortalRefs[groupIdx];
|
|
|
|
|
|
for (uint16_t pi = 0; pi < portalCount; pi++) {
|
|
|
|
|
|
uint16_t refIdx = portalStart + pi;
|
|
|
|
|
|
if (refIdx < model.portalRefs.size()) {
|
|
|
|
|
|
uint32_t tgt = model.portalRefs[refIdx].groupIndex;
|
|
|
|
|
|
if (tgt < model.groups.size()) {
|
|
|
|
|
|
activeGroup_.neighborGroups.push_back(tgt);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
bool WMORenderer::isInsideWMO(float glX, float glY, float glZ, uint32_t* outModelId) const {
|
2026-02-03 16:21:48 -08:00
|
|
|
|
QueryTimer timer(&queryTimeMs, &queryCallCount);
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
auto it = loadedModels.find(instance.modelId);
|
|
|
|
|
|
if (it == loadedModels.end()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
const ModelData& model = it->second;
|
|
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
// World-space pre-check: skip instance if no group's world bounds contain point
|
|
|
|
|
|
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));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
for (const auto& group : model.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) {
|
|
|
|
|
|
if (outModelId) *outModelId = instance.modelId;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 16:59:20 -08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 23:18:34 -08:00
|
|
|
|
float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3& direction, float maxDistance) const {
|
2026-02-03 16:21:48 -08:00
|
|
|
|
QueryTimer timer(&queryTimeMs, &queryCallCount);
|
2026-02-02 23:18:34 -08:00
|
|
|
|
float closestHit = maxDistance;
|
2026-02-03 16:04:21 -08:00
|
|
|
|
// Camera collision should primarily react to walls.
|
2026-02-08 17:38:30 -08:00
|
|
|
|
// Wall list pre-filters at abs(normal.z) < 0.55, but for camera raycast we want
|
|
|
|
|
|
// a stricter threshold to avoid ramp/stair geometry pulling the camera in.
|
2026-02-03 16:04:21 -08:00
|
|
|
|
constexpr float MAX_WALKABLE_ABS_NORMAL_Z = 0.20f;
|
|
|
|
|
|
constexpr float MAX_HIT_BELOW_ORIGIN = 0.90f;
|
|
|
|
|
|
constexpr float MAX_HIT_ABOVE_ORIGIN = 0.80f;
|
|
|
|
|
|
constexpr float MIN_SURFACE_ALIGNMENT = 0.25f;
|
2026-02-02 23:18:34 -08:00
|
|
|
|
|
2026-02-03 16:21:48 -08:00
|
|
|
|
glm::vec3 rayEnd = origin + direction * maxDistance;
|
|
|
|
|
|
glm::vec3 queryMin = glm::min(origin, rayEnd) - glm::vec3(1.0f);
|
|
|
|
|
|
glm::vec3 queryMax = glm::max(origin, rayEnd) + glm::vec3(1.0f);
|
|
|
|
|
|
gatherCandidates(queryMin, queryMax, candidateScratch);
|
|
|
|
|
|
|
|
|
|
|
|
for (size_t idx : candidateScratch) {
|
|
|
|
|
|
const auto& instance = instances[idx];
|
|
|
|
|
|
if (collisionFocusEnabled &&
|
|
|
|
|
|
pointAABBDistanceSq(collisionFocusPos, instance.worldBoundsMin, instance.worldBoundsMax) > collisionFocusRadiusSq) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 center = (instance.worldBoundsMin + instance.worldBoundsMax) * 0.5f;
|
|
|
|
|
|
float radius = glm::length(instance.worldBoundsMax - center);
|
|
|
|
|
|
if (glm::length(center - origin) > (maxDistance + radius + 1.0f)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 worldMin = instance.worldBoundsMin - glm::vec3(0.5f);
|
|
|
|
|
|
glm::vec3 worldMax = instance.worldBoundsMax + glm::vec3(0.5f);
|
|
|
|
|
|
if (!rayIntersectsAABB(origin, direction, worldMin, worldMax)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 23:18:34 -08:00
|
|
|
|
auto it = loadedModels.find(instance.modelId);
|
|
|
|
|
|
if (it == loadedModels.end()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
const ModelData& model = it->second;
|
|
|
|
|
|
|
2026-02-03 13:33:31 -08:00
|
|
|
|
// Use cached inverse matrix
|
|
|
|
|
|
glm::vec3 localOrigin = glm::vec3(instance.invModelMatrix * glm::vec4(origin, 1.0f));
|
|
|
|
|
|
glm::vec3 localDir = glm::normalize(glm::vec3(instance.invModelMatrix * glm::vec4(direction, 0.0f)));
|
2026-02-02 23:18:34 -08:00
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
|
for (size_t gi = 0; gi < model.groups.size(); ++gi) {
|
|
|
|
|
|
// World-space group cull — skip groups whose world AABB doesn't intersect the ray
|
|
|
|
|
|
if (gi < instance.worldGroupBounds.size()) {
|
|
|
|
|
|
const auto& [gMin, gMax] = instance.worldGroupBounds[gi];
|
|
|
|
|
|
if (!rayIntersectsAABB(origin, direction, gMin, gMax)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const auto& group = model.groups[gi];
|
|
|
|
|
|
// Local-space AABB cull
|
2026-02-03 15:17:54 -08:00
|
|
|
|
if (!rayIntersectsAABB(localOrigin, localDir, group.boundingBoxMin, group.boundingBoxMax)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 17:38:30 -08:00
|
|
|
|
// Narrow-phase: triangle raycast using spatial grid (wall-only).
|
2026-02-03 15:17:54 -08:00
|
|
|
|
const auto& verts = group.collisionVertices;
|
|
|
|
|
|
const auto& indices = group.collisionIndices;
|
2026-02-07 15:54:33 -08:00
|
|
|
|
|
|
|
|
|
|
// Compute local-space ray endpoint and query grid for XY range
|
|
|
|
|
|
glm::vec3 localEnd = localOrigin + localDir * (closestHit / glm::length(
|
|
|
|
|
|
glm::vec3(instance.modelMatrix * glm::vec4(localDir, 0.0f))));
|
|
|
|
|
|
float rMinX = std::min(localOrigin.x, localEnd.x) - 1.0f;
|
|
|
|
|
|
float rMinY = std::min(localOrigin.y, localEnd.y) - 1.0f;
|
|
|
|
|
|
float rMaxX = std::max(localOrigin.x, localEnd.x) + 1.0f;
|
|
|
|
|
|
float rMaxY = std::max(localOrigin.y, localEnd.y) + 1.0f;
|
2026-02-25 11:56:58 -08:00
|
|
|
|
group.getWallTrianglesInRange(rMinX, rMinY, rMaxX, rMaxY, triScratch_);
|
2026-02-07 15:54:33 -08:00
|
|
|
|
|
2026-02-25 11:56:58 -08:00
|
|
|
|
for (uint32_t triStart : triScratch_) {
|
2026-02-07 15:54:33 -08:00
|
|
|
|
const glm::vec3& v0 = verts[indices[triStart]];
|
|
|
|
|
|
const glm::vec3& v1 = verts[indices[triStart + 1]];
|
|
|
|
|
|
const glm::vec3& v2 = verts[indices[triStart + 2]];
|
2026-02-25 11:56:58 -08:00
|
|
|
|
glm::vec3 triNormal = group.triNormals[triStart / 3];
|
|
|
|
|
|
if (glm::dot(triNormal, triNormal) < 0.5f) continue; // degenerate
|
|
|
|
|
|
// Wall list pre-filters at 0.35; apply stricter camera threshold
|
2026-02-03 16:04:21 -08:00
|
|
|
|
if (std::abs(triNormal.z) > MAX_WALKABLE_ABS_NORMAL_Z) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Ignore near-grazing intersections that tend to come from ramps/arches
|
|
|
|
|
|
// and cause camera pull-in even when no meaningful wall is behind the player.
|
|
|
|
|
|
if (std::abs(glm::dot(triNormal, localDir)) < MIN_SURFACE_ALIGNMENT) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-02-03 15:17:54 -08:00
|
|
|
|
|
|
|
|
|
|
float t = rayTriangleIntersect(localOrigin, localDir, v0, v1, v2);
|
|
|
|
|
|
if (t <= 0.0f) {
|
|
|
|
|
|
// Two-sided collision.
|
|
|
|
|
|
t = rayTriangleIntersect(localOrigin, localDir, v0, v2, v1);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (t <= 0.0f) continue;
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 localHit = localOrigin + localDir * t;
|
|
|
|
|
|
glm::vec3 worldHit = glm::vec3(instance.modelMatrix * glm::vec4(localHit, 1.0f));
|
2026-02-03 16:04:21 -08:00
|
|
|
|
// Ignore low hits; camera floor handling already keeps the camera above ground.
|
|
|
|
|
|
// This avoids gate/ramp floor geometry pulling the camera in too aggressively.
|
|
|
|
|
|
if (worldHit.z < origin.z - MAX_HIT_BELOW_ORIGIN) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Ignore very high hits (arches/ceilings) that should not clamp normal chase-cam distance.
|
|
|
|
|
|
if (worldHit.z > origin.z + MAX_HIT_ABOVE_ORIGIN) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-02-03 15:17:54 -08:00
|
|
|
|
float worldDist = glm::length(worldHit - origin);
|
|
|
|
|
|
if (worldDist > 0.0f && worldDist < closestHit && worldDist <= maxDistance) {
|
|
|
|
|
|
closestHit = worldDist;
|
2026-02-02 23:18:34 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return closestHit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Occlusion queries stubbed out in Vulkan (were disabled by default anyway)
|
2026-02-05 15:51:08 -08:00
|
|
|
|
|
2026-02-22 02:59:24 -08:00
|
|
|
|
void WMORenderer::recreatePipelines() {
|
|
|
|
|
|
if (!vkCtx_) return;
|
|
|
|
|
|
VkDevice device = vkCtx_->getDevice();
|
|
|
|
|
|
|
|
|
|
|
|
// Destroy old main-pass pipelines (NOT shadow, NOT pipeline layout)
|
|
|
|
|
|
if (opaquePipeline_) { vkDestroyPipeline(device, opaquePipeline_, nullptr); opaquePipeline_ = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (transparentPipeline_) { vkDestroyPipeline(device, transparentPipeline_, nullptr); transparentPipeline_ = VK_NULL_HANDLE; }
|
2026-02-23 00:43:14 -08:00
|
|
|
|
if (glassPipeline_) { vkDestroyPipeline(device, glassPipeline_, nullptr); glassPipeline_ = VK_NULL_HANDLE; }
|
2026-02-22 02:59:24 -08:00
|
|
|
|
if (wireframePipeline_) { vkDestroyPipeline(device, wireframePipeline_, nullptr); wireframePipeline_ = VK_NULL_HANDLE; }
|
|
|
|
|
|
|
|
|
|
|
|
// --- Load shaders ---
|
|
|
|
|
|
VkShaderModule vertShader, fragShader;
|
|
|
|
|
|
if (!vertShader.loadFromFile(device, "assets/shaders/wmo.vert.spv") ||
|
|
|
|
|
|
!fragShader.loadFromFile(device, "assets/shaders/wmo.frag.spv")) {
|
|
|
|
|
|
core::Logger::getInstance().error("WMORenderer::recreatePipelines: failed to load shaders");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Vertex input ---
|
|
|
|
|
|
struct WMOVertexData {
|
|
|
|
|
|
glm::vec3 position;
|
|
|
|
|
|
glm::vec3 normal;
|
|
|
|
|
|
glm::vec2 texCoord;
|
|
|
|
|
|
glm::vec4 color;
|
2026-02-23 01:10:58 -08:00
|
|
|
|
glm::vec4 tangent;
|
2026-02-22 02:59:24 -08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
VkVertexInputBindingDescription vertexBinding{};
|
|
|
|
|
|
vertexBinding.binding = 0;
|
|
|
|
|
|
vertexBinding.stride = sizeof(WMOVertexData);
|
|
|
|
|
|
vertexBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
|
|
|
|
|
|
2026-02-23 01:10:58 -08:00
|
|
|
|
std::vector<VkVertexInputAttributeDescription> vertexAttribs(5);
|
2026-02-22 02:59:24 -08:00
|
|
|
|
vertexAttribs[0] = { 0, 0, VK_FORMAT_R32G32B32_SFLOAT,
|
|
|
|
|
|
static_cast<uint32_t>(offsetof(WMOVertexData, position)) };
|
|
|
|
|
|
vertexAttribs[1] = { 1, 0, VK_FORMAT_R32G32B32_SFLOAT,
|
|
|
|
|
|
static_cast<uint32_t>(offsetof(WMOVertexData, normal)) };
|
|
|
|
|
|
vertexAttribs[2] = { 2, 0, VK_FORMAT_R32G32_SFLOAT,
|
|
|
|
|
|
static_cast<uint32_t>(offsetof(WMOVertexData, texCoord)) };
|
|
|
|
|
|
vertexAttribs[3] = { 3, 0, VK_FORMAT_R32G32B32A32_SFLOAT,
|
|
|
|
|
|
static_cast<uint32_t>(offsetof(WMOVertexData, color)) };
|
2026-02-23 01:10:58 -08:00
|
|
|
|
vertexAttribs[4] = { 4, 0, VK_FORMAT_R32G32B32A32_SFLOAT,
|
|
|
|
|
|
static_cast<uint32_t>(offsetof(WMOVertexData, tangent)) };
|
2026-02-22 02:59:24 -08:00
|
|
|
|
|
|
|
|
|
|
VkRenderPass mainPass = vkCtx_->getImGuiRenderPass();
|
|
|
|
|
|
|
|
|
|
|
|
opaquePipeline_ = PipelineBuilder()
|
|
|
|
|
|
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
|
|
|
|
|
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
|
|
|
|
|
.setVertexInput({ vertexBinding }, vertexAttribs)
|
|
|
|
|
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
|
|
|
|
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
|
|
|
|
|
.setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL)
|
|
|
|
|
|
.setColorBlendAttachment(PipelineBuilder::blendDisabled())
|
|
|
|
|
|
.setMultisample(vkCtx_->getMsaaSamples())
|
|
|
|
|
|
.setLayout(pipelineLayout_)
|
|
|
|
|
|
.setRenderPass(mainPass)
|
|
|
|
|
|
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
|
|
|
|
|
.build(device);
|
|
|
|
|
|
|
|
|
|
|
|
transparentPipeline_ = PipelineBuilder()
|
|
|
|
|
|
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
|
|
|
|
|
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
|
|
|
|
|
.setVertexInput({ vertexBinding }, vertexAttribs)
|
|
|
|
|
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
|
|
|
|
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
|
|
|
|
|
.setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL)
|
|
|
|
|
|
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
|
|
|
|
|
|
.setMultisample(vkCtx_->getMsaaSamples())
|
|
|
|
|
|
.setLayout(pipelineLayout_)
|
|
|
|
|
|
.setRenderPass(mainPass)
|
2026-02-23 00:43:14 -08:00
|
|
|
|
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
|
|
|
|
|
.build(device);
|
|
|
|
|
|
|
|
|
|
|
|
glassPipeline_ = PipelineBuilder()
|
|
|
|
|
|
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
|
|
|
|
|
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
|
|
|
|
|
.setVertexInput({ vertexBinding }, vertexAttribs)
|
|
|
|
|
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
|
|
|
|
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
|
|
|
|
|
.setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL)
|
|
|
|
|
|
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
|
|
|
|
|
|
.setMultisample(vkCtx_->getMsaaSamples())
|
|
|
|
|
|
.setLayout(pipelineLayout_)
|
|
|
|
|
|
.setRenderPass(mainPass)
|
2026-02-22 02:59:24 -08:00
|
|
|
|
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
|
|
|
|
|
.build(device);
|
|
|
|
|
|
|
|
|
|
|
|
wireframePipeline_ = PipelineBuilder()
|
|
|
|
|
|
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
|
|
|
|
|
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
|
|
|
|
|
.setVertexInput({ vertexBinding }, vertexAttribs)
|
|
|
|
|
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
|
|
|
|
|
.setRasterization(VK_POLYGON_MODE_LINE, VK_CULL_MODE_NONE)
|
|
|
|
|
|
.setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL)
|
|
|
|
|
|
.setColorBlendAttachment(PipelineBuilder::blendDisabled())
|
|
|
|
|
|
.setMultisample(vkCtx_->getMsaaSamples())
|
|
|
|
|
|
.setLayout(pipelineLayout_)
|
|
|
|
|
|
.setRenderPass(mainPass)
|
|
|
|
|
|
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
|
|
|
|
|
.build(device);
|
|
|
|
|
|
|
|
|
|
|
|
vertShader.destroy();
|
|
|
|
|
|
fragShader.destroy();
|
|
|
|
|
|
|
|
|
|
|
|
core::Logger::getInstance().info("WMORenderer: pipelines recreated");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
} // namespace rendering
|
|
|
|
|
|
} // namespace wowee
|