feat(editor): smooth entire tile, snap-to-ground toggle, object list improvements

- Smooth Entire Tile: global smoothing pass with configurable iterations
  (1-10). Smooths across chunk boundaries for seamless results. Updates
  inner vertices from smoothed outer grid. Great after noise generation.
- Snap to Ground checkbox: on by default, objects placed at terrain
  surface. Disable for floating/airborne objects.
- Random Rotation + Snap Ground checkboxes side-by-side for fast setup
- Toast notifications on noise apply and smooth operations
- Smooth pass uses cross-chunk neighbor averaging for edge continuity
This commit is contained in:
Kelsi 2026-05-05 05:00:31 -07:00
parent 5df007b7b9
commit 9bc05fae87
4 changed files with 82 additions and 1 deletions

View file

@ -336,9 +336,16 @@ void EditorUI::renderBrushPanel(EditorApp& app) {
if (ImGui::Button("Apply Noise", ImVec2(-1, 0))) {
app.getTerrainEditor().applyNoise(noiseFreq, noiseAmp, noiseOctaves,
static_cast<uint32_t>(noiseSeed));
app.showToast("Noise applied");
}
static int smoothPasses = 2;
ImGui::SliderInt("Smooth Passes", &smoothPasses, 1, 10);
if (ImGui::Button("Smooth Entire Tile", ImVec2(-1, 0))) {
app.getTerrainEditor().smoothEntireTile(smoothPasses);
app.showToast("Tile smoothed");
}
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1),
"Adds procedural hills/valleys to entire tile");
"Generate terrain, then smooth for natural look");
}
ImGui::Separator();
@ -466,6 +473,10 @@ void EditorUI::renderObjectPanel(EditorApp& app) {
bool randRot = placer.getRandomRotation();
if (ImGui::Checkbox("Random Rotation", &randRot))
placer.setRandomRotation(randRot);
ImGui::SameLine();
bool snap = placer.getSnapToGround();
if (ImGui::Checkbox("Snap Ground", &snap))
placer.setSnapToGround(snap);
float scale = placer.getPlacementScale();
if (ImGui::SliderFloat("Scale", &scale, 0.1f, 10.0f, "%.2f"))
placer.setPlacementScale(scale);

View file

@ -58,6 +58,8 @@ public:
void setPlacementScale(float s) { placementScale_ = s; }
bool getRandomRotation() const { return randomRotation_; }
void setRandomRotation(bool v) { randomRotation_ = v; }
bool getSnapToGround() const { return snapToGround_; }
void setSnapToGround(bool v) { snapToGround_ = v; }
// Undo last placement
bool canUndoPlace() const { return !undoStack_.empty(); }
@ -81,6 +83,7 @@ private:
float placementRotY_ = 0.0f;
float placementScale_ = 1.0f;
bool randomRotation_ = false;
bool snapToGround_ = true;
};
} // namespace editor

View file

@ -588,6 +588,70 @@ void TerrainEditor::removeWater(const glm::vec3& center, float radius) {
}
}
void TerrainEditor::smoothEntireTile(int iterations) {
if (!terrain_) return;
for (int iter = 0; iter < iterations; iter++) {
// Snapshot all heights
std::array<std::array<float, 145>, 256> snap;
for (int ci = 0; ci < 256; ci++)
for (int v = 0; v < 145; v++)
snap[ci][v] = terrain_->chunks[ci].heightMap.heights[v];
for (int ci = 0; ci < 256; ci++) {
auto& chunk = terrain_->chunks[ci];
if (!chunk.hasHeightMap()) continue;
int cx = ci % 16, cy = ci / 16;
for (int v = 0; v < 145; v++) {
int row = v / 17, col = v % 17;
if (col > 8) continue; // smooth outer vertices only
float sum = snap[ci][v];
int count = 1;
// Same-chunk neighbors
if (col > 0) { sum += snap[ci][row * 17 + col - 1]; count++; }
if (col < 8) { sum += snap[ci][row * 17 + col + 1]; count++; }
if (row > 0) { sum += snap[ci][(row - 1) * 17 + col]; count++; }
if (row < 8) { sum += snap[ci][(row + 1) * 17 + col]; count++; }
// Cross-chunk neighbors at edges
if (col == 0 && cx > 0) { sum += snap[cy * 16 + cx - 1][row * 17 + 8]; count++; }
if (col == 8 && cx < 15) { sum += snap[cy * 16 + cx + 1][row * 17 + 0]; count++; }
if (row == 0 && cy > 0) { sum += snap[(cy - 1) * 16 + cx][8 * 17 + col]; count++; }
if (row == 8 && cy < 15) { sum += snap[(cy + 1) * 16 + cx][0 * 17 + col]; count++; }
chunk.heightMap.heights[v] = sum / static_cast<float>(count);
}
// Update inner vertices from smoothed outer vertices
for (int v = 0; v < 145; v++) {
int row = v / 17, col = v % 17;
if (col <= 8) continue;
int innerCol = col - 9;
// Average of 4 surrounding outer vertices
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 < 145 && tr < 145 && bl < 145 && br < 145)
chunk.heightMap.heights[v] = (chunk.heightMap.heights[tl] +
chunk.heightMap.heights[tr] + chunk.heightMap.heights[bl] +
chunk.heightMap.heights[br]) * 0.25f;
}
}
// Stitch all edges
for (int ci = 0; ci < 256; ci++)
stitchEdges(ci);
}
for (int ci = 0; ci < 256; ci++)
dirtyChunks_.push_back(ci);
dirty_ = true;
}
void TerrainEditor::applyErode(float dt) {
float factor = std::min(1.0f, brush_.settings().strength * dt * 0.3f);

View file

@ -54,6 +54,9 @@ public:
// Noise generator: applies procedural height noise to the terrain
void applyNoise(float frequency, float amplitude, int octaves, uint32_t seed);
// Global smooth pass across entire tile (N iterations)
void smoothEntireTile(int iterations);
// Import/export heightmap (raw 16-bit grayscale, 129x129)
bool importHeightmap(const std::string& path, float heightScale);
bool exportHeightmap(const std::string& path, float heightScale);