feat(editor): add standalone world editor (rough/WIP)

Standalone wowee_editor tool for creating custom WoW zones.
This is a rough initial implementation — many features work but
M2/WMO rendering still has issues (frame sync, texture layout
transitions) and needs further polish.

Terrain:
- Create new blank terrain with 10 biome types (Grassland, Forest,
  Jungle, Desert, Barrens, Snow, Swamp, Rocky, Beach, Volcanic)
- Load existing ADT tiles from extracted game data
- Sculpt brushes: Raise, Lower, Smooth, Flatten, Level
- Chunk edge stitching prevents seams between tiles
- Undo/redo (100-deep stack, Ctrl+Z/Ctrl+Shift+Z)
- Save to WoW ADT/WDT format

Texture Painting:
- Paint/Erase/Replace Base modes
- Full tileset texture browser (1285 textures from manifest)
- Per-zone directory filtering and search
- Alpha map editing with 4-layer limit (auto-replaces weakest)

Object Placement:
- M2 and WMO model placement with full manifest browser (11k M2s, 2k WMOs)
- M2Renderer + WMORenderer integrated (loads .skin files for WotLK)
- Ghost preview follows cursor before placing
- Ctrl+click selection, right-click context menu
- Transform gizmo (Move/Rotate/Scale with axis constraints)
- Position/rotation/scale editing in properties panel

NPC/Monster System:
- 631 creature presets scanned from manifest, categorized
  (Critters, Beasts, Humanoids, Undead, Demons, etc.)
- Stats editor: level, health, mana, damage, armor, faction
- Behavior: Stationary, Patrol, Wander, Scripted
- Aggro/leash radius, respawn time, flags (hostile/vendor/etc.)
- Save creature spawns to JSON

Water:
- Place water at configurable height per chunk
- Liquid types: Water, Ocean, Magma, Slime
- Rendered as translucent colored quads
- Saved in ADT MH2O format

Infrastructure:
- Free-fly camera (WASD/QE, right-drag look, scroll speed)
- 5-mode toolbar: Sculpt | Paint | Objects | Water | NPCs
- Asset browser indexes full manifest on startup
- Editor water/marker shaders (pos+color vertex format)
- forceNoCull added to M2Renderer for editor use
- AssetManifest::getEntries() and AssetManager::getManifest() exposed

Known issues:
- M2/WMO rendering may not display on first placement (frame index
  sync between update/render was misaligned — now fixed but untested
  end-to-end)
- Validation layer errors on shutdown (resource cleanup ordering)
- Object placement on steep terrain can miss raycast
- No undo for texture painting or object placement yet
This commit is contained in:
Kelsi 2026-05-05 03:47:03 -07:00
parent d138269a35
commit 2980ca83e7
42 changed files with 5647 additions and 3 deletions

View file

