feat(editor): auto-paint terrain by height bands

- Auto-Paint by Height: automatically sets base texture per chunk based
  on average height — configurable thresholds for sand/grass/rock/snow
- Uses Tanaris sand, Elwynn grass, Barrens rock, Dragonblight snow
- One-click to texture an entire procedurally generated terrain
- Adjustable height thresholds via drag floats
- Workflow: noise → smooth → clamp → auto-paint for instant biome
This commit is contained in:
Kelsi 2026-05-05 06:17:37 -07:00
parent 1ba1a50112
commit aa9a6a87a8
3 changed files with 59 additions and 0 deletions

View file

@ -551,6 +551,32 @@ void EditorUI::renderTexturePaintPanel(EditorApp& app) {
ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.0f), "Active: %s",
selectedTexture_.c_str());
// Auto-paint by height
if (ImGui::CollapsingHeader("Auto-Paint by Height")) {
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1),
"Sets base texture per chunk based on average height");
static float h1 = 90, h2 = 110, h3 = 140;
ImGui::DragFloat("Sand/Dirt max##ap", &h1, 1.0f);
ImGui::DragFloat("Grass max##ap", &h2, 1.0f);
ImGui::DragFloat("Rock max##ap", &h3, 1.0f);
ImGui::Text("Above %.0f: Snow", h3);
if (ImGui::Button("Apply Auto-Paint", ImVec2(-1, 0))) {
std::vector<TexturePainter::HeightBand> bands = {
{h1, "Tileset\\Tanaris\\TanarisSandBase01.blp"},
{h2, "Tileset\\Elwynn\\ElwynnGrassBase.blp"},
{h3, "Tileset\\Barrens\\BarrensRock01.blp"},
{99999.0f, "Tileset\\Expansion02\\Dragonblight\\DragonblightFreshSmoothSnowA.blp"}
};
app.getTexturePainter().autoPaintByHeight(bands);
// Force terrain refresh
auto mesh = app.getTerrainEditor().regenerateMesh();
// Mark all chunks dirty through a dummy edit
app.showToast("Auto-painted by height");
}
}
ImGui::Separator();
// Show textures on chunk under cursor
auto& brush = app.getTerrainEditor().brush();
if (brush.isActive()) {

View file

@ -165,6 +165,35 @@ std::vector<int> TexturePainter::paint(const glm::vec3& center, float radius,
return modified;
}
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;
}
}
}
}
std::vector<int> TexturePainter::erase(const glm::vec3& center, float radius,
float strength, float falloff) {
if (!terrain_ || activeTexture_.empty()) return {};

View file

@ -17,6 +17,10 @@ public:
const std::string& getActiveTexture() const { return activeTexture_; }
const std::vector<std::string>& getRecentTextures() const { return recentTextures_; }
// Auto-paint textures based on terrain height bands
struct HeightBand { float maxHeight; std::string texturePath; };
void autoPaintByHeight(const std::vector<HeightBand>& bands);
// 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);