diff --git a/doc/source/changelog.md b/doc/source/changelog.md index 54abefc8..591306f3 100644 --- a/doc/source/changelog.md +++ b/doc/source/changelog.md @@ -58,6 +58,7 @@ Godot 4 is required from this version. - `VoxelLodTerrain`: added debug gizmos to see mesh updates - `VoxelToolLodTerrain`: added *experimental* `do_sphere_async`, an alternative version of `do_sphere` which defers the task on threads to reduce stutter if the affected area is big. - `VoxelToolLodTerrain`: added `stamp_sdf` function to place a baked mesh SDF on the terrain + - `VoxelToolLodTerrain`: added `do_graph` to run a custom brush based on `VoxelGeneratorGraph` in a specific area. An `InputSDF` node was added in order to support SDF modifications. - `VoxelMesherTransvoxel`: initial support for deep SDF sampling, to affine vertex positions at low levels of details (slow and limited proof of concept for now). - `VoxelMesherTransvoxel`: Variable LOD: regular and transition meshes are now combined in one single mesh per chunk. A shader is required to render it, but creates far less mesh resources and reduces the amount of draw calls. diff --git a/doc/source/generators.md b/doc/source/generators.md index 86ecbeb6..27a562bb 100644 --- a/doc/source/generators.md +++ b/doc/source/generators.md @@ -332,6 +332,24 @@ Custom generator See [Scripting](scripting.md) +Using `VoxelGeneratorGraph` as a brush +----------------------------------------- + +This feature is currently only supported in `VoxelLodTerrain` and smooth voxels. + +`VoxelTool` offers simple functions to modify smooth terrain with `do_sphere` for example, but it is also possible to define procedural custom brushes using `VoxelGeneratorGraph`. The same workflow applies to making such a graph, except it can accept an `InputSDF` node, so the signed distance field can be modified, not just generated. + +Example of additive `do_sphere` recreated with a graph: + +![Additive sphere brush graph](images/graph_sphere_brush.webp) + +A more complex flattening brush, which both subtracts matter in a sphere and adds matter in a hemisphere to form a ledge (here defaulting to a radius of 30 for better preview, but making unit-sized brushes may be easier to re-use): + +![Dual flattening brush](images/graph_flatten_brush.webp) + +One more detail to consider, is how big the original brush is. Usually voxel generators have no particular bounds, but it matters here because it will be used locally. For example if your make a spherical brush, you might use a `SdfSphere` node with radius `1`. Then, your original size will be `(2,2,2)`. You can then transform that brush (scale, rotate...) when using `do_sphere` at the desired position. + + VoxelGeneratorGraph nodes ----------------------------- diff --git a/doc/source/images/graph_flatten_brush.webp b/doc/source/images/graph_flatten_brush.webp new file mode 100644 index 00000000..05c09780 Binary files /dev/null and b/doc/source/images/graph_flatten_brush.webp differ diff --git a/doc/source/images/graph_sphere_brush.webp b/doc/source/images/graph_sphere_brush.webp new file mode 100644 index 00000000..3e8c1b66 Binary files /dev/null and b/doc/source/images/graph_sphere_brush.webp differ diff --git a/edition/voxel_tool_lod_terrain.cpp b/edition/voxel_tool_lod_terrain.cpp index 27d1612f..f6226867 100644 --- a/edition/voxel_tool_lod_terrain.cpp +++ b/edition/voxel_tool_lod_terrain.cpp @@ -1,8 +1,10 @@ #include "voxel_tool_lod_terrain.h" #include "../constants/voxel_string_names.h" +#include "../generators/graph/voxel_generator_graph.h" #include "../storage/voxel_buffer_gd.h" #include "../storage/voxel_data_grid.h" #include "../terrain/variable_lod/voxel_lod_terrain.h" +#include "../util/dstack.h" #include "../util/godot/mesh.h" #include "../util/island_finder.h" #include "../util/math/conv.h" @@ -760,6 +762,130 @@ void VoxelToolLodTerrain::stamp_sdf( _post_edit(voxel_box); } +// Runs the given graph in a bounding box in the terrain. +// The graph must have an SDF output and can also have an SDF input to read source voxels. +// The transform contains the position of the edit, its orientation and scale. +// Graph base size is the original size of the brush, as designed in the graph. It will be scaled using the transform. +void VoxelToolLodTerrain::do_graph(Ref graph, Transform3D transform, Vector3 graph_base_size) { + ZN_PROFILE_SCOPE(); + ZN_DSTACK(); + ERR_FAIL_COND(_terrain == nullptr); + + const Vector3 area_size = math::abs(transform.basis.xform(graph_base_size)); + + const Box3i box = Box3i::from_min_max( // + math::floor_to_int(transform.origin - 0.5 * area_size), + math::ceil_to_int(transform.origin + 0.5 * area_size)) + .padded(2) + .clipped(_terrain->get_voxel_bounds()); + + if (!is_area_editable(box)) { + ZN_PRINT_VERBOSE("Area not editable"); + return; + } + + std::shared_ptr data = _terrain->get_storage(); + ERR_FAIL_COND(data == nullptr); + VoxelDataLodMap::Lod &data_lod = data->lods[0]; + + preload_box(*data, box, _terrain->get_generator().ptr(), !_terrain->is_full_load_mode_enabled()); + + const unsigned int channel_index = VoxelBufferInternal::CHANNEL_SDF; + + VoxelBufferInternal buffer; + buffer.create(box.size); + _terrain->copy(box.pos, buffer, 1 << channel_index); + + buffer.decompress_channel(channel_index); + + // Convert input SDF + static thread_local std::vector tls_in_sdf_full; + tls_in_sdf_full.resize(Vector3iUtil::get_volume(buffer.get_size())); + Span in_sdf_full = to_span(tls_in_sdf_full); + get_unscaled_sdf(buffer, in_sdf_full); + + static thread_local std::vector tls_in_x; + static thread_local std::vector tls_in_y; + static thread_local std::vector tls_in_z; + const unsigned int deck_area = box.size.x * box.size.y; + tls_in_x.resize(deck_area); + tls_in_y.resize(deck_area); + tls_in_z.resize(deck_area); + Span in_x = to_span(tls_in_x); + Span in_y = to_span(tls_in_y); + Span in_z = to_span(tls_in_z); + + const Transform3D inv_transform = transform.affine_inverse(); + + const int output_sdf_buffer_index = graph->get_sdf_output_port_address(); + ZN_ASSERT_RETURN_MSG(output_sdf_buffer_index != -1, "The graph has no SDF output, cannot use it as a brush"); + + // The graph works at a fixed dimension, so if we scale the operation with the Transform3D then we have to also + // scale the distance field the graph is working at + const float graph_scale = transform.basis.get_scale().length(); + const float inv_graph_scale = 1.f / graph_scale; + + for (unsigned int i = 0; i < in_sdf_full.size(); ++i) { + in_sdf_full[i] *= inv_graph_scale; + } + + const float op_strength = get_sdf_strength(); + + { + ZN_PROFILE_SCOPE_NAMED("Slices"); + // For each deck of the box (doing this to reduce memory usage since the graph will allocate temporary buffers + // for each operation, which can be a lot depending on the complexity of the graph) + Vector3i pos; + const Vector3i endpos = box.pos + box.size; + for (pos.z = box.pos.z; pos.z < endpos.z; ++pos.z) { + // Set positions + for (unsigned int i = 0; i < deck_area; ++i) { + in_z[i] = pos.z; + } + { + unsigned int i = 0; + for (pos.x = box.pos.x; pos.x < endpos.x; ++pos.x) { + for (pos.y = box.pos.y; pos.y < endpos.y; ++pos.y) { + in_x[i] = pos.x; + in_y[i] = pos.y; + ++i; + } + } + } + + // Transform positions to be local to the graph + for (unsigned int i = 0; i < deck_area; ++i) { + Vector3 pos(in_x[i], in_y[i], in_z[i]); + pos = inv_transform.xform(pos); + in_x[i] = pos.x; + in_y[i] = pos.y; + in_z[i] = pos.z; + } + + // Get SDF input + Span in_sdf = in_sdf_full.sub(deck_area * (pos.z - box.pos.z), deck_area); + + // Run graph + graph->generate_series(in_x, in_y, in_z, in_sdf); + + // Read result + const VoxelGraphRuntime::State &state = VoxelGeneratorGraph::get_last_state_from_current_thread(); + const VoxelGraphRuntime::Buffer &graph_buffer = state.get_buffer(output_sdf_buffer_index); + + // Apply strength and graph scale. Input serves as output too, shouldn't overlap + for (unsigned int i = 0; i < in_sdf.size(); ++i) { + in_sdf[i] = Math::lerp(in_sdf[i], graph_buffer.data[i] * graph_scale, op_strength); + } + } + } + + scale_and_store_sdf(buffer, in_sdf_full); + + _terrain->paste(box.pos, buffer, 1 << channel_index); + + _post_edit(box); +} + void VoxelToolLodTerrain::_bind_methods() { ClassDB::bind_method(D_METHOD("set_raycast_binary_search_iterations", "iterations"), &VoxelToolLodTerrain::set_raycast_binary_search_iterations); @@ -772,6 +898,7 @@ void VoxelToolLodTerrain::_bind_methods() { ClassDB::bind_method(D_METHOD("do_sphere_async", "center", "radius"), &VoxelToolLodTerrain::do_sphere_async); ClassDB::bind_method( D_METHOD("stamp_sdf", "mesh_sdf", "transform", "isolevel", "sdf_scale"), &VoxelToolLodTerrain::stamp_sdf); + ClassDB::bind_method(D_METHOD("do_graph", "graph", "transform", "area_size"), &VoxelToolLodTerrain::do_graph); ClassDB::bind_method(D_METHOD("do_hemisphere", "center", "radius", "flat_direction", "smoothness"), &VoxelToolLodTerrain::do_hemisphere, DEFVAL(0.0)); } diff --git a/edition/voxel_tool_lod_terrain.h b/edition/voxel_tool_lod_terrain.h index 7595a3a0..a3512d43 100644 --- a/edition/voxel_tool_lod_terrain.h +++ b/edition/voxel_tool_lod_terrain.h @@ -10,6 +10,7 @@ namespace zylann::voxel { class VoxelLodTerrain; class VoxelDataMap; class VoxelMeshSDF; +class VoxelGeneratorGraph; class VoxelToolLodTerrain : public VoxelTool { GDCLASS(VoxelToolLodTerrain, VoxelTool) @@ -31,6 +32,7 @@ public: float get_voxel_f_interpolated(Vector3 position) const; Array separate_floating_chunks(AABB world_box, Node *parent_node); void stamp_sdf(Ref mesh_sdf, Transform3D transform, float isolevel, float sdf_scale); + void do_graph(Ref graph, Transform3D transform, Vector3 area_size); protected: uint64_t _get_voxel(Vector3i pos) const override; diff --git a/generators/graph/voxel_generator_graph.cpp b/generators/graph/voxel_generator_graph.cpp index 792d1d15..168a0ac9 100644 --- a/generators/graph/voxel_generator_graph.cpp +++ b/generators/graph/voxel_generator_graph.cpp @@ -716,10 +716,12 @@ VoxelGenerator::Result VoxelGeneratorGraph::generate_block(VoxelGenerator::Voxel cache.x_cache.resize(slice_buffer_size); cache.y_cache.resize(slice_buffer_size); cache.z_cache.resize(slice_buffer_size); + cache.input_sdf_cache.resize(slice_buffer_size); - Span x_cache(cache.x_cache, 0, cache.x_cache.size()); - Span y_cache(cache.y_cache, 0, cache.y_cache.size()); - Span z_cache(cache.z_cache, 0, cache.z_cache.size()); + Span x_cache = to_span(cache.x_cache); + Span y_cache = to_span(cache.y_cache); + Span z_cache = to_span(cache.z_cache); + Span input_sdf_cache = to_span(cache.input_sdf_cache); const float air_sdf = _debug_clipped_blocks ? -1.f : 1.f; const float matter_sdf = _debug_clipped_blocks ? 1.f : -1.f; @@ -734,6 +736,22 @@ VoxelGenerator::Result VoxelGeneratorGraph::generate_block(VoxelGenerator::Voxel bool all_sdf_is_air = (sdf_output_buffer_index != -1) && (type_output_buffer_index == -1); bool all_sdf_is_matter = all_sdf_is_air; + math::Interval sdf_input_range; + if (runtime.has_input(VoxelGeneratorGraph::NODE_INPUT_SDF)) { + ZN_PROFILE_SCOPE(); + // const Vector3i bs = out_buffer.get_size(); + // const float midv = out_buffer.get_voxel_f(bs / 2, VoxelBufferInternal::CHANNEL_SDF) / sdf_scale; + // const float maxd = 0.501f * math::length(to_vec3f(bs << input.lod)); + // sdf_input_range = math::Interval(midv - maxd, midv + maxd); + + get_unscaled_sdf(out_buffer, input_sdf_cache); + + sdf_input_range = math::Interval::from_single_value(input_sdf_cache[0]); + for (unsigned int i = 0; i < input_sdf_cache.size(); ++i) { + sdf_input_range.add_point(input_sdf_cache[i]); + } + } + // For each subdivision of the block for (int sz = 0; sz < bs.z; sz += section_size.z) { for (int sy = 0; sy < bs.y; sy += section_size.y) { @@ -746,7 +764,7 @@ VoxelGenerator::Result VoxelGeneratorGraph::generate_block(VoxelGenerator::Voxel const Vector3i gmax = origin + (rmax << input.lod); // Do a quick analysis of the area. We'll only compute voxels if necessary. - runtime.analyze_range(cache.state, gmin, gmax); + runtime.analyze_range(cache.state, gmin, gmax, sdf_input_range); bool sdf_is_air = true; if (sdf_output_buffer_index != -1) { @@ -837,7 +855,8 @@ VoxelGenerator::Result VoxelGeneratorGraph::generate_block(VoxelGenerator::Voxel y_cache.fill(gy); // Full query (unless using execution map) - runtime.generate_set(cache.state, x_cache, y_cache, z_cache, _use_xz_caching && ry != rmin.y, + runtime.generate_set(cache.state, x_cache, y_cache, z_cache, input_sdf_cache, + _use_xz_caching && ry != rmin.y, _use_optimized_execution_map ? &cache.optimized_execution_map : nullptr); if (sdf_output_buffer_index != -1) { @@ -1073,13 +1092,35 @@ bool VoxelGeneratorGraph::is_good() const { return _runtime != nullptr; } +// TODO Rename `generate_series` void VoxelGeneratorGraph::generate_set(Span in_x, Span in_y, Span in_z) { RWLockRead rlock(_runtime_lock); ERR_FAIL_COND(_runtime == nullptr); Cache &cache = _cache; VoxelGraphRuntime &runtime = _runtime->runtime; + + // Support graphs having an SDF input, give it default values + Span in_sdf; + if (runtime.has_input(NODE_INPUT_SDF)) { + cache.input_sdf_cache.resize(in_x.size()); + in_sdf = to_span(cache.input_sdf_cache); + in_sdf.fill(0.f); + } + runtime.prepare_state(cache.state, in_x.size(), false); - runtime.generate_set(cache.state, in_x, in_y, in_z, false, nullptr); + runtime.generate_set(cache.state, in_x, in_y, in_z, in_sdf, false, nullptr); + // Note, when generating SDF, we don't scale it because the return values are uncompressed floats. Scale only + // matters if we are storing it inside 16-bit or 8-bit VoxelBuffer. +} + +void VoxelGeneratorGraph::generate_series(Span in_x, Span in_y, Span in_z, Span in_sdf) { + RWLockRead rlock(_runtime_lock); + ERR_FAIL_COND(_runtime == nullptr); + Cache &cache = _cache; + VoxelGraphRuntime &runtime = _runtime->runtime; + + runtime.prepare_state(cache.state, in_x.size(), false); + runtime.generate_set(cache.state, in_x, in_y, in_z, in_sdf, false, nullptr); // Note, when generating SDF, we don't scale it because the return values are uncompressed floats. Scale only // matters if we are storing it inside 16-bit or 8-bit VoxelBuffer. } @@ -1112,6 +1153,7 @@ void VoxelGeneratorGraph::generate_series(Span positions_x, Spansdf_output_buffer_index; +} + void VoxelGeneratorGraph::find_dependencies(uint32_t node_id, std::vector &out_dependencies) const { std::vector dst; dst.push_back(node_id); @@ -1205,6 +1253,7 @@ void VoxelGeneratorGraph::bake_sphere_bumpmap(Ref im, float ref_radius, f std::vector x_coords; std::vector y_coords; std::vector z_coords; + std::vector in_sdf_cache; Ref im; const VoxelGraphRuntime &runtime; VoxelGraphRuntime::State &state; @@ -1251,7 +1300,16 @@ void VoxelGeneratorGraph::bake_sphere_bumpmap(Ref im, float ref_radius, f } } - runtime.generate_set(state, to_span(x_coords), to_span(y_coords), to_span(z_coords), false, nullptr); + // Support graphs having an SDF input, give it default values + Span in_sdf; + if (runtime.has_input(NODE_INPUT_SDF)) { + in_sdf_cache.resize(x_coords.size()); + in_sdf = to_span(in_sdf_cache); + in_sdf.fill(0.f); + } + + runtime.generate_set( + state, to_span(x_coords), to_span(y_coords), to_span(z_coords), in_sdf, false, nullptr); const VoxelGraphRuntime::Buffer &buffer = state.get_buffer(sdf_buffer_index); // Calculate final pixels @@ -1301,6 +1359,7 @@ void VoxelGeneratorGraph::bake_sphere_normalmap(Ref im, float ref_radius, std::vector sdf_values_p; // TODO Could be used at the same time to get bump? std::vector sdf_values_px; std::vector sdf_values_py; + std::vector in_sdf_cache; unsigned int sdf_buffer_index; Ref im; const VoxelGraphRuntime &runtime; @@ -1343,6 +1402,14 @@ void VoxelGeneratorGraph::bake_sphere_normalmap(Ref im, float ref_radius, const VoxelGraphRuntime::Buffer &sdf_buffer = state.get_buffer(sdf_buffer_index); + // Support graphs having an SDF input, give it default values + Span in_sdf; + if (runtime.has_input(NODE_INPUT_SDF)) { + in_sdf_cache.resize(x_coords.size()); + in_sdf = to_span(in_sdf_cache); + in_sdf.fill(0.f); + } + // TODO instead of using 3 separate queries, interleave triplets of positions into a single array? // Get heights @@ -1358,7 +1425,8 @@ void VoxelGeneratorGraph::bake_sphere_normalmap(Ref im, float ref_radius, } } // TODO Perform range analysis on the range of coordinates, it might still yield performance benefits - runtime.generate_set(state, to_span(x_coords), to_span(y_coords), to_span(z_coords), false, nullptr); + runtime.generate_set( + state, to_span(x_coords), to_span(y_coords), to_span(z_coords), in_sdf, false, nullptr); CRASH_COND(sdf_values_p.size() != sdf_buffer.size); memcpy(sdf_values_p.data(), sdf_buffer.data, sdf_values_p.size() * sizeof(float)); @@ -1374,7 +1442,8 @@ void VoxelGeneratorGraph::bake_sphere_normalmap(Ref im, float ref_radius, ++i; } } - runtime.generate_set(state, to_span(x_coords), to_span(y_coords), to_span(z_coords), false, nullptr); + runtime.generate_set( + state, to_span(x_coords), to_span(y_coords), to_span(z_coords), in_sdf, false, nullptr); CRASH_COND(sdf_values_px.size() != sdf_buffer.size); memcpy(sdf_values_px.data(), sdf_buffer.data, sdf_values_px.size() * sizeof(float)); @@ -1390,7 +1459,8 @@ void VoxelGeneratorGraph::bake_sphere_normalmap(Ref im, float ref_radius, ++i; } } - runtime.generate_set(state, to_span(x_coords), to_span(y_coords), to_span(z_coords), false, nullptr); + runtime.generate_set( + state, to_span(x_coords), to_span(y_coords), to_span(z_coords), in_sdf, false, nullptr); CRASH_COND(sdf_values_py.size() != sdf_buffer.size); memcpy(sdf_values_py.data(), sdf_buffer.data, sdf_values_py.size() * sizeof(float)); @@ -1483,7 +1553,7 @@ math::Interval VoxelGeneratorGraph::debug_analyze_range( const VoxelGraphRuntime &runtime = runtime_ptr->runtime; // Note, buffer size is irrelevant here, because range analysis doesn't use buffers runtime.prepare_state(cache.state, 1, false); - runtime.analyze_range(cache.state, min_pos, max_pos); + runtime.analyze_range(cache.state, min_pos, max_pos, math::Interval()); if (optimize_execution_map) { runtime.generate_optimized_execution_map(cache.state, cache.optimized_execution_map, true); } @@ -1777,12 +1847,15 @@ float VoxelGeneratorGraph::debug_measure_microseconds_per_voxel( std::vector src_x; std::vector src_y; std::vector src_z; + std::vector src_sdf; src_x.resize(cube_volume); src_y.resize(cube_volume); src_z.resize(cube_volume); + src_sdf.resize(cube_volume); Span sx(src_x, 0, src_x.size()); Span sy(src_y, 0, src_y.size()); Span sz(src_z, 0, src_z.size()); + Span ssdf(src_sdf, 0, src_sdf.size()); const bool per_node_profiling = node_profiling_info != nullptr; runtime.prepare_state(cache.state, sx.size(), per_node_profiling); @@ -1791,7 +1864,7 @@ float VoxelGeneratorGraph::debug_measure_microseconds_per_voxel( profiling_clock.restart(); for (uint32_t y = 0; y < cube_size; ++y) { - runtime.generate_set(cache.state, sx, sy, sz, false, nullptr); + runtime.generate_set(cache.state, sx, sy, sz, ssdf, false, nullptr); } total_elapsed_us += profiling_clock.restart(); @@ -2171,6 +2244,7 @@ void VoxelGeneratorGraph::_bind_methods() { BIND_ENUM_CONSTANT(NODE_EXPRESSION); BIND_ENUM_CONSTANT(NODE_POWI); BIND_ENUM_CONSTANT(NODE_POW); + BIND_ENUM_CONSTANT(NODE_INPUT_SDF); BIND_ENUM_CONSTANT(NODE_TYPE_COUNT); } diff --git a/generators/graph/voxel_generator_graph.h b/generators/graph/voxel_generator_graph.h index fd280beb..78b95f11 100644 --- a/generators/graph/voxel_generator_graph.h +++ b/generators/graph/voxel_generator_graph.h @@ -74,6 +74,7 @@ public: NODE_EXPRESSION, NODE_POWI, // pow(x, constant positive integer) NODE_POW, // pow(x, y) + NODE_INPUT_SDF, NODE_TYPE_COUNT }; @@ -190,12 +191,14 @@ public: bool is_good() const; void generate_set(Span in_x, Span in_y, Span in_z); + void generate_series(Span in_x, Span in_y, Span in_z, Span in_sdf); // Returns state from the last generator used in the current thread static const VoxelGraphRuntime::State &get_last_state_from_current_thread(); static Span get_last_execution_map_debug_from_current_thread(); bool try_get_output_port_address(ProgramGraph::PortLocation port, uint32_t &out_address) const; + int get_sdf_output_port_address() const; void find_dependencies(uint32_t node_id, std::vector &out_dependencies) const; @@ -306,6 +309,7 @@ private: std::vector x_cache; std::vector y_cache; std::vector z_cache; + std::vector input_sdf_cache; VoxelGraphRuntime::State state; VoxelGraphRuntime::ExecutionMap optimized_execution_map; }; diff --git a/generators/graph/voxel_graph_compiler.cpp b/generators/graph/voxel_graph_compiler.cpp index 58b1c07c..ee0457aa 100644 --- a/generators/graph/voxel_graph_compiler.cpp +++ b/generators/graph/voxel_graph_compiler.cpp @@ -755,6 +755,14 @@ VoxelGraphRuntime::CompilationResult VoxelGraphRuntime::_compile( dg_node.is_input = true; continue; + case VoxelGeneratorGraph::NODE_INPUT_SDF: + if (_program.sdf_input_address == -1) { + _program.sdf_input_address = mem.add_binding(); + } + _program.output_port_addresses[ProgramGraph::PortLocation{ node_id, 0 }] = _program.sdf_input_address; + dg_node.is_input = true; + continue; + case VoxelGeneratorGraph::NODE_SDF_PREVIEW: continue; } diff --git a/generators/graph/voxel_graph_node_db.cpp b/generators/graph/voxel_graph_node_db.cpp index 03d73415..0ee5049d 100644 --- a/generators/graph/voxel_graph_node_db.cpp +++ b/generators/graph/voxel_graph_node_db.cpp @@ -300,6 +300,12 @@ VoxelGraphNodeDB::VoxelGraphNodeDB() { t.category = CATEGORY_INPUT; t.outputs.push_back(Port("z")); } + { + NodeType &t = types[VoxelGeneratorGraph::NODE_INPUT_SDF]; + t.name = "InputSDF"; + t.category = CATEGORY_INPUT; + t.outputs.push_back(Port("sdf")); + } { NodeType &t = types[VoxelGeneratorGraph::NODE_OUTPUT_SDF]; t.name = "OutputSDF"; @@ -309,6 +315,7 @@ VoxelGraphNodeDB::VoxelGraphNodeDB() { t.process_buffer_func = [](ProcessBufferContext &ctx) { const VoxelGraphRuntime::Buffer &input = ctx.get_input(0); VoxelGraphRuntime::Buffer &out = ctx.get_output(0); + ZN_ASSERT(out.data != nullptr); memcpy(out.data, input.data, input.size * sizeof(float)); }; t.range_analysis_func = [](RangeAnalysisContext &ctx) { @@ -1256,7 +1263,7 @@ VoxelGraphNodeDB::VoxelGraphNodeDB() { t.inputs.push_back(Port("y", AUTO_CONNECT_Y)); t.inputs.push_back(Port("z", AUTO_CONNECT_Z)); // TODO Is it worth it making radius an input? - t.inputs.push_back(Port("radius")); + t.inputs.push_back(Port("radius", 1.f)); t.outputs.push_back(Port("sdf")); t.process_buffer_func = [](ProcessBufferContext &ctx) { const VoxelGraphRuntime::Buffer &x = ctx.get_input(0); diff --git a/generators/graph/voxel_graph_runtime.cpp b/generators/graph/voxel_graph_runtime.cpp index aa9d6c14..2662ba28 100644 --- a/generators/graph/voxel_graph_runtime.cpp +++ b/generators/graph/voxel_graph_runtime.cpp @@ -230,8 +230,9 @@ void VoxelGraphRuntime::generate_optimized_execution_map( } void VoxelGraphRuntime::generate_single(State &state, Vector3f position_f, const ExecutionMap *execution_map) const { + float sd_in = 0.f; generate_set(state, Span(&position_f.x, 1), Span(&position_f.y, 1), Span(&position_f.z, 1), - false, execution_map); + Span(&sd_in, 1), false, execution_map); } void VoxelGraphRuntime::prepare_state(State &state, unsigned int buffer_size, bool with_profiling) const { @@ -241,7 +242,7 @@ void VoxelGraphRuntime::prepare_state(State &state, unsigned int buffer_size, bo } // Note: this must be after we resize the vector - Span buffers(state.buffers, 0, state.buffers.size()); + Span buffers = to_span(state.buffers); state.buffer_size = buffer_size; for (auto it = _program.buffer_specs.cbegin(); it != _program.buffer_specs.cend(); ++it) { @@ -251,10 +252,11 @@ void VoxelGraphRuntime::prepare_state(State &state, unsigned int buffer_size, bo if (buffer_spec.is_binding) { if (buffer.is_binding) { // Forgot to unbind? - CRASH_COND(buffer.data != nullptr); + ZN_ASSERT(buffer.data == nullptr); } else if (buffer.data != nullptr) { // Deallocate this buffer if it wasnt a binding and contained something memfree(buffer.data); + buffer.data = nullptr; } } @@ -271,7 +273,7 @@ void VoxelGraphRuntime::prepare_state(State &state, unsigned int buffer_size, bo continue; } // We don't expect previous stuff in those buffers since we just created their slots - CRASH_COND(buffer.data != nullptr); + ZN_ASSERT(buffer.data == nullptr); // TODO Use pool? // New buffers get an up-to-date size, but must also comply with common capacity const unsigned int bs = math::max(state.buffer_capacity, buffer_size); @@ -367,8 +369,24 @@ static inline Span read_params(Span operations, u return params; } -void VoxelGraphRuntime::generate_set(State &state, Span in_x, Span in_y, Span in_z, bool skip_xz, - const ExecutionMap *execution_map) const { +bool VoxelGraphRuntime::has_input(unsigned int node_type) const { + switch (node_type) { + case VoxelGeneratorGraph::NODE_INPUT_X: + return _program.x_input_address != -1; + case VoxelGeneratorGraph::NODE_INPUT_Y: + return _program.y_input_address != -1; + case VoxelGeneratorGraph::NODE_INPUT_Z: + return _program.z_input_address != -1; + case VoxelGeneratorGraph::NODE_INPUT_SDF: + return _program.sdf_input_address != -1; + default: + ZN_PRINT_ERROR(format("Unknown input node type {}", node_type)); + return false; + } +} + +void VoxelGraphRuntime::generate_set(State &state, Span in_x, Span in_y, Span in_z, + Span in_sdf, bool skip_xz, const ExecutionMap *execution_map) const { // I don't like putting private helper functions in headers. struct L { static inline void bind_buffer(Span buffers, int a, Span d) { @@ -390,6 +408,9 @@ void VoxelGraphRuntime::generate_set(State &state, Span in_x, Span #ifdef DEBUG_ENABLED // Each array must have the same size CRASH_COND(!(in_x.size() == in_y.size() && in_y.size() == in_z.size())); + if (_program.sdf_input_address != -1) { + CRASH_COND(in_sdf.size() != in_x.size()); + } #endif #ifdef TOOLS_ENABLED @@ -423,6 +444,9 @@ void VoxelGraphRuntime::generate_set(State &state, Span in_x, Span if (_program.z_input_address != -1) { L::bind_buffer(buffers, _program.z_input_address, in_z); } + if (_program.sdf_input_address != -1) { + L::bind_buffer(buffers, _program.sdf_input_address, in_sdf); + } const Span operations(_program.operations.data(), 0, _program.operations.size()); @@ -480,10 +504,14 @@ void VoxelGraphRuntime::generate_set(State &state, Span in_x, Span if (_program.z_input_address != -1) { L::unbind_buffer(buffers, _program.z_input_address); } + if (_program.sdf_input_address != -1) { + L::unbind_buffer(buffers, _program.sdf_input_address); + } } // TODO Accept float bounds -void VoxelGraphRuntime::analyze_range(State &state, Vector3i min_pos, Vector3i max_pos) const { +void VoxelGraphRuntime::analyze_range( + State &state, Vector3i min_pos, Vector3i max_pos, math::Interval sdf_input_range) const { ZN_PROFILE_SCOPE(); #ifdef TOOLS_ENABLED @@ -503,6 +531,9 @@ void VoxelGraphRuntime::analyze_range(State &state, Vector3i min_pos, Vector3i m ranges[_program.x_input_address] = math::Interval(min_pos.x, max_pos.x); ranges[_program.y_input_address] = math::Interval(min_pos.y, max_pos.y); ranges[_program.z_input_address] = math::Interval(min_pos.z, max_pos.z); + if (_program.sdf_input_address != -1) { + ranges[_program.sdf_input_address] = sdf_input_range; + } const Span operations(_program.operations.data(), 0, _program.operations.size()); diff --git a/generators/graph/voxel_graph_runtime.h b/generators/graph/voxel_graph_runtime.h index 05304a42..8fd40c58 100644 --- a/generators/graph/voxel_graph_runtime.h +++ b/generators/graph/voxel_graph_runtime.h @@ -159,8 +159,10 @@ public: // TODO Evaluate needs for double-precision in VoxelGraphRuntime void generate_single(State &state, Vector3f position_f, const ExecutionMap *execution_map) const; - void generate_set(State &state, Span in_x, Span in_y, Span in_z, bool skip_xz, - const ExecutionMap *execution_map) const; + void generate_set(State &state, Span in_x, Span in_y, Span in_z, Span in_sdf, + bool skip_xz, const ExecutionMap *execution_map) const; + + bool has_input(unsigned int node_type) const; inline unsigned int get_output_count() const { return _program.outputs_count; @@ -173,7 +175,7 @@ public: // Analyzes a specific region of inputs to find out what ranges of outputs we can expect. // It can be used to speed up calls to `generate_set` thanks to execution mapping, // so that operations can be optimized out if they don't contribute to the result. - void analyze_range(State &state, Vector3i min_pos, Vector3i max_pos) const; + void analyze_range(State &state, Vector3i min_pos, Vector3i max_pos, math::Interval sdf_input_range) const; // Call this after `analyze_range` if you intend to actually generate a set or single values in the area. // This allows to use the execution map optimization, until you choose another area. @@ -391,6 +393,8 @@ private: int y_input_address = -1; // Address within the State's array of buffers where the Z input may be. int z_input_address = -1; + // Address within the State's array of buffers where the SDF input may be. + int sdf_input_address = -1; FixedArray outputs; unsigned int outputs_count = 0; @@ -425,6 +429,7 @@ private: x_input_address = -1; y_input_address = -1; z_input_address = -1; + sdf_input_address = -1; outputs_count = 0; compilation_result = CompilationResult(); for (auto it = heap_resources.begin(); it != heap_resources.end(); ++it) { diff --git a/storage/modifiers.cpp b/storage/modifiers.cpp index 8e6b6d33..76d41bc7 100644 --- a/storage/modifiers.cpp +++ b/storage/modifiers.cpp @@ -45,6 +45,7 @@ Span get_positions_temporary( return positions; } +// TODO Use VoxelBufferInternal helper function Span decompress_sdf_to_temporary(VoxelBufferInternal &voxels) { ZN_DSTACK(); const Vector3i bs = voxels.get_size(); @@ -99,51 +100,6 @@ Span decompress_sdf_to_temporary(VoxelBufferInternal &voxels) { return sdf; } -void store_sdf(VoxelBufferInternal &voxels, Span sdf) { - const VoxelBufferInternal::ChannelId channel = VoxelBufferInternal::CHANNEL_SDF; - const VoxelBufferInternal::Depth depth = voxels.get_channel_depth(channel); - - const float scale = VoxelBufferInternal::get_sdf_quantization_scale(depth); - for (unsigned int i = 0; i < sdf.size(); ++i) { - sdf[i] *= scale; - } - - switch (depth) { - case VoxelBufferInternal::DEPTH_8_BIT: { - Span raw; - ZN_ASSERT(voxels.get_channel_data(channel, raw)); - for (unsigned int i = 0; i < sdf.size(); ++i) { - raw[i] = snorm_to_s8(sdf[i]); - } - } break; - - case VoxelBufferInternal::DEPTH_16_BIT: { - Span raw; - ZN_ASSERT(voxels.get_channel_data(channel, raw)); - for (unsigned int i = 0; i < sdf.size(); ++i) { - raw[i] = snorm_to_s16(sdf[i]); - } - } break; - - case VoxelBufferInternal::DEPTH_32_BIT: { - Span raw; - ZN_ASSERT(voxels.get_channel_data(channel, raw)); - memcpy(raw.data(), sdf.data(), sizeof(float) * sdf.size()); - } break; - - case VoxelBufferInternal::DEPTH_64_BIT: { - Span raw; - ZN_ASSERT(voxels.get_channel_data(channel, raw)); - for (unsigned int i = 0; i < sdf.size(); ++i) { - raw[i] = sdf[i]; - } - } break; - - default: - ZN_CRASH(); - } -} - } //namespace //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -231,7 +187,7 @@ void VoxelModifierStack::apply(VoxelBufferInternal &voxels, AABB aabb) const { } if (any_intersection) { - store_sdf(voxels, ctx.sdf); + scale_and_store_sdf(voxels, ctx.sdf); voxels.compress_uniform_channels(); } } diff --git a/storage/voxel_buffer_internal.cpp b/storage/voxel_buffer_internal.cpp index 5b0f68e1..f6cab3d3 100644 --- a/storage/voxel_buffer_internal.cpp +++ b/storage/voxel_buffer_internal.cpp @@ -955,4 +955,108 @@ void VoxelBufferInternal::copy_voxel_metadata(const VoxelBufferInternal &src_buf _block_metadata.copy_from(src_buffer._block_metadata); } +void get_unscaled_sdf(const VoxelBufferInternal &voxels, Span sdf) { + ZN_PROFILE_SCOPE(); + ZN_DSTACK(); + const uint64_t volume = Vector3iUtil::get_volume(voxels.get_size()); + ZN_ASSERT_RETURN(volume == sdf.size()); + + const VoxelBufferInternal::ChannelId channel = VoxelBufferInternal::CHANNEL_SDF; + const VoxelBufferInternal::Depth depth = voxels.get_channel_depth(channel); + + const float inv_scale = 1.f / VoxelBufferInternal::get_sdf_quantization_scale(depth); + + if (voxels.get_channel_compression(channel) == VoxelBufferInternal::COMPRESSION_UNIFORM) { + const float uniform_value = inv_scale * voxels.get_voxel_f(0, 0, 0, channel); + sdf.fill(uniform_value); + return; + } + + switch (depth) { + case VoxelBufferInternal::DEPTH_8_BIT: { + Span raw; + ZN_ASSERT(voxels.get_channel_data(channel, raw)); + for (unsigned int i = 0; i < sdf.size(); ++i) { + sdf[i] = s8_to_snorm(raw[i]); + } + } break; + + case VoxelBufferInternal::DEPTH_16_BIT: { + Span raw; + ZN_ASSERT(voxels.get_channel_data(channel, raw)); + for (unsigned int i = 0; i < sdf.size(); ++i) { + sdf[i] = s16_to_snorm(raw[i]); + } + } break; + + case VoxelBufferInternal::DEPTH_32_BIT: { + Span raw; + ZN_ASSERT(voxels.get_channel_data(channel, raw)); + memcpy(sdf.data(), raw.data(), sizeof(float) * sdf.size()); + } break; + + case VoxelBufferInternal::DEPTH_64_BIT: { + Span raw; + ZN_ASSERT(voxels.get_channel_data(channel, raw)); + for (unsigned int i = 0; i < sdf.size(); ++i) { + sdf[i] = raw[i]; + } + } break; + + default: + ZN_CRASH(); + } + + for (unsigned int i = 0; i < sdf.size(); ++i) { + sdf[i] *= inv_scale; + } +} + +void scale_and_store_sdf(VoxelBufferInternal &voxels, Span sdf) { + ZN_PROFILE_SCOPE(); + const VoxelBufferInternal::ChannelId channel = VoxelBufferInternal::CHANNEL_SDF; + const VoxelBufferInternal::Depth depth = voxels.get_channel_depth(channel); + ZN_ASSERT_RETURN(voxels.get_channel_compression(channel) == VoxelBufferInternal::COMPRESSION_NONE); + + const float scale = VoxelBufferInternal::get_sdf_quantization_scale(depth); + for (unsigned int i = 0; i < sdf.size(); ++i) { + sdf[i] *= scale; + } + + switch (depth) { + case VoxelBufferInternal::DEPTH_8_BIT: { + Span raw; + ZN_ASSERT(voxels.get_channel_data(channel, raw)); + for (unsigned int i = 0; i < sdf.size(); ++i) { + raw[i] = snorm_to_s8(sdf[i]); + } + } break; + + case VoxelBufferInternal::DEPTH_16_BIT: { + Span raw; + ZN_ASSERT(voxels.get_channel_data(channel, raw)); + for (unsigned int i = 0; i < sdf.size(); ++i) { + raw[i] = snorm_to_s16(sdf[i]); + } + } break; + + case VoxelBufferInternal::DEPTH_32_BIT: { + Span raw; + ZN_ASSERT(voxels.get_channel_data(channel, raw)); + memcpy(raw.data(), sdf.data(), sizeof(float) * sdf.size()); + } break; + + case VoxelBufferInternal::DEPTH_64_BIT: { + Span raw; + ZN_ASSERT(voxels.get_channel_data(channel, raw)); + for (unsigned int i = 0; i < sdf.size(); ++i) { + raw[i] = sdf[i]; + } + } break; + + default: + ZN_CRASH(); + } +} + } // namespace zylann::voxel diff --git a/storage/voxel_buffer_internal.h b/storage/voxel_buffer_internal.h index aa12b2e0..59e208e8 100644 --- a/storage/voxel_buffer_internal.h +++ b/storage/voxel_buffer_internal.h @@ -520,6 +520,9 @@ inline void debug_check_texture_indices_packed_u16(const VoxelBufferInternal &vo } } +void get_unscaled_sdf(const VoxelBufferInternal &voxels, Span sdf); +void scale_and_store_sdf(VoxelBufferInternal &voxels, Span sdf); + } // namespace zylann::voxel #endif // VOXEL_BUFFER_INTERNAL_H diff --git a/storage/voxel_data_map.cpp b/storage/voxel_data_map.cpp index aa0f23fe..b8b185cd 100644 --- a/storage/voxel_data_map.cpp +++ b/storage/voxel_data_map.cpp @@ -392,6 +392,7 @@ void preload_box(VoxelDataLodMap &data, Box3i voxel_box, VoxelGenerator *generat task.voxels->create(block_size); // TODO Format? if (generator != nullptr) { + ZN_PROFILE_SCOPE_NAMED("Generate"); VoxelGenerator::VoxelQueryData q{ *task.voxels, task.block_pos * (data_block_size << task.lod_index), task.lod_index }; generator->generate_block(q); diff --git a/terrain/variable_lod/voxel_lod_terrain.cpp b/terrain/variable_lod/voxel_lod_terrain.cpp index a6098b05..01128503 100644 --- a/terrain/variable_lod/voxel_lod_terrain.cpp +++ b/terrain/variable_lod/voxel_lod_terrain.cpp @@ -664,6 +664,7 @@ bool VoxelLodTerrain::try_set_voxel_without_update(Vector3i pos, unsigned int ch } void VoxelLodTerrain::copy(Vector3i p_origin_voxels, VoxelBufferInternal &dst_buffer, uint8_t channels_mask) { + ZN_PROFILE_SCOPE(); const VoxelDataLodMap::Lod &data_lod0 = _data->lods[0]; VoxelModifierStack &modifiers = _data->modifiers; @@ -691,6 +692,13 @@ void VoxelLodTerrain::copy(Vector3i p_origin_voxels, VoxelBufferInternal &dst_bu } } +void VoxelLodTerrain::paste( + Vector3i p_origin_voxels, const VoxelBufferInternal &src_buffer, unsigned int channels_mask) { + ZN_PROFILE_SCOPE(); + VoxelDataLodMap::Lod &data_lod0 = _data->lods[0]; + data_lod0.map.paste(p_origin_voxels, src_buffer, channels_mask, false, 0, false); +} + // Marks intersecting blocks in the area as modified, updates LODs and schedules remeshing. // The provided box must be at LOD0 coordinates. void VoxelLodTerrain::post_edit_area(Box3i p_box) { diff --git a/terrain/variable_lod/voxel_lod_terrain.h b/terrain/variable_lod/voxel_lod_terrain.h index 4826ee64..277e4759 100644 --- a/terrain/variable_lod/voxel_lod_terrain.h +++ b/terrain/variable_lod/voxel_lod_terrain.h @@ -115,6 +115,7 @@ public: VoxelSingleValue get_voxel(Vector3i pos, unsigned int channel, VoxelSingleValue defval); bool try_set_voxel_without_update(Vector3i pos, unsigned int channel, uint64_t value); void copy(Vector3i p_origin_voxels, VoxelBufferInternal &dst_buffer, uint8_t channels_mask); + void paste(Vector3i p_origin_voxels, const VoxelBufferInternal &src_buffer, unsigned int channels_mask); template void write_box(const Box3i &p_voxel_box, unsigned int channel, F action) { diff --git a/util/math/vector3i.h b/util/math/vector3i.h index e58912a7..dce1aafd 100644 --- a/util/math/vector3i.h +++ b/util/math/vector3i.h @@ -346,6 +346,10 @@ inline Vector3i clamp(const Vector3i a, const Vector3i minv, const Vector3i maxv math::clamp(a.x, minv.x, maxv.x), math::clamp(a.y, minv.y, maxv.y), math::clamp(a.z, minv.z, maxv.z)); } +inline Vector3i abs(const Vector3i v) { + return Vector3i(Math::abs(v.x), Math::abs(v.y), Math::abs(v.z)); +} + } // namespace math } // namespace zylann