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
2026-05-05 03:47:03 -07:00
|
|
|
#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;
|
2026-05-05 04:34:03 -07:00
|
|
|
// Track recent textures (max 10)
|
|
|
|
|
auto it = std::find(recentTextures_.begin(), recentTextures_.end(), texturePath);
|
|
|
|
|
if (it != recentTextures_.end()) recentTextures_.erase(it);
|
|
|
|
|
recentTextures_.insert(recentTextures_.begin(), texturePath);
|
|
|
|
|
if (recentTextures_.size() > 10) recentTextures_.pop_back();
|
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
2026-05-05 03:47:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 06:17:37 -07:00
|
|
|
void TexturePainter::autoPaintByHeight(const std::vector<HeightBand>& bands) {
|
|
|
|
|
if (!terrain_ || bands.empty()) return;
|
|
|
|
|
|
|
|
|
|
// Ensure all band textures are in the texture list
|
|
|
|
|
for (const auto& band : bands)
|
|
|
|
|
ensureTextureInList(band.texturePath);
|
|
|
|
|
|
|
|
|
|
for (int ci = 0; ci < 256; ci++) {
|
|
|
|
|
auto& chunk = terrain_->chunks[ci];
|
|
|
|
|
if (!chunk.hasHeightMap()) continue;
|
|
|
|
|
|
|
|
|
|
// Find average height of this chunk
|
|
|
|
|
float avgH = chunk.position[2];
|
|
|
|
|
float sum = 0;
|
|
|
|
|
for (int v = 0; v < 145; v++) sum += chunk.heightMap.heights[v];
|
|
|
|
|
avgH += sum / 145.0f;
|
|
|
|
|
|
|
|
|
|
// Find which band this chunk falls into
|
|
|
|
|
for (const auto& band : bands) {
|
|
|
|
|
if (avgH <= band.maxHeight) {
|
|
|
|
|
uint32_t texId = ensureTextureInList(band.texturePath);
|
|
|
|
|
if (!chunk.layers.empty())
|
|
|
|
|
chunk.layers[0].textureId = texId;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 07:19:05 -07:00
|
|
|
void TexturePainter::autoPaintBySlope(float slopeThreshold, const std::string& steepTexture) {
|
|
|
|
|
if (!terrain_) return;
|
|
|
|
|
uint32_t steepTexId = ensureTextureInList(steepTexture);
|
|
|
|
|
|
|
|
|
|
for (int ci = 0; ci < 256; ci++) {
|
|
|
|
|
auto& chunk = terrain_->chunks[ci];
|
|
|
|
|
if (!chunk.hasHeightMap() || chunk.layers.empty()) continue;
|
|
|
|
|
|
|
|
|
|
// Compute average slope from normals
|
|
|
|
|
float maxSlope = 0.0f;
|
|
|
|
|
for (int v = 0; v < 145; v++) {
|
|
|
|
|
float nz = static_cast<float>(chunk.normals[v * 3 + 2]) / 127.0f;
|
|
|
|
|
float slope = 1.0f - std::abs(nz);
|
|
|
|
|
maxSlope = std::max(maxSlope, slope);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (maxSlope > slopeThreshold) {
|
|
|
|
|
// Add steep texture as a layer
|
|
|
|
|
int layerIdx = ensureLayerOnChunk(ci, steepTexId);
|
|
|
|
|
if (layerIdx > 0) {
|
|
|
|
|
size_t off = chunk.layers[layerIdx].offsetMCAL;
|
|
|
|
|
if (off + 4096 <= chunk.alphaMap.size()) {
|
|
|
|
|
for (int ty = 0; ty < 64; ty++) {
|
|
|
|
|
for (int tx = 0; tx < 64; tx++) {
|
|
|
|
|
// Sample slope at this texel
|
|
|
|
|
int vi = (ty / 8) * 17 + (tx / 8);
|
|
|
|
|
if (vi >= 145) vi = 144;
|
|
|
|
|
float nz = static_cast<float>(chunk.normals[vi * 3 + 2]) / 127.0f;
|
|
|
|
|
float slope = 1.0f - std::abs(nz);
|
|
|
|
|
float alpha = std::clamp((slope - slopeThreshold * 0.5f) / (1.0f - slopeThreshold * 0.5f), 0.0f, 1.0f);
|
|
|
|
|
chunk.alphaMap[off + ty * 64 + tx] = static_cast<uint8_t>(alpha * 255.0f);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
2026-05-05 03:47:03 -07:00
|
|
|
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
|