mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 09:03:52 +00:00
feat(editor): flatten around object, scatter auto-align, manifest batch
- Flatten Ground: flattens terrain to object height with smooth falloff around placed buildings/structures (undoable). Button in object panel - Scatter auto-align: checkbox enables terrain snapping + slope alignment for scattered objects (trees snap to ground and lean with hillsides) - Batch convert now falls back to asset manifest when filesystem dir is empty — converts M2/WMO from game data without filesystem extraction - Public terrain editor wrappers: beginGeneratorUndo, endGeneratorUndo, markDirty, stitchChunkEdges, getChunkVertexWorldPos
This commit is contained in:
parent
115fe8436f
commit
d3e8f999c7
4 changed files with 76 additions and 8 deletions
|
|
@ -1298,6 +1298,35 @@ void EditorApp::snapSelectedToGround() {
|
|||
}
|
||||
}
|
||||
|
||||
void EditorApp::flattenAroundSelected(float radius) {
|
||||
auto* sel = objectPlacer_.getSelected();
|
||||
if (!sel || !terrain_.isLoaded()) return;
|
||||
|
||||
terrainEditor_.beginGeneratorUndo();
|
||||
float targetHeight = sel->position.z;
|
||||
for (int ci = 0; ci < 256; ci++) {
|
||||
auto& chunk = terrain_.chunks[ci];
|
||||
if (!chunk.hasHeightMap()) continue;
|
||||
bool modified = false;
|
||||
for (int v = 0; v < 145; v++) {
|
||||
glm::vec3 vpos = terrainEditor_.getChunkVertexWorldPos(ci, v);
|
||||
float dist = glm::length(glm::vec2(vpos.x - sel->position.x, vpos.y - sel->position.y));
|
||||
if (dist >= radius) continue;
|
||||
float t = dist / radius;
|
||||
float blend = t * t;
|
||||
float relTarget = targetHeight - chunk.position[2];
|
||||
chunk.heightMap.heights[v] = chunk.heightMap.heights[v] * blend + relTarget * (1.0f - blend);
|
||||
modified = true;
|
||||
}
|
||||
if (modified) {
|
||||
terrainEditor_.stitchChunkEdges(ci);
|
||||
terrainEditor_.markDirty(ci);
|
||||
}
|
||||
}
|
||||
terrainEditor_.endGeneratorUndo();
|
||||
showToast("Flattened terrain around object (r=" + std::to_string(static_cast<int>(radius)) + ")");
|
||||
}
|
||||
|
||||
void EditorApp::alignSelectedToTerrain() {
|
||||
auto& indices = objectPlacer_.getSelectedIndices();
|
||||
auto& objects = objectPlacer_.getObjects();
|
||||
|
|
@ -1323,14 +1352,27 @@ void EditorApp::alignSelectedToTerrain() {
|
|||
|
||||
int EditorApp::batchConvertAssets(const std::string& dataDir) {
|
||||
namespace fs = std::filesystem;
|
||||
if (!fs::exists(dataDir)) return 0;
|
||||
|
||||
int converted = 0;
|
||||
for (auto& entry : fs::recursive_directory_iterator(dataDir)) {
|
||||
if (!entry.is_regular_file()) continue;
|
||||
std::string ext = entry.path().extension().string();
|
||||
|
||||
// Collect paths from filesystem or manifest
|
||||
std::vector<std::string> assetPaths;
|
||||
if (fs::exists(dataDir)) {
|
||||
for (auto& entry : fs::recursive_directory_iterator(dataDir)) {
|
||||
if (entry.is_regular_file())
|
||||
assetPaths.push_back(fs::relative(entry.path(), dataDir).string());
|
||||
}
|
||||
}
|
||||
if (assetPaths.empty() && assetManager_) {
|
||||
for (const auto& [path, _] : assetManager_->getManifest().getEntries())
|
||||
assetPaths.push_back(path);
|
||||
LOG_INFO("Batch convert: using manifest (", assetPaths.size(), " entries)");
|
||||
}
|
||||
|
||||
for (const auto& relPath : assetPaths) {
|
||||
std::string ext;
|
||||
auto dot = relPath.rfind('.');
|
||||
if (dot != std::string::npos) ext = relPath.substr(dot);
|
||||
for (char& c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
std::string relPath = fs::relative(entry.path(), dataDir).string();
|
||||
|
||||
if (ext == ".m2") {
|
||||
auto wom = pipeline::WoweeModelLoader::fromM2(relPath, assetManager_.get());
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ public:
|
|||
void setSkyPreset(int preset); // 0=day, 1=dusk, 2=night
|
||||
void snapSelectedToGround();
|
||||
void alignSelectedToTerrain();
|
||||
void flattenAroundSelected(float radius = 30.0f);
|
||||
void flyToSelected();
|
||||
int batchConvertAssets(const std::string& dataDir);
|
||||
void clearAllObjects();
|
||||
|
|
|
|||
|
|
@ -1512,6 +1512,10 @@ void EditorUI::renderObjectPanel(EditorApp& app) {
|
|||
ImGui::SameLine();
|
||||
if (ImGui::Button("Align Slope", ImVec2(75, 0)))
|
||||
app.alignSelectedToTerrain();
|
||||
if (ImGui::Button("Flatten Ground", ImVec2(100, 0)))
|
||||
app.flattenAroundSelected();
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Flatten terrain around object to its height");
|
||||
if (ImGui::Button("Fly To", ImVec2(55, 0)))
|
||||
app.flyToSelected();
|
||||
ImGui::SameLine();
|
||||
|
|
@ -1544,16 +1548,32 @@ void EditorUI::renderObjectPanel(EditorApp& app) {
|
|||
ImGui::SliderInt("Count##objsc", &objScatterCount, 1, 50);
|
||||
ImGui::SliderFloat("Radius##objsc", &objScatterRadius, 10.0f, 300.0f);
|
||||
ImGui::DragFloatRange2("Scale##objsc", &objMinScale, &objMaxScale, 0.05f, 0.1f, 10.0f);
|
||||
static bool scatterAlign = true;
|
||||
ImGui::Checkbox("Align to terrain", &scatterAlign);
|
||||
auto& brush = app.getTerrainEditor().brush();
|
||||
if (ImGui::Button("Scatter at Cursor##obj", ImVec2(-1, 0))) {
|
||||
if (brush.isActive() && !placer.getActivePath().empty()) {
|
||||
size_t before = placer.objectCount();
|
||||
placer.scatter(brush.getPosition(), objScatterRadius,
|
||||
objScatterCount, objMinScale, objMaxScale);
|
||||
if (scatterAlign) {
|
||||
for (size_t i = before; i < placer.objectCount(); i++) {
|
||||
auto& obj = placer.getObjects()[i];
|
||||
rendering::Ray ray;
|
||||
ray.origin = obj.position + glm::vec3(0, 0, 500);
|
||||
ray.direction = glm::vec3(0, 0, -1);
|
||||
glm::vec3 hitPos;
|
||||
if (app.getTerrainEditor().raycastTerrain(ray, hitPos)) {
|
||||
obj.position.z = hitPos.z;
|
||||
glm::vec3 n = app.getTerrainEditor().sampleTerrainNormal(obj.position);
|
||||
obj.rotation.x = glm::degrees(std::asin(-n.x));
|
||||
obj.rotation.z = glm::degrees(std::asin(n.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
app.markObjectsDirty();
|
||||
}
|
||||
}
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1),
|
||||
"Scatters selected model with random rotation/scale");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ public:
|
|||
|
||||
// Get chunks modified since last call (for re-upload)
|
||||
std::vector<int> consumeDirtyChunks();
|
||||
void markDirty(int chunkIdx) { dirtyChunks_.push_back(chunkIdx); dirty_ = true; }
|
||||
void stitchChunkEdges(int chunkIdx) { stitchEdges(chunkIdx); }
|
||||
glm::vec3 getChunkVertexWorldPos(int ci, int vi) const { return chunkVertexWorldPos(ci, vi); }
|
||||
void beginGeneratorUndo() { recordGeneratorUndo(); }
|
||||
void endGeneratorUndo() { commitGeneratorUndo(); }
|
||||
|
||||
// Regenerate mesh for specific chunks
|
||||
pipeline::TerrainMesh regenerateMesh() const;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue