From 3108fb0065a51dfc3548836ea16b287e92cd8881 Mon Sep 17 00:00:00 2001 From: skal Date: Tue, 3 Feb 2026 19:06:41 +0100 Subject: feat: side-quest - Perlin noise sky and ProcGenFunc error handling - Updated ProcGenFunc signature to return bool for error reporting. - Implemented gen_perlin (Fractional Brownian Motion) in procedural/generator.cc. - Added support for sky texture in Renderer3D and its shader. - Integrated Perlin noise sky texture in test_3d_render.cc. - Caught and handled memory/generation errors in AssetManager and TextureManager. - Assigned reference numbers to all remaining tasks in documentation. handoff(Gemini): Side-quest complete. ProcGenFunc now returns bool. Perlin noise added and used for sky in 3D test. Windows build remains stable. All tasks numbered. --- src/procedural/generator.cc | 175 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 149 insertions(+), 26 deletions(-) (limited to 'src/procedural/generator.cc') diff --git a/src/procedural/generator.cc b/src/procedural/generator.cc index 5ae83e4..0eb8b03 100644 --- a/src/procedural/generator.cc +++ b/src/procedural/generator.cc @@ -15,65 +15,204 @@ constexpr float mix(float a, float b, float t) { return (a * (1.0f - t) + b * t); } +// Perlin noise generator (Fractional Brownian Motion using value noise) +// Params[0]: Seed +// Params[1]: Frequency (Scale) +// Params[2]: Amplitude +// Params[3]: Amplitude decay +// Params[4]: Number of octaves +bool gen_perlin(uint8_t* buffer, int w, int h, const float* params, + int num_params) { + float base_freq = (num_params > 1) ? params[1] : 4.0f; + float base_amp = (num_params > 2) ? params[2] : 1.0f; + float amp_decay = (num_params > 3) ? params[3] : 0.5f; + int octaves = (num_params > 4) ? (int)params[4] : 4; + if (num_params > 0 && params[0] != 0) { + srand((unsigned int)params[0]); + } + + // Pre-allocate temporary float buffer for accumulating noise + float* accum = (float*)calloc((size_t)w * h, sizeof(float)); + if (!accum) return false; + + float current_freq = base_freq; + float current_amp = base_amp; + float total_amp = 0.0f; + + for (int o = 0; o < octaves; ++o) { + const int lattice_w = (int)ceil(current_freq) + 1; + const int lattice_h = (int)ceil(current_freq) + 1; + float* lattice = (float*)malloc((size_t)lattice_w * lattice_h * sizeof(float)); + if (!lattice) { + free(accum); + return false; + } + + for (int i = 0; i < lattice_w * lattice_h; ++i) { + lattice[i] = (float)rand() / RAND_MAX; + } + + const float scale_u = current_freq / w; + const float scale_v = current_freq / h; + + for (int y = 0; y < h; ++y) { + const float v = scale_v * y; + const int ly = (int)floor(v); + const int ly_next = (ly + 1); // No wrap here for better octaves + float fv = smooth(v - ly); + for (int x = 0; x < w; ++x) { + float u = scale_u * x; + const int lx = (int)floor(u); + const int lx_next = (lx + 1); + float fu = smooth(u - lx); + + // Simple tiling for lattice access + auto get_lat = [&](int ix, int iy) { + return lattice[(iy % lattice_h) * lattice_w + (ix % lattice_w)]; + }; + + float n00 = get_lat(lx, ly); + float n10 = get_lat(lx_next, ly); + float n01 = get_lat(lx, ly_next); + float n11 = get_lat(lx_next, ly_next); + + const float noise = mix(mix(n00, n10, fu), mix(n01, n11, fu), fv); + accum[y * w + x] += noise * current_amp; + } + } + + total_amp += current_amp; + current_freq *= 2.0f; + current_amp *= amp_decay; + free(lattice); + } + + // Normalize and write to RGBA buffer + for (int i = 0; i < w * h; ++i) { + float val = accum[i] / total_amp; + uint8_t uval = (uint8_t)(fminf(fmaxf(val, 0.0f), 1.0f) * 255.0f); + buffer[4 * i + 0] = uval; + buffer[4 * i + 1] = uval; + buffer[4 * i + 2] = uval; + buffer[4 * i + 3] = 255; + } + + free(accum); + return true; +} + // Simple smooth noise generator (Value Noise-ish) + // Params[0]: Seed + // Params[1]: Frequency (Scale) -void gen_noise(uint8_t* buffer, int w, int h, const float* params, + +bool gen_noise(uint8_t* buffer, int w, int h, const float* params, + int num_params) { + float freq = (num_params > 1) ? params[1] : 4.0f; + if (num_params > 0 && params[0] != 0) { + srand((unsigned int)params[0]); + } + + // Create a small lattice of random values + const int lattice_w = (int)ceil(freq); + const int lattice_h = (int)ceil(freq); - float* lattice = - (float*)malloc((size_t)lattice_w * lattice_h * sizeof(float)); - if (!lattice) - return; + + float* lattice = (float*)malloc((size_t)lattice_w * lattice_h * sizeof(float)); + + if (!lattice) return false; + + for (int i = 0; i < lattice_w * lattice_h; ++i) { + lattice[i] = (float)rand() / RAND_MAX; + } + const float scale_u = 1.f * (lattice_w - 1) / w; + const float scale_v = 1.f * (lattice_h - 1) / h; + + for (int y = 0; y < h; ++y) { + const float v = scale_v * y; + const int ly = (int)floor(v); + const int ly_next = (ly + 1) % lattice_h; // Wrap + const float* const n0 = &lattice[ly * lattice_w]; + const float* const n1 = &lattice[ly_next * lattice_w]; + float fv = smooth(v - ly); + uint8_t* const dst = &buffer[y * w * 4]; + for (int x = 0; x < w; ++x) { + float u = scale_u * x; + const int lx = (int)floor(u); + const int lx_next = (lx + 1) % lattice_w; + float fu = smooth(u - lx); + + float n00 = n0[lx]; + float n10 = n0[lx_next]; + float n01 = n1[lx]; + float n11 = n1[lx_next]; + + const float noise = mix(mix(n00, n10, fu), mix(n01, n11, fu), fv); + + const uint8_t val = (uint8_t)(noise * 255.0f); + dst[4 * x + 0] = val; // R + dst[4 * x + 1] = val; // G + dst[4 * x + 2] = val; // B + dst[4 * x + 3] = 255; // A + } + } + free(lattice); + + return true; + } + + // Simple grid generator // Params[0]: Grid Size (pixels) // Params[1]: Line Thickness (pixels) -void gen_grid(uint8_t* buffer, int w, int h, const float* params, +bool gen_grid(uint8_t* buffer, int w, int h, const float* params, int num_params) { int grid_size = (num_params > 0) ? (int)params[0] : 32; int thickness = (num_params > 1) ? (int)params[1] : 2; @@ -95,13 +234,14 @@ void gen_grid(uint8_t* buffer, int w, int h, const float* params, buffer[idx + 3] = 255; } } + return true; } -void make_periodic(uint8_t* buffer, int w, int h, const float* params, +bool make_periodic(uint8_t* buffer, int w, int h, const float* params, int num_params) { float ratio = (num_params > 0) ? params[0] : 0.1f; if (ratio <= 0.0f) - return; + return true; if (ratio > 0.5f) ratio = 0.5f; @@ -124,24 +264,6 @@ void make_periodic(uint8_t* buffer, int w, int h, const float* params, buffer[idx_dst + c] = (uint8_t)mix(v_src, v_dst, t); } } - // Copy left edge back to right edge to ensure perfect pixel match? - // Actually, texture wrapping usually means buffer[w] ~ buffer[0]. - // buffer[w-1] should neighbor buffer[0]. - // With above logic: buffer[0] is blend(buffer[w-bx], buffer[0], 0) = - // buffer[w-bx]. So buffer[0] looks like the pixel at w-bx. This effectively - // shrinks the texture content by bx? A bit hacky but works for noise. To be - // seamless at w-1 -> 0: buffer[w-1] is original. buffer[0] matches - // buffer[w-bx]. There is still a jump at w-1 -> 0 if buffer[w-1] != - // buffer[w-bx-1]? - // - // Improved logic: Blend BOTH sides to the average? - // Let's modify the right side too. - for (int x = 0; x < bx; ++x) { - // We want buffer[w-bx+x] to blend towards buffer[x] (which is now - // modified? No, original) This is getting complicated. The simple "mix - // right side into left side" works if the texture frequency is high - // enough. Let's just stick to the simple one requested. - } } // Y pass @@ -158,6 +280,7 @@ void make_periodic(uint8_t* buffer, int w, int h, const float* params, } } } + return true; } } // namespace procedural -- cgit v1.2.3