Skip to content

Commit

Permalink
Fix incorrect range analysis of FastNoiseLite with 3D OpenSimplex2S
Browse files Browse the repository at this point in the history
  • Loading branch information
Zylann committed Oct 22, 2024
1 parent ea70aef commit c3aa609
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 3 deletions.
4 changes: 3 additions & 1 deletion doc/source/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ Primarily developped with Godot 4.3.
- Fixed blocks were saved with incorrect LOD index when they get unloaded using Clipbox, leading to holes and mismatched terrain (#691)
- Fixed incorrect loading of chunks near terrain borders when viewers are far away from bounds, when using the Clipbox streaming system
- `VoxelTerrain`: edits and copies across fixed bounds no longer behave as if terrain generates beyond (was causing "walls" to appear).
- `VoxelGeneratorGraph`: fix wrong values when using `OutputWeight` with optimized execution map enabled, when weights are determined to be locally constant
- `VoxelGeneratorGraph`:
- Fixed wrong values when using `OutputWeight` with optimized execution map enabled, when weights are determined to be locally constant
- Fixed occasional holes in terrain when using `FastNoise3D` nodes with the `OpenSimplex2S` noise type
- `VoxelMesherTransvoxel`: revert texturing logic that attempted to prevent air voxels from contributing, but was lowering quality. It is now optional as an experimental property.
- `VoxelStreamSQLite`: Fixed "empty size" errors when loading areas with edited `VoxelInstancer` data

Expand Down
113 changes: 113 additions & 0 deletions generators/graph/voxel_generator_graph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1716,10 +1716,24 @@ VoxelSingleValue VoxelGeneratorGraph::generate_single(Vector3i position, unsigne
}
}

math::Interval get_range(const Span<const float> values) {
float minv = values[0];
float maxv = minv;
for (const float v : values) {
minv = math::min(v, minv);
maxv = math::max(v, maxv);
}
return math::Interval(minv, maxv);
}

