fix(editor): normal recalculation after sculpt, mode switch cleanup

- Terrain normals recalculated after height changes (smooth lighting
  on sculpted terrain instead of flat-shaded appearance)
- Ghost preview and brush indicator cleared when switching modes
  (prevents stale model instances or circles persisting)
- File > Clear All resets undo history and selections
- Normal computation uses finite differences from neighbor heights,
  handles both outer (9x9) and inner (8x8) vertex grid positions
This commit is contained in:
Kelsi 2026-05-05 04:24:20 -07:00
parent 5daa359e74
commit ba96de7138
5 changed files with 70 additions and 1 deletions

View file

@ -469,6 +469,9 @@ void EditorApp::refreshDirtyChunks() {
auto dirty = terrainEditor_.consumeDirtyChunks();
if (dirty.empty()) return;
// Recalculate normals for modified chunks (better lighting)
terrainEditor_.recalcNormals(dirty);
// Regenerate full mesh and reload terrain
auto mesh = terrainEditor_.regenerateMesh();
viewport_.clearTerrain();

View file

@ -58,7 +58,13 @@ public:
core::Window* getWindow() { return window_.get(); }
EditorMode getMode() const { return mode_; }
void setMode(EditorMode m) { mode_ = m; }
void setMode(EditorMode m) {
if (m != mode_) {
viewport_.clearGhostPreview();
viewport_.setBrushIndicator({}, 0, false);
}
mode_ = m;
}
void markObjectsDirty() { objectsDirty_ = true; }
void startGizmoMode(TransformMode mode);

View file

@ -68,6 +68,11 @@ void EditorUI::renderMenuBar(EditorApp& app) {
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("New Terrain...", "Ctrl+N")) showNewDialog_ = true;
if (ImGui::MenuItem("Load ADT...", "Ctrl+O")) showLoadDialog_ = true;
if (ImGui::MenuItem("Clear All", nullptr, false, app.hasTerrainLoaded())) {
app.getTerrainEditor().history().clear();
app.getObjectPlacer().clearSelection();
app.getNpcSpawner().clearSelection();
}
ImGui::Separator();
if (ImGui::MenuItem("Quick Save", "Ctrl+S", false, app.hasTerrainLoaded()))
app.quickSave();

View file

@ -478,6 +478,58 @@ void TerrainEditor::undo() {
}
}
void TerrainEditor::recalcNormals(const std::vector<int>& chunkIndices) {
if (!terrain_) return;
float unitSize = CHUNK_SIZE / 8.0f;
for (int ci : chunkIndices) {
auto& chunk = terrain_->chunks[ci];
if (!chunk.hasHeightMap()) continue;
for (int i = 0; i < 145; i++) {
int row = i / 17;
int col = i % 17;
// Get heights of neighbors
float hC = chunk.heightMap.heights[i];
float hL = hC, hR = hC, hU = hC, hD = hC;
if (col <= 8) {
// Outer vertex
int li = row * 17 + std::max(0, col - 1);
int ri = row * 17 + std::min(8, col + 1);
int ui = std::max(0, row - 1) * 17 + col;
int di = std::min(8, row + 1) * 17 + col;
hL = chunk.heightMap.heights[li];
hR = chunk.heightMap.heights[ri];
hU = chunk.heightMap.heights[ui];
hD = chunk.heightMap.heights[di];
} else {
// Inner vertex — use adjacent outer verts
int innerCol = col - 9;
int tl = row * 17 + innerCol;
int tr = row * 17 + innerCol + 1;
int bl = (row + 1) * 17 + innerCol;
int br = (row + 1) * 17 + innerCol + 1;
if (tl >= 0 && tl < 145) hU = chunk.heightMap.heights[tl];
if (tr >= 0 && tr < 145) hR = chunk.heightMap.heights[tr];
if (bl >= 0 && bl < 145) hD = chunk.heightMap.heights[bl];
if (br >= 0 && br < 145) hL = chunk.heightMap.heights[br];
}
// Compute normal from height differences
float dx = (hL - hR) / (2.0f * unitSize);
float dy = (hU - hD) / (2.0f * unitSize);
float len = std::sqrt(dx * dx + dy * dy + 1.0f);
glm::vec3 n(dx / len, dy / len, 1.0f / len);
chunk.normals[i * 3 + 0] = static_cast<int8_t>(n.x * 127.0f);
chunk.normals[i * 3 + 1] = static_cast<int8_t>(n.y * 127.0f);
chunk.normals[i * 3 + 2] = static_cast<int8_t>(n.z * 127.0f);
}
}
}
void TerrainEditor::redo() {
if (!terrain_) return;
history_.redo(*terrain_);

View file

@ -48,6 +48,9 @@ public:
void undo();
void redo();
// Recalculate normals for modified chunks (improves lighting after sculpt)
void recalcNormals(const std::vector<int>& chunkIndices);
// Water editing
void setWaterLevel(const glm::vec3& center, float radius, float waterHeight, uint16_t liquidType = 0);
void removeWater(const glm::vec3& center, float radius);