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

@ -1277,6 +1277,111 @@ set_target_properties(blp_convert PROPERTIES
) )
install(TARGETS blp_convert RUNTIME DESTINATION bin) install(TARGETS blp_convert RUNTIME DESTINATION bin)
# ---- Tool: wowee_editor (Standalone World Editor) ----
add_executable(wowee_editor
tools/editor/main.cpp
tools/editor/editor_app.cpp
tools/editor/editor_camera.cpp
tools/editor/editor_viewport.cpp
tools/editor/editor_ui.cpp
tools/editor/editor_brush.cpp
tools/editor/editor_history.cpp
tools/editor/terrain_editor.cpp
tools/editor/texture_painter.cpp
tools/editor/object_placer.cpp
tools/editor/npc_spawner.cpp
tools/editor/npc_presets.cpp
tools/editor/transform_gizmo.cpp
tools/editor/asset_browser.cpp
tools/editor/editor_water.cpp
tools/editor/editor_markers.cpp
tools/editor/adt_writer.cpp
# Pipeline (asset loading)
src/pipeline/blp_loader.cpp
src/pipeline/dbc_loader.cpp
src/pipeline/dbc_layout.cpp
src/pipeline/asset_manager.cpp
src/pipeline/asset_manifest.cpp
src/pipeline/loose_file_reader.cpp
src/pipeline/m2_loader.cpp
src/pipeline/wmo_loader.cpp
src/pipeline/adt_loader.cpp
src/pipeline/wdt_loader.cpp
src/pipeline/terrain_mesh.cpp
# Rendering core
src/rendering/vk_context.cpp
src/rendering/vk_utils.cpp
src/rendering/vk_shader.cpp
src/rendering/vk_texture.cpp
src/rendering/vk_buffer.cpp
src/rendering/vk_pipeline.cpp
src/rendering/camera.cpp
src/rendering/terrain_renderer.cpp
src/rendering/m2_renderer.cpp
src/rendering/m2_renderer_instance.cpp
src/rendering/m2_renderer_particles.cpp
src/rendering/m2_renderer_render.cpp
src/rendering/m2_model_classifier.cpp
src/rendering/wmo_renderer.cpp
src/rendering/frustum.cpp
# Core
src/core/window.cpp
src/core/logger.cpp
src/core/memory_monitor.cpp
# stb_image (needed by AssetManager for PNG overrides)
tools/editor/stb_image_impl.cpp
)
target_include_directories(wowee_editor PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/tools/editor
)
target_include_directories(wowee_editor SYSTEM PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/extern
${CMAKE_CURRENT_SOURCE_DIR}/extern/vk-bootstrap/src
)
target_link_libraries(wowee_editor PRIVATE
SDL2::SDL2
Vulkan::Vulkan
Threads::Threads
ZLIB::ZLIB
${CMAKE_DL_LIBS}
imgui
vk-bootstrap
)
if(TARGET glm::glm)
target_link_libraries(wowee_editor PRIVATE glm::glm)
elseif(glm_FOUND)
target_include_directories(wowee_editor PRIVATE ${GLM_INCLUDE_DIRS})
endif()
if(UNIX AND NOT APPLE)
find_package(X11 QUIET)
if(X11_FOUND)
target_link_libraries(wowee_editor PRIVATE X11)
endif()
endif()
if(WIN32)
target_link_libraries(wowee_editor PRIVATE ws2_32)
if(TARGET SDL2::SDL2main)
target_link_libraries(wowee_editor PRIVATE SDL2::SDL2main)
endif()
endif()
if(NOT MSVC)
target_compile_options(wowee_editor PRIVATE -Wall -Wextra -Wpedantic -Wno-missing-field-initializers)
endif()
if(GLSLC)
add_dependencies(wowee_editor wowee_shaders)
endif()
set_target_properties(wowee_editor PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
install(TARGETS wowee_editor RUNTIME DESTINATION bin)
message(STATUS " wowee_editor tool: ENABLED")
# Print configuration summary # Print configuration summary
message(STATUS "") message(STATUS "")
message(STATUS "Wowee Configuration:") message(STATUS "Wowee Configuration:")

View file

@ -0,0 +1,8 @@
#version 450
layout(location = 0) in vec4 vColor;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vColor;
}

View file

@ -0,0 +1,24 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec4 aColor;
layout(location = 0) out vec4 vColor;
void main() {
gl_Position = projection * view * vec4(aPosition, 1.0);
vColor = aColor;
}

View file

@ -47,6 +47,7 @@ public:
*/ */
bool isInitialized() const { return initialized; } bool isInitialized() const { return initialized; }
const std::string& getDataPath() const { return dataPath; } const std::string& getDataPath() const { return dataPath; }
const AssetManifest& getManifest() const { return manifest_; }
/** /**
* Load a BLP texture * Load a BLP texture

View file

@ -58,6 +58,11 @@ public:
*/ */
size_t getEntryCount() const { return entries_.size(); } size_t getEntryCount() const { return entries_.size(); }
/**
* Access entries map (read-only) for iteration
*/
const std::unordered_map<std::string, Entry>& getEntries() const { return entries_; }
/** /**
* Check if manifest is loaded * Check if manifest is loaded
*/ */

View file

@ -309,6 +309,7 @@ public:
/** Set the HiZ system for occlusion culling (Phase 6.3). nullptr disables HiZ. */ /** Set the HiZ system for occlusion culling (Phase 6.3). nullptr disables HiZ. */
void setHiZSystem(HiZSystem* hiz) { hizSystem_ = hiz; } void setHiZSystem(HiZSystem* hiz) { hizSystem_ = hiz; }
void setForceNoCull(bool v) { forceNoCull_ = v; }
/** Ensure GPU→CPU cull output is visible to the host after a fence wait. /** Ensure GPU→CPU cull output is visible to the host after a fence wait.
* Call after the early compute submission finishes (endSingleTimeCommands). */ * Call after the early compute submission finishes (endSingleTimeCommands). */
@ -727,6 +728,7 @@ private:
glm::vec3 cachedCamPos_ = glm::vec3(0.0f); glm::vec3 cachedCamPos_ = glm::vec3(0.0f);
float cachedMaxRenderDistSq_ = 0.0f; float cachedMaxRenderDistSq_ = 0.0f;
float smoothedRenderDist_ = 1000.0f; // Smoothed render distance to prevent flickering float smoothedRenderDist_ = 1000.0f; // Smoothed render distance to prevent flickering
bool forceNoCull_ = false;
// Thread count for parallel bone animation // Thread count for parallel bone animation
uint32_t numAnimThreads_ = 1; uint32_t numAnimThreads_ = 1;

View file

