diff --git a/tests/g3etest.cpp b/tests/g3etest.cpp index 20eb774..4875639 100644 --- a/tests/g3etest.cpp +++ b/tests/g3etest.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "../util/grid/grid3eigen.hpp" #include "../util/output/bmpwriter.hpp" @@ -54,6 +55,43 @@ struct stardefaults { bool enabled = true; }; +enum class NoiseType { + Perlin = 0, + Value = 1, + Fractal = 2, + Turbulence = 3, + Ridged = 4, + Billow = 5, + WhiteNoise = 6, + WorleyNoise = 7, + VoronoiNoise = 8, + CrystalNoise = 9, + DomainWarp = 10, + CurlNoise = 11 +}; + +struct NoisePreviewState { + int width = 512; + int height = 512; + NoiseType currentType = NoiseType::Perlin; + NoiseType currentSubType = NoiseType::Perlin; + + int seed = 1337; + float scale = 0.02f; + int octaves = 4; + float persistence = 0.5f; + float lacunarity = 2.0f; + float ridgeOffset = 1.0f; + float strength = 2.0f; + float substrength = 2.0f; + + // Visualization + float offset[2] = {0.0f, 0.0f}; // Panning + GLuint textureId = 0; + std::vector pixelBuffer; + bool needsUpdate = true; +}; + std::mutex PreviewMutex; GLuint textu = 0; bool textureInitialized = false; @@ -72,10 +110,135 @@ bool firstFrameMeasured = false; // Stats update timer std::chrono::steady_clock::time_point lastStatsUpdate; -const std::chrono::seconds STATS_UPDATE_INTERVAL(60); // Update stats once per minute +const std::chrono::seconds STATS_UPDATE_INTERVAL(60); std::string cachedStats; bool statsNeedUpdate = true; +// Helper to generate the 2D noise texture +void updateNoiseTexture(NoisePreviewState& state) { + glGenTextures(1, &state.textureId); + glBindTexture(GL_TEXTURE_2D, state.textureId); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + state.pixelBuffer.resize(state.width * state.height * 3); + + // Create a local noise generator with current seed + PNoise2 generator(state.seed); + + #pragma omp parallel for + for (int y = 0; y < state.height; ++y) { + for (int x = 0; x < state.width; ++x) { + float nx = (x + state.offset[0]) * state.scale; + float ny = (y + state.offset[1]) * state.scale; + Eigen::Vector2f point(nx, ny); + + float val = 0.0f; + + // Call specific PNoise2 method + switch (state.currentType) { + case NoiseType::Perlin: + val = generator.permute(point); // [-1, 1] + break; + case NoiseType::Value: + val = generator.valueNoise(point); // [-1, 1] + break; + case NoiseType::Fractal: + val = generator.fractalNoise(point, state.octaves, state.persistence, state.lacunarity); + break; + case NoiseType::Turbulence: + val = generator.turbulence(point, state.octaves); // [0, unbounded usually] + val = (val * 2.0f) - 1.0f; + break; + case NoiseType::Ridged: + val = generator.ridgedNoise(point, state.octaves, state.ridgeOffset); + // Ridged output can be large, scale down slightly for preview + val = (val * 0.5f) - 1.0f; + break; + case NoiseType::Billow: + val = generator.billowNoise(point, state.octaves); + val = (val * 2.0f) - 1.0f; + break; + case NoiseType::WhiteNoise: + val = generator.whiteNoise(point); + break; + case NoiseType::WorleyNoise: + val = generator.worleyNoise(point); + break; + case NoiseType::VoronoiNoise: + val = generator.voronoiNoise(point); + break; + case NoiseType::CrystalNoise: + val = generator.crystalNoise(point); + break; + case NoiseType::DomainWarp: + val = generator.domainWarp(point, state.strength); + break; + case NoiseType::CurlNoise: + Eigen::Vector2f flow = generator.curlNoise(point); + flow = point + (flow * state.strength); + switch (state.currentSubType) { + case NoiseType::Perlin: + val = generator.permute(flow); // [-1, 1] + break; + case NoiseType::Value: + val = generator.valueNoise(flow); // [-1, 1] + break; + case NoiseType::Fractal: + val = generator.fractalNoise(flow, state.octaves, state.persistence, state.lacunarity); + break; + case NoiseType::Turbulence: + val = generator.turbulence(flow, state.octaves); // [0, unbounded usually] + val = (val * 2.0f) - 1.0f; + break; + case NoiseType::Ridged: + val = generator.ridgedNoise(flow, state.octaves, state.ridgeOffset); + // Ridged output can be large, scale down slightly for preview + val = (val * 0.5f) - 1.0f; + break; + case NoiseType::Billow: + val = generator.billowNoise(flow, state.octaves); + val = (val * 2.0f) - 1.0f; + break; + case NoiseType::WhiteNoise: + val = generator.whiteNoise(flow); + break; + case NoiseType::WorleyNoise: + val = generator.worleyNoise(flow); + break; + case NoiseType::VoronoiNoise: + val = generator.voronoiNoise(flow); + break; + case NoiseType::CrystalNoise: + val = generator.crystalNoise(flow); + break; + case NoiseType::DomainWarp: + val = generator.domainWarp(flow, state.substrength); + break; + } + break; + } + + float norm = (val + 1.0f) * 0.5f; + norm = std::clamp(norm, 0.0f, 1.0f); + + uint8_t color = static_cast(norm * 255); + + int idx = (y * state.width + x) * 3; + state.pixelBuffer[idx] = color; + state.pixelBuffer[idx+1] = color; + state.pixelBuffer[idx+2] = color; + } + } + + glBindTexture(GL_TEXTURE_2D, state.textureId); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, state.width, state.height, + 0, GL_RGB, GL_UNSIGNED_BYTE, state.pixelBuffer.data()); + + state.needsUpdate = false; +} + void createSphere(const defaults& config, const spheredefaults& sconfig, Octree& grid) { if (!grid.empty()) grid.clear(); @@ -181,15 +344,13 @@ void livePreview(Octree& grid, defaults& config, const Camera& cam) { std::lock_guard lock(PreviewMutex); updatePreview = true; - // Measure render time auto renderStart = std::chrono::high_resolution_clock::now(); - + frame currentPreviewFrame = grid.fastRenderFrame(cam, config.outWidth, config.outHeight, frame::colormap::RGB); auto renderEnd = std::chrono::high_resolution_clock::now(); renderFrameTime = std::chrono::duration(renderEnd - renderStart).count(); - // Update FPS calculations if (!firstFrameMeasured) { renderFrameTimes.resize(FRAME_HISTORY_SIZE, renderFrameTime); firstFrameMeasured = true; @@ -198,7 +359,6 @@ void livePreview(Octree& grid, defaults& config, const Camera& cam) { renderFrameTimes[frameHistoryIndex] = renderFrameTime; frameHistoryIndex = (frameHistoryIndex + 1) % FRAME_HISTORY_SIZE; - // Calculate average frame time and FPS avgRenderFrameTime = 0.0; int validFrames = 0; for (int i = 0; i < FRAME_HISTORY_SIZE; i++) { @@ -217,7 +377,6 @@ void livePreview(Octree& grid, defaults& config, const Camera& cam) { updateStatsCache(grid); } - // Update texture if (textu == 0) { glGenTextures(1, &textu); } @@ -303,8 +462,6 @@ int main() { #endif ImGui_ImplOpenGL3_Init(glsl_version); - bool show_demo_window = true; - bool show_another_window = false; ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); defaults config; @@ -317,6 +474,10 @@ int main() { spheredefaults sphereConf; stardefaults starConf; + // Initialize Noise Preview State + NoisePreviewState noiseState; + updateNoiseTexture(noiseState); // Initial generation + sphereConf.centerX = ghalf; sphereConf.centerY = ghalf; sphereConf.centerZ = ghalf; @@ -343,17 +504,15 @@ int main() { float camspeed = 50; Camera cam(PointType(400, 400, 400), PointType(0,0,1), PointType(0,1,0), 80, camspeed); - // Keyboard state tracking std::map keyStates; bool mouseCaptured = false; double lastMouseX = 0, lastMouseY = 0; float deltaTime = 0.016f; - - // Initialize render frame times vector renderFrameTimes.resize(FRAME_HISTORY_SIZE, 0.0); lastStatsUpdate = std::chrono::steady_clock::now(); statsNeedUpdate = true; + bool worldPreview = false; if (grid.load("output/Treegrid.yggs")) { gridInitialized = true; @@ -423,7 +582,6 @@ int main() { camvZ = cam.direction[2]; camspeed = cam.movementSpeed; - // Start the Dear ImGui frame ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); @@ -438,16 +596,12 @@ int main() { sphereConf.centerY = pos[1]; sphereConf.centerZ = pos[2]; } - ImGui::DragFloat("Radius", &sphereConf.radius, 0.5f, 1.0f, 250.0f); - - // Replaced traditional voxel sizing with Point Count logic ImGui::DragInt("Point Count", &sphereConf.numPoints, 100, 100, 200000); ImGui::DragFloat("Density (Overlap)", &sphereConf.voxelSize, 0.05f, 0.1f, 5.0f); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Multiplies calculated point size. >1.0 ensures solid surface."); } - ImGui::ColorEdit3("Color", sphereConf.color); ImGui::Separator(); @@ -494,7 +648,9 @@ int main() { } { - ImGui::Begin("Preview"); + ImGui::Begin("Planet Preview"); + if (ImGui::Checkbox("update Preview", &worldPreview)) if (gridInitialized) livePreview(grid, config, cam); + if (gridInitialized && textureInitialized) { ImGui::Image((void*)(intptr_t)textu, ImVec2(config.outWidth, config.outHeight)); } else if (gridInitialized) { @@ -503,8 +659,6 @@ int main() { ImGui::Text("No grid generated"); } - ImGui::Separator(); - ImGui::Text("Render Performance:"); if (renderFPS > 0) { // Color code based on FPS @@ -525,23 +679,16 @@ int main() { // Show latest frame time ImGui::Text("Latest: %.1f ms", renderFrameTime * 1000.0); - } else { - ImGui::Text("No render data yet"); - } - - if (gridInitialized) { - // Show time since last update - auto now = std::chrono::steady_clock::now(); - auto timeSinceUpdate = std::chrono::duration_cast(now - lastStatsUpdate); - - if (!(timeSinceUpdate < STATS_UPDATE_INTERVAL)) { - updateStatsCache(grid); - } - - // Display cached stats - ImGui::TextUnformatted(cachedStats.c_str()); } + ImGui::Separator(); + ImGui::Text("Performance: %.1f FPS (%.1f ms)", renderFPS, avgRenderFrameTime * 1000.0); + + if (gridInitialized) { + auto now = std::chrono::steady_clock::now(); + if ((now - lastStatsUpdate) > STATS_UPDATE_INTERVAL) updateStatsCache(grid); + ImGui::TextUnformatted(cachedStats.c_str()); + } ImGui::End(); } @@ -702,8 +849,101 @@ int main() { ImGui::End(); } + + { + ImGui::Begin("2D Noise Lab"); + bool changed = false; + + const char* items[] = { "Perlin", "Value", "Fractal (Octave)", "Turbulence", "Ridged Multifractal", "Billow", "White", "Worley", "Voronoi", "Crystal", "Domain Warp", "Curl" }; + int currentItem = static_cast(noiseState.currentType); + if (ImGui::Combo("Method", ¤tItem, items, IM_ARRAYSIZE(items))) { + noiseState.currentType = static_cast(currentItem); + changed = true; + } + if (noiseState.currentType == NoiseType::CurlNoise) { + int currentsubitem = static_cast(noiseState.currentSubType); + if (ImGui::Combo("Sub Method", ¤tsubitem, items, IM_ARRAYSIZE(items))) { + noiseState.currentSubType = static_cast(currentsubitem); + changed = true; + } + } + + changed |= ImGui::InputInt("Seed", &noiseState.seed); + if (ImGui::Button("Randomize Seed")) { + noiseState.seed = rand(); + changed = true; + } + + ImGui::Separator(); + ImGui::Text("Base Parameters"); + changed |= ImGui::SliderFloat("Scale (Freq)", &noiseState.scale, 0.001f, 1.f, "%.4f"); + changed |= ImGui::DragFloat2("Offset", noiseState.offset, 1.0f); - if (gridInitialized) livePreview(grid, config, cam); + // Conditional parameters based on noise type + bool usesOctaves = (noiseState.currentType == NoiseType::Fractal || + noiseState.currentType == NoiseType::Turbulence || + noiseState.currentType == NoiseType::Ridged || + noiseState.currentType == NoiseType::Billow); + + if (usesOctaves) { + ImGui::Separator(); + ImGui::Text("Fractal Parameters"); + changed |= ImGui::SliderInt("Octaves", &noiseState.octaves, 1, 10); + + if (noiseState.currentType == NoiseType::Fractal) { + changed |= ImGui::SliderFloat("Persistence", &noiseState.persistence, 0.0f, 1.0f); + changed |= ImGui::SliderFloat("Lacunarity", &noiseState.lacunarity, 1.0f, 4.0f); + } + } + + if (noiseState.currentType == NoiseType::Ridged) { + ImGui::Separator(); + changed |= ImGui::SliderFloat("Ridge Offset", &noiseState.ridgeOffset, 0.0f, 2.0f); + } + + if (noiseState.currentType == NoiseType::DomainWarp || noiseState.currentType == NoiseType::CurlNoise) { + ImGui::Separator(); + changed |= ImGui::SliderFloat("Strength", &noiseState.strength, 0.1f, 4.0f); + } + + if (noiseState.currentType == NoiseType::CurlNoise) { + ImGui::Separator(); + ImGui::Text("Sub Parameters"); + bool usesSubOctaves = (noiseState.currentSubType == NoiseType::Fractal || + noiseState.currentSubType == NoiseType::Turbulence || + noiseState.currentSubType == NoiseType::Ridged || + noiseState.currentSubType == NoiseType::Billow); + + if (usesSubOctaves) { + ImGui::Separator(); + ImGui::Text("Fractal Parameters"); + changed |= ImGui::SliderInt("Octaves (sub)", &noiseState.octaves, 1, 10); + + if (noiseState.currentSubType == NoiseType::Fractal) { + changed |= ImGui::SliderFloat("Persistence (sub)", &noiseState.persistence, 0.0f, 1.0f); + changed |= ImGui::SliderFloat("Lacunarity (sub)", &noiseState.lacunarity, 1.0f, 4.0f); + } + } + + if (noiseState.currentSubType == NoiseType::Ridged) { + ImGui::Separator(); + changed |= ImGui::SliderFloat("Ridge Offset (sub)", &noiseState.ridgeOffset, 0.0f, 2.0f); + } + + if (noiseState.currentSubType == NoiseType::DomainWarp || noiseState.currentSubType == NoiseType::CurlNoise) { + ImGui::Separator(); + changed |= ImGui::SliderFloat("Strength (sub)", &noiseState.substrength, 0.1f, 4.0f); + } + } + ImGui::Separator(); + ImGui::Text("Preview (%dx%d)", noiseState.width, noiseState.height); + // Display the generated texture + + ImGui::Image((void*)(intptr_t)noiseState.textureId, ImVec2((float)noiseState.width, (float)noiseState.height)); + updateNoiseTexture(noiseState); + + ImGui::End(); + } ImGui::Render(); int display_w, display_h; @@ -729,6 +969,10 @@ int main() { glDeleteTextures(1, &textu); textu = 0; } + if (noiseState.textureId != 0) { + glDeleteTextures(1, &noiseState.textureId); + noiseState.textureId = 0; + } glfwTerminate(); FunctionTimer::printStats(FunctionTimer::Mode::ENHANCED); diff --git a/util/noise/pnoise2.hpp b/util/noise/pnoise2.hpp index 13c7971..26a79ad 100644 --- a/util/noise/pnoise2.hpp +++ b/util/noise/pnoise2.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "../../eigen/Eigen/Core" #include "../timing_decorator.hpp" @@ -173,6 +174,30 @@ private: return (permutation[(z + permutation[(y + permutation[x & 255]) & 255]) & 255] / 255.0f) * 2.0f - 1.0f; } + /// @brief Pseudo-random vector for Worley noise (2D) + Vector2f hashVector(const Vector2f& gridPoint) { + int x = (int)gridPoint.x() & 255; + int y = (int)gridPoint.y() & 255; + // Generate pseudo-random float [0,1] for x and y offsets + float hx = permutation[(x + permutation[y]) & 255] / 255.0f; + float hy = permutation[(y + permutation[(x + 1) & 255]) & 255] / 255.0f; + return Vector2f(hx, hy); + } + + /// @brief Pseudo-random vector for Worley noise (3D) + Vector3f hashVector(const Vector3f& gridPoint) { + int x = (int)gridPoint.x() & 255; + int y = (int)gridPoint.y() & 255; + int z = (int)gridPoint.z() & 255; + + int h_xy = permutation[(x + permutation[y]) & 255]; + float hx = permutation[(h_xy + z) & 255] / 255.0f; + float hy = permutation[(h_xy + permutation[(z + 1) & 255]) & 255] / 255.0f; + float hz = permutation[(permutation[(x+1)&255] + permutation[(y+1)&255] + z) & 255] / 255.0f; + + return Vector3f(hx, hy, hz); + } + public: /// @brief Default constructor with random seed /// @note Uses random_device for seed; different runs produce different noise @@ -192,7 +217,7 @@ public: /// @return Noise value in [-1,1] range /// @note Core 2D noise function; changes affect all 2D noise outputs float permute(const Vector2f& point) { - TIME_FUNCTION; + // TIME_FUNCTION; float x = point.x(); float y = point.y(); int X = (int)floor(x); @@ -231,7 +256,7 @@ public: /// @param point 3D coordinate /// @return Noise value in [-1,1] range float permute(const Vector3f& point) { - TIME_FUNCTION; + // TIME_FUNCTION; float x = point.x(); float y = point.y(); float z = point.z(); @@ -373,7 +398,7 @@ public: /// @return Vector4f containing RGBA noise values /// @note Each channel uses different offset; changes affect color patterns Vector4f permuteColor(const Vector3f& point) { - TIME_FUNCTION; + // TIME_FUNCTION; float noiseR = permute(point); float noiseG = permute(Vector3f(point + Vector3f(100.0f, 100.0f, 100.0f))); float noiseB = permute(Vector3f(point + Vector3f(200.0f, 200.0f, 200.0f))); @@ -548,6 +573,205 @@ public: return value; } + + /// @brief Pure White Noise + /// @param point Input coordinate + /// @return Random value [-1, 1] based solely on integer coordinate hashing + float whiteNoise(const Vector2f& point) { + return hash((int)floor(point.x()), (int)floor(point.y())); + } + + /// @brief Pure White Noise 3D + float whiteNoise(const Vector3f& point) { + return hash((int)floor(point.x()), (int)floor(point.y()), (int)floor(point.z())); + } + + /// @brief Worley (Cellular) Noise 2D + /// @param point Input coordinate + /// @return Distance to the nearest feature point [0, 1+] + /// @note Used for stone, water caustics, biological cells + float worleyNoise(const Vector2f& point) { + Vector2f p = Vector2f(floor(point.x()), floor(point.y())); + Vector2f f = Vector2f(point.x() - p.x(), point.y() - p.y()); + + float minDist = 1.0f; + + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + Vector2f neighbor(x, y); + // Get random point inside the neighbor cell + Vector2f pointInCell = hashVector(Vector2f(p + neighbor)); + + // Vector from current pixel to that point + Vector2f diff = neighbor + pointInCell - f; + + float dist = diff.norm(); + if (dist < minDist) minDist = dist; + } + } + return minDist; // Usually clamped or inverted for visuals + } + + /// @brief Worley Noise 3D + /// @param point Input coordinate + /// @return Distance to nearest feature point + float worleyNoise(const Vector3f& point) { + Vector3f p = Vector3f(floor(point.x()), floor(point.y()), floor(point.z())); + Vector3f f = Vector3f(point.x() - p.x(), point.y() - p.y(), point.z() - p.z()); + + float minDist = 1.0f; + + for (int z = -1; z <= 1; z++) { + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + Vector3f neighbor(x, y, z); + Vector3f pointInCell = hashVector(Vector3f(p + neighbor)); + Vector3f diff = neighbor + pointInCell - f; + float dist = diff.norm(); + if (dist < minDist) minDist = dist; + } + } + } + return minDist; + } + + /// @brief Voronoi Noise 2D (Cell ID) + /// @param point Input coordinate + /// @return Random hash value [-1, 1] unique to the closest cell + float voronoiNoise(const Vector2f& point) { + Vector2f p = Vector2f(floor(point.x()), floor(point.y())); + Vector2f f = Vector2f(point.x() - p.x(), point.y() - p.y()); + + float minDist = 100.0f; + Vector2f cellID = p; + + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + Vector2f neighbor(x, y); + Vector2f pointInCell = hashVector(Vector2f(p + neighbor)); + Vector2f diff = neighbor + pointInCell - f; + float dist = diff.squaredNorm(); // Faster than norm + if (dist < minDist) { + minDist = dist; + cellID = p + neighbor; + } + } + } + return hash((int)cellID.x(), (int)cellID.y()); + } + + /// @brief "Crystals" Noise (Variant of Worley) + /// @param point Input coordinate + /// @return F2 - F1 (Distance to 2nd closest - Distance to closest) + /// @note Creates cell-like borders, cracks, or crystal facets + float crystalNoise(const Vector2f& point) { + Vector2f p = Vector2f(floor(point.x()), floor(point.y())); + Vector2f f = Vector2f(point.x() - p.x(), point.y() - p.y()); + + float d1 = 10.0f; // Closest + float d2 = 10.0f; // 2nd Closest + + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + Vector2f neighbor(x, y); + Vector2f pointInCell = hashVector(Vector2f(p + neighbor)); + Vector2f diff = neighbor + pointInCell - f; + float dist = diff.norm(); + + if (dist < d1) { + d2 = d1; + d1 = dist; + } else if (dist < d2) { + d2 = dist; + } + } + } + return d2 - d1; + } + + /// @brief Domain Warping + /// @param point Input coordinate + /// @param strength Magnitude of the warp + /// @return Warped Perlin noise value + /// @note Calculates noise(p + noise(p)) for marble/fluid effects + float domainWarp(const Vector2f& point, float strength = 1.0f) { + Vector2f q( + permute(point), + permute(Vector2f(point + Vector2f(5.2f, 1.3f))) + ); + return permute(Vector2f(point + q * strength)); + } + + /// @brief 3D Domain Warping + float domainWarp(const Vector3f& point, float strength = 1.0f) { + Vector3f q( + permute(point), + permute(Vector3f(point + Vector3f(5.2f, 1.3f, 2.8f))), + permute(Vector3f(point + Vector3f(1.1f, 8.4f, 5.5f))) + ); + return permute(Vector3f(point + q * strength)); + } + + /// @brief Curl Noise 2D + /// @param point Input coordinate + /// @return Divergence-free vector field (useful for particle simulation) + /// @note Calculated via finite difference curl of a potential field + Vector2f curlNoise(const Vector2f& point) { + float e = 0.01f; // Epsilon + float n1 = permute(Vector2f(point + Vector2f(0, e))); + float n2 = permute(Vector2f(point + Vector2f(0, -e))); + float n3 = permute(Vector2f(point + Vector2f(e, 0))); + float n4 = permute(Vector2f(point + Vector2f(-e, 0))); + + float dx = (n3 - n4) / (2.0f * e); + float dy = (n1 - n2) / (2.0f * e); + + // Curl of scalar field in 2D is (d/dy, -d/dx) + return Vector2f(dy, -dx).normalized(); + } + + /// @brief Curl Noise 3D + /// @param point Input coordinate + /// @return Divergence-free vector field + /// @note Uses 3 offsets of Perlin noise as Vector Potential + Vector3f curlNoise(const Vector3f& point) { + float e = 0.01f; + + Vector3f dx(e, 0.0f, 0.0f); + Vector3f dy(0.0f, e, 0.0f); + Vector3f dz(0.0f, 0.0f, e); + + // We need a vector potential (3 uncorrelated noise values) + // We reuse permuteColor's logic but keep it local to avoid overhead + auto potential = [&](const Vector3f& p) -> Vector3f { + return Vector3f( + permute(p), + permute(Vector3f(p + Vector3f(123.4f, 129.1f, 827.0f))), + permute(Vector3f(p + Vector3f(492.5f, 991.2f, 351.4f))) + ); + }; + + Vector3f p_dx_p = potential(point + dx); + Vector3f p_dx_m = potential(point - dx); + Vector3f p_dy_p = potential(point + dy); + Vector3f p_dy_m = potential(point - dy); + Vector3f p_dz_p = potential(point + dz); + Vector3f p_dz_m = potential(point - dz); + + // Finite difference + float dFz_dy = (p_dy_p.z() - p_dy_m.z()) / (2.0f * e); + float dFy_dz = (p_dz_p.y() - p_dz_m.y()) / (2.0f * e); + float dFx_dz = (p_dz_p.x() - p_dz_m.x()) / (2.0f * e); + float dFz_dx = (p_dx_p.z() - p_dx_m.z()) / (2.0f * e); + float dFy_dx = (p_dx_p.y() - p_dx_m.y()) / (2.0f * e); + float dFx_dy = (p_dy_p.x() - p_dy_m.x()) / (2.0f * e); + + return Vector3f( + dFz_dy - dFy_dz, + dFx_dz - dFz_dx, + dFy_dx - dFx_dy + ).normalized(); + } }; #endif \ No newline at end of file