@ -0,0 +1,286 @@
#include "transform_gizmo.hpp"
#include "rendering/vk_context.hpp"
#include "rendering/vk_shader.hpp"
#include "core/logger.hpp"
#include <cstring>
#include <cmath>
namespace wowee {
namespace editor {
TransformGizmo::TransformGizmo() = default;
TransformGizmo::~TransformGizmo() { shutdown(); }
bool TransformGizmo::initialize(rendering::VkContext* ctx, VkRenderPass renderPass,
VkDescriptorSetLayout perFrameLayout) {
vkCtx_ = ctx;
renderPass_ = renderPass;
perFrameLayout_ = perFrameLayout;
return createPipeline();
}
void TransformGizmo::shutdown() {
if (!vkCtx_) return;
if (vertexBuffer_) {
vmaDestroyBuffer(vkCtx_->getAllocator(), vertexBuffer_, vertexAlloc_);
vertexBuffer_ = VK_NULL_HANDLE;
}
if (pipeline_) { vkDestroyPipeline(vkCtx_->getDevice(), pipeline_, nullptr); pipeline_ = VK_NULL_HANDLE; }
if (pipelineLayout_) { vkDestroyPipelineLayout(vkCtx_->getDevice(), pipelineLayout_, nullptr); pipelineLayout_ = VK_NULL_HANDLE; }
vkCtx_ = nullptr;
}
void TransformGizmo::setTarget(const glm::vec3& position, float scale) {
targetPos_ = position;
targetScale_ = scale;
visible_ = true;
updateBuffers();
}
void TransformGizmo::beginDrag(const glm::vec2& screenPos) {
dragging_ = true;
dragStart_ = screenPos;
dragCurrent_ = screenPos;
moveDelta_ = glm::vec3(0);
rotateDelta_ = glm::vec3(0);
scaleDelta_ = 0.0f;
}
void TransformGizmo::updateDrag(const glm::vec2& screenPos, const rendering::Camera& camera,
float screenW, float screenH) {
if (!dragging_) return;
glm::vec2 delta = screenPos - dragCurrent_;
dragCurrent_ = screenPos;
float sensitivity = 1.0f;
if (mode_ == TransformMode::Move) {
glm::vec3 right = camera.getRight();
glm::vec3 forward = camera.getForward();
forward.z = 0; forward = glm::normalize(forward);
if (axis_ == TransformAxis::X || axis_ == TransformAxis::All)
moveDelta_ += right * delta.x * sensitivity;
if (axis_ == TransformAxis::Y || axis_ == TransformAxis::All)
moveDelta_ -= forward * delta.y * sensitivity;
if (axis_ == TransformAxis::Z)
moveDelta_.z -= delta.y * sensitivity;
} else if (mode_ == TransformMode::Rotate) {
float rotSpeed = 0.5f;
if (axis_ == TransformAxis::Z || axis_ == TransformAxis::All)
rotateDelta_.z += delta.x * rotSpeed;
if (axis_ == TransformAxis::X)
rotateDelta_.x += delta.y * rotSpeed;
if (axis_ == TransformAxis::Y)
rotateDelta_.y += delta.y * rotSpeed;
} else if (mode_ == TransformMode::Scale) {
scaleDelta_ += delta.x * 0.01f;
}
(void)screenW; (void)screenH;
}
void TransformGizmo::endDrag() {
dragging_ = false;
}
void TransformGizmo::updateBuffers() {
if (!vkCtx_ || !visible_) return;
if (vertexBuffer_) {
vmaDestroyBuffer(vkCtx_->getAllocator(), vertexBuffer_, vertexAlloc_);
vertexBuffer_ = VK_NULL_HANDLE;
}
std::vector<GizmoVertex> verts;
float len = 15.0f * targetScale_;
float tip = 3.0f * targetScale_;
float w = 0.8f * targetScale_;
glm::vec3 p = targetPos_;
auto addLine = [&](glm::vec3 a, glm::vec3 b, float r, float g, float bl, float alpha) {
// Thick line as thin quad
glm::vec3 dir = glm::normalize(b - a);
glm::vec3 up(0,0,1);
if (std::abs(glm::dot(dir, up)) > 0.99f) up = glm::vec3(1,0,0);
glm::vec3 side = glm::normalize(glm::cross(dir, up)) * w * 0.5f;
GizmoVertex v;
v.color[0] = r; v.color[1] = g; v.color[2] = bl; v.color[3] = alpha;
v.pos[0] = a.x+side.x; v.pos[1] = a.y+side.y; v.pos[2] = a.z+side.z; verts.push_back(v);
v.pos[0] = a.x-side.x; v.pos[1] = a.y-side.y; v.pos[2] = a.z-side.z; verts.push_back(v);
v.pos[0] = b.x+side.x; v.pos[1] = b.y+side.y; v.pos[2] = b.z+side.z; verts.push_back(v);
v.pos[0] = b.x+side.x; v.pos[1] = b.y+side.y; v.pos[2] = b.z+side.z; verts.push_back(v);
v.pos[0] = a.x-side.x; v.pos[1] = a.y-side.y; v.pos[2] = a.z-side.z; verts.push_back(v);
v.pos[0] = b.x-side.x; v.pos[1] = b.y-side.y; v.pos[2] = b.z-side.z; verts.push_back(v);
};
auto addArrowhead = [&](glm::vec3 base, glm::vec3 tipPt, float r, float g, float bl) {
glm::vec3 dir = glm::normalize(tipPt - base);
glm::vec3 up(0,0,1);
if (std::abs(glm::dot(dir, up)) > 0.99f) up = glm::vec3(1,0,0);
glm::vec3 s1 = glm::normalize(glm::cross(dir, up)) * tip * 0.4f;
glm::vec3 s2 = glm::normalize(glm::cross(dir, s1)) * tip * 0.4f;
GizmoVertex v;
v.color[0] = r; v.color[1] = g; v.color[2] = bl; v.color[3] = 1.0f;
// 4 faces
auto tri = [&](glm::vec3 a, glm::vec3 b, glm::vec3 c) {
v.pos[0]=a.x; v.pos[1]=a.y; v.pos[2]=a.z; verts.push_back(v);
v.pos[0]=b.x; v.pos[1]=b.y; v.pos[2]=b.z; verts.push_back(v);
v.pos[0]=c.x; v.pos[1]=c.y; v.pos[2]=c.z; verts.push_back(v);
};
tri(tipPt, base+s1, base+s2);
tri(tipPt, base+s2, base-s1);
tri(tipPt, base-s1, base-s2);
tri(tipPt, base-s2, base+s1);
};
bool showMove = (mode_ == TransformMode::Move || mode_ == TransformMode::None);
bool showRot = (mode_ == TransformMode::Rotate);
bool showScale = (mode_ == TransformMode::Scale);
float xAlpha = (axis_ == TransformAxis::X || axis_ == TransformAxis::All) ? 1.0f : 0.3f;
float yAlpha = (axis_ == TransformAxis::Y || axis_ == TransformAxis::All) ? 1.0f : 0.3f;
float zAlpha = (axis_ == TransformAxis::Z || axis_ == TransformAxis::All) ? 1.0f : 0.3f;
if (showMove || showRot) {
// X axis - Red
addLine(p, p + glm::vec3(len, 0, 0), 1, 0.2f, 0.2f, xAlpha);
addArrowhead(p + glm::vec3(len, 0, 0), p + glm::vec3(len + tip, 0, 0), 1, 0.2f, 0.2f);
// Y axis - Green
addLine(p, p + glm::vec3(0, len, 0), 0.2f, 1, 0.2f, yAlpha);
addArrowhead(p + glm::vec3(0, len, 0), p + glm::vec3(0, len + tip, 0), 0.2f, 1, 0.2f);
// Z axis - Blue
addLine(p, p + glm::vec3(0, 0, len), 0.3f, 0.3f, 1, zAlpha);
addArrowhead(p + glm::vec3(0, 0, len), p + glm::vec3(0, 0, len + tip), 0.3f, 0.3f, 1);
}
if (showScale) {
// Scale indicator: box at each axis end
float bs = tip * 0.5f;
auto addBox = [&](glm::vec3 c, float r, float g, float bl) {
// Simple cube from 12 triangles
GizmoVertex v; v.color[0]=r; v.color[1]=g; v.color[2]=bl; v.color[3]=1;
auto face = [&](glm::vec3 a, glm::vec3 b, glm::vec3 cc, glm::vec3 d) {
v.pos[0]=a.x;v.pos[1]=a.y;v.pos[2]=a.z;verts.push_back(v);
v.pos[0]=b.x;v.pos[1]=b.y;v.pos[2]=b.z;verts.push_back(v);
v.pos[0]=cc.x;v.pos[1]=cc.y;v.pos[2]=cc.z;verts.push_back(v);
v.pos[0]=cc.x;v.pos[1]=cc.y;v.pos[2]=cc.z;verts.push_back(v);
v.pos[0]=d.x;v.pos[1]=d.y;v.pos[2]=d.z;verts.push_back(v);
v.pos[0]=a.x;v.pos[1]=a.y;v.pos[2]=a.z;verts.push_back(v);
};
face(c+glm::vec3(-bs,-bs,bs),c+glm::vec3(bs,-bs,bs),c+glm::vec3(bs,bs,bs),c+glm::vec3(-bs,bs,bs));
face(c+glm::vec3(-bs,-bs,-bs),c+glm::vec3(-bs,bs,-bs),c+glm::vec3(bs,bs,-bs),c+glm::vec3(bs,-bs,-bs));
};
addLine(p, p + glm::vec3(len, 0, 0), 1, 0.5f, 0, 1);
addBox(p + glm::vec3(len, 0, 0), 1, 0.5f, 0);
addLine(p, p + glm::vec3(0, len, 0), 0.5f, 1, 0, 1);
addBox(p + glm::vec3(0, len, 0), 0.5f, 1, 0);
addLine(p, p + glm::vec3(0, 0, len), 0, 0.5f, 1, 1);
addBox(p + glm::vec3(0, 0, len), 0, 0.5f, 1);
}
if (verts.empty()) return;
vertexCount_ = static_cast<uint32_t>(verts.size());
VkBufferCreateInfo bufInfo{};
bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufInfo.size = verts.size() * sizeof(GizmoVertex);
bufInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
VmaAllocationCreateInfo allocInfo{};
allocInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU;
allocInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT;
VmaAllocationInfo mapInfo{};
if (vmaCreateBuffer(vkCtx_->getAllocator(), &bufInfo, &allocInfo,
&vertexBuffer_, &vertexAlloc_, &mapInfo) == VK_SUCCESS) {
std::memcpy(mapInfo.pMappedData, verts.data(), verts.size() * sizeof(GizmoVertex));
}
}
void TransformGizmo::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
if (!visible_ || !vertexBuffer_ || vertexCount_ == 0 || !pipeline_) return;
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_,
0, 1, &perFrameSet, 0, nullptr);
VkDeviceSize offset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer_, &offset);
vkCmdDraw(cmd, vertexCount_, 1, 0, 0);
}
bool TransformGizmo::createPipeline() {
VkDevice dev = vkCtx_->getDevice();
VkPipelineLayoutCreateInfo layoutInfo{};
layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
layoutInfo.setLayoutCount = 1;
layoutInfo.pSetLayouts = &perFrameLayout_;
if (vkCreatePipelineLayout(dev, &layoutInfo, nullptr, &pipelineLayout_) != VK_SUCCESS)
return false;
rendering::VkShaderModule vertMod, fragMod;
if (!vertMod.loadFromFile(dev, "assets/shaders/editor_water.vert.spv") ||
!fragMod.loadFromFile(dev, "assets/shaders/editor_water.frag.spv")) {
LOG_WARNING("Gizmo shaders not found");
return true;
}
VkPipelineShaderStageCreateInfo stages[2] = { vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT) };
VkVertexInputBindingDescription binding{}; binding.stride = sizeof(GizmoVertex);
VkVertexInputAttributeDescription attrs[2]{};
attrs[0].location=0; attrs[0].format=VK_FORMAT_R32G32B32_SFLOAT; attrs[0].offset=0;
attrs[1].location=1; attrs[1].format=VK_FORMAT_R32G32B32A32_SFLOAT; attrs[1].offset=12;
VkPipelineVertexInputStateCreateInfo vi{}; vi.sType=VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vi.vertexBindingDescriptionCount=1; vi.pVertexBindingDescriptions=&binding;
vi.vertexAttributeDescriptionCount=2; vi.pVertexAttributeDescriptions=attrs;
VkPipelineInputAssemblyStateCreateInfo ia{}; ia.sType=VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
ia.topology=VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
VkPipelineViewportStateCreateInfo vps{}; vps.sType=VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
vps.viewportCount=1; vps.scissorCount=1;
VkPipelineRasterizationStateCreateInfo rast{}; rast.sType=VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rast.polygonMode=VK_POLYGON_MODE_FILL; rast.cullMode=VK_CULL_MODE_NONE; rast.lineWidth=1;
VkPipelineMultisampleStateCreateInfo ms{}; ms.sType=VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
ms.rasterizationSamples=vkCtx_->getMsaaSamples();
VkPipelineDepthStencilStateCreateInfo ds{}; ds.sType=VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
ds.depthTestEnable=VK_FALSE; // Always on top
VkPipelineColorBlendAttachmentState blend{};
blend.blendEnable=VK_TRUE;
blend.srcColorBlendFactor=VK_BLEND_FACTOR_SRC_ALPHA; blend.dstColorBlendFactor=VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
blend.colorBlendOp=VK_BLEND_OP_ADD;
blend.srcAlphaBlendFactor=VK_BLEND_FACTOR_ONE; blend.dstAlphaBlendFactor=VK_BLEND_FACTOR_ZERO;
blend.alphaBlendOp=VK_BLEND_OP_ADD;
blend.colorWriteMask=0xF;
VkPipelineColorBlendStateCreateInfo cb{}; cb.sType=VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
cb.attachmentCount=1; cb.pAttachments=&blend;
VkDynamicState dynStates[]={VK_DYNAMIC_STATE_VIEWPORT,VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dyn{}; dyn.sType=VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dyn.dynamicStateCount=2; dyn.pDynamicStates=dynStates;
VkGraphicsPipelineCreateInfo pci{}; pci.sType=VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pci.stageCount=2; pci.pStages=stages;
pci.pVertexInputState=&vi; pci.pInputAssemblyState=&ia; pci.pViewportState=&vps;
pci.pRasterizationState=&rast; pci.pMultisampleState=&ms; pci.pDepthStencilState=&ds;
pci.pColorBlendState=&cb; pci.pDynamicState=&dyn;
pci.layout=pipelineLayout_; pci.renderPass=renderPass_;
vkCreateGraphicsPipelines(dev, vkCtx_->getPipelineCache(), 1, &pci, nullptr, &pipeline_);
return true;
}
} // namespace editor
} // namespace wowee