feat(editor): procedural noise generator, sky presets, viewport lighting

- Noise Generator in Sculpt panel: applies procedural value noise
  with configurable frequency, amplitude, octaves, and seed
  to create hills/valleys across entire tile instantly
- Sky/Lighting presets: View > Sky menu with Day (blue sky, high sun),
  Dusk (orange, low sun), Night (dark blue, moonlight)
- Viewport clear color and light direction now configurable at runtime
- Noise uses smoothstep interpolation with octave fractal layering
This commit is contained in:
Kelsi 2026-05-05 04:40:37 -07:00
parent f5fe9a0101
commit 42749e9b58
7 changed files with 108 additions and 2 deletions

View file

@ -124,7 +124,9 @@ void EditorApp::run() {
rpInfo.renderArea.extent = vkCtx->getSwapchainExtent();
VkClearValue clearValues[4]{};
clearValues[0].color = {{0.15f, 0.15f, 0.2f, 1.0f}};
float cr, cg, cb;
viewport_.getClearColor(cr, cg, cb);
clearValues[0].color = {{cr, cg, cb, 1.0f}};
clearValues[1].depthStencil = {1.0f, 0};
rpInfo.clearValueCount = 2;
rpInfo.pClearValues = clearValues;
@ -594,6 +596,23 @@ void EditorApp::requestQuit() {
window_->setShouldClose(true);
}
void EditorApp::setSkyPreset(int preset) {
switch (preset) {
case 0: // Day
viewport_.setClearColor(0.4f, 0.6f, 0.9f);
viewport_.setLightDir(glm::normalize(glm::vec3(0.5f, -1.0f, 0.8f)));
break;
case 1: // Dusk
viewport_.setClearColor(0.6f, 0.3f, 0.2f);
viewport_.setLightDir(glm::normalize(glm::vec3(0.8f, -0.3f, 0.1f)));
break;
case 2: // Night
viewport_.setClearColor(0.05f, 0.05f, 0.12f);
viewport_.setLightDir(glm::normalize(glm::vec3(0.2f, -0.5f, 0.8f)));
break;
}
}
void EditorApp::startGizmoMode(TransformMode mode) {
auto& giz = viewport_.getGizmo();
giz.setMode(mode);

View file

@ -69,6 +69,7 @@ public:
void startGizmoMode(TransformMode mode);
void setGizmoAxis(TransformAxis axis);
void setSkyPreset(int preset); // 0=day, 1=dusk, 2=night
void snapSelectedToGround();
// Multi-tile support

View file

@ -101,6 +101,13 @@ void EditorUI::renderMenuBar(EditorApp& app) {
if (ImGui::MenuItem("Wireframe", "F3", &wf)) app.setWireframe(wf);
if (ImGui::MenuItem("Reset Camera")) app.resetCamera();
ImGui::Separator();
if (ImGui::BeginMenu("Sky / Lighting")) {
if (ImGui::MenuItem("Day")) app.setSkyPreset(0);
if (ImGui::MenuItem("Dusk")) app.setSkyPreset(1);
if (ImGui::MenuItem("Night")) app.setSkyPreset(2);
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::MenuItem("Save Bookmark", "F5")) app.saveBookmark("");
auto& bmarks = app.getBookmarks();
if (!bmarks.empty() && ImGui::BeginMenu("Load Bookmark")) {
@ -230,6 +237,24 @@ void EditorUI::renderBrushPanel(EditorApp& app) {
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Set target height from cursor position");
}
ImGui::Separator();
if (ImGui::CollapsingHeader("Noise Generator")) {
static float noiseFreq = 0.005f;
static float noiseAmp = 20.0f;
static int noiseOctaves = 4;
static int noiseSeed = 42;
ImGui::SliderFloat("Frequency", &noiseFreq, 0.001f, 0.05f, "%.4f");
ImGui::SliderFloat("Amplitude", &noiseAmp, 1.0f, 200.0f, "%.0f");
ImGui::SliderInt("Octaves", &noiseOctaves, 1, 8);
ImGui::InputInt("Seed", &noiseSeed);
if (ImGui::Button("Apply Noise", ImVec2(-1, 0))) {
app.getTerrainEditor().applyNoise(noiseFreq, noiseAmp, noiseOctaves,
static_cast<uint32_t>(noiseSeed));
}
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1),
"Adds procedural hills/valleys to entire tile");
}
ImGui::Separator();
ImGui::Text("Terrain Holes (cave entrances):");
auto& brush = app.getTerrainEditor().brush();

View file

@ -568,7 +568,7 @@ void EditorViewport::updatePerFrameUBO() {
data.view = camera_->getViewMatrix();
data.projection = camera_->getProjectionMatrix();
data.lightSpaceMatrix = glm::mat4(1.0f);
data.lightDir = glm::vec4(glm::normalize(glm::vec3(0.5f, -1.0f, 0.3f)), 0.0f);
data.lightDir = glm::vec4(lightDir_, 0.0f);
data.lightColor = glm::vec4(1.0f, 0.95f, 0.85f, 0.0f);
data.ambientColor = glm::vec4(0.3f, 0.3f, 0.35f, 0.0f);
data.viewPos = glm::vec4(camera_->getPosition(), 0.0f);

View file

@ -55,6 +55,11 @@ public:
void setWireframe(bool enabled);
bool isWireframe() const { return wireframe_; }
void setClearColor(float r, float g, float b) { clearR_=r; clearG_=g; clearB_=b; }
void getClearColor(float& r, float& g, float& b) const { r=clearR_; g=clearG_; b=clearB_; }
void setLightDir(const glm::vec3& d) { lightDir_ = d; }
glm::vec3 getLightDir() const { return lightDir_; }
rendering::TerrainRenderer* getTerrainRenderer() { return terrainRenderer_.get(); }
private:
@ -85,6 +90,8 @@ private:
VkSampler shadowSampler_ = VK_NULL_HANDLE;
bool wireframe_ = false;
float clearR_ = 0.15f, clearG_ = 0.15f, clearB_ = 0.2f;
glm::vec3 lightDir_ = glm::normalize(glm::vec3(0.5f, -1.0f, 0.3f));
// Ghost preview state
std::string ghostModelPath_;

View file

@ -586,6 +586,57 @@ void TerrainEditor::removeWater(const glm::vec3& center, float radius) {
}
}
void TerrainEditor::applyNoise(float frequency, float amplitude, int octaves, uint32_t seed) {
if (!terrain_) return;
// Simple value noise with octaves
auto hash2d = [](int x, int y, uint32_t s) -> float {
uint32_t h = static_cast<uint32_t>(x * 374761393 + y * 668265263 + s * 1274126177);
h = (h ^ (h >> 13)) * 1274126177;
h = h ^ (h >> 16);
return static_cast<float>(h & 0xFFFF) / 65535.0f * 2.0f - 1.0f;
};
auto smoothNoise = [&](float fx, float fy, uint32_t s) -> float {
int ix = static_cast<int>(std::floor(fx));
int iy = static_cast<int>(std::floor(fy));
float fracX = fx - ix;
float fracY = fy - iy;
// Smoothstep
fracX = fracX * fracX * (3.0f - 2.0f * fracX);
fracY = fracY * fracY * (3.0f - 2.0f * fracY);
float v00 = hash2d(ix, iy, s);
float v10 = hash2d(ix + 1, iy, s);
float v01 = hash2d(ix, iy + 1, s);
float v11 = hash2d(ix + 1, iy + 1, s);
float i0 = v00 + (v10 - v00) * fracX;
float i1 = v01 + (v11 - v01) * fracX;
return i0 + (i1 - i0) * fracY;
};
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++) {
glm::vec3 wpos = chunkVertexWorldPos(ci, v);
float total = 0.0f;
float amp = amplitude;
float freq = frequency;
for (int o = 0; o < octaves; o++) {
total += smoothNoise(wpos.x * freq, wpos.y * freq, seed + o * 97) * amp;
freq *= 2.0f;
amp *= 0.5f;
}
chunk.heightMap.heights[v] += total;
}
dirtyChunks_.push_back(ci);
}
dirty_ = true;
}
void TerrainEditor::punchHole(const glm::vec3& center, float radius) {
if (!terrain_) return;
auto affected = getAffectedChunks(center, radius);

View file

@ -51,6 +51,9 @@ public:
// Recalculate normals for modified chunks (improves lighting after sculpt)
void recalcNormals(const std::vector<int>& chunkIndices);
// Noise generator: applies procedural height noise to the terrain
void applyNoise(float frequency, float amplitude, int octaves, uint32_t seed);
// Water editing
void setWaterLevel(const glm::vec3& center, float radius, float waterHeight, uint16_t liquidType = 0);
void removeWater(const glm::vec3& center, float radius);