mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 00:53:52 +00:00
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:
parent
d138269a35
commit
2980ca83e7
42 changed files with 5647 additions and 3 deletions
105
CMakeLists.txt
105
CMakeLists.txt
|
|
@ -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:")
|
||||||
|
|
|
||||||
8
assets/shaders/editor_water.frag.glsl
Normal file
8
assets/shaders/editor_water.frag.glsl
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) in vec4 vColor;
|
||||||
|
layout(location = 0) out vec4 outColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
outColor = vColor;
|
||||||
|
}
|
||||||
24
assets/shaders/editor_water.vert.glsl
Normal file
24
assets/shaders/editor_water.vert.glsl
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
337
tools/editor/adt_writer.cpp
Normal 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
|
||||||
38
tools/editor/adt_writer.hpp
Normal file
38
tools/editor/adt_writer.hpp
Normal 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
|
||||||
98
tools/editor/asset_browser.cpp
Normal file
98
tools/editor/asset_browser.cpp
Normal 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
|
||||||
46
tools/editor/asset_browser.hpp
Normal file
46
tools/editor/asset_browser.hpp
Normal 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
603
tools/editor/editor_app.cpp
Normal 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
107
tools/editor/editor_app.hpp
Normal 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
|
||||||
20
tools/editor/editor_brush.cpp
Normal file
20
tools/editor/editor_brush.cpp
Normal 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
|
||||||
44
tools/editor/editor_brush.hpp
Normal file
44
tools/editor/editor_brush.hpp
Normal 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
|
||||||
81
tools/editor/editor_camera.cpp
Normal file
81
tools/editor/editor_camera.cpp
Normal 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
|
||||||
39
tools/editor/editor_camera.hpp
Normal file
39
tools/editor/editor_camera.hpp
Normal 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
|
||||||
83
tools/editor/editor_history.cpp
Normal file
83
tools/editor/editor_history.cpp
Normal 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
|
||||||
48
tools/editor/editor_history.hpp
Normal file
48
tools/editor/editor_history.hpp
Normal 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
|
||||||
214
tools/editor/editor_markers.cpp
Normal file
214
tools/editor/editor_markers.cpp
Normal 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
|
||||||
48
tools/editor/editor_markers.hpp
Normal file
48
tools/editor/editor_markers.hpp
Normal 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
682
tools/editor/editor_ui.cpp
Normal 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
|
||||||
72
tools/editor/editor_ui.hpp
Normal file
72
tools/editor/editor_ui.hpp
Normal 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
|
||||||
505
tools/editor/editor_viewport.cpp
Normal file
505
tools/editor/editor_viewport.cpp
Normal 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
|
||||||
96
tools/editor/editor_viewport.hpp
Normal file
96
tools/editor/editor_viewport.hpp
Normal 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
|
||||||
228
tools/editor/editor_water.cpp
Normal file
228
tools/editor/editor_water.cpp
Normal 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
|
||||||
48
tools/editor/editor_water.hpp
Normal file
48
tools/editor/editor_water.hpp
Normal 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
49
tools/editor/main.cpp
Normal 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;
|
||||||
|
}
|
||||||
191
tools/editor/npc_presets.cpp
Normal file
191
tools/editor/npc_presets.cpp
Normal 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
|
||||||
59
tools/editor/npc_presets.hpp
Normal file
59
tools/editor/npc_presets.hpp
Normal 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
|
||||||
111
tools/editor/npc_spawner.cpp
Normal file
111
tools/editor/npc_spawner.cpp
Normal 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
|
||||||
90
tools/editor/npc_spawner.hpp
Normal file
90
tools/editor/npc_spawner.hpp
Normal 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
|
||||||
160
tools/editor/object_placer.cpp
Normal file
160
tools/editor/object_placer.cpp
Normal 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
|
||||||
75
tools/editor/object_placer.hpp
Normal file
75
tools/editor/object_placer.hpp
Normal 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
|
||||||
2
tools/editor/stb_image_impl.cpp
Normal file
2
tools/editor/stb_image_impl.cpp
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
|
#include "stb_image.h"
|
||||||
113
tools/editor/terrain_biomes.hpp
Normal file
113
tools/editor/terrain_biomes.hpp
Normal 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
|
||||||
530
tools/editor/terrain_editor.cpp
Normal file
530
tools/editor/terrain_editor.cpp
Normal 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
|
||||||
83
tools/editor/terrain_editor.hpp
Normal file
83
tools/editor/terrain_editor.hpp
Normal 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
|
||||||
194
tools/editor/texture_painter.cpp
Normal file
194
tools/editor/texture_painter.cpp
Normal 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
|
||||||
42
tools/editor/texture_painter.hpp
Normal file
42
tools/editor/texture_painter.hpp
Normal 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
|
||||||
286
tools/editor/transform_gizmo.cpp
Normal file
286
tools/editor/transform_gizmo.cpp
Normal 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
|
||||||
77
tools/editor/transform_gizmo.hpp
Normal file
77
tools/editor/transform_gizmo.hpp
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue