Added `do_graph` to VoxelToolLodTerrain

master
Marc Gilleron 2022-08-23 22:58:20 +01:00
parent f535e7c089
commit c7c14d2e17
19 changed files with 423 additions and 69 deletions

View File

@ -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.

View File

@ -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
-----------------------------

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -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<VoxelGeneratorGraph> 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<VoxelDataLodMap> 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<float> tls_in_sdf_full;
tls_in_sdf_full.resize(Vector3iUtil::get_volume(buffer.get_size()));
Span<float> in_sdf_full = to_span(tls_in_sdf_full);
get_unscaled_sdf(buffer, in_sdf_full);
static thread_local std::vector<float> tls_in_x;
static thread_local std::vector<float> tls_in_y;
static thread_local std::vector<float> 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<float> in_x = to_span(tls_in_x);
Span<float> in_y = to_span(tls_in_y);
Span<float> 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<float> 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));
}

View File

@ -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<VoxelMeshSDF> mesh_sdf, Transform3D transform, float isolevel, float sdf_scale);
void do_graph(Ref<VoxelGeneratorGraph> graph, Transform3D transform, Vector3 area_size);
protected:
uint64_t _get_voxel(Vector3i pos) const override;

View File

@ -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<float> x_cache(cache.x_cache, 0, cache.x_cache.size());
Span<float> y_cache(cache.y_cache, 0, cache.y_cache.size());
Span<float> z_cache(cache.z_cache, 0, cache.z_cache.size());
Span<float> x_cache = to_span(cache.x_cache);
Span<float> y_cache = to_span(cache.y_cache);
Span<float> z_cache = to_span(cache.z_cache);
Span<float> 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<float> in_x, Span<float> in_y, Span<float> 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<float> 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<float> in_x, Span<float> in_y, Span<float> in_z, Span<float> 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<const float> positions_x, Span<co
}
if (buffer_index == -1) {
// The graph does not define such output
for (unsigned int i = 0; i < out_values.size(); ++i) {
out_values[i] = defval;
}
@ -1148,6 +1190,12 @@ bool VoxelGeneratorGraph::try_get_output_port_address(ProgramGraph::PortLocation
return res;
}
int VoxelGeneratorGraph::get_sdf_output_port_address() const {
RWLockRead rlock(_runtime_lock);
ERR_FAIL_COND_V(_runtime == nullptr, -1);
return _runtime->sdf_output_buffer_index;
}
void VoxelGeneratorGraph::find_dependencies(uint32_t node_id, std::vector<uint32_t> &out_dependencies) const {
std::vector<uint32_t> dst;
dst.push_back(node_id);
@ -1205,6 +1253,7 @@ void VoxelGeneratorGraph::bake_sphere_bumpmap(Ref<Image> im, float ref_radius, f
std::vector<float> x_coords;
std::vector<float> y_coords;
std::vector<float> z_coords;
std::vector<float> in_sdf_cache;
Ref<Image> im;
const VoxelGraphRuntime &runtime;
VoxelGraphRuntime::State &state;
@ -1251,7 +1300,16 @@ void VoxelGeneratorGraph::bake_sphere_bumpmap(Ref<Image> 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<float> 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<Image> im, float ref_radius,
std::vector<float> sdf_values_p; // TODO Could be used at the same time to get bump?
std::vector<float> sdf_values_px;
std::vector<float> sdf_values_py;
std::vector<float> in_sdf_cache;
unsigned int sdf_buffer_index;
Ref<Image> im;
const VoxelGraphRuntime &runtime;
@ -1343,6 +1402,14 @@ void VoxelGeneratorGraph::bake_sphere_normalmap(Ref<Image> 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<float> 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<Image> 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<Image> 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<Image> 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<float> src_x;
std::vector<float> src_y;
std::vector<float> src_z;
std::vector<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(src_x, 0, src_x.size());
Span<float> sy(src_y, 0, src_y.size());
Span<float> sz(src_z, 0, src_z.size());
Span<float> 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);
}

View File

@ -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<float> in_x, Span<float> in_y, Span<float> in_z);
void generate_series(Span<float> in_x, Span<float> in_y, Span<float> in_z, Span<float> 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<const uint32_t> 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<uint32_t> &out_dependencies) const;
@ -306,6 +309,7 @@ private:
std::vector<float> x_cache;
std::vector<float> y_cache;
std::vector<float> z_cache;
std::vector<float> input_sdf_cache;
VoxelGraphRuntime::State state;
VoxelGraphRuntime::ExecutionMap optimized_execution_map;
};

View File

@ -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;
}

View File

@ -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);

View File

@ -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<float>(&position_f.x, 1), Span<float>(&position_f.y, 1), Span<float>(&position_f.z, 1),
false, execution_map);
Span<float>(&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<Buffer> buffers(state.buffers, 0, state.buffers.size());
Span<Buffer> 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<const uint8_t> read_params(Span<const uint16_t> operations, u
return params;
}
void VoxelGraphRuntime::generate_set(State &state, Span<float> in_x, Span<float> in_y, Span<float> 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<float> in_x, Span<float> in_y, Span<float> in_z,
Span<float> 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<Buffer> buffers, int a, Span<float> d) {
@ -390,6 +408,9 @@ void VoxelGraphRuntime::generate_set(State &state, Span<float> in_x, Span<float>
#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<float> in_x, Span<float>
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<const uint16_t> operations(_program.operations.data(), 0, _program.operations.size());
@ -480,10 +504,14 @@ void VoxelGraphRuntime::generate_set(State &state, Span<float> in_x, Span<float>
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<const uint16_t> operations(_program.operations.data(), 0, _program.operations.size());

View File

@ -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<float> in_x, Span<float> in_y, Span<float> in_z, bool skip_xz,
const ExecutionMap *execution_map) const;
void generate_set(State &state, Span<float> in_x, Span<float> in_y, Span<float> in_z, Span<float> 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<OutputInfo, MAX_OUTPUTS> 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) {

View File

@ -45,6 +45,7 @@ Span<const Vector3> get_positions_temporary(
return positions;
}
// TODO Use VoxelBufferInternal helper function
Span<float> decompress_sdf_to_temporary(VoxelBufferInternal &voxels) {
ZN_DSTACK();
const Vector3i bs = voxels.get_size();
@ -99,51 +100,6 @@ Span<float> decompress_sdf_to_temporary(VoxelBufferInternal &voxels) {
return sdf;
}
void store_sdf(VoxelBufferInternal &voxels, Span<float> 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<int8_t> 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<int16_t> 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<float> 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<double> 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();
}
}

View File

@ -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<float> 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<int8_t> 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<int16_t> 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<float> 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<double> 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<float> 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<int8_t> 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<int16_t> 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<float> 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<double> 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

View File

@ -520,6 +520,9 @@ inline void debug_check_texture_indices_packed_u16(const VoxelBufferInternal &vo
}
}
void get_unscaled_sdf(const VoxelBufferInternal &voxels, Span<float> sdf);
void scale_and_store_sdf(VoxelBufferInternal &voxels, Span<float> sdf);
} // namespace zylann::voxel
#endif // VOXEL_BUFFER_INTERNAL_H

View File

@ -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);

View File

@ -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) {

View File

@ -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 <typename F>
void write_box(const Box3i &p_voxel_box, unsigned int channel, F action) {

View File

@ -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