// Note, this wrapper may not be used for main generation tasks.
// It is mostly used as a debug tool.
math::Interval VoxelGeneratorGraph::debug_analyze_range(Vector3i min_pos, Vector3i max_pos, bool optimize_execution_map)
const {
ZN_ASSERT_RETURN_V(max_pos.x >= min_pos.x, math::Interval());
ZN_ASSERT_RETURN_V(max_pos.y >= min_pos.y, math::Interval());
ZN_ASSERT_RETURN_V(max_pos.z >= min_pos.z, math::Interval());

std::shared_ptr<const Runtime> runtime_ptr;
{
RWLockRead rlock(_runtime_lock);
Expand All @@ -1743,6 +1757,105 @@ math::Interval VoxelGeneratorGraph::debug_analyze_range(Vector3i min_pos, Vector
if (optimize_execution_map) {
runtime.generate_optimized_execution_map(cache.state, cache.optimized_execution_map, true);
}

#if 0
const bool range_validation = true;
if (range_validation) {
// Actually calculate every value and check if range analysis bounds them.
// If it does not, then it's a bug.
// This is not 100% accurate. We would have to calculate every value at every point in space in the area which
// is not possible, we can only sample a finite number within a grid.

// TODO Make sure the graph is compiled in debug mode, otherwise buffer lookups won't match
// TODO Even with debug on, it appears buffers don't really match???

const Vector3i cube_size = max_pos - min_pos;
if (cube_size.x > 64 || cube_size.y > 64 || cube_size.z > 64) {
ZN_PRINT_ERROR("Area too big for range validation");
} else {
const int64_t cube_volume = Vector3iUtil::get_volume(cube_size);
ZN_ASSERT_RETURN_V(cube_volume >= 0, math::Interval());

StdVector<float> src_x;
StdVector<float> src_y;
StdVector<float> src_z;
StdVector<float> src_sdf;
src_x.resize(cube_volume);
src_y.resize(cube_volume);
src_z.resize(cube_volume);
src_sdf.resize(cube_volume);
Span<float> sx = to_span(src_x);
Span<float> sy = to_span(src_y);
Span<float> sz = to_span(src_z);
Span<float> ssdf = to_span(src_sdf);

{
unsigned int i = 0;
for (int z = min_pos.z; z < max_pos.z; ++z) {
for (int y = min_pos.y; y < max_pos.y; ++y) {
for (int x = min_pos.x; x < max_pos.x; ++x) {
src_x[i] = x;
src_y[i] = y;
src_z[i] = z;
++i;
}
}
}
}

QueryInputs inputs(*runtime_ptr, sx, sy, sz, ssdf);

runtime.prepare_state(cache.state, sx.size(), false);
runtime.generate_set(cache.state, inputs.get(), false, nullptr);

const pg::Runtime::State &state = get_last_state_from_current_thread();

_main_function->get_graph().for_each_node_const([this, &state](const ProgramGraph::Node &node) {
for (uint32_t output_index = 0; output_index < node.outputs.size(); ++output_index) {
// const ProgramGraph::Port &output_port = node.outputs[output_index];
const ProgramGraph::PortLocation loc{ node.id, output_index };

uint32_t address;
if (!try_get_output_port_address(loc, address)) {
continue;
}

const math::Interval analytic_range = state.get_range(address);

const pg::Runtime::Buffer &buffer = state.get_buffer(address);

if (buffer.data == nullptr) {
if (buffer.is_binding) {
// Not supported
continue;
}
ZN_PRINT_ERROR("Didn't expect nullptr in buffer data");
continue;
}

const Span<const float> values(buffer.data, buffer.size);
const math::Interval empiric_range = get_range(values);

if (!analytic_range.contains(empiric_range)) {
const String node_name = node.name;
const pg::NodeType &node_type = pg::NodeTypeDB::get_singleton().get_type(node.type_id);
ZN_PRINT_WARNING(
format("Empiric range not included in analytic range. A: {}, E: {}; {} "
"output {} instance {} \"{}\"",
analytic_range,
empiric_range,
node_type.name,
output_index,
node.id,
node_name)
);
}
}
});
}
}
#endif

// TODO Change return value to allow checking other outputs
if (runtime_ptr->sdf_output_buffer_index != -1) {
return cache.state.get_range(runtime_ptr->sdf_output_buffer_index);
Expand Down
2 changes: 2 additions & 0 deletions tests/tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "util/test_flat_map.h"
#include "util/test_island_finder.h"
#include "util/test_math_funcs.h"
#include "util/test_noise.h"
#include "util/test_slot_map.h"
#include "util/test_spatial_lock.h"
#include "util/test_string_funcs.h"
Expand Down Expand Up @@ -129,6 +130,7 @@ void run_voxel_tests() {
VOXEL_TEST(test_voxel_stream_sqlite_basic);
VOXEL_TEST(test_voxel_stream_sqlite_coordinate_format);
VOXEL_TEST(test_sdf_hemisphere);
VOXEL_TEST(test_fnl_range);

print_line("------------ Voxel tests end -------------");
}
Expand Down
51 changes: 51 additions & 0 deletions tests/util/test_noise.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#include "test_noise.h"
#include "../../util/noise/fast_noise_lite/fast_noise_lite.h"
#include "../../util/noise/fast_noise_lite/fast_noise_lite_range.h"
#include "../testing.h"

namespace zylann::tests {

void test_fnl_range() {
Ref<ZN_FastNoiseLite> noise;
noise.instantiate();
noise->set_noise_type(ZN_FastNoiseLite::TYPE_OPEN_SIMPLEX_2S);
noise->set_fractal_type(ZN_FastNoiseLite::FRACTAL_NONE);
// noise->set_fractal_type(ZN_FastNoiseLite::FRACTAL_FBM);
noise->set_fractal_octaves(1);
noise->set_fractal_lacunarity(2.0);
noise->set_fractal_gain(0.5);
noise->set_period(512);
noise->set_seed(0);

const Vector3i min_pos(-1074, 1838, 5587);
// const Vector3i max_pos(-1073, 1839, 5588);
const Vector3i max_pos(-1058, 1854, 5603);

const math::Interval x_range(min_pos.x, max_pos.x - 1);
const math::Interval y_range(min_pos.y, max_pos.y - 1);
const math::Interval z_range(min_pos.z, max_pos.z - 1);

const math::Interval analytic_range = get_fnl_range_3d(**noise, x_range, y_range, z_range);

math::Interval empiric_range;
bool first_value = true;
for (int z = min_pos.z; z < max_pos.z; ++z) {
for (int y = min_pos.y; y < max_pos.y; ++y) {
for (int x = min_pos.x; x < max_pos.x; ++x) {
const float n = noise->get_noise_3d(x, y, z);
if (first_value) {
empiric_range.min = n;
empiric_range.max = n;
first_value = false;
} else {
empiric_range.min = math::min<real_t>(empiric_range.min, n);
empiric_range.max = math::max<real_t>(empiric_range.max, n);
}
}
}
}

ZN_TEST_ASSERT(analytic_range.contains(empiric_range));
}

} // namespace zylann::tests
10 changes: 10 additions & 0 deletions tests/util/test_noise.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#ifndef ZN_TESTS_NOISE_H
#define ZN_TESTS_NOISE_H

namespace zylann::tests {

void test_fnl_range();

} // namespace zylann::tests

#endif // ZN_TESTS_NOISE_H
3 changes: 2 additions & 1 deletion util/math/vector3i.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ inline int64_t get_volume(const Vector3i &v) {
#ifdef DEBUG_ENABLED
ZN_ASSERT_RETURN_V(v.x >= 0 && v.y >= 0 && v.z >= 0, 0);
#endif
return v.x * v.y * v.z;
// TODO Overflow-checking multiplication in debug builds?
return static_cast<int64_t>(v.x) * static_cast<int64_t>(v.y) * static_cast<int64_t>(v.z);
}

inline unsigned int get_zxy_index(const Vector3i &v, const Vector3i area_size) {
Expand Down
2 changes: 1 addition & 1 deletion util/noise/fast_noise_lite/fast_noise_lite_range.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ Interval fnl_single_opensimplex2s(
) {
return get_noise_range_3d(
[&fn, seed](real_t x, real_t y, real_t z) { //
return fn.SingleOpenSimplex2(seed, x, y, z);
return fn.SingleOpenSimplex2S(seed, x, y, z);
},
// Max derivative found from empiric tests
p_x,
Expand Down

0 comments on commit c3aa609

Please sign in to comment.