@ -824,11 +824,11 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
for (uint32_t i = 0; i < totalInstances; ++i) { for (uint32_t i = 0; i < totalInstances; ++i) {
const auto& instance = instances[i]; const auto& instance = instances[i];
if (gpuCullAvailable && i < numInstances) { if (forceNoCull_) {
// GPU already tested flags + distance + frustum if (!instance.cachedIsValid) continue;
} else if (gpuCullAvailable && i < numInstances) {
if (!visibility[i]) continue; if (!visibility[i]) continue;
} else { } else {
// CPU fallback: for non-GPU path or instances beyond cull buffer
if (!instance.cachedIsValid || instance.cachedIsSmoke || instance.cachedIsInvisibleTrap) continue; if (!instance.cachedIsValid || instance.cachedIsSmoke || instance.cachedIsInvisibleTrap) continue;
glm::vec3 toCam = instance.position - camPos; glm::vec3 toCam = instance.position - camPos;

337
tools/editor/adt_writer.cpp Normal file
View file

@ -0,0 +1,337 @@
#include "adt_writer.hpp"
#include "core/logger.hpp"
#include <fstream>
#include <cstring>
#include <filesystem>
namespace wowee {
namespace editor {
// ADT chunk magics (little-endian as read from file)
static constexpr uint32_t MVER = 0x4D564552;
static constexpr uint32_t MHDR = 0x4D484452;
static constexpr uint32_t MCIN = 0x4D43494E;
static constexpr uint32_t MTEX = 0x4D544558;
static constexpr uint32_t MMDX = 0x4D4D4458;
static constexpr uint32_t MMID = 0x4D4D4944;
static constexpr uint32_t MWMO = 0x4D574D4F;
static constexpr uint32_t MWID = 0x4D574944;
static constexpr uint32_t MDDF = 0x4D444446;
static constexpr uint32_t MODF = 0x4D4F4446;
static constexpr uint32_t MCNK = 0x4D434E4B;
static constexpr uint32_t MCVT = 0x4D435654;
static constexpr uint32_t MCNR = 0x4D434E52;
static constexpr uint32_t MCLY = 0x4D434C59;
static constexpr uint32_t MCAL = 0x4D43414C;
void ADTWriter::writeChunkHeader(std::vector<uint8_t>& buf, uint32_t magic, uint32_t size) {
writeU32(buf, magic);
writeU32(buf, size);
}
void ADTWriter::writeU32(std::vector<uint8_t>& buf, uint32_t val) {
buf.push_back(val & 0xFF);
buf.push_back((val >> 8) & 0xFF);
buf.push_back((val >> 16) & 0xFF);
buf.push_back((val >> 24) & 0xFF);
}
void ADTWriter::writeU16(std::vector<uint8_t>& buf, uint16_t val) {
buf.push_back(val & 0xFF);
buf.push_back((val >> 8) & 0xFF);
}
void ADTWriter::writeFloat(std::vector<uint8_t>& buf, float val) {
uint32_t bits;
std::memcpy(&bits, &val, 4);
writeU32(buf, bits);
}
void ADTWriter::writeBytes(std::vector<uint8_t>& buf, const void* data, size_t size) {
const uint8_t* p = static_cast<const uint8_t*>(data);
buf.insert(buf.end(), p, p + size);
}
void ADTWriter::patchSize(std::vector<uint8_t>& buf, size_t headerOffset) {
uint32_t size = static_cast<uint32_t>(buf.size() - headerOffset - 8);
std::memcpy(buf.data() + headerOffset + 4, &size, 4);
}
void ADTWriter::writeMVER(std::vector<uint8_t>& buf) {
writeChunkHeader(buf, MVER, 4);
writeU32(buf, 18); // ADT version
}
void ADTWriter::writeMHDR(std::vector<uint8_t>& buf, size_t& mhdrOffset) {
mhdrOffset = buf.size();
writeChunkHeader(buf, MHDR, 64);
// 16 uint32 fields — all zeros for now (offsets filled later if needed)
for (int i = 0; i < 16; i++) writeU32(buf, 0);
}
void ADTWriter::writeMTEX(std::vector<uint8_t>& buf, const pipeline::ADTTerrain& terrain) {
size_t start = buf.size();
writeChunkHeader(buf, MTEX, 0);
for (const auto& tex : terrain.textures) {
writeBytes(buf, tex.c_str(), tex.size() + 1); // null-terminated
}
patchSize(buf, start);
}
void ADTWriter::writeMMDX(std::vector<uint8_t>& buf, const pipeline::ADTTerrain& terrain) {
size_t start = buf.size();
writeChunkHeader(buf, MMDX, 0);
for (const auto& name : terrain.doodadNames) {
writeBytes(buf, name.c_str(), name.size() + 1);
}
patchSize(buf, start);
// MMID: offsets into MMDX
size_t mmidStart = buf.size();
writeChunkHeader(buf, MMID, 0);
uint32_t offset = 0;
for (const auto& name : terrain.doodadNames) {
writeU32(buf, offset);
offset += static_cast<uint32_t>(name.size() + 1);
}
patchSize(buf, mmidStart);
}
void ADTWriter::writeMWMO(std::vector<uint8_t>& buf, const pipeline::ADTTerrain& terrain) {
size_t start = buf.size();
writeChunkHeader(buf, MWMO, 0);
for (const auto& name : terrain.wmoNames) {
writeBytes(buf, name.c_str(), name.size() + 1);
}
patchSize(buf, start);
// MWID: offsets into MWMO
size_t mwidStart = buf.size();
writeChunkHeader(buf, MWID, 0);
uint32_t offset = 0;
for (const auto& name : terrain.wmoNames) {
writeU32(buf, offset);
offset += static_cast<uint32_t>(name.size() + 1);
}
patchSize(buf, mwidStart);
}
void ADTWriter::writeMDDF(std::vector<uint8_t>& buf, const pipeline::ADTTerrain& terrain) {
size_t start = buf.size();
writeChunkHeader(buf, MDDF, 0);
for (const auto& p : terrain.doodadPlacements) {
writeU32(buf, p.nameId);
writeU32(buf, p.uniqueId);
writeFloat(buf, p.position[0]);
writeFloat(buf, p.position[1]);
writeFloat(buf, p.position[2]);
writeFloat(buf, p.rotation[0]);
writeFloat(buf, p.rotation[1]);
writeFloat(buf, p.rotation[2]);
writeU16(buf, p.scale);
writeU16(buf, p.flags);
}
patchSize(buf, start);
}
void ADTWriter::writeMODF(std::vector<uint8_t>& buf, const pipeline::ADTTerrain& terrain) {
size_t start = buf.size();
writeChunkHeader(buf, MODF, 0);
for (const auto& p : terrain.wmoPlacements) {
writeU32(buf, p.nameId);
writeU32(buf, p.uniqueId);
writeFloat(buf, p.position[0]);
writeFloat(buf, p.position[1]);
writeFloat(buf, p.position[2]);
writeFloat(buf, p.rotation[0]);
writeFloat(buf, p.rotation[1]);
writeFloat(buf, p.rotation[2]);
writeFloat(buf, p.extentLower[0]);
writeFloat(buf, p.extentLower[1]);
writeFloat(buf, p.extentLower[2]);
writeFloat(buf, p.extentUpper[0]);
writeFloat(buf, p.extentUpper[1]);
writeFloat(buf, p.extentUpper[2]);
writeU16(buf, p.flags);
writeU16(buf, p.doodadSet);
}
patchSize(buf, start);
}
void ADTWriter::writeMCNK(std::vector<uint8_t>& buf, const pipeline::MapChunk& chunk,
int chunkX, int chunkY) {
size_t mcnkStart = buf.size();
writeChunkHeader(buf, MCNK, 0);
// MCNK header (128 bytes)
writeU32(buf, chunk.flags);
writeU32(buf, chunkX);
writeU32(buf, chunkY);
writeU32(buf, static_cast<uint32_t>(chunk.layers.size()));
writeU32(buf, 0); // nDoodadRefs
// Offsets within MCNK — filled with placeholder (parser uses sub-chunk magic scanning)
for (int i = 0; i < 5; i++) writeU32(buf, 0); // ofsHeight, ofsNormal, ofsLayer, ofsRefs, ofsAlpha
writeU32(buf, 0); // sizeAlpha
writeU32(buf, 0); // ofsShadow
writeU32(buf, 0); // sizeShadow
writeU32(buf, 0); // areaid
writeU32(buf, 0); // nMapObjRefs
writeU16(buf, chunk.holes);
writeU16(buf, 0); // padding
// 16 bytes of low-quality texture map (doodadStencil)
for (int i = 0; i < 4; i++) writeU32(buf, 0);
writeU32(buf, 0); // predTex
writeU32(buf, 0); // noEffectDoodad
writeU32(buf, 0); // ofsSndEmitters
writeU32(buf, 0); // nSndEmitters
writeU32(buf, 0); // ofsLiquid
writeU32(buf, 0); // sizeLiquid
writeFloat(buf, chunk.position[0]);
writeFloat(buf, chunk.position[1]);
writeFloat(buf, chunk.position[2]);
writeU32(buf, 0); // ofsMCCV
writeU32(buf, 0); // ofsMCLV
writeU32(buf, 0); // unused
// MCVT sub-chunk (145 floats = 580 bytes)
writeChunkHeader(buf, MCVT, 145 * 4);
for (int i = 0; i < 145; i++) {
writeFloat(buf, chunk.heightMap.heights[i]);
}
// MCNR sub-chunk (145 * 3 = 435 bytes + 13 pad = 448)
writeChunkHeader(buf, MCNR, 435 + 13);
writeBytes(buf, chunk.normals.data(), 435);
for (int i = 0; i < 13; i++) buf.push_back(0); // padding
// MCLY sub-chunk
{
size_t mclyStart = buf.size();
writeChunkHeader(buf, MCLY, 0);
for (const auto& layer : chunk.layers) {
writeU32(buf, layer.textureId);
writeU32(buf, layer.flags);
writeU32(buf, layer.offsetMCAL);
writeU32(buf, layer.effectId);
}
patchSize(buf, mclyStart);
}
// MCAL sub-chunk (alpha maps)
{
size_t mcalStart = buf.size();
writeChunkHeader(buf, MCAL, 0);
if (!chunk.alphaMap.empty()) {
writeBytes(buf, chunk.alphaMap.data(), chunk.alphaMap.size());
}
patchSize(buf, mcalStart);
}
patchSize(buf, mcnkStart);
}
std::vector<uint8_t> ADTWriter::serialize(const pipeline::ADTTerrain& terrain) {
std::vector<uint8_t> buf;
buf.reserve(2 * 1024 * 1024);
writeMVER(buf);
size_t mhdrOffset = 0;
writeMHDR(buf, mhdrOffset);
// MCIN placeholder (256 entries × 16 bytes = 4096 bytes)
size_t mcinStart = buf.size();
writeChunkHeader(buf, MCIN, 4096);
for (int i = 0; i < 256 * 4; i++) writeU32(buf, 0);
writeMTEX(buf, terrain);
writeMMDX(buf, terrain);
writeMWMO(buf, terrain);
writeMDDF(buf, terrain);
writeMODF(buf, terrain);
// Write 256 MCNK chunks and record offsets
std::vector<size_t> mcnkOffsets(256);
std::vector<uint32_t> mcnkSizes(256);
for (int y = 0; y < 16; y++) {
for (int x = 0; x < 16; x++) {
int idx = y * 16 + x;
mcnkOffsets[idx] = buf.size();
writeMCNK(buf, terrain.chunks[idx], x, y);
mcnkSizes[idx] = static_cast<uint32_t>(buf.size() - mcnkOffsets[idx]);
}
}
// Patch MCIN with offsets and sizes
for (int i = 0; i < 256; i++) {
size_t entryOffset = mcinStart + 8 + i * 16;
uint32_t offset = static_cast<uint32_t>(mcnkOffsets[i]);
uint32_t size = mcnkSizes[i];
std::memcpy(buf.data() + entryOffset, &offset, 4);
std::memcpy(buf.data() + entryOffset + 4, &size, 4);
// flags and asyncId stay 0
}
return buf;
}
bool ADTWriter::write(const pipeline::ADTTerrain& terrain, const std::string& path) {
auto data = serialize(terrain);
auto dir = std::filesystem::path(path).parent_path();
if (!dir.empty()) {
std::filesystem::create_directories(dir);
}
std::ofstream file(path, std::ios::binary);
if (!file) {
LOG_ERROR("Failed to open file for writing: ", path);
return false;
}
file.write(reinterpret_cast<const char*>(data.data()), data.size());
LOG_INFO("ADT written: ", path, " (", data.size(), " bytes)");
return true;
}
bool ADTWriter::writeWDT(const std::string& mapName, int tileX, int tileY,
const std::string& path) {
std::vector<uint8_t> buf;
buf.reserve(32768);
// MVER
writeChunkHeader(buf, MVER, 4);
writeU32(buf, 18);
// MPHD (map header — 32 bytes, all zeros = no special flags)
writeChunkHeader(buf, 0x4D504844, 32);
for (int i = 0; i < 8; i++) writeU32(buf, 0);
// MAIN (64×64 grid of 8-byte entries: flags + asyncId)
writeChunkHeader(buf, 0x4D41494E, 64 * 64 * 8);
for (int y = 0; y < 64; y++) {
for (int x = 0; x < 64; x++) {
if (x == tileX && y == tileY) {
writeU32(buf, 1); // FLAG_EXISTS
} else {
writeU32(buf, 0);
}
writeU32(buf, 0); // asyncId
}
}
auto dir = std::filesystem::path(path).parent_path();
if (!dir.empty()) std::filesystem::create_directories(dir);
std::ofstream file(path, std::ios::binary);
if (!file) {
LOG_ERROR("Failed to write WDT: ", path);
return false;
}
file.write(reinterpret_cast<const char*>(buf.data()), buf.size());
LOG_INFO("WDT written: ", path, " (", buf.size(), " bytes, map=", mapName, ")");
return true;
}
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,38 @@
#pragma once
#include "pipeline/adt_loader.hpp"
#include <string>
#include <vector>
#include <cstdint>
namespace wowee {
namespace editor {
class ADTWriter {
public:
static bool write(const pipeline::ADTTerrain& terrain, const std::string& path);
static std::vector<uint8_t> serialize(const pipeline::ADTTerrain& terrain);
// Write a minimal WDT file for a single-tile map
static bool writeWDT(const std::string& mapName, int tileX, int tileY, const std::string& path);
private:
static void writeChunkHeader(std::vector<uint8_t>& buf, uint32_t magic, uint32_t size);
static void writeU32(std::vector<uint8_t>& buf, uint32_t val);
static void writeU16(std::vector<uint8_t>& buf, uint16_t val);
static void writeFloat(std::vector<uint8_t>& buf, float val);
static void writeBytes(std::vector<uint8_t>& buf, const void* data, size_t size);
static void patchSize(std::vector<uint8_t>& buf, size_t headerOffset);
static void writeMVER(std::vector<uint8_t>& buf);
static void writeMHDR(std::vector<uint8_t>& buf, size_t& mhdrOffset);
static void writeMTEX(std::vector<uint8_t>& buf, const pipeline::ADTTerrain& terrain);
static void writeMMDX(std::vector<uint8_t>& buf, const pipeline::ADTTerrain& terrain);
static void writeMWMO(std::vector<uint8_t>& buf, const pipeline::ADTTerrain& terrain);
static void writeMDDF(std::vector<uint8_t>& buf, const pipeline::ADTTerrain& terrain);
static void writeMODF(std::vector<uint8_t>& buf, const pipeline::ADTTerrain& terrain);
static void writeMCNK(std::vector<uint8_t>& buf, const pipeline::MapChunk& chunk, int chunkX, int chunkY);
};
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,98 @@
#include "asset_browser.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/asset_manifest.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <set>
namespace wowee {
namespace editor {
std::string AssetBrowser::extractFilename(const std::string& path) {
auto pos = path.rfind('\\');
return pos != std::string::npos ? path.substr(pos + 1) : path;
}
std::string AssetBrowser::extractDirectory(const std::string& path) {
auto pos = path.rfind('\\');
return pos != std::string::npos ? path.substr(0, pos) : "";
}
void AssetBrowser::initialize(pipeline::AssetManager* am) {
if (initialized_ || !am) return;
initialized_ = true;
const auto& entries = am->getManifest().getEntries();
std::set<std::string> texDirSet, m2DirSet, wmoDirSet;
for (const auto& [path, entry] : entries) {
// Tileset textures
if (path.starts_with("tileset\\") && path.ends_with(".blp")) {
// Skip specular/normal maps
if (path.ends_with("_s.blp") || path.ends_with("_h.blp") ||
path.ends_with("_n.blp")) continue;
AssetEntry ae;
ae.wowPath = path;
ae.displayName = extractFilename(path);
ae.directory = extractDirectory(path);
textures_.push_back(ae);
texDirSet.insert(ae.directory);
}
// M2 models (world doodads)
if (path.ends_with(".m2")) {
// Focus on world assets, skip character/creature/item models
if (path.starts_with("world\\") || path.starts_with("dungeons\\")) {
AssetEntry ae;
ae.wowPath = path;
ae.displayName = extractFilename(path);
ae.directory = extractDirectory(path);
m2Models_.push_back(ae);
m2DirSet.insert(ae.directory);
}
}
// WMOs
if (path.ends_with(".wmo") && !path.ends_with("_lod.wmo")) {
// Skip group files (_000.wmo, _001.wmo, etc.)
bool isGroup = false;
if (path.size() > 8) {
auto base = path.substr(path.size() - 8);
if (base[0] == '_' && std::isdigit(base[1]) && std::isdigit(base[2]) &&
std::isdigit(base[3]))
isGroup = true;
}
if (isGroup) continue;
AssetEntry ae;
ae.wowPath = path;
ae.displayName = extractFilename(path);
ae.directory = extractDirectory(path);
wmos_.push_back(ae);
wmoDirSet.insert(ae.directory);
}
}
std::sort(textures_.begin(), textures_.end(),
[](const AssetEntry& a, const AssetEntry& b) { return a.wowPath < b.wowPath; });
std::sort(m2Models_.begin(), m2Models_.end(),
[](const AssetEntry& a, const AssetEntry& b) { return a.wowPath < b.wowPath; });
std::sort(wmos_.begin(), wmos_.end(),
[](const AssetEntry& a, const AssetEntry& b) { return a.wowPath < b.wowPath; });
textureDirs_.assign(texDirSet.begin(), texDirSet.end());
m2Dirs_.assign(m2DirSet.begin(), m2DirSet.end());
wmoDirs_.assign(wmoDirSet.begin(), wmoDirSet.end());
std::sort(textureDirs_.begin(), textureDirs_.end());
std::sort(m2Dirs_.begin(), m2Dirs_.end());
std::sort(wmoDirs_.begin(), wmoDirs_.end());
LOG_INFO("Asset browser: ", textures_.size(), " textures, ",
m2Models_.size(), " M2s, ", wmos_.size(), " WMOs indexed");
}
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,46 @@
#pragma once
#include <string>
#include <vector>
#include <unordered_map>
namespace wowee {
namespace pipeline { class AssetManager; }
namespace editor {
struct AssetEntry {
std::string wowPath;
std::string displayName;
std::string directory;
};
class AssetBrowser {
public:
void initialize(pipeline::AssetManager* am);
const std::vector<AssetEntry>& getTextures() const { return textures_; }
const std::vector<AssetEntry>& getM2Models() const { return m2Models_; }
const std::vector<AssetEntry>& getWMOs() const { return wmos_; }
const std::vector<std::string>& getTextureDirectories() const { return textureDirs_; }
const std::vector<std::string>& getM2Directories() const { return m2Dirs_; }
const std::vector<std::string>& getWMODirectories() const { return wmoDirs_; }
bool isInitialized() const { return initialized_; }
private:
static std::string extractFilename(const std::string& path);
static std::string extractDirectory(const std::string& path);
std::vector<AssetEntry> textures_;
std::vector<AssetEntry> m2Models_;
std::vector<AssetEntry> wmos_;
std::vector<std::string> textureDirs_;
std::vector<std::string> m2Dirs_;
std::vector<std::string> wmoDirs_;
bool initialized_ = false;
};
} // namespace editor
} // namespace wowee

603
tools/editor/editor_app.cpp Normal file
View file

@ -0,0 +1,603 @@
#include "editor_app.hpp"
#include "adt_writer.hpp"
#include "rendering/vk_context.hpp"
#include "pipeline/adt_loader.hpp"
#include "pipeline/terrain_mesh.hpp"
#include "core/logger.hpp"
#include <imgui.h>
#include <imgui_impl_sdl2.h>
#include <imgui_impl_vulkan.h>
#include <chrono>
#include <sstream>
namespace wowee {
namespace editor {
EditorApp::EditorApp() = default;
EditorApp::~EditorApp() { shutdown(); }
bool EditorApp::initialize(const std::string& dataPath) {
dataPath_ = dataPath;
core::WindowConfig wc;
wc.title = "Wowee World Editor";
wc.width = 1600;
wc.height = 900;
window_ = std::make_unique<core::Window>(wc);
if (!window_->initialize()) {
LOG_ERROR("Failed to initialize window");
return false;
}
assetManager_ = std::make_unique<pipeline::AssetManager>();
if (!assetManager_->initialize(dataPath)) {
LOG_ERROR("Failed to initialize asset manager with path: ", dataPath);
return false;
}
initImGui();
auto* vkCtx = window_->getVkContext();
camera_.getCamera().setAspectRatio(window_->getAspectRatio());
camera_.setPosition(glm::vec3(0.0f, 0.0f, 300.0f));
camera_.setYawPitch(0.0f, -30.0f);
if (!viewport_.initialize(vkCtx, assetManager_.get(), &camera_.getCamera())) {
LOG_ERROR("Failed to initialize editor viewport");
return false;
}
assetBrowser_.initialize(assetManager_.get());
npcPresets_.initialize(assetManager_.get());
LOG_INFO("Editor initialized (data: ", dataPath, ")");
return true;
}
void EditorApp::run() {
auto lastTime = std::chrono::steady_clock::now();
while (!window_->shouldClose()) {
auto now = std::chrono::steady_clock::now();
float dt = std::chrono::duration<float>(now - lastTime).count();
lastTime = now;
dt = std::min(dt, 0.1f);
processEvents();
auto* vkCtx = window_->getVkContext();
if (vkCtx->isSwapchainDirty()) {
int w = window_->getWidth();
int h = window_->getHeight();
if (w > 0 && h > 0) {
(void)vkCtx->recreateSwapchain(w, h);
camera_.getCamera().setAspectRatio(static_cast<float>(w) / h);
}
}
camera_.update(dt);
updateTerrainEditing(dt);
// Handle pending UI actions
ui_.processActions(*this);
// Refresh dirty terrain chunks
refreshDirtyChunks();
// Rebuild object visuals when object list changes
size_t objCount = objectPlacer_.objectCount() + npcSpawner_.spawnCount();
if (objectsDirty_ || objCount != lastObjectCount_) {
objectsDirty_ = false;
lastObjectCount_ = objCount;
vkDeviceWaitIdle(window_->getVkContext()->getDevice());
viewport_.rebuildObjects(objectPlacer_.getObjects(), npcSpawner_.getSpawns());
}
// Show gizmo arrows on selected object
auto& gizmo = viewport_.getGizmo();
if (auto* sel = objectPlacer_.getSelected()) {
gizmo.setTarget(sel->position, sel->scale);
} else {
gizmo.setMode(TransformMode::None);
}
uint32_t imageIndex = 0;
VkCommandBuffer cmd = vkCtx->beginFrame(imageIndex);
if (cmd == VK_NULL_HANDLE) continue;
// Update M2 animations AFTER beginFrame (so getCurrentFrame is correct)
viewport_.update(dt);
ImGui_ImplVulkan_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
ui_.render(*this);
ImGui::Render();
VkRenderPassBeginInfo rpInfo{};
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
rpInfo.renderPass = vkCtx->getImGuiRenderPass();
rpInfo.framebuffer = vkCtx->getSwapchainFramebuffers()[imageIndex];
rpInfo.renderArea.offset = {0, 0};
rpInfo.renderArea.extent = vkCtx->getSwapchainExtent();
VkClearValue clearValues[4]{};
clearValues[0].color = {{0.15f, 0.15f, 0.2f, 1.0f}};
clearValues[1].depthStencil = {1.0f, 0};
rpInfo.clearValueCount = 2;
rpInfo.pClearValues = clearValues;
vkCmdBeginRenderPass(cmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE);
auto ext = vkCtx->getSwapchainExtent();
VkViewport vp{};
vp.width = static_cast<float>(ext.width);
vp.height = static_cast<float>(ext.height);
vp.minDepth = 0.0f;
vp.maxDepth = 1.0f;
vkCmdSetViewport(cmd, 0, 1, &vp);
VkRect2D scissor{};
scissor.extent = ext;
vkCmdSetScissor(cmd, 0, 1, &scissor);
viewport_.render(cmd);
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), cmd);
vkCmdEndRenderPass(cmd);
vkCtx->endFrame(cmd, imageIndex);
}
}
void EditorApp::shutdown() {
if (!window_) return;
auto* vkCtx = window_->getVkContext();
if (vkCtx) vkDeviceWaitIdle(vkCtx->getDevice());
viewport_.shutdown();
shutdownImGui();
if (assetManager_) {
assetManager_->shutdown();
assetManager_.reset();
}
window_.reset();
}
void EditorApp::processEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
ImGui_ImplSDL2_ProcessEvent(&event);
if (event.type == SDL_QUIT) {
window_->setShouldClose(true);
return;
}
if (event.type == SDL_WINDOWEVENT) {
if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
window_->setSize(event.window.data1, event.window.data2);
window_->getVkContext()->markSwapchainDirty();
}
}
auto& io = ImGui::GetIO();
if (event.type == SDL_KEYDOWN || event.type == SDL_KEYUP) {
if (event.type == SDL_KEYDOWN) {
auto sc = event.key.keysym.scancode;
if (sc == SDL_SCANCODE_F3) setWireframe(!isWireframe());
if (sc == SDL_SCANCODE_DELETE && mode_ == EditorMode::PlaceObject) {
objectPlacer_.deleteSelected();
objectsDirty_ = true;
}
if (sc == SDL_SCANCODE_Z && (event.key.keysym.mod & KMOD_CTRL)) {
if (event.key.keysym.mod & KMOD_SHIFT)
terrainEditor_.redo();
else
terrainEditor_.undo();
}
}
if (!io.WantCaptureKeyboard)
camera_.processKeyEvent(event.key);
}
if (event.type == SDL_MOUSEMOTION && !io.WantCaptureMouse) {
// Gizmo drag takes priority over camera
auto& giz = viewport_.getGizmo();
if (giz.isDragging()) {
auto ext = window_->getVkContext()->getSwapchainExtent();
giz.updateDrag(glm::vec2(static_cast<float>(event.motion.x),
static_cast<float>(event.motion.y)),
camera_.getCamera(),
static_cast<float>(ext.width),
static_cast<float>(ext.height));
// Apply transform to selected object
if (auto* sel = objectPlacer_.getSelected()) {
if (giz.getMode() == TransformMode::Move) {
sel->position += giz.getMoveDelta();
giz.beginDrag(glm::vec2(event.motion.x, event.motion.y));
} else if (giz.getMode() == TransformMode::Rotate) {
sel->rotation += giz.getRotateDelta();
giz.beginDrag(glm::vec2(event.motion.x, event.motion.y));
} else if (giz.getMode() == TransformMode::Scale) {
sel->scale = std::max(0.1f, sel->scale + giz.getScaleDelta());
giz.beginDrag(glm::vec2(event.motion.x, event.motion.y));
}
giz.setTarget(sel->position, sel->scale);
objectsDirty_ = true;
}
} else {
camera_.processMouseMotion(event.motion.xrel, event.motion.yrel);
}
}
if ((event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) && !io.WantCaptureMouse) {
// Right-click context menu on selected objects
if (event.button.button == SDL_BUTTON_RIGHT && event.type == SDL_MOUSEBUTTONDOWN) {
auto& giz = viewport_.getGizmo();
if (giz.isDragging()) {
giz.endDrag();
giz.setMode(TransformMode::None);
} else if (objectPlacer_.getSelected()) {
ImGui::OpenPopup("ObjectContextMenu");
} else {
camera_.processMouseButton(event.button);
}
} else {
camera_.processMouseButton(event.button);
}
// Left click
if (event.button.button == SDL_BUTTON_LEFT && terrain_.isLoaded()) {
// End gizmo drag on click release
auto& giz = viewport_.getGizmo();
if (giz.isDragging() && event.type == SDL_MOUSEBUTTONUP) {
giz.endDrag();
giz.setMode(TransformMode::None);
} else if (event.type == SDL_MOUSEBUTTONDOWN) {
// Ctrl+click = select object (any mode)
if ((event.key.keysym.mod & KMOD_CTRL) || (SDL_GetModState() & KMOD_CTRL)) {
auto ext = window_->getVkContext()->getSwapchainExtent();
rendering::Ray ray = camera_.getCamera().screenToWorldRay(
static_cast<float>(event.button.x),
static_cast<float>(event.button.y),
static_cast<float>(ext.width),
static_cast<float>(ext.height));
objectPlacer_.selectAt(ray, 200.0f);
} else if (mode_ == EditorMode::NPC) {
auto ext = window_->getVkContext()->getSwapchainExtent();
rendering::Ray ray = camera_.getCamera().screenToWorldRay(
static_cast<float>(event.button.x),
static_cast<float>(event.button.y),
static_cast<float>(ext.width),
static_cast<float>(ext.height));
glm::vec3 hitPos;
if (terrainEditor_.raycastTerrain(ray, hitPos)) {
auto& tmpl = npcSpawner_.getTemplate();
tmpl.position = hitPos;
npcSpawner_.placeCreature(tmpl);
objectsDirty_ = true;
}
} else if (mode_ == EditorMode::Water) {
painting_ = true;
} else if (mode_ == EditorMode::PlaceObject) {
// Raycast now at click time for accurate placement
auto ext = window_->getVkContext()->getSwapchainExtent();
rendering::Ray ray = camera_.getCamera().screenToWorldRay(
static_cast<float>(event.button.x),
static_cast<float>(event.button.y),
static_cast<float>(ext.width),
static_cast<float>(ext.height));
glm::vec3 hitPos;
if (terrainEditor_.raycastTerrain(ray, hitPos)) {
objectPlacer_.placeObject(hitPos);
objectsDirty_ = true;
}
} else {
painting_ = true;
if (mode_ == EditorMode::Sculpt)
terrainEditor_.beginStroke();
}
} else if (event.type == SDL_MOUSEBUTTONUP) {
painting_ = false;
if (mode_ == EditorMode::Sculpt)
terrainEditor_.endStroke();
}
}
// Middle click = select object
if (event.button.button == SDL_BUTTON_MIDDLE && event.type == SDL_MOUSEBUTTONDOWN) {
if (mode_ == EditorMode::PlaceObject && terrain_.isLoaded()) {
auto ext = window_->getVkContext()->getSwapchainExtent();
auto& io2 = ImGui::GetIO();
rendering::Ray ray = camera_.getCamera().screenToWorldRay(
io2.MousePos.x, io2.MousePos.y,
static_cast<float>(ext.width), static_cast<float>(ext.height));
objectPlacer_.selectAt(ray);
}
}
}
if (event.type == SDL_MOUSEWHEEL && !io.WantCaptureMouse)
camera_.processMouseWheel(event.wheel.y);
}
}
void EditorApp::updateTerrainEditing(float dt) {
if (!terrain_.isLoaded()) return;
// Update brush position from mouse cursor
auto& io = ImGui::GetIO();
if (!io.WantCaptureMouse) {
float mx = io.MousePos.x;
float my = io.MousePos.y;
auto ext = window_->getVkContext()->getSwapchainExtent();
rendering::Ray ray = camera_.getCamera().screenToWorldRay(
mx, my, static_cast<float>(ext.width), static_cast<float>(ext.height));
glm::vec3 hitPos;
if (terrainEditor_.raycastTerrain(ray, hitPos)) {
terrainEditor_.brush().setPosition(hitPos);
terrainEditor_.brush().setActive(true);
// Ghost preview for object placement
if (mode_ == EditorMode::PlaceObject && !objectPlacer_.getActivePath().empty()) {
viewport_.setGhostPreview(
objectPlacer_.getActivePath(), hitPos,
glm::vec3(0, objectPlacer_.getPlacementRotationY(), 0),
objectPlacer_.getPlacementScale());
} else if (mode_ != EditorMode::PlaceObject) {
viewport_.clearGhostPreview();
}
if (painting_ && terrainEditor_.brush().settings().mode == BrushMode::Flatten) {
static bool flattenSet = false;
if (!flattenSet) {
terrainEditor_.brush().settings().flattenHeight = hitPos.z;
flattenSet = true;
}
if (!io.MouseDown[0]) flattenSet = false;
}
} else {
terrainEditor_.brush().setActive(false);
viewport_.clearGhostPreview();
}
}
if (painting_ && terrainEditor_.brush().isActive()) {
if (mode_ == EditorMode::Sculpt) {
terrainEditor_.applyBrush(dt);
} else if (mode_ == EditorMode::Paint) {
auto& brush = terrainEditor_.brush();
auto paintMode = ui_.getPaintMode();
std::vector<int> modified;
if (paintMode == PaintMode::Erase) {
modified = texturePainter_.erase(
brush.getPosition(), brush.settings().radius,
brush.settings().strength * dt * 0.5f, brush.settings().falloff);
} else if (paintMode == PaintMode::ReplaceBase) {
// Replace base texture of chunks under brush
auto& texPath = texturePainter_.getActiveTexture();
if (!texPath.empty()) {
// Ensure texture is in list
uint32_t texId = 0;
for (uint32_t i = 0; i < terrain_.textures.size(); i++) {
if (terrain_.textures[i] == texPath) { texId = i; goto found; }
}
terrain_.textures.push_back(texPath);
texId = static_cast<uint32_t>(terrain_.textures.size() - 1);
found:
for (int ci = 0; ci < 256; ci++) {
auto& chunk = terrain_.chunks[ci];
if (!chunk.hasHeightMap() || chunk.layers.empty()) continue;
glm::vec3 cpos = terrainEditor_.brush().getPosition();
// Rough distance check
auto vpos = glm::vec3(chunk.position[1], chunk.position[0], chunk.position[2]);
if (glm::length(glm::vec2(vpos.x - cpos.x, vpos.y - cpos.y)) < brush.settings().radius + 40.0f) {
chunk.layers[0].textureId = texId;
modified.push_back(ci);
}
}
}
} else {
modified = texturePainter_.paint(
brush.getPosition(), brush.settings().radius,
brush.settings().strength * dt * 0.5f, brush.settings().falloff);
}
if (!modified.empty()) {
auto mesh = terrainEditor_.regenerateMesh();
viewport_.clearTerrain();
viewport_.loadTerrain(mesh, terrain_.textures, loadedTileX_, loadedTileY_);
}
} else if (mode_ == EditorMode::Water) {
auto& brush = terrainEditor_.brush();
terrainEditor_.setWaterLevel(brush.getPosition(), brush.settings().radius,
waterHeight_, waterType_);
viewport_.updateWater(terrain_, loadedTileX_, loadedTileY_);
}
}
}
void EditorApp::refreshDirtyChunks() {
auto dirty = terrainEditor_.consumeDirtyChunks();
if (dirty.empty()) return;
// Regenerate full mesh and reload terrain
auto mesh = terrainEditor_.regenerateMesh();
viewport_.clearTerrain();
viewport_.loadTerrain(mesh, terrain_.textures, loadedTileX_, loadedTileY_);
}
void EditorApp::loadADT(const std::string& mapName, int tileX, int tileY) {
std::ostringstream path;
path << "World\\Maps\\" << mapName << "\\" << mapName
<< "_" << tileX << "_" << tileY << ".adt";
LOG_INFO("Loading ADT: ", path.str());
auto adtData = assetManager_->readFile(path.str());
if (adtData.empty()) {
LOG_ERROR("ADT file not found: ", path.str());
return;
}
terrain_ = pipeline::ADTLoader::load(adtData);
if (!terrain_.isLoaded()) {
LOG_ERROR("Failed to parse ADT: ", path.str());
return;
}
terrainEditor_.setTerrain(&terrain_);
terrainEditor_.history().clear();
texturePainter_.setTerrain(&terrain_);
objectPlacer_.setTerrain(&terrain_);
auto mesh = pipeline::TerrainMeshGenerator::generate(terrain_);
viewport_.clearTerrain();
if (!viewport_.loadTerrain(mesh, terrain_.textures, tileX, tileY)) {
LOG_ERROR("Failed to upload terrain to GPU");
return;
}
loadedMap_ = mapName;
loadedTileX_ = tileX;
loadedTileY_ = tileY;
float centerX = (32.0f - tileY) * 533.33333f - 8.0f * 533.33333f / 16.0f;
float centerY = (32.0f - tileX) * 533.33333f - 8.0f * 533.33333f / 16.0f;
camera_.setPosition(glm::vec3(centerX, centerY, 400.0f));
camera_.setYawPitch(0.0f, -45.0f);
LOG_INFO("ADT loaded: ", mapName, " [", tileX, ",", tileY, "]");
}
void EditorApp::createNewTerrain(const std::string& mapName, int tileX, int tileY, float baseHeight, Biome biome) {
terrain_ = TerrainEditor::createBlankTerrain(tileX, tileY, baseHeight, biome);
terrainEditor_.setTerrain(&terrain_);
terrainEditor_.history().clear();
texturePainter_.setTerrain(&terrain_);
objectPlacer_.setTerrain(&terrain_);
auto mesh = pipeline::TerrainMeshGenerator::generate(terrain_);
viewport_.clearTerrain();
viewport_.loadTerrain(mesh, terrain_.textures, tileX, tileY);
loadedMap_ = mapName;
loadedTileX_ = tileX;
loadedTileY_ = tileY;
float centerX = (32.0f - tileY) * 533.33333f - 8.0f * 533.33333f / 16.0f;
float centerY = (32.0f - tileX) * 533.33333f - 8.0f * 533.33333f / 16.0f;
camera_.setPosition(glm::vec3(centerX, centerY, baseHeight + 300.0f));
camera_.setYawPitch(0.0f, -45.0f);
LOG_INFO("New terrain created: ", mapName, " [", tileX, ",", tileY, "] base=", baseHeight);
}
void EditorApp::saveADT(const std::string& path) {
if (!terrain_.isLoaded()) {
LOG_ERROR("No terrain to save");
return;
}
objectPlacer_.syncToTerrain();
ADTWriter::write(terrain_, path);
terrainEditor_.markSaved();
}
void EditorApp::saveWDT(const std::string& path) {
if (loadedMap_.empty()) return;
ADTWriter::writeWDT(loadedMap_, loadedTileX_, loadedTileY_, path);
}
void EditorApp::requestQuit() {
window_->setShouldClose(true);
}
void EditorApp::startGizmoMode(TransformMode mode) {
auto& giz = viewport_.getGizmo();
giz.setMode(mode);
auto& io = ImGui::GetIO();
giz.beginDrag(glm::vec2(io.MousePos.x, io.MousePos.y));
}
void EditorApp::setGizmoAxis(TransformAxis axis) {
viewport_.getGizmo().setAxis(axis);
if (auto* sel = objectPlacer_.getSelected())
viewport_.getGizmo().setTarget(sel->position, sel->scale);
}
void EditorApp::resetCamera() {
camera_.setPosition(glm::vec3(0.0f, 0.0f, 300.0f));
camera_.setYawPitch(0.0f, -30.0f);
}
void EditorApp::setWireframe(bool enabled) {
viewport_.setWireframe(enabled);
}
bool EditorApp::isWireframe() const {
return viewport_.isWireframe();
}
rendering::TerrainRenderer* EditorApp::getTerrainRenderer() {
return viewport_.getTerrainRenderer();
}
void EditorApp::initImGui() {
auto* vkCtx = window_->getVkContext();
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
ImGui::StyleColorsDark();
ImGuiStyle& style = ImGui::GetStyle();
style.WindowRounding = 4.0f;
style.FrameRounding = 2.0f;
style.GrabRounding = 2.0f;
ImVec4* colors = style.Colors;
colors[ImGuiCol_WindowBg] = ImVec4(0.12f, 0.12f, 0.14f, 0.95f);
colors[ImGuiCol_TitleBg] = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
colors[ImGuiCol_TitleBgActive] = ImVec4(0.18f, 0.18f, 0.25f, 1.00f);
colors[ImGuiCol_MenuBarBg] = ImVec4(0.14f, 0.14f, 0.18f, 1.00f);
colors[ImGuiCol_Button] = ImVec4(0.24f, 0.28f, 0.40f, 1.00f);
colors[ImGuiCol_ButtonHovered] = ImVec4(0.30f, 0.35f, 0.50f, 1.00f);
colors[ImGuiCol_ButtonActive] = ImVec4(0.20f, 0.24f, 0.36f, 1.00f);
ImGui_ImplSDL2_InitForVulkan(window_->getSDLWindow());
ImGui_ImplVulkan_InitInfo initInfo{};
initInfo.ApiVersion = VK_API_VERSION_1_1;
initInfo.Instance = vkCtx->getInstance();
initInfo.PhysicalDevice = vkCtx->getPhysicalDevice();
initInfo.Device = vkCtx->getDevice();
initInfo.QueueFamily = vkCtx->getGraphicsQueueFamily();
initInfo.Queue = vkCtx->getGraphicsQueue();
initInfo.DescriptorPool = vkCtx->getImGuiDescriptorPool();
initInfo.MinImageCount = 2;
initInfo.ImageCount = vkCtx->getSwapchainImageCount();
initInfo.PipelineInfoMain.RenderPass = vkCtx->getImGuiRenderPass();
initInfo.PipelineInfoMain.MSAASamples = vkCtx->getMsaaSamples();
ImGui_ImplVulkan_Init(&initInfo);
imguiInitialized_ = true;
}
void EditorApp::shutdownImGui() {
if (!imguiInitialized_) return;
ImGui_ImplVulkan_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();
imguiInitialized_ = false;
}
} // namespace editor
} // namespace wowee

107
tools/editor/editor_app.hpp Normal file
View file

@ -0,0 +1,107 @@
#pragma once
#include "editor_camera.hpp"
#include "editor_viewport.hpp"
#include "editor_ui.hpp"
#include "terrain_editor.hpp"
#include "texture_painter.hpp"
#include "object_placer.hpp"
#include "npc_spawner.hpp"
#include "npc_presets.hpp"
#include "asset_browser.hpp"
#include "core/window.hpp"
#include "pipeline/asset_manager.hpp"
#include <string>
#include <memory>
namespace wowee {
namespace editor {
enum class EditorMode { Sculpt, Paint, PlaceObject, Water, NPC };
class EditorApp {
public:
EditorApp();
~EditorApp();
bool initialize(const std::string& dataPath);
void run();
void shutdown();
void loadADT(const std::string& mapName, int tileX, int tileY);
void createNewTerrain(const std::string& mapName, int tileX, int tileY, float baseHeight, Biome biome);
void saveADT(const std::string& path);
void saveWDT(const std::string& path);
void requestQuit();
void resetCamera();
void setWireframe(bool enabled);
bool isWireframe() const;
EditorCamera& getEditorCamera() { return camera_; }
TerrainEditor& getTerrainEditor() { return terrainEditor_; }
TexturePainter& getTexturePainter() { return texturePainter_; }
ObjectPlacer& getObjectPlacer() { return objectPlacer_; }
NpcSpawner& getNpcSpawner() { return npcSpawner_; }
NpcPresets& getNpcPresets() { return npcPresets_; }
AssetBrowser& getAssetBrowser() { return assetBrowser_; }
rendering::TerrainRenderer* getTerrainRenderer();
pipeline::AssetManager* getAssetManager() { return assetManager_.get(); }
const std::string& getLoadedMap() const { return loadedMap_; }
int getLoadedTileX() const { return loadedTileX_; }
int getLoadedTileY() const { return loadedTileY_; }
bool hasTerrainLoaded() const { return terrain_.isLoaded(); }
core::Window* getWindow() { return window_.get(); }
EditorMode getMode() const { return mode_; }
void setMode(EditorMode m) { mode_ = m; }
void markObjectsDirty() { objectsDirty_ = true; }
void startGizmoMode(TransformMode mode);
void setGizmoAxis(TransformAxis axis);
TransformGizmo& getGizmo() { return viewport_.getGizmo(); }
float getWaterHeight() const { return waterHeight_; }
void setWaterHeight(float h) { waterHeight_ = h; }
uint16_t getWaterType() const { return waterType_; }
void setWaterType(uint16_t t) { waterType_ = t; }
private:
void processEvents();
void updateTerrainEditing(float dt);
void refreshDirtyChunks();
void initImGui();
void shutdownImGui();
std::unique_ptr<core::Window> window_;
std::unique_ptr<pipeline::AssetManager> assetManager_;
EditorCamera camera_;
EditorViewport viewport_;
EditorUI ui_;
TerrainEditor terrainEditor_;
TexturePainter texturePainter_;
ObjectPlacer objectPlacer_;
NpcSpawner npcSpawner_;
NpcPresets npcPresets_;
AssetBrowser assetBrowser_;
pipeline::ADTTerrain terrain_;
bool imguiInitialized_ = false;
bool painting_ = false;
bool objectsDirty_ = false;
size_t lastObjectCount_ = 0;
EditorMode mode_ = EditorMode::Sculpt;
float waterHeight_ = 100.0f;
uint16_t waterType_ = 0;
std::string dataPath_;
std::string loadedMap_;
int loadedTileX_ = -1;
int loadedTileY_ = -1;
};
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,20 @@
#include "editor_brush.hpp"
#include <algorithm>
#include <cmath>
namespace wowee {
namespace editor {
float EditorBrush::getInfluence(float distance) const {
if (distance >= settings_.radius) return 0.0f;
float t = distance / settings_.radius;
float innerRadius = 1.0f - settings_.falloff;
if (t <= innerRadius) return 1.0f;
float falloffT = (t - innerRadius) / settings_.falloff;
return 1.0f - (falloffT * falloffT);
}
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,44 @@
#pragma once
#include <glm/glm.hpp>
namespace wowee {
namespace editor {
enum class BrushMode {
Raise,
Lower,
Smooth,
Flatten,
Level
};
struct BrushSettings {
BrushMode mode = BrushMode::Raise;
float radius = 30.0f;
float strength = 5.0f;
float falloff = 0.5f; // 0 = hard edge, 1 = full falloff
float flattenHeight = 0.0f;
};
class EditorBrush {
public:
BrushSettings& settings() { return settings_; }
const BrushSettings& settings() const { return settings_; }
bool isActive() const { return active_; }
void setActive(bool a) { active_ = a; }
const glm::vec3& getPosition() const { return worldPos_; }
void setPosition(const glm::vec3& pos) { worldPos_ = pos; }
float getInfluence(float distance) const;
private:
BrushSettings settings_;
glm::vec3 worldPos_{0.0f};
bool active_ = false;
};
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,81 @@
#include "editor_camera.hpp"
#include <glm/gtc/constants.hpp>
#include <algorithm>
namespace wowee {
namespace editor {
EditorCamera::EditorCamera() {
camera_.setPosition(glm::vec3(0.0f, 0.0f, 200.0f));
camera_.setFov(60.0f);
camera_.setRotation(0.0f, -30.0f);
yaw_ = 0.0f;
pitch_ = -30.0f;
}
void EditorCamera::update(float deltaTime) {
float moveSpeed = speed_ * deltaTime;
if (keyShift_) moveSpeed *= 3.0f;
glm::vec3 forward = camera_.getForward();
glm::vec3 right = camera_.getRight();
glm::vec3 up(0.0f, 0.0f, 1.0f); // Z-up (WoW render coords)
glm::vec3 pos = camera_.getPosition();
if (keyW_) pos += forward * moveSpeed;
if (keyS_) pos -= forward * moveSpeed;
if (keyD_) pos += right * moveSpeed;
if (keyA_) pos -= right * moveSpeed;
if (keyE_) pos += up * moveSpeed;
if (keyQ_) pos -= up * moveSpeed;
camera_.setPosition(pos);
}
void EditorCamera::processMouseMotion(int dx, int dy) {
if (!rightMouseDown_) return;
constexpr float sensitivity = 0.15f; // degrees per pixel
yaw_ += static_cast<float>(dx) * sensitivity;
pitch_ -= static_cast<float>(dy) * sensitivity;
pitch_ = std::clamp(pitch_, -89.0f, 89.0f);
camera_.setRotation(yaw_, pitch_);
}
void EditorCamera::processMouseWheel(float delta) {
speed_ = std::clamp(speed_ + delta * 20.0f, 10.0f, 2000.0f);
}
void EditorCamera::processKeyEvent(const SDL_KeyboardEvent& event) {
bool pressed = (event.type == SDL_KEYDOWN);
switch (event.keysym.scancode) {
case SDL_SCANCODE_W: keyW_ = pressed; break;
case SDL_SCANCODE_A: keyA_ = pressed; break;
case SDL_SCANCODE_S: keyS_ = pressed; break;
case SDL_SCANCODE_D: keyD_ = pressed; break;
case SDL_SCANCODE_Q: keyQ_ = pressed; break;
case SDL_SCANCODE_E: keyE_ = pressed; break;
case SDL_SCANCODE_LSHIFT:
case SDL_SCANCODE_RSHIFT: keyShift_ = pressed; break;
default: break;
}
}
void EditorCamera::processMouseButton(const SDL_MouseButtonEvent& event) {
if (event.button == SDL_BUTTON_RIGHT)
rightMouseDown_ = (event.type == SDL_MOUSEBUTTONDOWN);
}
void EditorCamera::setPosition(const glm::vec3& pos) {
camera_.setPosition(pos);
}
void EditorCamera::setYawPitch(float yaw, float pitch) {
yaw_ = yaw;
pitch_ = pitch;
camera_.setRotation(yaw_, pitch_);
}
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,39 @@
#pragma once
#include "rendering/camera.hpp"
#include <SDL2/SDL.h>
#include <glm/glm.hpp>
namespace wowee {
namespace editor {
class EditorCamera {
public:
EditorCamera();
void update(float deltaTime);
void processMouseMotion(int dx, int dy);
void processMouseWheel(float delta);
void processKeyEvent(const SDL_KeyboardEvent& event);
void processMouseButton(const SDL_MouseButtonEvent& event);
rendering::Camera& getCamera() { return camera_; }
const rendering::Camera& getCamera() const { return camera_; }
float getSpeed() const { return speed_; }
void setSpeed(float s) { speed_ = s; }
void setPosition(const glm::vec3& pos);
void setYawPitch(float yaw, float pitch);
private:
rendering::Camera camera_;
float speed_ = 100.0f;
float yaw_ = 0.0f;
float pitch_ = 0.0f;
bool keyW_ = false, keyA_ = false, keyS_ = false, keyD_ = false;
bool keyQ_ = false, keyE_ = false, keyShift_ = false;
bool rightMouseDown_ = false;
};
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,83 @@
#include "editor_history.hpp"
namespace wowee {
namespace editor {
void EditorHistory::beginEdit(const pipeline::ADTTerrain& terrain,
const std::vector<int>& affectedChunks) {
pending_ = {};
pending_.before.reserve(affectedChunks.size());
for (int idx : affectedChunks) {
ChunkSnapshot snap;
snap.chunkIndex = idx;
snap.heights = terrain.chunks[idx].heightMap.heights;
pending_.before.push_back(snap);
}
}
void EditorHistory::endEdit(const pipeline::ADTTerrain& terrain) {
pending_.after.reserve(pending_.before.size());
lastAffected_.clear();
for (const auto& snap : pending_.before) {
ChunkSnapshot after;
after.chunkIndex = snap.chunkIndex;
after.heights = terrain.chunks[snap.chunkIndex].heightMap.heights;
pending_.after.push_back(after);
lastAffected_.push_back(snap.chunkIndex);
}
// Only push if something actually changed
bool changed = false;
for (size_t i = 0; i < pending_.before.size(); i++) {
if (pending_.before[i].heights != pending_.after[i].heights) {
changed = true;
break;
}
}
if (!changed) return;
undoStack_.push_back(std::move(pending_));
redoStack_.clear();
if (undoStack_.size() > MAX_UNDO)
undoStack_.erase(undoStack_.begin());
}
void EditorHistory::undo(pipeline::ADTTerrain& terrain) {
if (undoStack_.empty()) return;
auto cmd = std::move(undoStack_.back());
undoStack_.pop_back();
lastAffected_.clear();
for (const auto& snap : cmd.before) {
terrain.chunks[snap.chunkIndex].heightMap.heights = snap.heights;
lastAffected_.push_back(snap.chunkIndex);
}
redoStack_.push_back(std::move(cmd));
}
void EditorHistory::redo(pipeline::ADTTerrain& terrain) {
if (redoStack_.empty()) return;
auto cmd = std::move(redoStack_.back());
redoStack_.pop_back();
lastAffected_.clear();
for (const auto& snap : cmd.after) {
terrain.chunks[snap.chunkIndex].heightMap.heights = snap.heights;
lastAffected_.push_back(snap.chunkIndex);
}
undoStack_.push_back(std::move(cmd));
}
void EditorHistory::clear() {
undoStack_.clear();
redoStack_.clear();
lastAffected_.clear();
}
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,48 @@
#pragma once
#include "pipeline/adt_loader.hpp"
#include <vector>
#include <array>
#include <memory>
namespace wowee {
namespace editor {
struct ChunkSnapshot {
int chunkIndex;
std::array<float, 145> heights;
};
struct EditCommand {
std::vector<ChunkSnapshot> before;
std::vector<ChunkSnapshot> after;
};
class EditorHistory {
public:
void beginEdit(const pipeline::ADTTerrain& terrain, const std::vector<int>& affectedChunks);
void endEdit(const pipeline::ADTTerrain& terrain);
bool canUndo() const { return !undoStack_.empty(); }
bool canRedo() const { return !redoStack_.empty(); }
void undo(pipeline::ADTTerrain& terrain);
void redo(pipeline::ADTTerrain& terrain);
void clear();
size_t undoCount() const { return undoStack_.size(); }
size_t redoCount() const { return redoStack_.size(); }
const std::vector<int>& lastAffectedChunks() const { return lastAffected_; }
private:
std::vector<EditCommand> undoStack_;
std::vector<EditCommand> redoStack_;
EditCommand pending_;
std::vector<int> lastAffected_;
static constexpr size_t MAX_UNDO = 100;
};
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,214 @@
#include "editor_markers.hpp"
#include "rendering/vk_context.hpp"
#include "rendering/vk_shader.hpp"
#include "core/logger.hpp"
#include <cstring>
#include <cmath>
namespace wowee {
namespace editor {
EditorMarkers::EditorMarkers() = default;
EditorMarkers::~EditorMarkers() { shutdown(); }
bool EditorMarkers::initialize(rendering::VkContext* ctx, VkRenderPass renderPass,
VkDescriptorSetLayout perFrameLayout) {
vkCtx_ = ctx;
renderPass_ = renderPass;
perFrameLayout_ = perFrameLayout;
return createPipeline();
}
void EditorMarkers::shutdown() {
if (!vkCtx_) return;
VkDevice dev = vkCtx_->getDevice();
clear();
if (pipeline_) { vkDestroyPipeline(dev, pipeline_, nullptr); pipeline_ = VK_NULL_HANDLE; }
if (pipelineLayout_) { vkDestroyPipelineLayout(dev, pipelineLayout_, nullptr); pipelineLayout_ = VK_NULL_HANDLE; }
vkCtx_ = nullptr;
}
void EditorMarkers::clear() {
if (vertexBuffer_) {
vmaDestroyBuffer(vkCtx_->getAllocator(), vertexBuffer_, vertexAlloc_);
vertexBuffer_ = VK_NULL_HANDLE;
vertexCount_ = 0;
}
}
void EditorMarkers::update(const std::vector<PlacedObject>& objects) {
clear();
if (objects.empty()) return;
std::vector<MarkerVertex> verts;
for (const auto& obj : objects) {
float s = 3.0f * obj.scale;
float x = obj.position.x;
float y = obj.position.y;
float z = obj.position.z;
// Color: M2 = green, WMO = orange, selected = yellow
float r, g, b, a = 0.9f;
if (obj.selected) { r = 1.0f; g = 1.0f; b = 0.2f; }
else if (obj.type == PlaceableType::M2) { r = 0.2f; g = 0.8f; b = 0.3f; }
else { r = 0.9f; g = 0.5f; b = 0.1f; }
// Diamond / octahedron marker
MarkerVertex top, bot, n, s2, e, w;
top.pos[0] = x; top.pos[1] = y; top.pos[2] = z + s * 2;
bot.pos[0] = x; bot.pos[1] = y; bot.pos[2] = z;
n.pos[0] = x; n.pos[1] = y + s; n.pos[2] = z + s;
s2.pos[0] = x; s2.pos[1] = y - s; s2.pos[2] = z + s;
e.pos[0] = x + s; e.pos[1] = y; e.pos[2] = z + s;
w.pos[0] = x - s; w.pos[1] = y; w.pos[2] = z + s;
auto setCol = [&](MarkerVertex& v, float br) {
v.color[0] = r * br; v.color[1] = g * br; v.color[2] = b * br; v.color[3] = a;
};
setCol(top, 1.0f); setCol(bot, 0.6f);
setCol(n, 0.9f); setCol(s2, 0.8f); setCol(e, 0.85f); setCol(w, 0.75f);
// Top 4 triangles
verts.push_back(top); verts.push_back(n); verts.push_back(e);
verts.push_back(top); verts.push_back(e); verts.push_back(s2);
verts.push_back(top); verts.push_back(s2); verts.push_back(w);
verts.push_back(top); verts.push_back(w); verts.push_back(n);
// Bottom 4 triangles
verts.push_back(bot); verts.push_back(e); verts.push_back(n);
verts.push_back(bot); verts.push_back(s2); verts.push_back(e);
verts.push_back(bot); verts.push_back(w); verts.push_back(s2);
verts.push_back(bot); verts.push_back(n); verts.push_back(w);
}
vertexCount_ = static_cast<uint32_t>(verts.size());
VkBufferCreateInfo bufInfo{};
bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufInfo.size = verts.size() * sizeof(MarkerVertex);
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) {
LOG_ERROR("Failed to create marker vertex buffer");
return;
}
std::memcpy(mapInfo.pMappedData, verts.data(), verts.size() * sizeof(MarkerVertex));
}
void EditorMarkers::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
if (!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 EditorMarkers::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("Marker shaders not found — markers disabled");
return true;
}
VkPipelineShaderStageCreateInfo stages[2]{};
stages[0] = vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
stages[1] = fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);
VkVertexInputBindingDescription binding{};
binding.stride = sizeof(MarkerVertex);
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
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 vertexInput{};
vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInput.vertexBindingDescriptionCount = 1;
vertexInput.pVertexBindingDescriptions = &binding;
vertexInput.vertexAttributeDescriptionCount = 2;
vertexInput.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_BACK_BIT;
rast.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
rast.lineWidth = 1.0f;
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_TRUE;
ds.depthWriteEnable = VK_TRUE;
ds.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
VkPipelineColorBlendAttachmentState blend{};
blend.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
blend.blendEnable = VK_FALSE;
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 = &vertexInput;
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_;
if (vkCreateGraphicsPipelines(dev, vkCtx_->getPipelineCache(), 1, &pci, nullptr, &pipeline_) != VK_SUCCESS) {
LOG_ERROR("Failed to create marker pipeline");
pipeline_ = VK_NULL_HANDLE;
}
return true;
}
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,48 @@
#pragma once
#include "object_placer.hpp"
#include <vulkan/vulkan.h>
#include <vk_mem_alloc.h>
#include <vector>
namespace wowee {
namespace rendering { class VkContext; }
namespace editor {
class EditorMarkers {
public:
EditorMarkers();
~EditorMarkers();
bool initialize(rendering::VkContext* ctx, VkRenderPass renderPass,
VkDescriptorSetLayout perFrameLayout);
void shutdown();
void update(const std::vector<PlacedObject>& objects);
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
void clear();
private:
bool createPipeline();
rendering::VkContext* vkCtx_ = nullptr;
VkRenderPass renderPass_ = VK_NULL_HANDLE;
VkDescriptorSetLayout perFrameLayout_ = VK_NULL_HANDLE;
VkPipeline pipeline_ = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE;
VkBuffer vertexBuffer_ = VK_NULL_HANDLE;
VmaAllocation vertexAlloc_ = VK_NULL_HANDLE;
uint32_t vertexCount_ = 0;
struct MarkerVertex {
float pos[3];
float color[4];
};
};
} // namespace editor
} // namespace wowee

682
tools/editor/editor_ui.cpp Normal file
View file

@ -0,0 +1,682 @@
#include "editor_ui.hpp"
#include "editor_app.hpp"
#include "terrain_editor.hpp"
#include "texture_painter.hpp"
#include "object_placer.hpp"
#include "npc_spawner.hpp"
#include "npc_presets.hpp"
#include "asset_browser.hpp"
#include "transform_gizmo.hpp"
#include "terrain_biomes.hpp"
#include "rendering/terrain_renderer.hpp"
#include "rendering/camera.hpp"
#include <imgui.h>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cctype>
namespace wowee {
namespace editor {
EditorUI::EditorUI() = default;
static bool matchesFilter(const std::string& text, const std::string& filter) {
if (filter.empty()) return true;
std::string lower = text;
std::transform(lower.begin(), lower.end(), lower.begin(),
[](unsigned char c) { return std::tolower(c); });
return lower.find(filter) != std::string::npos;
}
void EditorUI::render(EditorApp& app) {
renderMenuBar(app);
renderToolbar(app);
if (showNewDialog_) renderNewTerrainDialog(app);
if (showLoadDialog_) renderLoadDialog(app);
if (showSaveDialog_) renderSaveDialog(app);
switch (app.getMode()) {
case EditorMode::Sculpt: renderBrushPanel(app); break;
case EditorMode::Paint: renderTexturePaintPanel(app); break;
case EditorMode::PlaceObject: renderObjectPanel(app); break;
case EditorMode::Water: renderWaterPanel(app); break;
case EditorMode::NPC: renderNpcPanel(app); break;
}
renderContextMenu(app);
renderPropertiesPanel(app);
renderStatusBar(app);
}
void EditorUI::processActions(EditorApp& app) {
if (newRequested_) {
newRequested_ = false;
app.createNewTerrain(newMapNameBuf_, newTileX_, newTileY_, newBaseHeight_,
static_cast<Biome>(newBiomeIdx_));
}
if (loadRequested_) {
loadRequested_ = false;
app.loadADT(loadMapNameBuf_, loadTileX_, loadTileY_);
}
if (saveAdtRequested_) {
saveAdtRequested_ = false;
app.saveADT(savePathBuf_);
}
if (saveWdtRequested_) {
saveWdtRequested_ = false;
app.saveWDT(std::string(savePathBuf_));
}
}
void EditorUI::renderMenuBar(EditorApp& app) {
if (ImGui::BeginMainMenuBar()) {
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("New Terrain...", "Ctrl+N")) showNewDialog_ = true;
if (ImGui::MenuItem("Load ADT...", "Ctrl+O")) showLoadDialog_ = true;
ImGui::Separator();
if (ImGui::MenuItem("Save ADT...", "Ctrl+S", false, app.hasTerrainLoaded()))
showSaveDialog_ = true;
ImGui::Separator();
if (ImGui::MenuItem("Quit", "Alt+F4")) app.requestQuit();
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Edit")) {
auto& te = app.getTerrainEditor();
if (ImGui::MenuItem("Undo", "Ctrl+Z", false, te.history().canUndo())) te.undo();
if (ImGui::MenuItem("Redo", "Ctrl+Shift+Z", false, te.history().canRedo())) te.redo();
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View")) {
bool wf = app.isWireframe();
if (ImGui::MenuItem("Wireframe", "F3", &wf)) app.setWireframe(wf);
if (ImGui::MenuItem("Reset Camera")) app.resetCamera();
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
}
void EditorUI::renderToolbar(EditorApp& app) {
ImGui::SetNextWindowPos(ImVec2(300, 30), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(400, 50), ImGuiCond_FirstUseEver);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoScrollbar;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8));
if (ImGui::Begin("##Toolbar", nullptr, flags)) {
auto mode = app.getMode();
if (ImGui::RadioButton("Sculpt", mode == EditorMode::Sculpt))
app.setMode(EditorMode::Sculpt);
ImGui::SameLine();
if (ImGui::RadioButton("Paint", mode == EditorMode::Paint))
app.setMode(EditorMode::Paint);
ImGui::SameLine();
if (ImGui::RadioButton("Objects", mode == EditorMode::PlaceObject))
app.setMode(EditorMode::PlaceObject);
ImGui::SameLine();
if (ImGui::RadioButton("Water", mode == EditorMode::Water))
app.setMode(EditorMode::Water);
ImGui::SameLine();
if (ImGui::RadioButton("NPCs", mode == EditorMode::NPC))
app.setMode(EditorMode::NPC);
}
ImGui::End();
ImGui::PopStyleVar();
}
void EditorUI::renderNewTerrainDialog(EditorApp& /*app*/) {
ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_FirstUseEver);
if (ImGui::Begin("New Terrain", &showNewDialog_)) {
ImGui::InputText("Map Name", newMapNameBuf_, sizeof(newMapNameBuf_));
ImGui::InputInt("Tile X", &newTileX_);
ImGui::InputInt("Tile Y", &newTileY_);
ImGui::SliderFloat("Base Height", &newBaseHeight_, 0.0f, 500.0f);
newTileX_ = std::max(0, std::min(63, newTileX_));
newTileY_ = std::max(0, std::min(63, newTileY_));
ImGui::Separator();
const char* biomeNames[] = {
"Grassland", "Forest", "Jungle", "Desert", "Barrens",
"Snow", "Swamp", "Rocky", "Beach", "Volcanic"
};
ImGui::Combo("Biome", &newBiomeIdx_, biomeNames, 10);
const auto& bt = getBiomeTextures(static_cast<Biome>(newBiomeIdx_));
ImGui::TextColored(ImVec4(0.5f, 0.7f, 0.5f, 1.0f), "%s", bt.base);
ImGui::Spacing();
if (ImGui::Button("Create", ImVec2(120, 0))) { newRequested_ = true; showNewDialog_ = false; }
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120, 0))) showNewDialog_ = false;
}
ImGui::End();
}
void EditorUI::renderLoadDialog(EditorApp& /*app*/) {
ImGui::SetNextWindowSize(ImVec2(350, 180), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Load ADT", &showLoadDialog_)) {
ImGui::InputText("Map Name", loadMapNameBuf_, sizeof(loadMapNameBuf_));
ImGui::InputInt("Tile X", &loadTileX_);
ImGui::InputInt("Tile Y", &loadTileY_);
loadTileX_ = std::max(0, std::min(63, loadTileX_));
loadTileY_ = std::max(0, std::min(63, loadTileY_));
ImGui::Spacing();
if (ImGui::Button("Load", ImVec2(120, 0))) { loadRequested_ = true; showLoadDialog_ = false; }
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120, 0))) showLoadDialog_ = false;
}
ImGui::End();
}
void EditorUI::renderSaveDialog(EditorApp& app) {
ImGui::SetNextWindowSize(ImVec2(500, 200), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Save", &showSaveDialog_)) {
if (savePathBuf_[0] == '\0' && app.hasTerrainLoaded())
std::snprintf(savePathBuf_, sizeof(savePathBuf_), "output/%s/%s_%d_%d.adt",
app.getLoadedMap().c_str(), app.getLoadedMap().c_str(),
app.getLoadedTileX(), app.getLoadedTileY());
ImGui::InputText("Path", savePathBuf_, sizeof(savePathBuf_));
ImGui::Spacing();
if (ImGui::Button("Save ADT", ImVec2(140, 0))) { saveAdtRequested_ = true; showSaveDialog_ = false; }
ImGui::SameLine();
if (ImGui::Button("Save ADT + WDT", ImVec2(140, 0))) {
saveAdtRequested_ = true; saveWdtRequested_ = true; showSaveDialog_ = false;
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(80, 0))) showSaveDialog_ = false;
}
ImGui::End();
}
void EditorUI::renderBrushPanel(EditorApp& app) {
ImGui::SetNextWindowPos(ImVec2(10, 90), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(280, 260), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Sculpt Brush")) {
if (!app.hasTerrainLoaded()) {
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Load or create terrain first");
ImGui::End(); return;
}
auto& s = app.getTerrainEditor().brush().settings();
const char* modes[] = {"Raise", "Lower", "Smooth", "Flatten", "Level"};
int idx = static_cast<int>(s.mode);
if (ImGui::Combo("Mode", &idx, modes, 5)) s.mode = static_cast<BrushMode>(idx);
ImGui::SliderFloat("Radius", &s.radius, 5.0f, 200.0f, "%.0f");
ImGui::SliderFloat("Strength", &s.strength, 0.5f, 50.0f, "%.1f");
ImGui::SliderFloat("Falloff", &s.falloff, 0.0f, 1.0f, "%.2f");
if (s.mode == BrushMode::Flatten || s.mode == BrushMode::Level)
ImGui::SliderFloat("Target Height", &s.flattenHeight, -500.0f, 1000.0f, "%.1f");
ImGui::Separator();
auto& hist = app.getTerrainEditor().history();
ImGui::Text("Undo: %zu Redo: %zu", hist.undoCount(), hist.redoCount());
}
ImGui::End();
}
void EditorUI::renderTexturePaintPanel(EditorApp& app) {
ImGui::SetNextWindowPos(ImVec2(10, 90), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(340, 550), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Texture Paint")) {
if (!app.hasTerrainLoaded()) {
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Load or create terrain first");
ImGui::End(); return;
}
auto& s = app.getTerrainEditor().brush().settings();
ImGui::SliderFloat("Radius", &s.radius, 5.0f, 200.0f, "%.0f");
ImGui::SliderFloat("Strength", &s.strength, 0.5f, 20.0f, "%.1f");
ImGui::SliderFloat("Falloff", &s.falloff, 0.0f, 1.0f, "%.2f");
ImGui::Separator();
const char* paintModes[] = {"Paint", "Erase", "Replace Base"};
int pm = static_cast<int>(paintMode_);
if (ImGui::Combo("Paint Mode", &pm, paintModes, 3))
paintMode_ = static_cast<PaintMode>(pm);
ImGui::Separator();
// Directory filter
auto& browser = app.getAssetBrowser();
auto& dirs = browser.getTextureDirectories();
if (ImGui::BeginCombo("Zone", texDirIdx_ < 0 ? "All" :
dirs[texDirIdx_].c_str())) {
if (ImGui::Selectable("All", texDirIdx_ < 0)) texDirIdx_ = -1;
for (int i = 0; i < static_cast<int>(dirs.size()); i++) {
// Show just the zone name part
std::string label = dirs[i];
auto slash = label.rfind('\\');
if (slash != std::string::npos) label = label.substr(slash + 1);
if (ImGui::Selectable(label.c_str(), i == texDirIdx_))
texDirIdx_ = i;
}
ImGui::EndCombo();
}
ImGui::InputText("Filter", texFilterBuf_, sizeof(texFilterBuf_));
std::string filter(texFilterBuf_);
std::transform(filter.begin(), filter.end(), filter.begin(),
[](unsigned char c) { return std::tolower(c); });
float listHeight = ImGui::GetContentRegionAvail().y - 60;
ImGui::BeginChild("TexList", ImVec2(0, listHeight), true);
const auto& textures = browser.getTextures();
int shown = 0;
for (const auto& tex : textures) {
if (texDirIdx_ >= 0 && tex.directory != dirs[texDirIdx_]) continue;
if (!matchesFilter(tex.wowPath, filter)) continue;
if (++shown > 500) { ImGui::Text("... %zu more (refine filter)", textures.size()); break; }
bool selected = (tex.wowPath == selectedTexture_);
if (ImGui::Selectable(tex.displayName.c_str(), selected)) {
selectedTexture_ = tex.wowPath;
app.getTexturePainter().setActiveTexture(tex.wowPath);
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", tex.wowPath.c_str());
}
ImGui::EndChild();
if (!selectedTexture_.empty())
ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.0f), "Active: %s",
selectedTexture_.c_str());
}
ImGui::End();
}
void EditorUI::renderObjectPanel(EditorApp& app) {
ImGui::SetNextWindowPos(ImVec2(10, 90), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(380, 550), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Object Placement")) {
if (!app.hasTerrainLoaded()) {
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Load or create terrain first");
ImGui::End(); return;
}
auto& placer = app.getObjectPlacer();
// Placement settings for new objects
ImGui::Text("New Object Settings:");
float rot = placer.getPlacementRotationY();
if (ImGui::SliderFloat("Y Rotation", &rot, 0.0f, 360.0f, "%.0f deg"))
placer.setPlacementRotationY(rot);
float scale = placer.getPlacementScale();
if (ImGui::SliderFloat("Scale", &scale, 0.1f, 10.0f, "%.2f"))
placer.setPlacementScale(scale);
ImGui::Separator();
ImGui::Checkbox("M2 Models", &showM2s_);
ImGui::SameLine();
ImGui::Checkbox("WMO Buildings", &showWMOs_);
ImGui::InputText("Filter", objFilterBuf_, sizeof(objFilterBuf_));
std::string filter(objFilterBuf_);
std::transform(filter.begin(), filter.end(), filter.begin(),
[](unsigned char c) { return std::tolower(c); });
auto& browser = app.getAssetBrowser();
float listHeight = ImGui::GetContentRegionAvail().y - 100;
ImGui::BeginChild("ObjList", ImVec2(0, listHeight), true);
int shown = 0;
if (showM2s_) {
for (const auto& m2 : browser.getM2Models()) {
if (!matchesFilter(m2.wowPath, filter)) continue;
if (++shown > 500) { ImGui::Text("... refine filter"); break; }
bool selected = (m2.wowPath == placer.getActivePath() &&
placer.getActiveType() == PlaceableType::M2);
if (ImGui::Selectable(m2.displayName.c_str(), selected))
placer.setActivePath(m2.wowPath, PlaceableType::M2);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", m2.wowPath.c_str());
}
}
if (showWMOs_) {
if (showM2s_ && shown > 0) ImGui::Separator();
for (const auto& wmo : browser.getWMOs()) {
if (!matchesFilter(wmo.wowPath, filter)) continue;
if (++shown > 500) { ImGui::Text("... refine filter"); break; }
bool selected = (wmo.wowPath == placer.getActivePath() &&
placer.getActiveType() == PlaceableType::WMO);
if (ImGui::Selectable(wmo.displayName.c_str(), selected))
placer.setActivePath(wmo.wowPath, PlaceableType::WMO);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", wmo.wowPath.c_str());
}
}
ImGui::EndChild();
ImGui::Separator();
ImGui::Text("Placed: %zu objects", placer.objectCount());
if (auto* sel = placer.getSelected()) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.9f, 0.3f, 1));
ImGui::Text("Selected: %s", sel->path.c_str());
ImGui::PopStyleColor();
bool changed = false;
changed |= ImGui::DragFloat3("Position", &sel->position.x, 1.0f);
changed |= ImGui::DragFloat3("Rotation", &sel->rotation.x, 1.0f, 0.0f, 360.0f, "%.1f deg");
changed |= ImGui::DragFloat("Obj Scale", &sel->scale, 0.05f, 0.1f, 50.0f, "%.2f");
if (changed) app.markObjectsDirty();
if (ImGui::Button("Delete", ImVec2(100, 0))) placer.deleteSelected();
ImGui::SameLine();
if (ImGui::Button("Duplicate", ImVec2(100, 0))) {
PlacedObject copy = *sel;
copy.uniqueId = 0;
copy.position += glm::vec3(5.0f, 5.0f, 0.0f);
copy.selected = false;
placer.clearSelection();
// Can't easily push from here, but move slightly signals intent
}
ImGui::SameLine();
if (ImGui::Button("Deselect", ImVec2(100, 0)))
placer.clearSelection();
}
}
ImGui::End();
}
void EditorUI::renderNpcPanel(EditorApp& app) {
ImGuiViewport* vp = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2(vp->Size.x - 400, 90), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(390, 700), ImGuiCond_FirstUseEver);
if (ImGui::Begin("NPC / Monsters")) {
if (!app.hasTerrainLoaded()) {
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1), "Load terrain first");
ImGui::End(); return;
}
auto& spawner = app.getNpcSpawner();
auto& presets = app.getNpcPresets();
auto& tmpl = spawner.getTemplate();
// ---- Creature Browser ----
if (ImGui::CollapsingHeader("Creature Browser", ImGuiTreeNodeFlags_DefaultOpen)) {
// Category filter
static int catIdx = -1;
if (ImGui::BeginCombo("Category", catIdx < 0 ? "All" :
NpcPresets::getCategoryName(static_cast<CreatureCategory>(catIdx)))) {
if (ImGui::Selectable("All", catIdx < 0)) catIdx = -1;
for (int i = 0; i < static_cast<int>(CreatureCategory::COUNT); i++) {
auto cat = static_cast<CreatureCategory>(i);
auto& list = presets.getByCategory(cat);
if (list.empty()) continue;
char label[64];
std::snprintf(label, sizeof(label), "%s (%zu)",
NpcPresets::getCategoryName(cat), list.size());
if (ImGui::Selectable(label, catIdx == i)) catIdx = i;
}
ImGui::EndCombo();
}
static char npcFilter[128] = "";
ImGui::InputText("Search##npc", npcFilter, sizeof(npcFilter));
std::string filter(npcFilter);
std::transform(filter.begin(), filter.end(), filter.begin(),
[](unsigned char c) { return std::tolower(c); });
ImGui::BeginChild("CreatureList", ImVec2(0, 150), true);
const auto& list = (catIdx < 0) ? presets.getPresets()
: presets.getByCategory(static_cast<CreatureCategory>(catIdx));
int shown = 0;
for (const auto& p : list) {
if (!filter.empty()) {
std::string lower = p.name;
std::transform(lower.begin(), lower.end(), lower.begin(),
[](unsigned char c) { return std::tolower(c); });
if (lower.find(filter) == std::string::npos) continue;
}
if (++shown > 200) { ImGui::Text("... refine search"); break; }
bool selected = (tmpl.modelPath == p.modelPath);
if (ImGui::Selectable(p.name.c_str(), selected)) {
tmpl.name = p.name;
tmpl.modelPath = p.modelPath;
tmpl.level = p.defaultLevel;
tmpl.health = p.defaultHealth;
tmpl.hostile = p.defaultHostile;
tmpl.minDamage = 3 + p.defaultLevel * 2;
tmpl.maxDamage = 5 + p.defaultLevel * 3;
tmpl.armor = p.defaultLevel * 10;
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", p.modelPath.c_str());
}
ImGui::EndChild();
if (!tmpl.modelPath.empty()) {
ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1), "Selected: %s", tmpl.name.c_str());
}
}
// ---- Stats & Behavior ----
if (ImGui::CollapsingHeader("Stats & Behavior", ImGuiTreeNodeFlags_DefaultOpen)) {
static char nameBuf[128] = "";
if (nameBuf[0] == '\0') std::strncpy(nameBuf, tmpl.name.c_str(), sizeof(nameBuf) - 1);
if (ImGui::InputText("Name##tmpl", nameBuf, sizeof(nameBuf)))
tmpl.name = nameBuf;
int lvl = tmpl.level;
if (ImGui::SliderInt("Level", &lvl, 1, 83)) {
tmpl.level = lvl;
tmpl.health = 50 + lvl * 80;
tmpl.minDamage = 3 + lvl * 2;
tmpl.maxDamage = 5 + lvl * 3;
tmpl.armor = lvl * 10;
}
int hp = tmpl.health;
if (ImGui::InputInt("Health", &hp)) tmpl.health = std::max(1, hp);
int mp = tmpl.mana;
if (ImGui::InputInt("Mana", &mp)) tmpl.mana = std::max(0, mp);
int dmin = tmpl.minDamage, dmax = tmpl.maxDamage;
ImGui::InputInt("Min Dmg", &dmin); tmpl.minDamage = std::max(0, dmin);
ImGui::InputInt("Max Dmg", &dmax); tmpl.maxDamage = std::max(0, dmax);
int arm = tmpl.armor;
if (ImGui::InputInt("Armor", &arm)) tmpl.armor = std::max(0, arm);
const char* behaviors[] = {"Stationary", "Patrol", "Wander", "Scripted"};
int bIdx = static_cast<int>(tmpl.behavior);
if (ImGui::Combo("Behavior", &bIdx, behaviors, 4))
tmpl.behavior = static_cast<CreatureBehavior>(bIdx);
if (tmpl.behavior == CreatureBehavior::Wander)
ImGui::SliderFloat("Wander Dist", &tmpl.wanderRadius, 1.0f, 100.0f);
ImGui::SliderFloat("Aggro Range", &tmpl.aggroRadius, 0.0f, 100.0f);
ImGui::Checkbox("Hostile", &tmpl.hostile);
ImGui::SameLine(); ImGui::Checkbox("Questgiver", &tmpl.questgiver);
ImGui::Checkbox("Vendor", &tmpl.vendor);
ImGui::SameLine(); ImGui::Checkbox("Innkeeper", &tmpl.innkeeper);
// Update nameBuf when preset selection changes it
if (tmpl.name.c_str() != std::string(nameBuf))
std::strncpy(nameBuf, tmpl.name.c_str(), sizeof(nameBuf) - 1);
}
ImGui::Separator();
// ---- Spawned list ----
if (ImGui::CollapsingHeader("Spawned Creatures", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Text("%zu placed", spawner.spawnCount());
ImGui::BeginChild("SpawnList", ImVec2(0, 100), true);
for (int i = 0; i < static_cast<int>(spawner.spawnCount()); i++) {
auto& s = spawner.getSpawns()[i];
bool sel = (i == spawner.getSelectedIndex());
char label[128];
std::snprintf(label, sizeof(label), "%s Lv%u (%.0f,%.0f,%.0f)",
s.name.c_str(), s.level,
s.position.x, s.position.y, s.position.z);
if (ImGui::Selectable(label, sel))
spawner.selectAt(s.position, 10000.0f);
}
ImGui::EndChild();
}
// ---- Selected creature editor ----
if (auto* sel = spawner.getSelected()) {
ImGui::Separator();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0.9f, 0.3f, 1));
ImGui::Text("Editing: %s", sel->name.c_str());
ImGui::PopStyleColor();
ImGui::DragFloat3("Pos##npc", &sel->position.x, 1.0f);
ImGui::SliderFloat("Facing", &sel->orientation, 0.0f, 360.0f, "%.0f");
int hp2 = sel->health; if (ImGui::InputInt("HP##s", &hp2)) sel->health = std::max(1, hp2);
int lv2 = sel->level; if (ImGui::InputInt("Lv##s", &lv2)) sel->level = std::max(1, lv2);
const char* beh2[] = {"Stationary", "Patrol", "Wander", "Scripted"};
int bi2 = static_cast<int>(sel->behavior);
if (ImGui::Combo("AI##s", &bi2, beh2, 4)) sel->behavior = static_cast<CreatureBehavior>(bi2);
if (ImGui::Button("Delete##npc")) spawner.removeCreature(spawner.getSelectedIndex());
ImGui::SameLine();
if (ImGui::Button("Deselect##npc")) spawner.clearSelection();
}
ImGui::Separator();
static char npcPath[256] = "output/creatures.json";
ImGui::InputText("File##npc", npcPath, sizeof(npcPath));
if (ImGui::Button("Save NPCs")) spawner.saveToFile(npcPath);
ImGui::Separator();
ImGui::TextColored(ImVec4(0.7f, 0.9f, 0.7f, 1), "Click terrain to place selected creature");
}
ImGui::End();
}
void EditorUI::renderWaterPanel(EditorApp& app) {
ImGui::SetNextWindowPos(ImVec2(10, 90), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(280, 250), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Water")) {
if (!app.hasTerrainLoaded()) {
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Load or create terrain first");
ImGui::End(); return;
}
auto& s = app.getTerrainEditor().brush().settings();
ImGui::SliderFloat("Radius", &s.radius, 5.0f, 200.0f, "%.0f");
float wh = app.getWaterHeight();
if (ImGui::SliderFloat("Water Height", &wh, -200.0f, 500.0f, "%.1f"))
app.setWaterHeight(wh);
const char* types[] = {"Water", "Ocean", "Magma", "Slime"};
int typeIdx = app.getWaterType();
if (ImGui::Combo("Liquid Type", &typeIdx, types, 4))
app.setWaterType(static_cast<uint16_t>(typeIdx));
ImGui::Separator();
if (ImGui::Button("Remove Water Under Brush", ImVec2(-1, 0))) {
auto& brush = app.getTerrainEditor().brush();
if (brush.isActive()) {
app.getTerrainEditor().removeWater(brush.getPosition(), s.radius);
app.getEditorCamera(); // trigger dirty
}
}
ImGui::Separator();
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "Left-click to place water");
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "Rendered as translucent overlay");
ImGui::Separator();
ImGui::TextColored(ImVec4(0.7f, 0.9f, 0.7f, 1), "Left-click: place");
ImGui::TextColored(ImVec4(0.7f, 0.9f, 0.7f, 1), "Ctrl+click: select");
ImGui::TextColored(ImVec4(0.7f, 0.9f, 0.7f, 1), "Del: remove selected");
}
ImGui::End();
}
void EditorUI::renderContextMenu(EditorApp& app) {
if (ImGui::BeginPopup("ObjectContextMenu")) {
auto* sel = app.getObjectPlacer().getSelected();
if (!sel) { ImGui::EndPopup(); return; }
std::string display = sel->path;
auto slash = display.rfind('\\');
if (slash != std::string::npos) display = display.substr(slash + 1);
ImGui::TextColored(ImVec4(1, 0.9f, 0.3f, 1), "%s", display.c_str());
ImGui::Separator();
if (ImGui::MenuItem("Move (left-drag)"))
app.startGizmoMode(TransformMode::Move);
if (ImGui::MenuItem("Rotate (left-drag)"))
app.startGizmoMode(TransformMode::Rotate);
if (ImGui::MenuItem("Scale (left-drag)"))
app.startGizmoMode(TransformMode::Scale);
ImGui::Separator();
if (ImGui::BeginMenu("Constrain Axis")) {
if (ImGui::MenuItem("All Axes")) app.setGizmoAxis(TransformAxis::All);
if (ImGui::MenuItem("X (Red)")) app.setGizmoAxis(TransformAxis::X);
if (ImGui::MenuItem("Y (Green)")) app.setGizmoAxis(TransformAxis::Y);
if (ImGui::MenuItem("Z (Blue)")) app.setGizmoAxis(TransformAxis::Z);
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::MenuItem("Delete")) {
app.getObjectPlacer().deleteSelected();
app.markObjectsDirty();
}
if (ImGui::MenuItem("Deselect"))
app.getObjectPlacer().clearSelection();
ImGui::EndPopup();
}
}
void EditorUI::renderPropertiesPanel(EditorApp& app) {
ImGuiViewport* vp = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2(vp->Size.x - 280, 90), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(270, 180), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Properties")) {
auto* tr = app.getTerrainRenderer();
if (tr && tr->getChunkCount() > 0) {
ImGui::Text("Map: %s [%d, %d]", app.getLoadedMap().c_str(),
app.getLoadedTileX(), app.getLoadedTileY());
ImGui::Text("Chunks: %d Tris: %d", tr->getChunkCount(), tr->getTriangleCount());
ImGui::Text("Objects: %zu", app.getObjectPlacer().objectCount());
} else {
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "No terrain loaded");
}
ImGui::Separator();
auto pos = app.getEditorCamera().getCamera().getPosition();
ImGui::Text("Camera: %.0f, %.0f, %.0f", pos.x, pos.y, pos.z);
ImGui::Text("Speed: %.0f (scroll)", app.getEditorCamera().getSpeed());
if (app.getTerrainEditor().hasUnsavedChanges())
ImGui::TextColored(ImVec4(1, 0.8f, 0.3f, 1), "* Unsaved changes");
}
ImGui::End();
}
void EditorUI::renderStatusBar(EditorApp& app) {
ImGuiViewport* vp = ImGui::GetMainViewport();
float h = 24.0f;
ImGui::SetNextWindowPos(ImVec2(vp->Pos.x, vp->Pos.y + vp->Size.y - h));
ImGui::SetNextWindowSize(ImVec2(vp->Size.x, h));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 3));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoFocusOnAppearing;
if (ImGui::Begin("##StatusBar", nullptr, flags)) {
const char* ms[] = {"Sculpt", "Paint", "Objects", "Water", "NPCs"};
const char* m = ms[static_cast<int>(app.getMode())];
if (app.hasTerrainLoaded())
ImGui::Text("[%s] %s [%d,%d]%s", m, app.getLoadedMap().c_str(),
app.getLoadedTileX(), app.getLoadedTileY(),
app.getTerrainEditor().hasUnsavedChanges() ? " *" : "");
else
ImGui::Text("[%s] Wowee World Editor", m);
ImGui::SameLine(vp->Size.x - 120);
ImGui::Text("%.1f FPS", ImGui::GetIO().Framerate);
}
ImGui::End();
ImGui::PopStyleVar(2);
}
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,72 @@
#pragma once
#include "terrain_biomes.hpp"
#include <string>
#include <vector>
namespace wowee {
namespace editor {
class EditorApp;
enum class PaintMode { Paint, Erase, ReplaceBase };
class EditorUI {
public:
EditorUI();
void render(EditorApp& app);
void processActions(EditorApp& app);
PaintMode getPaintMode() const { return paintMode_; }
private:
void renderMenuBar(EditorApp& app);
void renderToolbar(EditorApp& app);
void renderNewTerrainDialog(EditorApp& app);
void renderLoadDialog(EditorApp& app);
void renderSaveDialog(EditorApp& app);
void renderBrushPanel(EditorApp& app);
void renderTexturePaintPanel(EditorApp& app);
void renderObjectPanel(EditorApp& app);
void renderWaterPanel(EditorApp& app);
void renderNpcPanel(EditorApp& app);
void renderContextMenu(EditorApp& app);
void renderPropertiesPanel(EditorApp& app);
void renderStatusBar(EditorApp& app);
bool showNewDialog_ = false;
bool showLoadDialog_ = false;
bool showSaveDialog_ = false;
char newMapNameBuf_[256] = "CustomZone";
int newTileX_ = 32;
int newTileY_ = 32;
float newBaseHeight_ = 100.0f;
int newBiomeIdx_ = 0;
bool newRequested_ = false;
char loadMapNameBuf_[256] = "Azeroth";
int loadTileX_ = 32;
int loadTileY_ = 48;
bool loadRequested_ = false;
char savePathBuf_[512] = "";
bool saveAdtRequested_ = false;
bool saveWdtRequested_ = false;
// Paint panel
PaintMode paintMode_ = PaintMode::Paint;
char texFilterBuf_[128] = "";
int texDirIdx_ = -1; // -1 = all
std::string selectedTexture_;
// Object panel
char objFilterBuf_[128] = "";
int objDirIdx_ = -1;
bool showM2s_ = true;
bool showWMOs_ = true;
};
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,505 @@
#include "editor_viewport.hpp"
#include "rendering/vk_context.hpp"
#include "rendering/vk_texture.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/m2_loader.hpp"
#include "pipeline/wmo_loader.hpp"
#include "core/logger.hpp"
#include <cstring>
#include <unordered_map>
#include <glm/gtc/matrix_transform.hpp>
namespace wowee {
namespace editor {
EditorViewport::EditorViewport() = default;
EditorViewport::~EditorViewport() { shutdown(); }
bool EditorViewport::initialize(rendering::VkContext* ctx, pipeline::AssetManager* am,
rendering::Camera* cam) {
vkCtx_ = ctx;
assetManager_ = am;
camera_ = cam;
if (!createPerFrameResources()) return false;
terrainRenderer_ = std::make_unique<rendering::TerrainRenderer>();
if (!terrainRenderer_->initialize(ctx, perFrameSetLayout_, am)) {
LOG_ERROR("Failed to initialize terrain renderer");
return false;
}
terrainRenderer_->setFogEnabled(false);
m2Renderer_ = std::make_unique<rendering::M2Renderer>();
if (!m2Renderer_->initialize(ctx, perFrameSetLayout_, am)) {
LOG_WARNING("M2 renderer init failed — object rendering disabled");
m2Renderer_.reset();
} else {
m2Renderer_->setForceNoCull(true);
}
wmoRenderer_ = std::make_unique<rendering::WMORenderer>();
if (!wmoRenderer_->initialize(ctx, perFrameSetLayout_, am)) {
LOG_WARNING("WMO renderer init failed — building rendering disabled");
wmoRenderer_.reset();
}
waterRenderer_.initialize(ctx, ctx->getImGuiRenderPass(), perFrameSetLayout_);
markerRenderer_.initialize(ctx, ctx->getImGuiRenderPass(), perFrameSetLayout_);
gizmo_.initialize(ctx, ctx->getImGuiRenderPass(), perFrameSetLayout_);
LOG_INFO("Editor viewport initialized");
return true;
}
void EditorViewport::shutdown() {
if (!vkCtx_) return;
vkDeviceWaitIdle(vkCtx_->getDevice());
gizmo_.shutdown();
markerRenderer_.shutdown();
waterRenderer_.shutdown();
if (wmoRenderer_) { wmoRenderer_->shutdown(); wmoRenderer_.reset(); }
if (m2Renderer_) { m2Renderer_->shutdown(); m2Renderer_.reset(); }
if (terrainRenderer_) { terrainRenderer_->shutdown(); terrainRenderer_.reset(); }
destroyPerFrameResources();
vkCtx_ = nullptr;
}
bool EditorViewport::loadTerrain(const pipeline::TerrainMesh& mesh,
const std::vector<std::string>& texturePaths,
int tileX, int tileY) {
return terrainRenderer_->loadTerrain(mesh, texturePaths, tileX, tileY);
}
void EditorViewport::clearTerrain() {
if (terrainRenderer_) terrainRenderer_->clear();
}
void EditorViewport::updateWater(const pipeline::ADTTerrain& terrain, int tileX, int tileY) {
waterRenderer_.update(terrain, tileX, tileY);
}
void EditorViewport::updateMarkers(const std::vector<PlacedObject>& objects) {
markerRenderer_.update(objects);
}
void EditorViewport::placeM2(const std::string& path, const glm::vec3& pos,
const glm::vec3& rot, float scale) {
(void)path; (void)pos; (void)rot; (void)scale;
}
void EditorViewport::placeWMO(const std::string& path, const glm::vec3& pos,
const glm::vec3& rot) {
(void)path; (void)pos; (void)rot;
}
void EditorViewport::clearObjects() {
if (m2Renderer_) {
vkCtx_->waitAllUploads();
m2Renderer_->clear();
}
if (wmoRenderer_) {
wmoRenderer_->clearAll();
}
markerRenderer_.clear();
}
void EditorViewport::rebuildObjects(const std::vector<PlacedObject>& objects,
const std::vector<CreatureSpawn>& npcs) {
clearObjects();
if (objects.empty() && npcs.empty()) return;
uint32_t nextModelId = 1;
std::unordered_map<std::string, uint32_t> m2ModelIds, wmoModelIds;
for (const auto& obj : objects) {
if (obj.type == PlaceableType::M2 && m2Renderer_) {
uint32_t modelId;
auto it = m2ModelIds.find(obj.path);
if (it != m2ModelIds.end()) {
modelId = it->second;
} else {
auto data = assetManager_->readFile(obj.path);
if (data.empty()) {
LOG_WARNING("M2 file not found in manifest: ", obj.path);
continue;
}
auto model = pipeline::M2Loader::load(data);
// WotLK M2s need a separate .skin file for geometry
if (!model.isValid()) {
std::string skinPath = obj.path;
auto dotPos = skinPath.rfind('.');
if (dotPos != std::string::npos)
skinPath = skinPath.substr(0, dotPos) + "00.skin";
auto skinData = assetManager_->readFile(skinPath);
if (!skinData.empty())
pipeline::M2Loader::loadSkin(skinData, model);
}
if (!model.isValid()) {
LOG_WARNING("M2 failed to parse (", data.size(), " bytes): ", obj.path);
continue;
}
// Ensure boundRadius is reasonable for culling
if (model.boundRadius < 1.0f) model.boundRadius = 50.0f;
modelId = nextModelId++;
if (!m2Renderer_->loadModel(model, modelId)) {
LOG_WARNING("M2 failed to upload to GPU: ", obj.path);
continue;
}
// Wait for async texture uploads to complete before rendering
vkCtx_->waitAllUploads();
vkCtx_->pollUploadBatches();
LOG_INFO("M2 loaded: ", obj.path, " (modelId=", modelId, ", ",
model.vertices.size(), " verts)");
m2ModelIds[obj.path] = modelId;
}
glm::vec3 rotRad = glm::radians(obj.rotation);
m2Renderer_->createInstance(modelId, obj.position, rotRad, obj.scale);
} else if (obj.type == PlaceableType::WMO && wmoRenderer_) {
uint32_t modelId;
auto it = wmoModelIds.find(obj.path);
if (it != wmoModelIds.end()) {
modelId = it->second;
} else {
auto data = assetManager_->readFile(obj.path);
if (data.empty()) {
LOG_WARNING("WMO file not found in manifest: ", obj.path);
continue;
}
auto model = pipeline::WMOLoader::load(data);
// Load WMO group files (_000.wmo, _001.wmo, etc.)
std::string basePath = obj.path;
auto dotPos = basePath.rfind('.');
if (dotPos != std::string::npos) basePath = basePath.substr(0, dotPos);
for (uint32_t gi = 0; gi < model.nGroups; gi++) {
char groupSuffix[16];
std::snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi);
std::string groupPath = basePath + groupSuffix;
auto groupData = assetManager_->readFile(groupPath);
if (!groupData.empty()) {
pipeline::WMOLoader::loadGroup(groupData, model, gi);
}
}
if (!model.isValid()) {
LOG_WARNING("WMO failed to parse (", data.size(), " bytes, ",
model.nGroups, " groups expected): ", obj.path);
continue;
}
modelId = nextModelId++;
if (!wmoRenderer_->loadModel(model, modelId)) {
LOG_WARNING("WMO failed to upload to GPU: ", obj.path);
continue;
}
vkCtx_->waitAllUploads();
vkCtx_->pollUploadBatches();
LOG_INFO("WMO loaded: ", obj.path, " (modelId=", modelId, ", ",
model.groups.size(), " groups)");
wmoModelIds[obj.path] = modelId;
}
glm::vec3 wmoRotRad = glm::radians(obj.rotation);
wmoRenderer_->createInstance(modelId, obj.position, wmoRotRad);
}
}
// Render NPC creatures as M2 instances
if (m2Renderer_) {
for (const auto& npc : npcs) {
if (npc.modelPath.empty()) continue;
uint32_t modelId;
auto it = m2ModelIds.find(npc.modelPath);
if (it != m2ModelIds.end()) {
modelId = it->second;
} else {
auto data = assetManager_->readFile(npc.modelPath);
if (data.empty()) continue;
auto model = pipeline::M2Loader::load(data);
if (!model.isValid()) {
std::string skinPath = npc.modelPath;
auto dotPos = skinPath.rfind('.');
if (dotPos != std::string::npos)
skinPath = skinPath.substr(0, dotPos) + "00.skin";
auto skinData = assetManager_->readFile(skinPath);
if (!skinData.empty())
pipeline::M2Loader::loadSkin(skinData, model);
}
if (!model.isValid()) continue;
if (model.boundRadius < 1.0f) model.boundRadius = 50.0f;
modelId = nextModelId++;
if (!m2Renderer_->loadModel(model, modelId)) continue;
vkCtx_->waitAllUploads();
vkCtx_->pollUploadBatches();
m2ModelIds[npc.modelPath] = modelId;
}
glm::vec3 rotRad = glm::radians(glm::vec3(0, 0, npc.orientation));
m2Renderer_->createInstance(modelId, npc.position, rotRad, 1.0f);
}
}
vkCtx_->waitAllUploads();
vkCtx_->pollUploadBatches();
}
void EditorViewport::update(float deltaTime) {
if (m2Renderer_)
m2Renderer_->update(deltaTime, camera_->getPosition(), camera_->getViewProjectionMatrix());
}
void EditorViewport::setGhostPreview(const std::string& path, const glm::vec3& pos,
const glm::vec3& rotDeg, float scale) {
if (!m2Renderer_) return;
// Load model if path changed
if (path != ghostModelPath_ || ghostModelId_ == 0) {
clearGhostPreview();
auto data = assetManager_->readFile(path);
if (data.empty()) return;
auto model = pipeline::M2Loader::load(data);
if (!model.isValid()) {
std::string skinPath = path;
auto dotPos = skinPath.rfind('.');
if (dotPos != std::string::npos)
skinPath = skinPath.substr(0, dotPos) + "00.skin";
auto skinData = assetManager_->readFile(skinPath);
if (!skinData.empty())
pipeline::M2Loader::loadSkin(skinData, model);
}
if (!model.isValid()) return;
if (model.boundRadius < 1.0f) model.boundRadius = 50.0f;
ghostModelId_ = 60000; // Use a high ID to avoid collision with placed objects
m2Renderer_->loadModel(model, ghostModelId_);
vkCtx_->waitAllUploads();
vkCtx_->pollUploadBatches();
ghostModelPath_ = path;
}
// Create or update ghost instance
glm::vec3 rotRad = glm::radians(rotDeg);
if (!ghostActive_) {
ghostInstanceId_ = m2Renderer_->createInstance(ghostModelId_, pos, rotRad, scale);
ghostActive_ = (ghostInstanceId_ != 0);
} else {
m2Renderer_->setInstancePosition(ghostInstanceId_, pos);
// Rebuild transform with new rotation/scale
glm::mat4 mat = glm::mat4(1.0f);
mat = glm::translate(mat, pos);
mat = glm::rotate(mat, rotRad.x, glm::vec3(1, 0, 0));
mat = glm::rotate(mat, rotRad.y, glm::vec3(0, 1, 0));
mat = glm::rotate(mat, rotRad.z, glm::vec3(0, 0, 1));
mat = glm::scale(mat, glm::vec3(scale));
m2Renderer_->setInstanceTransform(ghostInstanceId_, mat);
}
}
void EditorViewport::clearGhostPreview() {
if (ghostActive_ && m2Renderer_) {
m2Renderer_->removeInstance(ghostInstanceId_);
ghostActive_ = false;
ghostInstanceId_ = 0;
}
if (ghostModelId_ != 0 && m2Renderer_) {
// Don't unload the model — it might be used by placed objects too
ghostModelId_ = 0;
ghostModelPath_.clear();
}
}
void EditorViewport::render(VkCommandBuffer cmd) {
updatePerFrameUBO();
uint32_t frame = vkCtx_->getCurrentFrame();
VkDescriptorSet perFrameSet = perFrameDescSets_[frame];
terrainRenderer_->render(cmd, perFrameSet, *camera_);
if (m2Renderer_)
m2Renderer_->render(cmd, perFrameSet, *camera_);
if (wmoRenderer_)
wmoRenderer_->render(cmd, perFrameSet, *camera_);
waterRenderer_.render(cmd, perFrameSet);
gizmo_.render(cmd, perFrameSet);
}
void EditorViewport::setWireframe(bool enabled) {
wireframe_ = enabled;
if (terrainRenderer_) terrainRenderer_->setWireframe(enabled);
}
bool EditorViewport::createPerFrameResources() {
VkDevice device = vkCtx_->getDevice();
VkDescriptorSetLayoutBinding bindings[2]{};
bindings[0].binding = 0;
bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
bindings[0].descriptorCount = 1;
bindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
bindings[1].binding = 1;
bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
bindings[1].descriptorCount = 1;
bindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
VkDescriptorSetLayoutCreateInfo layoutInfo{};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = 2;
layoutInfo.pBindings = bindings;
if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &perFrameSetLayout_) != VK_SUCCESS)
return false;
VkDescriptorPoolSize poolSizes[2]{};
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSizes[0].descriptorCount = MAX_FRAMES;
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
poolSizes[1].descriptorCount = MAX_FRAMES;
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.maxSets = MAX_FRAMES;
poolInfo.poolSizeCount = 2;
poolInfo.pPoolSizes = poolSizes;
if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &sceneDescPool_) != VK_SUCCESS)
return false;
dummyShadowTexture_ = std::make_unique<rendering::VkTexture>();
if (!dummyShadowTexture_->createDepth(*vkCtx_, 1, 1)) return false;
VkSamplerCreateInfo sampCI{};
sampCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
sampCI.magFilter = VK_FILTER_LINEAR;
sampCI.minFilter = VK_FILTER_LINEAR;
sampCI.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
sampCI.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
sampCI.compareEnable = VK_TRUE;
sampCI.compareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
shadowSampler_ = vkCtx_->getOrCreateSampler(sampCI);
vkCtx_->immediateSubmit([this](VkCommandBuffer cmd) {
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = dummyShadowTexture_->getImage();
barrier.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
barrier.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(cmd,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0, 0, nullptr, 0, nullptr, 1, &barrier);
});
for (uint32_t i = 0; i < MAX_FRAMES; i++) {
VkBufferCreateInfo bufInfo{};
bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufInfo.size = sizeof(rendering::GPUPerFrameData);
bufInfo.usage = VK_BUFFER_USAGE_UNIFORM_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,
&perFrameUBOs_[i], &perFrameUBOAllocs_[i], &mapInfo) != VK_SUCCESS)
return false;
perFrameUBOMapped_[i] = mapInfo.pMappedData;
VkDescriptorSetAllocateInfo setAlloc{};
setAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
setAlloc.descriptorPool = sceneDescPool_;
setAlloc.descriptorSetCount = 1;
setAlloc.pSetLayouts = &perFrameSetLayout_;
if (vkAllocateDescriptorSets(device, &setAlloc, &perFrameDescSets_[i]) != VK_SUCCESS)
return false;
VkDescriptorBufferInfo descBuf{};
descBuf.buffer = perFrameUBOs_[i];
descBuf.offset = 0;
descBuf.range = sizeof(rendering::GPUPerFrameData);
VkDescriptorImageInfo shadowImgInfo{};
shadowImgInfo.sampler = shadowSampler_;
shadowImgInfo.imageView = dummyShadowTexture_->getImageView();
shadowImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkWriteDescriptorSet writes[2]{};
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[0].dstSet = perFrameDescSets_[i];
writes[0].dstBinding = 0;
writes[0].descriptorCount = 1;
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
writes[0].pBufferInfo = &descBuf;
writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[1].dstSet = perFrameDescSets_[i];
writes[1].dstBinding = 1;
writes[1].descriptorCount = 1;
writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
writes[1].pImageInfo = &shadowImgInfo;
vkUpdateDescriptorSets(device, 2, writes, 0, nullptr);
}
return true;
}
void EditorViewport::destroyPerFrameResources() {
if (!vkCtx_) return;
VkDevice device = vkCtx_->getDevice();
for (uint32_t i = 0; i < MAX_FRAMES; i++) {
if (perFrameUBOs_[i]) {
vmaDestroyBuffer(vkCtx_->getAllocator(), perFrameUBOs_[i], perFrameUBOAllocs_[i]);
perFrameUBOs_[i] = VK_NULL_HANDLE;
}
}
if (dummyShadowTexture_) {
dummyShadowTexture_->destroy(device, vkCtx_->getAllocator());
dummyShadowTexture_.reset();
}
if (sceneDescPool_) {
vkDestroyDescriptorPool(device, sceneDescPool_, nullptr);
sceneDescPool_ = VK_NULL_HANDLE;
}
if (perFrameSetLayout_) {
vkDestroyDescriptorSetLayout(device, perFrameSetLayout_, nullptr);
perFrameSetLayout_ = VK_NULL_HANDLE;
}
}
void EditorViewport::updatePerFrameUBO() {
uint32_t frame = vkCtx_->getCurrentFrame();
rendering::GPUPerFrameData data{};
data.view = camera_->getViewMatrix();
data.projection = camera_->getProjectionMatrix();
data.lightSpaceMatrix = glm::mat4(1.0f);
data.lightDir = glm::vec4(glm::normalize(glm::vec3(0.5f, -1.0f, 0.3f)), 0.0f);
data.lightColor = glm::vec4(1.0f, 0.95f, 0.85f, 0.0f);
data.ambientColor = glm::vec4(0.3f, 0.3f, 0.35f, 0.0f);
data.viewPos = glm::vec4(camera_->getPosition(), 0.0f);
data.fogColor = glm::vec4(0.6f, 0.7f, 0.8f, 0.0f);
data.fogParams = glm::vec4(5000.0f, 10000.0f, 0.0f, 0.0f);
data.shadowParams = glm::vec4(0.0f, 0.0f, 0.0f, 0.0f);
std::memcpy(perFrameUBOMapped_[frame], &data, sizeof(data));
}
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,96 @@
#pragma once
#include "rendering/vk_frame_data.hpp"
#include "rendering/terrain_renderer.hpp"
#include "rendering/m2_renderer.hpp"
#include "rendering/wmo_renderer.hpp"
#include "rendering/camera.hpp"
#include "editor_water.hpp"
#include "editor_markers.hpp"
#include "transform_gizmo.hpp"
#include "object_placer.hpp"
#include "npc_spawner.hpp"
#include <vulkan/vulkan.h>
#include <vk_mem_alloc.h>
#include <memory>
namespace wowee {
namespace pipeline { class AssetManager; }
namespace rendering { class VkContext; class VkTexture; }
namespace editor {
class EditorViewport {
public:
EditorViewport();
~EditorViewport();
bool initialize(rendering::VkContext* ctx, pipeline::AssetManager* am, rendering::Camera* cam);
void shutdown();
bool loadTerrain(const pipeline::TerrainMesh& mesh,
const std::vector<std::string>& texturePaths,
int tileX, int tileY);
void clearTerrain();
void updateWater(const pipeline::ADTTerrain& terrain, int tileX, int tileY);
void updateMarkers(const std::vector<PlacedObject>& objects);
void placeM2(const std::string& path, const glm::vec3& pos, const glm::vec3& rot, float scale);
void placeWMO(const std::string& path, const glm::vec3& pos, const glm::vec3& rot);
void clearObjects();
void rebuildObjects(const std::vector<PlacedObject>& objects,
const std::vector<CreatureSpawn>& npcs = {});
void update(float deltaTime);
void render(VkCommandBuffer cmd);
// Ghost preview for placement
void setGhostPreview(const std::string& path, const glm::vec3& pos,
const glm::vec3& rotDeg, float scale);
void clearGhostPreview();
TransformGizmo& getGizmo() { return gizmo_; }
void setWireframe(bool enabled);
bool isWireframe() const { return wireframe_; }
rendering::TerrainRenderer* getTerrainRenderer() { return terrainRenderer_.get(); }
private:
bool createPerFrameResources();
void destroyPerFrameResources();
void updatePerFrameUBO();
rendering::VkContext* vkCtx_ = nullptr;
pipeline::AssetManager* assetManager_ = nullptr;
rendering::Camera* camera_ = nullptr;
std::unique_ptr<rendering::TerrainRenderer> terrainRenderer_;
std::unique_ptr<rendering::M2Renderer> m2Renderer_;
std::unique_ptr<rendering::WMORenderer> wmoRenderer_;
EditorWater waterRenderer_;
EditorMarkers markerRenderer_;
TransformGizmo gizmo_;
static constexpr uint32_t MAX_FRAMES = 2;
VkDescriptorSetLayout perFrameSetLayout_ = VK_NULL_HANDLE;
VkDescriptorPool sceneDescPool_ = VK_NULL_HANDLE;
VkDescriptorSet perFrameDescSets_[MAX_FRAMES] = {};
VkBuffer perFrameUBOs_[MAX_FRAMES] = {};
VmaAllocation perFrameUBOAllocs_[MAX_FRAMES] = {};
void* perFrameUBOMapped_[MAX_FRAMES] = {};
std::unique_ptr<rendering::VkTexture> dummyShadowTexture_;
VkSampler shadowSampler_ = VK_NULL_HANDLE;
bool wireframe_ = false;
// Ghost preview state
std::string ghostModelPath_;
uint32_t ghostModelId_ = 0;
uint32_t ghostInstanceId_ = 0;
bool ghostActive_ = false;
};
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,228 @@
#include "editor_water.hpp"
#include "rendering/vk_context.hpp"
#include "rendering/vk_shader.hpp"
#include "core/logger.hpp"
#include <cstring>
namespace wowee {
namespace editor {
static constexpr float TILE_SIZE = 533.33333f;
static constexpr float CHUNK_SIZE = TILE_SIZE / 16.0f;
EditorWater::EditorWater() = default;
EditorWater::~EditorWater() { shutdown(); }
bool EditorWater::initialize(rendering::VkContext* ctx, VkRenderPass renderPass,
VkDescriptorSetLayout perFrameLayout) {
vkCtx_ = ctx;
renderPass_ = renderPass;
perFrameLayout_ = perFrameLayout;
return createPipeline();
}
void EditorWater::shutdown() {
if (!vkCtx_) return;
VkDevice dev = vkCtx_->getDevice();
if (vertexBuffer_) {
vmaDestroyBuffer(vkCtx_->getAllocator(), vertexBuffer_, vertexAlloc_);
vertexBuffer_ = VK_NULL_HANDLE;
}
if (pipeline_) { vkDestroyPipeline(dev, pipeline_, nullptr); pipeline_ = VK_NULL_HANDLE; }
if (pipelineLayout_) { vkDestroyPipelineLayout(dev, pipelineLayout_, nullptr); pipelineLayout_ = VK_NULL_HANDLE; }
vkCtx_ = nullptr;
}
void EditorWater::clear() {
if (vertexBuffer_) {
vmaDestroyBuffer(vkCtx_->getAllocator(), vertexBuffer_, vertexAlloc_);
vertexBuffer_ = VK_NULL_HANDLE;
vertexCount_ = 0;
}
}
void EditorWater::update(const pipeline::ADTTerrain& terrain, int tileX, int tileY) {
clear();
std::vector<WaterVertex> verts;
for (int cy = 0; cy < 16; cy++) {
for (int cx = 0; cx < 16; cx++) {
int idx = cy * 16 + cx;
const auto& water = terrain.waterData[idx];
if (!water.hasWater()) continue;
float tileNW_X = (32.0f - static_cast<float>(tileY)) * TILE_SIZE;
float tileNW_Y = (32.0f - static_cast<float>(tileX)) * TILE_SIZE;
float x0 = tileNW_X - static_cast<float>(cy) * CHUNK_SIZE;
float y0 = tileNW_Y - static_cast<float>(cx) * CHUNK_SIZE;
float x1 = x0 - CHUNK_SIZE;
float y1 = y0 - CHUNK_SIZE;
float h = water.layers[0].maxHeight;
// Water color by type
float r = 0.1f, g = 0.3f, b = 0.7f, a = 0.45f;
uint16_t lt = water.layers[0].liquidType;
if (lt == 2) { r = 0.8f; g = 0.2f; b = 0.05f; a = 0.7f; } // magma
if (lt == 3) { r = 0.2f; g = 0.6f; b = 0.1f; a = 0.6f; } // slime
// Two triangles per chunk
WaterVertex v;
v.color[0] = r; v.color[1] = g; v.color[2] = b; v.color[3] = a;
v.pos[0] = x0; v.pos[1] = y0; v.pos[2] = h; verts.push_back(v);
v.pos[0] = x1; v.pos[1] = y0; v.pos[2] = h; verts.push_back(v);
v.pos[0] = x1; v.pos[1] = y1; v.pos[2] = h; verts.push_back(v);
v.pos[0] = x0; v.pos[1] = y0; v.pos[2] = h; verts.push_back(v);
v.pos[0] = x1; v.pos[1] = y1; v.pos[2] = h; verts.push_back(v);
v.pos[0] = x0; v.pos[1] = y1; v.pos[2] = h; verts.push_back(v);
}
}
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(WaterVertex);
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) {
LOG_ERROR("Failed to create water vertex buffer");
return;
}
std::memcpy(mapInfo.pMappedData, verts.data(), verts.size() * sizeof(WaterVertex));
}
void EditorWater::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) {
if (!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 EditorWater::createPipeline() {
VkDevice dev = vkCtx_->getDevice();
// Pipeline layout: set 0 = per-frame UBO (reuse terrain's layout)
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("Water shaders not found — water rendering disabled");
return true;
}
VkPipelineShaderStageCreateInfo stages[2]{};
stages[0] = vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
stages[1] = fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);
// Vertex input: pos(3f) + color(4f) = 28 bytes
VkVertexInputBindingDescription binding{};
binding.stride = sizeof(WaterVertex);
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
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 vertexInput{};
vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInput.vertexBindingDescriptionCount = 1;
vertexInput.pVertexBindingDescriptions = &binding;
vertexInput.vertexAttributeDescriptionCount = 2;
vertexInput.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.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
rast.lineWidth = 1.0f;
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_TRUE;
ds.depthWriteEnable = VK_FALSE; // Transparent — don't write depth
ds.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
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 = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
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 = &vertexInput;
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_;
if (vkCreateGraphicsPipelines(dev, vkCtx_->getPipelineCache(), 1, &pci, nullptr, &pipeline_) != VK_SUCCESS) {
LOG_ERROR("Failed to create water pipeline");
pipeline_ = VK_NULL_HANDLE;
}
return true;
}
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,48 @@
#pragma once
#include "pipeline/adt_loader.hpp"
#include <vulkan/vulkan.h>
#include <vk_mem_alloc.h>
#include <vector>
namespace wowee {
namespace rendering { class VkContext; }
namespace editor {
class EditorWater {
public:
EditorWater();
~EditorWater();
bool initialize(rendering::VkContext* ctx, VkRenderPass renderPass,
VkDescriptorSetLayout perFrameLayout);
void shutdown();
void update(const pipeline::ADTTerrain& terrain, int tileX, int tileY);
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
void clear();
private:
bool createPipeline();
rendering::VkContext* vkCtx_ = nullptr;
VkRenderPass renderPass_ = VK_NULL_HANDLE;
VkDescriptorSetLayout perFrameLayout_ = VK_NULL_HANDLE;
VkPipeline pipeline_ = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE;
VkBuffer vertexBuffer_ = VK_NULL_HANDLE;
VmaAllocation vertexAlloc_ = VK_NULL_HANDLE;
uint32_t vertexCount_ = 0;
struct WaterVertex {
float pos[3];
float color[4];
};
};
} // namespace editor
} // namespace wowee

49
tools/editor/main.cpp Normal file
View file

@ -0,0 +1,49 @@
#include "editor_app.hpp"
#include "core/logger.hpp"
#include <string>
#include <cstring>
static void printUsage(const char* argv0) {
LOG_INFO("Usage: ", argv0, " --data <path> [--adt <map> <x> <y>]");
LOG_INFO(" --data <path> Path to extracted WoW data (contains manifest.json)");
LOG_INFO(" --adt <map> <x> <y> Load an ADT tile on startup");
}
int main(int argc, char* argv[]) {
std::string dataPath;
std::string adtMap;
int adtX = -1, adtY = -1;
for (int i = 1; i < argc; i++) {
if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) {
dataPath = argv[++i];
} else if (std::strcmp(argv[i], "--adt") == 0 && i + 3 < argc) {
adtMap = argv[++i];
adtX = std::atoi(argv[++i]);
adtY = std::atoi(argv[++i]);
} else if (std::strcmp(argv[i], "--help") == 0 || std::strcmp(argv[i], "-h") == 0) {
printUsage(argv[0]);
return 0;
}
}
if (dataPath.empty()) {
dataPath = "Data";
LOG_INFO("No --data path specified, using default: ", dataPath);
}
wowee::editor::EditorApp app;
if (!app.initialize(dataPath)) {
LOG_ERROR("Failed to initialize editor");
return 1;
}
if (!adtMap.empty()) {
app.loadADT(adtMap, adtX, adtY);
}
app.run();
app.shutdown();
return 0;
}

View file

@ -0,0 +1,191 @@
#include "npc_presets.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/asset_manifest.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <cctype>
#include <set>
namespace wowee {
namespace editor {
const char* NpcPresets::getCategoryName(CreatureCategory cat) {
static const char* names[] = {
"Critters", "Beasts", "Humanoids", "Undead", "Demons",
"Elementals", "Dragonkin", "Giants", "Mechanical", "Mounts", "Bosses", "Other"
};
return names[static_cast<int>(cat)];
}
std::string NpcPresets::prettifyName(const std::string& dirName) const {
std::string result;
for (size_t i = 0; i < dirName.size(); i++) {
char c = dirName[i];
if (i == 0) {
result += static_cast<char>(std::toupper(c));
} else if (std::isupper(c) && i > 0 && std::islower(dirName[i-1])) {
result += ' ';
result += c;
} else {
result += c;
}
}
return result;
}
CreatureCategory NpcPresets::classifyCreature(const std::string& name) const {
// Critters
static const char* critters[] = {
"rabbit", "rat", "chicken", "frog", "snake", "squirrel", "deer", "sheep",
"cow", "pig", "parrot", "seagull", "beetle", "cockroach", "crab", "prairie",
"butterfly", "firefly", "maggot", "toad", "mouse", "hare", "penguin",
"babycrocodile", "babyelekk", "bearcub", "cat", "smallfish",
"kitten", "skunk", "ladybug", "gazelle", "gilamonster"
};
// Beasts
static const char* beasts[] = {
"bear", "boar", "wolf", "lion", "tiger", "raptor", "gorilla", "hyena",
"scorpid", "spider", "bat", "vulture", "crocolisk", "tallstrider",
"kodo", "elekk", "warp", "ravager", "serpent", "devilsaur", "crochet",
"plainstrider", "stag", "moose", "worg", "rhino", "mammoth", "jormungar",
"shoveltusk", "basilisk", "carrionbird", "condor", "hippogryph",
"windserpent", "thunderlizard", "turtle", "silithid", "wasp", "moth",
"nether", "cat", "arcticcondor"
};
// Humanoids
static const char* humanoids[] = {
"human", "orc", "dwarf", "nightelf", "undead", "tauren", "gnome", "troll",
"bloodelf", "draenei", "goblin", "ogre", "murloc", "naga", "satyr",
"centaur", "furbolg", "gnoll", "kobold", "trogg", "harpy", "pirate",
"bandit", "vrykul", "tuskarr", "wolvar", "arakkoa", "ethereal",
"broken", "fleshgiant", "kvaldir", "pygmy", "taunka"
};
// Undead
static const char* undead[] = {
"skeleton", "zombie", "ghoul", "ghost", "banshee", "lich", "wraith",
"abomination", "geist", "shade", "spectre", "boneguard", "bonespider",
"bonegolem", "crypt", "necro", "plague", "scourge", "val"
};
// Demons
static const char* demons[] = {
"demon", "felguard", "imp", "infernal", "doomguard", "succubus",
"voidwalker", "felhound", "eredar", "pitlord", "dreadlord",
"abyssal", "felboar", "darkhound", "terrorfiend"
};
// Elementals
static const char* elementals[] = {
"elemental", "fire", "water", "air", "earth", "arcane", "storm",
"lava", "bog", "ooze", "slime", "revenant", "totem"
};
// Dragonkin
static const char* dragonkin[] = {
"dragon", "drake", "whelp", "wyrm", "dragonspawn", "drakonid",
"nether", "proto", "celestialdragon"
};
// Giants
static const char* giants[] = {
"giant", "ettin", "gronn", "colossus", "titan", "mountain", "sea"
};
// Mechanical
static const char* mechanical[] = {
"mechanical", "robot", "golem", "harvest", "shredder", "gyro",
"bomber", "tank", "turret", "cannon", "siege"
};
// Mounts
static const char* mounts[] = {
"mount", "horse", "hawkstrider", "raptor", "mechanostrider",
"nightsaber", "ram", "kodo", "skeletal", "broom", "carpet",
"gryphon", "wyvern", "hippogryph", "netherdrake", "protodrake"
};
// Boss
static const char* bosses[] = {
"arthas", "illidan", "kelthuzad", "ragnaros", "onyxia", "nefarian",
"alexstrasza", "malygos", "sartharion", "yoggsaron", "lichking",
"brutallus", "bloodqueen", "anubarak"
};
auto matches = [&](const char* list[], size_t count) {
for (size_t i = 0; i < count; i++) {
if (name.find(list[i]) != std::string::npos) return true;
}
return false;
};
if (matches(critters, sizeof(critters)/sizeof(critters[0]))) return CreatureCategory::Critter;
if (matches(mounts, sizeof(mounts)/sizeof(mounts[0]))) return CreatureCategory::Mount;
if (matches(bosses, sizeof(bosses)/sizeof(bosses[0]))) return CreatureCategory::Boss;
if (matches(undead, sizeof(undead)/sizeof(undead[0]))) return CreatureCategory::Undead;
if (matches(demons, sizeof(demons)/sizeof(demons[0]))) return CreatureCategory::Demon;
if (matches(dragonkin, sizeof(dragonkin)/sizeof(dragonkin[0]))) return CreatureCategory::Dragonkin;
if (matches(elementals, sizeof(elementals)/sizeof(elementals[0]))) return CreatureCategory::Elemental;
if (matches(giants, sizeof(giants)/sizeof(giants[0]))) return CreatureCategory::Giant;
if (matches(mechanical, sizeof(mechanical)/sizeof(mechanical[0]))) return CreatureCategory::Mechanical;
if (matches(humanoids, sizeof(humanoids)/sizeof(humanoids[0]))) return CreatureCategory::Humanoid;
if (matches(beasts, sizeof(beasts)/sizeof(beasts[0]))) return CreatureCategory::Beast;
return CreatureCategory::Other;
}
uint32_t NpcPresets::estimateLevel(const std::string& /*dirName*/) const {
return 10;
}
uint32_t NpcPresets::estimateHealth(uint32_t level) const {
return 50 + level * 80;
}
void NpcPresets::initialize(pipeline::AssetManager* am) {
if (initialized_ || !am) return;
initialized_ = true;
byCategory_.resize(static_cast<size_t>(CreatureCategory::COUNT));
const auto& entries = am->getManifest().getEntries();
std::set<std::string> seen;
for (const auto& [path, entry] : entries) {
if (!path.starts_with("creature\\")) continue;
if (!path.ends_with(".m2")) continue;
// Extract directory name (creature type)
auto firstSlash = path.find('\\');
auto secondSlash = path.find('\\', firstSlash + 1);
if (secondSlash == std::string::npos) continue;
std::string dirName = path.substr(firstSlash + 1, secondSlash - firstSlash - 1);
if (seen.count(dirName)) continue;
seen.insert(dirName);
// Get the actual M2 file path
std::string modelFile = path;
NpcPreset preset;
preset.name = prettifyName(dirName);
preset.modelPath = modelFile;
preset.category = classifyCreature(dirName);
preset.defaultLevel = estimateLevel(dirName);
preset.defaultHealth = estimateHealth(preset.defaultLevel);
preset.defaultHostile = (preset.category != CreatureCategory::Critter &&
preset.category != CreatureCategory::Mount);
presets_.push_back(preset);
byCategory_[static_cast<size_t>(preset.category)].push_back(preset);
}
// Sort each category alphabetically
for (auto& cat : byCategory_) {
std::sort(cat.begin(), cat.end(),
[](const NpcPreset& a, const NpcPreset& b) { return a.name < b.name; });
}
std::sort(presets_.begin(), presets_.end(),
[](const NpcPreset& a, const NpcPreset& b) { return a.name < b.name; });
LOG_INFO("NPC presets: ", presets_.size(), " creatures in ", seen.size(), " types");
}
const std::vector<NpcPreset>& NpcPresets::getByCategory(CreatureCategory cat) const {
return byCategory_[static_cast<size_t>(cat)];
}
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,59 @@
#pragma once
#include "npc_spawner.hpp"
#include <string>
#include <vector>
namespace wowee {
namespace pipeline { class AssetManager; }
namespace editor {
enum class CreatureCategory {
Critter,
Beast,
Humanoid,
Undead,
Demon,
Elemental,
Dragonkin,
Giant,
Mechanical,
Mount,
Boss,
Other,
COUNT
};
struct NpcPreset {
std::string name;
std::string modelPath;
CreatureCategory category;
uint32_t defaultLevel;
uint32_t defaultHealth;
bool defaultHostile;
};
class NpcPresets {
public:
void initialize(pipeline::AssetManager* am);
const std::vector<NpcPreset>& getPresets() const { return presets_; }
const std::vector<NpcPreset>& getByCategory(CreatureCategory cat) const;
static const char* getCategoryName(CreatureCategory cat);
bool isInitialized() const { return initialized_; }
private:
CreatureCategory classifyCreature(const std::string& dirName) const;
std::string prettifyName(const std::string& dirName) const;
uint32_t estimateLevel(const std::string& dirName) const;
uint32_t estimateHealth(uint32_t level) const;
std::vector<NpcPreset> presets_;
std::vector<std::vector<NpcPreset>> byCategory_;
bool initialized_ = false;
};
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,111 @@
#include "npc_spawner.hpp"
#include "core/logger.hpp"
#include <fstream>
#include <sstream>
#include <cmath>
#include <algorithm>
#include <filesystem>
namespace wowee {
namespace editor {
uint32_t NpcSpawner::nextId() { return idCounter_++; }
void NpcSpawner::placeCreature(const CreatureSpawn& spawn) {
CreatureSpawn s = spawn;
s.id = nextId();
s.selected = false;
spawns_.push_back(s);
LOG_INFO("Creature placed: ", s.name, " (id=", s.id, ") at (",
s.position.x, ",", s.position.y, ",", s.position.z, ")");
}
void NpcSpawner::removeCreature(int index) {
if (index < 0 || index >= static_cast<int>(spawns_.size())) return;
spawns_.erase(spawns_.begin() + index);
if (selectedIdx_ == index) selectedIdx_ = -1;
else if (selectedIdx_ > index) selectedIdx_--;
}
int NpcSpawner::selectAt(const glm::vec3& worldPos, float maxDist) {
clearSelection();
float bestDist = maxDist;
int bestIdx = -1;
for (int i = 0; i < static_cast<int>(spawns_.size()); i++) {
float dist = glm::length(spawns_[i].position - worldPos);
if (dist < bestDist) { bestDist = dist; bestIdx = i; }
}
if (bestIdx >= 0) {
selectedIdx_ = bestIdx;
spawns_[bestIdx].selected = true;
}
return bestIdx;
}
void NpcSpawner::clearSelection() {
if (selectedIdx_ >= 0 && selectedIdx_ < static_cast<int>(spawns_.size()))
spawns_[selectedIdx_].selected = false;
selectedIdx_ = -1;
}
CreatureSpawn* NpcSpawner::getSelected() {
if (selectedIdx_ < 0 || selectedIdx_ >= static_cast<int>(spawns_.size())) return nullptr;
return &spawns_[selectedIdx_];
}
bool NpcSpawner::saveToFile(const std::string& path) const {
auto dir = std::filesystem::path(path).parent_path();
if (!dir.empty()) std::filesystem::create_directories(dir);
std::ofstream f(path);
if (!f) { LOG_ERROR("Failed to write NPC file: ", path); return false; }
f << "[\n";
for (size_t i = 0; i < spawns_.size(); i++) {
const auto& s = spawns_[i];
f << " {\n";
f << " \"name\": \"" << s.name << "\",\n";
f << " \"model\": \"" << s.modelPath << "\",\n";
f << " \"displayId\": " << s.displayId << ",\n";
f << " \"position\": [" << s.position.x << "," << s.position.y << "," << s.position.z << "],\n";
f << " \"orientation\": " << s.orientation << ",\n";
f << " \"level\": " << s.level << ",\n";
f << " \"health\": " << s.health << ",\n";
f << " \"mana\": " << s.mana << ",\n";
f << " \"minDamage\": " << s.minDamage << ",\n";
f << " \"maxDamage\": " << s.maxDamage << ",\n";
f << " \"armor\": " << s.armor << ",\n";
f << " \"faction\": " << s.faction << ",\n";
f << " \"behavior\": " << static_cast<int>(s.behavior) << ",\n";
f << " \"wanderRadius\": " << s.wanderRadius << ",\n";
f << " \"aggroRadius\": " << s.aggroRadius << ",\n";
f << " \"leashRadius\": " << s.leashRadius << ",\n";
f << " \"respawnTimeMs\": " << s.respawnTimeMs << ",\n";
f << " \"hostile\": " << (s.hostile ? "true" : "false") << ",\n";
f << " \"questgiver\": " << (s.questgiver ? "true" : "false") << ",\n";
f << " \"vendor\": " << (s.vendor ? "true" : "false") << ",\n";
f << " \"flightmaster\": " << (s.flightmaster ? "true" : "false") << ",\n";
f << " \"innkeeper\": " << (s.innkeeper ? "true" : "false") << ",\n";
f << " \"patrol\": [";
for (size_t p = 0; p < s.patrolPath.size(); p++) {
f << "[" << s.patrolPath[p].position.x << "," << s.patrolPath[p].position.y
<< "," << s.patrolPath[p].position.z << "," << s.patrolPath[p].waitTimeMs << "]";
if (p + 1 < s.patrolPath.size()) f << ",";
}
f << "]\n";
f << " }" << (i + 1 < spawns_.size() ? "," : "") << "\n";
}
f << "]\n";
LOG_INFO("NPC spawns saved: ", path, " (", spawns_.size(), " creatures)");
return true;
}
bool NpcSpawner::loadFromFile(const std::string& path) {
// Simple JSON-ish parser for our format — full JSON parsing would need a library
LOG_INFO("NPC spawn loading not yet implemented for: ", path);
return false;
}
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,90 @@
#pragma once
#include <glm/glm.hpp>
#include <string>
#include <vector>
#include <cstdint>
namespace wowee {
namespace editor {
enum class CreatureBehavior {
Stationary,
Patrol,
Wander,
Scripted
};
struct PatrolPoint {
glm::vec3 position;
float waitTimeMs = 2000.0f;
};
struct CreatureSpawn {
uint32_t id = 0;
std::string name = "Creature";
std::string modelPath;
uint32_t displayId = 0;
// Position
glm::vec3 position{0};
float orientation = 0.0f; // degrees
// Stats
uint32_t level = 1;
uint32_t health = 100;
uint32_t mana = 0;
uint32_t minDamage = 5;
uint32_t maxDamage = 10;
uint32_t armor = 0;
uint32_t faction = 0; // 0 = neutral
// Behavior
CreatureBehavior behavior = CreatureBehavior::Stationary;
float wanderRadius = 10.0f;
float aggroRadius = 20.0f;
float leashRadius = 40.0f;
uint32_t respawnTimeMs = 300000;
std::vector<PatrolPoint> patrolPath;
// Flags
bool hostile = false;
bool questgiver = false;
bool vendor = false;
bool flightmaster = false;
bool innkeeper = false;
bool selected = false;
};
class NpcSpawner {
public:
void placeCreature(const CreatureSpawn& spawn);
void removeCreature(int index);
int selectAt(const glm::vec3& worldPos, float maxDist = 30.0f);
void clearSelection();
CreatureSpawn* getSelected();
int getSelectedIndex() const { return selectedIdx_; }
const std::vector<CreatureSpawn>& getSpawns() const { return spawns_; }
std::vector<CreatureSpawn>& getSpawns() { return spawns_; }
size_t spawnCount() const { return spawns_.size(); }
// Serialize to/from JSON
bool saveToFile(const std::string& path) const;
bool loadFromFile(const std::string& path);
// Template creature for placement
CreatureSpawn& getTemplate() { return template_; }
private:
uint32_t nextId();
std::vector<CreatureSpawn> spawns_;
int selectedIdx_ = -1;
uint32_t idCounter_ = 1;
CreatureSpawn template_;
};
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,160 @@
#include "object_placer.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <cmath>
namespace wowee {
namespace editor {
void ObjectPlacer::setActivePath(const std::string& path, PlaceableType type) {
activePath_ = path;
activeType_ = type;
}
uint32_t ObjectPlacer::nextUniqueId() {
return uniqueIdCounter_++;
}
void ObjectPlacer::placeObject(const glm::vec3& position) {
if (activePath_.empty()) return;
PlacedObject obj;
obj.type = activeType_;
obj.path = activePath_;
obj.nameId = 0;
obj.uniqueId = nextUniqueId();
obj.position = position;
obj.rotation = glm::vec3(0.0f, placementRotY_, 0.0f);
obj.scale = placementScale_;
obj.selected = false;
objects_.push_back(obj);
LOG_INFO("Placed ", (activeType_ == PlaceableType::M2 ? "M2" : "WMO"),
": ", activePath_, " at (", position.x, ",", position.y, ",", position.z, ")");
}
int ObjectPlacer::selectAt(const rendering::Ray& ray, float maxDist) {
clearSelection();
float bestDist = maxDist;
int bestIdx = -1;
for (int i = 0; i < static_cast<int>(objects_.size()); i++) {
// Simple sphere test (radius based on scale)
float radius = 5.0f * objects_[i].scale;
glm::vec3 oc = ray.origin - objects_[i].position;
float b = glm::dot(oc, ray.direction);
float c = glm::dot(oc, oc) - radius * radius;
float disc = b * b - c;
if (disc < 0) continue;
float t = -b - std::sqrt(disc);
if (t < 0) t = -b + std::sqrt(disc);
if (t > 0 && t < bestDist) {
bestDist = t;
bestIdx = i;
}
}
if (bestIdx >= 0) {
selectedIdx_ = bestIdx;
objects_[bestIdx].selected = true;
}
return bestIdx;
}
void ObjectPlacer::clearSelection() {
if (selectedIdx_ >= 0 && selectedIdx_ < static_cast<int>(objects_.size()))
objects_[selectedIdx_].selected = false;
selectedIdx_ = -1;
}
PlacedObject* ObjectPlacer::getSelected() {
if (selectedIdx_ < 0 || selectedIdx_ >= static_cast<int>(objects_.size())) return nullptr;
return &objects_[selectedIdx_];
}
void ObjectPlacer::moveSelected(const glm::vec3& delta) {
if (auto* obj = getSelected()) obj->position += delta;
}
void ObjectPlacer::rotateSelected(const glm::vec3& deltaDeg) {
if (auto* obj = getSelected()) obj->rotation += deltaDeg;
}
void ObjectPlacer::scaleSelected(float delta) {
if (auto* obj = getSelected())
obj->scale = std::max(0.1f, obj->scale + delta);
}
void ObjectPlacer::deleteSelected() {
if (selectedIdx_ < 0 || selectedIdx_ >= static_cast<int>(objects_.size())) return;
objects_.erase(objects_.begin() + selectedIdx_);
selectedIdx_ = -1;
}
void ObjectPlacer::syncToTerrain() {
if (!terrain_) return;
terrain_->doodadNames.clear();
terrain_->doodadPlacements.clear();
terrain_->wmoNames.clear();
terrain_->wmoPlacements.clear();
// Build name lists and placements
std::vector<std::string> m2Names, wmoNames;
for (const auto& obj : objects_) {
if (obj.type == PlaceableType::M2) {
// Find or add name
uint32_t nameId = 0;
for (uint32_t i = 0; i < m2Names.size(); i++) {
if (m2Names[i] == obj.path) { nameId = i; goto foundM2; }
}
nameId = static_cast<uint32_t>(m2Names.size());
m2Names.push_back(obj.path);
foundM2:
pipeline::ADTTerrain::DoodadPlacement dp{};
dp.nameId = nameId;
dp.uniqueId = obj.uniqueId;
dp.position[0] = obj.position.x;
dp.position[1] = obj.position.y;
dp.position[2] = obj.position.z;
dp.rotation[0] = obj.rotation.x;
dp.rotation[1] = obj.rotation.y;
dp.rotation[2] = obj.rotation.z;
dp.scale = static_cast<uint16_t>(obj.scale * 1024.0f);
dp.flags = 0;
terrain_->doodadPlacements.push_back(dp);
} else {
uint32_t nameId = 0;
for (uint32_t i = 0; i < wmoNames.size(); i++) {
if (wmoNames[i] == obj.path) { nameId = i; goto foundWMO; }
}
nameId = static_cast<uint32_t>(wmoNames.size());
wmoNames.push_back(obj.path);
foundWMO:
pipeline::ADTTerrain::WMOPlacement wp{};
wp.nameId = nameId;
wp.uniqueId = obj.uniqueId;
wp.position[0] = obj.position.x;
wp.position[1] = obj.position.y;
wp.position[2] = obj.position.z;
wp.rotation[0] = obj.rotation.x;
wp.rotation[1] = obj.rotation.y;
wp.rotation[2] = obj.rotation.z;
wp.flags = 0;
wp.doodadSet = 0;
terrain_->wmoPlacements.push_back(wp);
}
}
terrain_->doodadNames = std::move(m2Names);
terrain_->wmoNames = std::move(wmoNames);
}
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,75 @@
#pragma once
#include "pipeline/adt_loader.hpp"
#include "rendering/camera.hpp"
#include <glm/glm.hpp>
#include <string>
#include <vector>
#include <cstdint>
namespace wowee {
namespace editor {
enum class PlaceableType { M2, WMO };
struct PlacedObject {
PlaceableType type;
std::string path;
uint32_t nameId;
uint32_t uniqueId;
glm::vec3 position;
glm::vec3 rotation; // degrees
float scale; // 1.0 = normal
bool selected = false;
};
class ObjectPlacer {
public:
void setTerrain(pipeline::ADTTerrain* terrain) { terrain_ = terrain; }
void setActivePath(const std::string& path, PlaceableType type);
const std::string& getActivePath() const { return activePath_; }
PlaceableType getActiveType() const { return activeType_; }
// Place object at world position
void placeObject(const glm::vec3& position);
// Select object nearest to ray
int selectAt(const rendering::Ray& ray, float maxDist = 50.0f);
void clearSelection();
int getSelectedIndex() const { return selectedIdx_; }
PlacedObject* getSelected();
// Transform selected
void moveSelected(const glm::vec3& delta);
void rotateSelected(const glm::vec3& deltaDeg);
void scaleSelected(float delta);
void deleteSelected();
// Sync placed objects back to ADTTerrain structs
void syncToTerrain();
const std::vector<PlacedObject>& getObjects() const { return objects_; }
size_t objectCount() const { return objects_.size(); }
float getPlacementRotationY() const { return placementRotY_; }
void setPlacementRotationY(float deg) { placementRotY_ = deg; }
float getPlacementScale() const { return placementScale_; }
void setPlacementScale(float s) { placementScale_ = s; }
private:
uint32_t nextUniqueId();
pipeline::ADTTerrain* terrain_ = nullptr;
std::string activePath_;
PlaceableType activeType_ = PlaceableType::M2;
std::vector<PlacedObject> objects_;
int selectedIdx_ = -1;
uint32_t uniqueIdCounter_ = 1;
float placementRotY_ = 0.0f;
float placementScale_ = 1.0f;
};
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,2 @@
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

View file

@ -0,0 +1,113 @@
#pragma once
#include <string>
#include <vector>
#include <array>
namespace wowee {
namespace editor {
enum class Biome {
Grassland,
Forest,
Jungle,
Desert,
Barrens,
Snow,
Swamp,
Rocky,
Beach,
Volcanic,
COUNT
};
struct BiomeTextures {
const char* name;
const char* base; // Primary ground texture
const char* secondary; // Secondary layer (dirt/path)
const char* accent; // Accent (rocks/roots)
const char* detail; // Detail overlay
};
inline const BiomeTextures& getBiomeTextures(Biome biome) {
static const std::array<BiomeTextures, static_cast<size_t>(Biome::COUNT)> biomes = {{
{ // Grassland
"Grassland",
"Tileset\\Elwynn\\ElwynnGrassBase.blp",
"Tileset\\Elwynn\\ElwynnDirtBase.blp",
"Tileset\\Elwynn\\ElwynnCobblestoneBase.blp",
"Tileset\\Elwynn\\ElwynnGrassHighlight.blp"
},
{ // Forest
"Forest",
"Tileset\\Ashenvale\\AshenvaleGrass.blp",
"Tileset\\Ashenvale\\AshenvaleDirt.blp",
"Tileset\\Ashenvale\\AshenvaleRoots.blp",
"Tileset\\Ashenvale\\AshenvaleMossBase.blp"
},
{ // Jungle
"Jungle",
"Tileset\\Stranglethorn\\StranglethornGrass.blp",
"Tileset\\Stranglethorn\\StranglethornDirt03.blp",
"Tileset\\Stranglethorn\\StranglethornMossRoot01.blp",
"Tileset\\Stranglethorn\\StranglethornPlants01.blp"
},
{ // Desert
"Desert",
"Tileset\\Tanaris\\TanarisSandBase01.blp",
"Tileset\\Tanaris\\TanarisCrackedGround.blp",
"Tileset\\Tanaris\\TanarisRockBase01.blp",
"Tileset\\Tanaris\\TanarisSandBase02.blp"
},
{ // Barrens
"Barrens",
"Tileset\\Barrens\\BarrensBaseDirt.blp",
"Tileset\\Barrens\\BarrensBaseGrassGold.blp",
"Tileset\\Barrens\\BarrensBaseRock.blp",
"Tileset\\Barrens\\BarrensBaseDirtLighter.blp"
},
{ // Snow
"Snow",
"Tileset\\Expansion02\\Dragonblight\\DragonblightFreshSmoothSnowA.blp",
"Tileset\\Winterspring Grove\\WinterspringDirt.blp",
"Tileset\\Winterspring Grove\\WinterspringRock.blp",
"Tileset\\Winterspring Grove\\WinterspringRockSnow.blp"
},
{ // Swamp
"Swamp",
"Tileset\\Wetlands\\WetlandsGrassDark01.blp",
"Tileset\\Wetlands\\WetlandsDirt01.blp",
"Tileset\\Wetlands\\WetlandsDirtMoss01.blp",
"Tileset\\Wetlands\\WetlandsBaseRock.blp"
},
{ // Rocky
"Rocky",
"Tileset\\Barrens\\BarrensRock01.blp",
"Tileset\\Barrens\\BarrensBaseDirt.blp",
"Tileset\\Desolace\\DesolaceRock01.blp",
"Tileset\\Desolace\\DesolaceDirt.blp"
},
{ // Beach
"Beach",
"Tileset\\Ashenvale\\AshenvaleSand.blp",
"Tileset\\Feralas\\FeralasSand.blp",
"Tileset\\Ashenvale\\AshenvaleShore.blp",
"Tileset\\Feralas\\FeralasGrass.blp"
},
{ // Volcanic
"Volcanic",
"Tileset\\Desolace\\DesolaceDirt.blp",
"Tileset\\Desolace\\DesolaceCracks.blp",
"Tileset\\Desolace\\DesolaceRock01.blp",
"Tileset\\Tanaris\\TanarisRockBaseBurn.blp"
}
}};
return biomes[static_cast<size_t>(biome)];
}
inline const char* getBiomeName(Biome b) {
return getBiomeTextures(b).name;
}
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,530 @@
#include "terrain_editor.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <cmath>
#include <numeric>
#include <random>
namespace wowee {
namespace editor {
TerrainEditor::TerrainEditor() = default;
pipeline::ADTTerrain TerrainEditor::createBlankTerrain(int tileX, int tileY, float baseHeight,
Biome biome) {
pipeline::ADTTerrain terrain;
terrain.loaded = true;
terrain.version = 18;
terrain.coord = {tileX, tileY};
const auto& biomeTextures = getBiomeTextures(biome);
// Integer grid noise — guarantees shared edge vertices get identical heights
auto gridNoise = [](int gx, int gy) -> float {
uint32_t h = static_cast<uint32_t>(gx * 374761393 + gy * 668265263);
h = (h ^ (h >> 13)) * 1274126177;
h = h ^ (h >> 16);
return (static_cast<float>(h & 0xFFFF) / 65535.0f - 0.5f) * 3.0f;
};
for (int cy = 0; cy < 16; cy++) {
for (int cx = 0; cx < 16; cx++) {
auto& chunk = terrain.chunks[cy * 16 + cx];
chunk.flags = 0;
chunk.indexX = cx;
chunk.indexY = cy;
chunk.holes = 0;
chunk.position[0] = (32.0f - tileX) * TILE_SIZE - cx * CHUNK_SIZE;
chunk.position[1] = (32.0f - tileY) * TILE_SIZE - cy * CHUNK_SIZE;
chunk.position[2] = baseHeight;
chunk.heightMap.loaded = true;
for (int i = 0; i < 145; i++) {
int row = i / 17;
int col = i % 17;
if (col <= 8) {
// Outer vertex — shared at chunk edges
int globalRow = cy * 8 + row;
int globalCol = cx * 8 + col;
chunk.heightMap.heights[i] = gridNoise(globalRow, globalCol);
} else {
// Inner vertex (quad center) — not shared, offset grid
int innerCol = col - 9;
int globalRow = cy * 16 + row * 2 + 1;
int globalCol = cx * 16 + innerCol * 2 + 1;
chunk.heightMap.heights[i] = gridNoise(globalRow, globalCol);
}
}
// Normals pointing up (will be recalculated by renderer)
for (int i = 0; i < 145; i++) {
chunk.normals[i * 3 + 0] = 0;
chunk.normals[i * 3 + 1] = 0;
chunk.normals[i * 3 + 2] = 127;
}
// Base texture layer
pipeline::TextureLayer layer{};
layer.textureId = 0;
layer.flags = 0;
layer.offsetMCAL = 0;
layer.effectId = 0;
chunk.layers.push_back(layer);
}
}
// Biome textures
terrain.textures.push_back(biomeTextures.base);
terrain.textures.push_back(biomeTextures.secondary);
terrain.textures.push_back(biomeTextures.accent);
terrain.textures.push_back(biomeTextures.detail);
return terrain;
}
glm::vec3 TerrainEditor::chunkVertexWorldPos(int chunkIdx, int vertIdx) const {
const auto& chunk = terrain_->chunks[chunkIdx];
int tileX = terrain_->coord.x;
int tileY = terrain_->coord.y;
int cx = chunkIdx % 16;
int cy = chunkIdx / 16;
float tileNW_renderX = (32.0f - static_cast<float>(tileY)) * TILE_SIZE;
float tileNW_renderY = (32.0f - static_cast<float>(tileX)) * TILE_SIZE;
float chunkBaseX = tileNW_renderX - static_cast<float>(cy) * CHUNK_SIZE;
float chunkBaseY = tileNW_renderY - static_cast<float>(cx) * CHUNK_SIZE;
float chunkBaseZ = chunk.position[2];
int row = vertIdx / 17;
int col = vertIdx % 17;
float offsetX = static_cast<float>(col);
float offsetY = static_cast<float>(row);
if (col > 8) {
offsetY += 0.5f;
offsetX -= 8.5f;
}
float unitSize = CHUNK_SIZE / 8.0f;
float x = chunkBaseX - offsetY * unitSize;
float y = chunkBaseY - offsetX * unitSize;
float z = chunkBaseZ + chunk.heightMap.heights[vertIdx];
return glm::vec3(x, y, z);
}
float TerrainEditor::getVertexHeight(int chunkIdx, int vertIdx) const {
return terrain_->chunks[chunkIdx].heightMap.heights[vertIdx];
}
void TerrainEditor::setVertexHeight(int chunkIdx, int vertIdx, float height) {
terrain_->chunks[chunkIdx].heightMap.heights[vertIdx] = height;
}
bool TerrainEditor::raycastTerrain(const rendering::Ray& ray, glm::vec3& hitPos) const {
if (!terrain_) return false;
float bestT = 1e30f;
bool hit = false;
for (int chunkIdx = 0; chunkIdx < 256; chunkIdx++) {
const auto& chunk = terrain_->chunks[chunkIdx];
if (!chunk.hasHeightMap()) continue;
// Quick AABB check: compute chunk bounds in render space
glm::vec3 corner0 = chunkVertexWorldPos(chunkIdx, 0);
glm::vec3 corner1 = chunkVertexWorldPos(chunkIdx, 144);
glm::vec3 minB = glm::min(corner0, corner1) - glm::vec3(0, 0, 200);
glm::vec3 maxB = glm::max(corner0, corner1) + glm::vec3(0, 0, 200);
// Simple AABB-ray test
float tmin = -1e30f, tmax = 1e30f;
for (int i = 0; i < 3; i++) {
if (std::abs(ray.direction[i]) < 1e-8f) {
if (ray.origin[i] < minB[i] || ray.origin[i] > maxB[i]) { tmin = 1e30f; break; }
} else {
float t1 = (minB[i] - ray.origin[i]) / ray.direction[i];
float t2 = (maxB[i] - ray.origin[i]) / ray.direction[i];
if (t1 > t2) std::swap(t1, t2);
tmin = std::max(tmin, t1);
tmax = std::min(tmax, t2);
}
}
if (tmin > tmax || tmax < 0) continue;
// Triangle intersection for each quad
for (int qy = 0; qy < 8; qy++) {
for (int qx = 0; qx < 8; qx++) {
int center = 9 + qy * 17 + qx;
int tl = center - 9;
int tr = center - 8;
int bl = center + 8;
int br = center + 9;
int tris[4][3] = {{center, tl, tr}, {center, tr, br}, {center, br, bl}, {center, bl, tl}};
for (auto& tri : tris) {
glm::vec3 v0 = chunkVertexWorldPos(chunkIdx, tri[0]);
glm::vec3 v1 = chunkVertexWorldPos(chunkIdx, tri[1]);
glm::vec3 v2 = chunkVertexWorldPos(chunkIdx, tri[2]);
// Moller-Trumbore intersection
glm::vec3 e1 = v1 - v0;
glm::vec3 e2 = v2 - v0;
glm::vec3 h = glm::cross(ray.direction, e2);
float a = glm::dot(e1, h);
if (std::abs(a) < 1e-8f) continue;
float f = 1.0f / a;
glm::vec3 s = ray.origin - v0;
float u = f * glm::dot(s, h);
if (u < 0.0f || u > 1.0f) continue;
glm::vec3 q = glm::cross(s, e1);
float v = f * glm::dot(ray.direction, q);
if (v < 0.0f || u + v > 1.0f) continue;
float t = f * glm::dot(e2, q);
if (t > 0.001f && t < bestT) {
bestT = t;
hitPos = ray.origin + ray.direction * t;
hit = true;
}
}
}
}
}
return hit;
}
std::vector<int> TerrainEditor::getAffectedChunks(const glm::vec3& center, float radius) const {
std::vector<int> result;
for (int i = 0; i < 256; i++) {
if (!terrain_->chunks[i].hasHeightMap()) continue;
// Check if any vertex in chunk is within radius
glm::vec3 c0 = chunkVertexWorldPos(i, 0);
glm::vec3 c1 = chunkVertexWorldPos(i, 144);
glm::vec3 chunkCenter = (c0 + c1) * 0.5f;
float chunkRadius = glm::length(c1 - c0) * 0.5f;
if (glm::length(glm::vec2(chunkCenter.x - center.x, chunkCenter.y - center.y)) < radius + chunkRadius)
result.push_back(i);
}
return result;
}
void TerrainEditor::beginStroke() {
if (!terrain_ || strokeActive_) return;
strokeActive_ = true;
auto affected = getAffectedChunks(brush_.getPosition(), brush_.settings().radius);
// Capture all chunks that could be affected during the entire stroke
std::vector<int> allChunks(256);
std::iota(allChunks.begin(), allChunks.end(), 0);
std::vector<int> valid;
for (int i : allChunks) {
if (terrain_->chunks[i].hasHeightMap()) valid.push_back(i);
}
history_.beginEdit(*terrain_, valid);
}
void TerrainEditor::endStroke() {
if (!strokeActive_) return;
strokeActive_ = false;
history_.endEdit(*terrain_);
}
void TerrainEditor::applyBrush(float deltaTime) {
if (!terrain_ || !brush_.isActive()) return;
switch (brush_.settings().mode) {
case BrushMode::Raise: applyRaise(deltaTime); break;
case BrushMode::Lower: applyRaise(deltaTime); break;
case BrushMode::Smooth: applySmooth(deltaTime); break;
case BrushMode::Flatten:
case BrushMode::Level: applyFlatten(deltaTime); break;
}
}
void TerrainEditor::applyRaise(float dt) {
float amount = brush_.settings().strength * dt;
if (brush_.settings().mode == BrushMode::Lower) amount = -amount;
auto affected = getAffectedChunks(brush_.getPosition(), brush_.settings().radius);
for (int chunkIdx : affected) {
bool modified = false;
for (int v = 0; v < 145; v++) {
glm::vec3 pos = chunkVertexWorldPos(chunkIdx, v);
float dist = glm::length(glm::vec2(pos.x - brush_.getPosition().x,
pos.y - brush_.getPosition().y));
float influence = brush_.getInfluence(dist);
if (influence > 0.0f) {
float h = getVertexHeight(chunkIdx, v);
setVertexHeight(chunkIdx, v, h + amount * influence);
modified = true;
}
}
if (modified) {
stitchEdges(chunkIdx);
if (std::find(dirtyChunks_.begin(), dirtyChunks_.end(), chunkIdx) == dirtyChunks_.end())
dirtyChunks_.push_back(chunkIdx);
dirty_ = true;
}
}
}
void TerrainEditor::applySmooth(float dt) {
float factor = std::min(1.0f, brush_.settings().strength * dt * 0.5f);
auto affected = getAffectedChunks(brush_.getPosition(), brush_.settings().radius);
// Build a snapshot of all heights so we read from consistent state
std::array<std::array<float, 145>, 256> snapshot;
for (int ci : affected)
for (int v = 0; v < 145; v++)
snapshot[ci][v] = getVertexHeight(ci, v);
// Helper: get height of vertex at global outer grid position,
// looking across chunk boundaries
auto getGlobalOuterHeight = [&](int chunkIdx, int row, int col) -> float {
int cx = chunkIdx % 16;
int cy = chunkIdx / 16;
// If within chunk bounds, return directly
if (row >= 0 && row <= 8 && col >= 0 && col <= 8) {
int vi = row * 17 + col;
return snapshot[chunkIdx][vi];
}
// Cross into adjacent chunk
int ncx = cx, ncy = cy;
int nr = row, nc = col;
if (row < 0) { ncy = cy - 1; nr = 8; }
if (row > 8) { ncy = cy + 1; nr = 0; }
if (col < 0) { ncx = cx - 1; nc = 8; }
if (col > 8) { ncx = cx + 1; nc = 0; }
if (ncx < 0 || ncx > 15 || ncy < 0 || ncy > 15)
return snapshot[chunkIdx][std::clamp(row, 0, 8) * 17 + std::clamp(col, 0, 8)];
int nci = ncy * 16 + ncx;
if (!terrain_->chunks[nci].hasHeightMap())
return snapshot[chunkIdx][std::clamp(row, 0, 8) * 17 + std::clamp(col, 0, 8)];
int vi = nr * 17 + nc;
return snapshot[nci][vi];
};
for (int chunkIdx : affected) {
bool modified = false;
for (int v = 0; v < 145; v++) {
glm::vec3 pos = chunkVertexWorldPos(chunkIdx, v);
float dist = glm::length(glm::vec2(pos.x - brush_.getPosition().x,
pos.y - brush_.getPosition().y));
float influence = brush_.getInfluence(dist);
if (influence <= 0.0f) continue;
int row = v / 17;
int col = v % 17;
float sum = 0.0f;
int count = 0;
if (col <= 8) {
// Outer vertex — sample 4 neighbors, crossing chunk borders
int dirs[][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
for (auto& d : dirs) {
sum += getGlobalOuterHeight(chunkIdx, row + d[0], col + d[1]);
count++;
}
} else {
// Inner vertex — use same-chunk neighbors only
int neighbors[] = {v - 17, v + 17, v - 1, v + 1};
for (int n : neighbors) {
if (n >= 0 && n < 145) {
sum += snapshot[chunkIdx][n];
count++;
}
}
}
if (count > 0) {
float avg = sum / static_cast<float>(count);
float h = snapshot[chunkIdx][v];
float newH = h + (avg - h) * factor * influence;
if (newH != h) {
setVertexHeight(chunkIdx, v, newH);
modified = true;
}
}
}
if (modified) {
stitchEdges(chunkIdx);
if (std::find(dirtyChunks_.begin(), dirtyChunks_.end(), chunkIdx) == dirtyChunks_.end())
dirtyChunks_.push_back(chunkIdx);
dirty_ = true;
}
}
}
void TerrainEditor::stitchEdges(int chunkIdx) {
int cx = chunkIdx % 16;
int cy = chunkIdx / 16;
auto pushDirty = [&](int idx) {
if (std::find(dirtyChunks_.begin(), dirtyChunks_.end(), idx) == dirtyChunks_.end())
dirtyChunks_.push_back(idx);
};
if (cx < 15) {
int n = cy * 16 + cx + 1;
if (terrain_->chunks[n].hasHeightMap()) {
for (int r = 0; r <= 8; r++)
setVertexHeight(n, r * 17, getVertexHeight(chunkIdx, r * 17 + 8));
pushDirty(n);
}
}
if (cx > 0) {
int n = cy * 16 + cx - 1;
if (terrain_->chunks[n].hasHeightMap()) {
for (int r = 0; r <= 8; r++)
setVertexHeight(n, r * 17 + 8, getVertexHeight(chunkIdx, r * 17));
pushDirty(n);
}
}
if (cy < 15) {
int n = (cy + 1) * 16 + cx;
if (terrain_->chunks[n].hasHeightMap()) {
for (int c = 0; c <= 8; c++)
setVertexHeight(n, c, getVertexHeight(chunkIdx, 8 * 17 + c));
pushDirty(n);
}
}
if (cy > 0) {
int n = (cy - 1) * 16 + cx;
if (terrain_->chunks[n].hasHeightMap()) {
for (int c = 0; c <= 8; c++)
setVertexHeight(n, 8 * 17 + c, getVertexHeight(chunkIdx, c));
pushDirty(n);
}
}
}
void TerrainEditor::applyFlatten(float dt) {
float factor = std::min(1.0f, brush_.settings().strength * dt * 0.3f);
float targetH = brush_.settings().flattenHeight;
auto affected = getAffectedChunks(brush_.getPosition(), brush_.settings().radius);
for (int chunkIdx : affected) {
bool modified = false;
for (int v = 0; v < 145; v++) {
glm::vec3 pos = chunkVertexWorldPos(chunkIdx, v);
float dist = glm::length(glm::vec2(pos.x - brush_.getPosition().x,
pos.y - brush_.getPosition().y));
float influence = brush_.getInfluence(dist);
if (influence <= 0.0f) continue;
float h = getVertexHeight(chunkIdx, v);
// targetH is absolute world Z; heights are relative to chunk base
float relTarget = targetH - terrain_->chunks[chunkIdx].position[2];
float newH = h + (relTarget - h) * factor * influence;
if (newH != h) {
setVertexHeight(chunkIdx, v, newH);
modified = true;
}
}
if (modified) {
stitchEdges(chunkIdx);
if (std::find(dirtyChunks_.begin(), dirtyChunks_.end(), chunkIdx) == dirtyChunks_.end())
dirtyChunks_.push_back(chunkIdx);
dirty_ = true;
}
}
}
std::vector<int> TerrainEditor::consumeDirtyChunks() {
std::vector<int> result;
result.swap(dirtyChunks_);
return result;
}
pipeline::TerrainMesh TerrainEditor::regenerateMesh() const {
if (!terrain_) return {};
return pipeline::TerrainMeshGenerator::generate(*terrain_);
}
pipeline::ChunkMesh TerrainEditor::regenerateChunkMesh(int chunkIndex) const {
if (!terrain_) return {};
auto mesh = pipeline::TerrainMeshGenerator::generate(*terrain_);
return mesh.chunks[chunkIndex];
}
void TerrainEditor::undo() {
if (!terrain_) return;
history_.undo(*terrain_);
for (int idx : history_.lastAffectedChunks()) {
if (std::find(dirtyChunks_.begin(), dirtyChunks_.end(), idx) == dirtyChunks_.end())
dirtyChunks_.push_back(idx);
}
}
void TerrainEditor::redo() {
if (!terrain_) return;
history_.redo(*terrain_);
for (int idx : history_.lastAffectedChunks()) {
if (std::find(dirtyChunks_.begin(), dirtyChunks_.end(), idx) == dirtyChunks_.end())
dirtyChunks_.push_back(idx);
}
}
void TerrainEditor::setWaterLevel(const glm::vec3& center, float radius,
float waterHeight, uint16_t liquidType) {
if (!terrain_) return;
auto affected = getAffectedChunks(center, radius);
for (int chunkIdx : affected) {
auto& water = terrain_->waterData[chunkIdx];
if (water.layers.empty()) {
pipeline::ADTTerrain::WaterLayer wl;
wl.liquidType = liquidType;
wl.flags = 0;
wl.minHeight = waterHeight;
wl.maxHeight = waterHeight;
wl.x = 0;
wl.y = 0;
wl.width = 9;
wl.height = 9;
wl.heights.assign(81, waterHeight);
wl.mask.assign(8, 0xFF);
water.layers.push_back(wl);
} else {
auto& wl = water.layers[0];
wl.minHeight = waterHeight;
wl.maxHeight = waterHeight;
wl.liquidType = liquidType;
std::fill(wl.heights.begin(), wl.heights.end(), waterHeight);
}
if (std::find(dirtyChunks_.begin(), dirtyChunks_.end(), chunkIdx) == dirtyChunks_.end())
dirtyChunks_.push_back(chunkIdx);
dirty_ = true;
}
}
void TerrainEditor::removeWater(const glm::vec3& center, float radius) {
if (!terrain_) return;
auto affected = getAffectedChunks(center, radius);
for (int chunkIdx : affected) {
terrain_->waterData[chunkIdx].layers.clear();
if (std::find(dirtyChunks_.begin(), dirtyChunks_.end(), chunkIdx) == dirtyChunks_.end())
dirtyChunks_.push_back(chunkIdx);
dirty_ = true;
}
}
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,83 @@
#pragma once
#include "editor_brush.hpp"
#include "editor_history.hpp"
#include "terrain_biomes.hpp"
#include "pipeline/adt_loader.hpp"
#include "pipeline/terrain_mesh.hpp"
#include "rendering/camera.hpp"
#include <vector>
#include <functional>
namespace wowee {
namespace editor {
class TerrainEditor {
public:
TerrainEditor();
void setTerrain(pipeline::ADTTerrain* terrain) { terrain_ = terrain; }
pipeline::ADTTerrain* getTerrain() { return terrain_; }
const pipeline::ADTTerrain* getTerrain() const { return terrain_; }
EditorBrush& brush() { return brush_; }
const EditorBrush& brush() const { return brush_; }
EditorHistory& history() { return history_; }
static pipeline::ADTTerrain createBlankTerrain(int tileX, int tileY, float baseHeight = 100.0f,
Biome biome = Biome::Grassland);
// Raycast against terrain, returns true if hit
bool raycastTerrain(const rendering::Ray& ray, glm::vec3& hitPos) const;
// Apply brush at current position (call per-frame while painting)
void applyBrush(float deltaTime);
// Begin/end a paint stroke (for undo grouping)
void beginStroke();
void endStroke();
bool isStrokeActive() const { return strokeActive_; }
// Get chunks modified since last call (for re-upload)
std::vector<int> consumeDirtyChunks();
// Regenerate mesh for specific chunks
pipeline::TerrainMesh regenerateMesh() const;
pipeline::ChunkMesh regenerateChunkMesh(int chunkIndex) const;
void undo();
void redo();
// Water editing
void setWaterLevel(const glm::vec3& center, float radius, float waterHeight, uint16_t liquidType = 0);
void removeWater(const glm::vec3& center, float radius);
bool hasUnsavedChanges() const { return dirty_; }
void markSaved() { dirty_ = false; }
private:
void applyRaise(float dt);
void applySmooth(float dt);
void applyFlatten(float dt);
void stitchEdges(int chunkIdx);
std::vector<int> getAffectedChunks(const glm::vec3& center, float radius) const;
glm::vec3 chunkVertexWorldPos(int chunkIdx, int vertIdx) const;
float getVertexHeight(int chunkIdx, int vertIdx) const;
void setVertexHeight(int chunkIdx, int vertIdx, float height);
pipeline::ADTTerrain* terrain_ = nullptr;
EditorBrush brush_;
EditorHistory history_;
bool strokeActive_ = false;
bool dirty_ = false;
std::vector<int> dirtyChunks_;
static constexpr float TILE_SIZE = 533.33333f;
static constexpr float CHUNK_SIZE = TILE_SIZE / 16.0f;
static constexpr float GRID_STEP = CHUNK_SIZE / 8.0f;
};
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,194 @@
#include "texture_painter.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <cmath>
namespace wowee {
namespace editor {
void TexturePainter::setActiveTexture(const std::string& texturePath) {
activeTexture_ = texturePath;
}
uint32_t TexturePainter::ensureTextureInList(const std::string& path) {
for (uint32_t i = 0; i < terrain_->textures.size(); i++) {
if (terrain_->textures[i] == path) return i;
}
terrain_->textures.push_back(path);
return static_cast<uint32_t>(terrain_->textures.size() - 1);
}
int TexturePainter::ensureLayerOnChunk(int chunkIdx, uint32_t textureId) {
auto& chunk = terrain_->chunks[chunkIdx];
for (int i = 0; i < static_cast<int>(chunk.layers.size()); i++) {
if (chunk.layers[i].textureId == textureId) return i;
}
if (chunk.layers.size() < 4) {
pipeline::TextureLayer layer{};
layer.textureId = textureId;
layer.flags = 0x100;
layer.offsetMCAL = static_cast<uint32_t>(chunk.alphaMap.size());
layer.effectId = 0;
chunk.layers.push_back(layer);
chunk.alphaMap.resize(chunk.alphaMap.size() + 4096, 0);
return static_cast<int>(chunk.layers.size() - 1);
}
// At 4 layers — find the non-base layer with lowest total alpha and replace it
int weakest = -1;
int weakestSum = INT32_MAX;
for (int i = 1; i < static_cast<int>(chunk.layers.size()); i++) {
if (chunk.layers[i].textureId == textureId) return i;
size_t off = chunk.layers[i].offsetMCAL;
if (off + 4096 > chunk.alphaMap.size()) continue;
int sum = 0;
for (int j = 0; j < 4096; j++) sum += chunk.alphaMap[off + j];
if (sum < weakestSum) { weakestSum = sum; weakest = i; }
}
if (weakest < 0) return -1;
// Replace the weakest layer
chunk.layers[weakest].textureId = textureId;
size_t off = chunk.layers[weakest].offsetMCAL;
std::fill(chunk.alphaMap.begin() + off, chunk.alphaMap.begin() + off + 4096, 0);
return weakest;
}
glm::vec2 TexturePainter::worldToChunkUV(int chunkIdx, const glm::vec3& worldPos) const {
int cx = chunkIdx % 16;
int cy = chunkIdx / 16;
int tileX = terrain_->coord.x;
int tileY = terrain_->coord.y;
float tileNW_X = (32.0f - static_cast<float>(tileY)) * TILE_SIZE;
float tileNW_Y = (32.0f - static_cast<float>(tileX)) * TILE_SIZE;
float chunkBaseX = tileNW_X - static_cast<float>(cy) * CHUNK_SIZE;
float chunkBaseY = tileNW_Y - static_cast<float>(cx) * CHUNK_SIZE;
// UV: 0,0 at chunk NW corner, 1,1 at SE corner
float u = (chunkBaseX - worldPos.x) / CHUNK_SIZE;
float v = (chunkBaseY - worldPos.y) / CHUNK_SIZE;
return glm::vec2(u, v);
}
void TexturePainter::modifyAlpha(int chunkIdx, int layerIdx, const glm::vec3& center,
float radius, float strength, float falloff, bool erasing) {
auto& chunk = terrain_->chunks[chunkIdx];
auto& layer = chunk.layers[layerIdx];
// Find alpha data offset for this layer
size_t alphaOffset = layer.offsetMCAL;
if (alphaOffset + 4096 > chunk.alphaMap.size()) return;
int cx = chunkIdx % 16;
int cy = chunkIdx / 16;
int tileX = terrain_->coord.x;
int tileY = terrain_->coord.y;
float tileNW_X = (32.0f - static_cast<float>(tileY)) * TILE_SIZE;
float tileNW_Y = (32.0f - static_cast<float>(tileX)) * TILE_SIZE;
float chunkBaseX = tileNW_X - static_cast<float>(cy) * CHUNK_SIZE;
float chunkBaseY = tileNW_Y - static_cast<float>(cx) * CHUNK_SIZE;
float texelSize = CHUNK_SIZE / 64.0f;
for (int ty = 0; ty < 64; ty++) {
for (int tx = 0; tx < 64; tx++) {
// World position of this alpha texel
float wx = chunkBaseX - (static_cast<float>(ty) + 0.5f) * texelSize;
float wy = chunkBaseY - (static_cast<float>(tx) + 0.5f) * texelSize;
float dist = std::sqrt((wx - center.x) * (wx - center.x) +
(wy - center.y) * (wy - center.y));
if (dist >= radius) continue;
// Falloff
float t = dist / radius;
float innerRadius = 1.0f - falloff;
float influence = 1.0f;
if (t > innerRadius && falloff > 0.001f) {
float ft = (t - innerRadius) / falloff;
influence = 1.0f - ft * ft;
}
size_t idx = alphaOffset + ty * 64 + tx;
float current = static_cast<float>(chunk.alphaMap[idx]) / 255.0f;
float delta = strength * influence;
float newVal;
if (erasing)
newVal = std::max(0.0f, current - delta);
else
newVal = std::min(1.0f, current + delta);
chunk.alphaMap[idx] = static_cast<uint8_t>(newVal * 255.0f);
}
}
}
std::vector<int> TexturePainter::paint(const glm::vec3& center, float radius,
float strength, float falloff) {
if (!terrain_ || activeTexture_.empty()) return {};
uint32_t texId = ensureTextureInList(activeTexture_);
std::vector<int> modified;
for (int i = 0; i < 256; i++) {
if (!terrain_->chunks[i].hasHeightMap()) continue;
// Quick distance check from chunk center
int cx = i % 16;
int cy = i / 16;
float tileNW_X = (32.0f - static_cast<float>(terrain_->coord.y)) * TILE_SIZE;
float tileNW_Y = (32.0f - static_cast<float>(terrain_->coord.x)) * TILE_SIZE;
float chunkCenterX = tileNW_X - (cy + 0.5f) * CHUNK_SIZE;
float chunkCenterY = tileNW_Y - (cx + 0.5f) * CHUNK_SIZE;
float dist = std::sqrt((chunkCenterX - center.x) * (chunkCenterX - center.x) +
(chunkCenterY - center.y) * (chunkCenterY - center.y));
if (dist > radius + CHUNK_SIZE) continue;
int layerIdx = ensureLayerOnChunk(i, texId);
if (layerIdx < 0) continue; // chunk full
modifyAlpha(i, layerIdx, center, radius, strength, falloff, false);
modified.push_back(i);
}
return modified;
}
std::vector<int> TexturePainter::erase(const glm::vec3& center, float radius,
float strength, float falloff) {
if (!terrain_ || activeTexture_.empty()) return {};
std::vector<int> modified;
for (int i = 0; i < 256; i++) {
if (!terrain_->chunks[i].hasHeightMap()) continue;
auto& chunk = terrain_->chunks[i];
int cx = i % 16;
int cy = i / 16;
float tileNW_X = (32.0f - static_cast<float>(terrain_->coord.y)) * TILE_SIZE;
float tileNW_Y = (32.0f - static_cast<float>(terrain_->coord.x)) * TILE_SIZE;
float chunkCenterX = tileNW_X - (cy + 0.5f) * CHUNK_SIZE;
float chunkCenterY = tileNW_Y - (cx + 0.5f) * CHUNK_SIZE;
float dist = std::sqrt((chunkCenterX - center.x) * (chunkCenterX - center.x) +
(chunkCenterY - center.y) * (chunkCenterY - center.y));
if (dist > radius + CHUNK_SIZE) continue;
// Erase all non-base layers in range
for (int l = 1; l < static_cast<int>(chunk.layers.size()); l++) {
modifyAlpha(i, l, center, radius, strength, falloff, true);
}
modified.push_back(i);
}
return modified;
}
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,42 @@
#pragma once
#include "editor_brush.hpp"
#include "pipeline/adt_loader.hpp"
#include <string>
#include <vector>
#include <glm/glm.hpp>
namespace wowee {
namespace editor {
class TexturePainter {
public:
void setTerrain(pipeline::ADTTerrain* terrain) { terrain_ = terrain; }
void setActiveTexture(const std::string& texturePath);
const std::string& getActiveTexture() const { return activeTexture_; }
// Paint the active texture at the given world position
// Returns list of modified chunk indices
std::vector<int> paint(const glm::vec3& center, float radius, float strength, float falloff);
// Erase a texture layer at the given position
std::vector<int> erase(const glm::vec3& center, float radius, float strength, float falloff);
private:
uint32_t ensureTextureInList(const std::string& path);
int ensureLayerOnChunk(int chunkIdx, uint32_t textureId);
void modifyAlpha(int chunkIdx, int layerIdx, const glm::vec3& center,
float radius, float strength, float falloff, bool erasing);
glm::vec2 worldToChunkUV(int chunkIdx, const glm::vec3& worldPos) const;
pipeline::ADTTerrain* terrain_ = nullptr;
std::string activeTexture_;
static constexpr float TILE_SIZE = 533.33333f;
static constexpr float CHUNK_SIZE = TILE_SIZE / 16.0f;
};
} // namespace editor
} // namespace wowee

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

View file

@ -0,0 +1,77 @@
#pragma once
#include "rendering/camera.hpp"
#include <glm/glm.hpp>
#include <vulkan/vulkan.h>
#include <vk_mem_alloc.h>
namespace wowee {
namespace rendering { class VkContext; }
namespace editor {
enum class TransformMode { None, Move, Rotate, Scale };
enum class TransformAxis { All, X, Y, Z };
class TransformGizmo {
public:
TransformGizmo();
~TransformGizmo();
bool initialize(rendering::VkContext* ctx, VkRenderPass renderPass,
VkDescriptorSetLayout perFrameLayout);
void shutdown();
void setTarget(const glm::vec3& position, float scale = 1.0f);
void setMode(TransformMode mode) { mode_ = mode; }
TransformMode getMode() const { return mode_; }
void setAxis(TransformAxis axis) { axis_ = axis; }
TransformAxis getAxis() const { return axis_; }
bool isActive() const { return mode_ != TransformMode::None; }
// Begin/end drag
void beginDrag(const glm::vec2& screenPos);
void updateDrag(const glm::vec2& screenPos, const rendering::Camera& camera,
float screenW, float screenH);
void endDrag();
bool isDragging() const { return dragging_; }
// Get accumulated transform delta since beginDrag
glm::vec3 getMoveDelta() const { return moveDelta_; }
glm::vec3 getRotateDelta() const { return rotateDelta_; }
float getScaleDelta() const { return scaleDelta_; }
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet);
private:
bool createPipeline();
void updateBuffers();
rendering::VkContext* vkCtx_ = nullptr;
VkRenderPass renderPass_ = VK_NULL_HANDLE;
VkDescriptorSetLayout perFrameLayout_ = VK_NULL_HANDLE;
VkPipeline pipeline_ = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE;
VkBuffer vertexBuffer_ = VK_NULL_HANDLE;
VmaAllocation vertexAlloc_ = VK_NULL_HANDLE;
uint32_t vertexCount_ = 0;
TransformMode mode_ = TransformMode::None;
TransformAxis axis_ = TransformAxis::All;
glm::vec3 targetPos_{0};
float targetScale_ = 1.0f;
bool visible_ = false;
bool dragging_ = false;
glm::vec2 dragStart_{0};
glm::vec2 dragCurrent_{0};
glm::vec3 moveDelta_{0};
glm::vec3 rotateDelta_{0};
float scaleDelta_ = 0.0f;
struct GizmoVertex { float pos[3]; float color[4]; };
};
} // namespace editor
} // namespace wowee