Added `do_hemisphere` to terrain tools

master
Marc Gilleron 2022-07-16 17:44:55 +01:00
parent c6372c4ae2
commit b7d5ff20a8
11 changed files with 333 additions and 115 deletions

View File

@ -22,6 +22,7 @@ Godot 4 is required from this version.
- Added `VoxelMeshSDF` to bake SDF from meshes, which can be used in voxel sculpting.
- Mesh resources are now fully built on threads with the Godot Vulkan renderer
- Editor: terrain bounds are now shown in the inspector as min/max instead of position/size
- Added `do_hemisphere` to `VoxelToolTerrain` and `VoxelToolLodTerrain`, which can be used as flattening brush
- `VoxelGeneratorGraph`: added support for outputting to the TYPE channel, allowing use with `VoxelMesherBlocky`
- `VoxelGeneratorGraph`: editor: unconnected inputs show their default value directly on the node
- `VoxelGeneratorGraph`: editor: allow to change the axes on preview nodes 3D slices
@ -42,9 +43,9 @@ Godot 4 is required from this version.
- `VoxelLodTerrain`: Editor: added option to show octree nodes in editor
- `VoxelLodTerrain`: Editor: added option to show octree grid in editor, now off by default
- `VoxelLodTerrain`: Added option to run a major part of the process logic into another thread
- `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 debug gizmos to see mesh updates
- `VoxelInstancer`: Allow to dump VoxelInstancer as scene for debug inspection
- `VoxelInstancer`: Editor: instance chunks are shown when the node is selected
- `VoxelInstanceLibraryMultiMeshItem`: Support setting up mesh LODs from a scene with name `LODx` suffixes

View File

@ -2,9 +2,11 @@
#define VOXEL_EDITION_FUNCS_H
#include "../storage/funcs.h"
#include "../storage/voxel_data_grid.h"
#include "../util/fixed_array.h"
#include "../util/math/conv.h"
#include "../util/math/sdf.h"
#include "../util/profiling.h"
#include <core/math/transform_3d.h>
@ -169,13 +171,71 @@ struct SdfSet {
}
};
template <typename Value_T, typename Shape_T>
struct BlockySetOperation {
Shape_T shape;
Value_T value;
inline Value_T operator()(Vector3i pos, Value_T src) const {
return shape.is_inside(Vector3(pos)) ? value : src;
}
};
inline Box3i get_sdf_sphere_box(Vector3 center, real_t radius) {
return Box3i::from_min_max( //
math::floor_to_int(center - Vector3(radius, radius, radius)),
math::ceil_to_int(center + Vector3(radius, radius, radius)))
// That padding is for SDF to have some margin
.padded(2);
}
struct SdfSphere {
Vector3 center;
real_t radius;
real_t scale;
real_t sdf_scale;
inline real_t operator()(Vector3 pos) const {
return scale * zylann::math::sdf_sphere(pos, center, radius);
return sdf_scale * math::sdf_sphere(pos, center, radius);
}
inline bool is_inside(Vector3 pos) const {
// Faster than the true SDF, we avoid a square root
return center.distance_squared_to(pos) < radius * radius;
}
inline const char *name() const {
return "SdfSphere";
}
inline Box3i get_box() {
return get_sdf_sphere_box(center, radius);
}
};
struct SdfHemisphere {
Vector3 center;
Vector3 flat_direction;
real_t plane_d;
real_t radius;
real_t sdf_scale;
real_t smoothness;
inline real_t operator()(Vector3 pos) const {
return sdf_scale *
math::sdf_smooth_subtract( //
math::sdf_sphere(pos, center, radius), //
math::sdf_plane(pos, flat_direction, plane_d), smoothness);
}
inline bool is_inside(Vector3 pos) const {
return (*this)(pos) < 0;
}
inline const char *name() const {
return "SdfHemisphere";
}
inline Box3i get_box() {
return get_sdf_sphere_box(center, radius);
}
};
@ -196,6 +256,10 @@ struct SdfBufferShape {
}
return interpolate_trilinear(buffer, buffer_size, lpos) * sdf_scale - isolevel;
}
inline const char *name() const {
return "SdfBufferShape";
}
};
struct TextureParams {
@ -204,6 +268,7 @@ struct TextureParams {
unsigned int index = 0;
};
// Optimized for spheres
struct TextureBlendSphereOp {
Vector3 center;
float radius;
@ -219,6 +284,7 @@ struct TextureBlendSphereOp {
inline void operator()(Vector3i pos, uint16_t &indices, uint16_t &weights) const {
const float distance_squared = Vector3(pos).distance_squared_to(center);
// Avoiding square root on the hot path
if (distance_squared < radius_squared) {
const float distance_from_radius = radius - Math::sqrt(distance_squared);
const float target_weight =
@ -228,6 +294,139 @@ struct TextureBlendSphereOp {
}
};
template <typename Shape_T>
struct TextureBlendOp {
Shape_T shape;
TextureParams texture_params;
inline void operator()(Vector3i pos, uint16_t &indices, uint16_t &weights) const {
const float sd = shape(pos);
if (sd <= 0) {
// TODO We don't know the full size of the shape so sharpness may be adjusted
const float target_weight = texture_params.opacity * math::clamp(-sd * texture_params.sharpness, 0.f, 1.f);
blend_texture_packed_u16(texture_params.index, target_weight, indices, weights);
}
}
};
enum Mode { //
MODE_ADD,
MODE_REMOVE,
MODE_SET,
MODE_TEXTURE_PAINT
};
// This one is implemented manually for a fast-path in texture paint
// TODO Find a nicer way to do this without copypasta
struct DoSphere {
SdfSphere shape;
Mode mode;
VoxelDataGrid blocks;
Box3i box;
TextureParams texture_params;
VoxelBufferInternal::ChannelId channel;
uint32_t blocky_value;
void operator()() {
ZN_PROFILE_SCOPE();
if (channel = VoxelBufferInternal::CHANNEL_SDF) {
switch (mode) {
case MODE_ADD: {
// TODO Support other depths, format should be accessible from the volume
SdfOperation16bit<SdfUnion, SdfSphere> op;
op.shape = shape;
blocks.write_box(box, VoxelBufferInternal::CHANNEL_SDF, op);
} break;
case MODE_REMOVE: {
SdfOperation16bit<SdfSubtract, SdfSphere> op;
op.shape = shape;
blocks.write_box(box, VoxelBufferInternal::CHANNEL_SDF, op);
} break;
case MODE_SET: {
SdfOperation16bit<SdfSet, SdfSphere> op;
op.shape = shape;
blocks.write_box(box, VoxelBufferInternal::CHANNEL_SDF, op);
} break;
case MODE_TEXTURE_PAINT: {
blocks.write_box_2(box, VoxelBufferInternal::CHANNEL_INDICES, VoxelBufferInternal::CHANNEL_WEIGHTS,
TextureBlendSphereOp(shape.center, shape.radius, texture_params));
} break;
default:
ERR_PRINT("Unknown mode");
break;
}
} else {
BlockySetOperation<uint32_t, SdfSphere> op;
op.shape = shape;
op.value = blocky_value;
blocks.write_box(box, channel, op);
}
}
};
template <typename Shape_T>
struct DoShape {
Shape_T shape;
Mode mode;
VoxelDataGrid blocks;
Box3i box;
VoxelBufferInternal::ChannelId channel;
TextureParams texture_params;
uint32_t blocky_value;
void operator()() {
ZN_PROFILE_SCOPE();
if (channel = VoxelBufferInternal::CHANNEL_SDF) {
switch (mode) {
case MODE_ADD: {
// TODO Support other depths, format should be accessible from the volume. Or separate encoding?
SdfOperation16bit<SdfUnion, Shape_T> op;
op.shape = shape;
blocks.write_box(box, VoxelBufferInternal::CHANNEL_SDF, op);
} break;
case MODE_REMOVE: {
SdfOperation16bit<SdfSubtract, Shape_T> op;
op.shape = shape;
blocks.write_box(box, VoxelBufferInternal::CHANNEL_SDF, op);
} break;
case MODE_SET: {
SdfOperation16bit<SdfSet, Shape_T> op;
op.shape = shape;
blocks.write_box(box, VoxelBufferInternal::CHANNEL_SDF, op);
} break;
case MODE_TEXTURE_PAINT: {
TextureBlendOp<Shape_T> op;
op.shape = shape;
op.texture_params = texture_params;
blocks.write_box_2(
box, VoxelBufferInternal::CHANNEL_INDICES, VoxelBufferInternal::CHANNEL_WEIGHTS, op);
} break;
default:
ERR_PRINT("Unknown mode");
break;
}
} else {
BlockySetOperation<uint32_t, Shape_T> op;
op.shape = shape;
op.value = blocky_value;
blocks.write_box(box, channel, op);
}
}
};
typedef DoShape<SdfHemisphere> DoHemisphere;
}; // namespace zylann::voxel::ops
#endif // VOXEL_EDITION_FUNCS_H

View File

@ -172,6 +172,10 @@ inline float sdf_blend(float src_value, float dst_value, VoxelTool::Mode mode) {
}
} // namespace
// The following are default legacy implementations. They may be slower than specialized ones, so they can often be
// defined in subclasses of VoxelTool. Ideally, a function may be exposed on the base class only if it has an optimal
// definition in all specialized classes.
void VoxelTool::do_sphere(Vector3 center, float radius) {
ZN_PROFILE_SCOPE();

View File

@ -21,11 +21,11 @@ namespace zylann::voxel {
class VoxelTool : public RefCounted {
GDCLASS(VoxelTool, RefCounted)
public:
enum Mode { //
MODE_ADD,
MODE_REMOVE,
MODE_SET,
MODE_TEXTURE_PAINT
enum Mode {
MODE_ADD = ops::MODE_ADD,
MODE_REMOVE = ops::MODE_REMOVE,
MODE_SET = ops::MODE_SET,
MODE_TEXTURE_PAINT = ops::MODE_TEXTURE_PAINT
};
VoxelTool();

View File

@ -94,7 +94,7 @@ float approximate_distance_to_isosurface_binary_search(
Ref<VoxelRaycastResult> VoxelToolLodTerrain::raycast(
Vector3 pos, Vector3 dir, float max_distance, uint32_t collision_mask) {
// TODO Transform input if the terrain is rotated
// TODO Implement broad-phase on blocks to minimize locking and increase performance
// TODO Optimization: implement broad-phase on blocks to minimize locking and increase performance
// TODO Implement reverse raycast? (going from inside ground to air, could be useful for undigging)
struct RaycastPredicate {
@ -166,72 +166,19 @@ Ref<VoxelRaycastResult> VoxelToolLodTerrain::raycast(
return res;
}
namespace ops {
struct DoSphere {
Vector3 center;
float radius;
VoxelTool::Mode mode;
VoxelDataGrid blocks;
float sdf_scale;
Box3i box;
TextureParams texture_params;
void operator()() {
ZN_PROFILE_SCOPE();
switch (mode) {
case VoxelTool::MODE_ADD: {
// TODO Support other depths, format should be accessible from the volume
SdfOperation16bit<SdfUnion, SdfSphere> op;
op.shape.center = center;
op.shape.radius = radius;
op.shape.scale = sdf_scale;
blocks.write_box(box, VoxelBufferInternal::CHANNEL_SDF, op);
} break;
case VoxelTool::MODE_REMOVE: {
SdfOperation16bit<SdfSubtract, SdfSphere> op;
op.shape.center = center;
op.shape.radius = radius;
op.shape.scale = sdf_scale;
blocks.write_box(box, VoxelBufferInternal::CHANNEL_SDF, op);
} break;
case VoxelTool::MODE_SET: {
SdfOperation16bit<SdfSet, SdfSphere> op;
op.shape.center = center;
op.shape.radius = radius;
op.shape.scale = sdf_scale;
blocks.write_box(box, VoxelBufferInternal::CHANNEL_SDF, op);
} break;
case VoxelTool::MODE_TEXTURE_PAINT: {
blocks.write_box_2(box, VoxelBufferInternal::CHANNEL_INDICES, VoxelBufferInternal::CHANNEL_WEIGHTS,
TextureBlendSphereOp{ center, radius, texture_params });
} break;
default:
ERR_PRINT("Unknown mode");
break;
}
}
};
} //namespace ops
void VoxelToolLodTerrain::do_sphere(Vector3 center, float radius) {
ZN_PROFILE_SCOPE();
ERR_FAIL_COND(_terrain == nullptr);
const Box3i box = Box3i::from_min_max( //
math::floor_to_int(center - Vector3(radius, radius, radius)),
math::ceil_to_int(center + Vector3(radius, radius, radius)))
// That padding is for SDF to have some margin
.padded(2)
.clipped(_terrain->get_voxel_bounds());
ops::DoSphere op;
op.shape.center = center;
op.shape.radius = radius;
op.shape.sdf_scale = get_sdf_scale();
op.box = op.shape.get_box().clipped(_terrain->get_voxel_bounds());
op.mode = ops::Mode(get_mode());
op.texture_params = _texture_params;
if (!is_area_editable(box)) {
if (!is_area_editable(op.box)) {
ZN_PRINT_VERBOSE("Area not editable");
return;
}
@ -240,22 +187,48 @@ void VoxelToolLodTerrain::do_sphere(Vector3 center, float radius) {
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());
preload_box(*data, op.box, _terrain->get_generator().ptr(), !_terrain->is_full_load_mode_enabled());
ops::DoSphere op;
op.box = box;
op.center = center;
op.mode = get_mode();
op.radius = radius;
op.sdf_scale = get_sdf_scale();
op.texture_params = _texture_params;
{
RWLockRead rlock(data_lod.map_lock);
op.blocks.reference_area(data_lod.map, box);
op.blocks.reference_area(data_lod.map, op.box);
op();
}
_post_edit(box);
_post_edit(op.box);
}
void VoxelToolLodTerrain::do_hemisphere(Vector3 center, float radius, Vector3 flat_direction, float smoothness) {
ZN_PROFILE_SCOPE();
ERR_FAIL_COND(_terrain == nullptr);
ops::DoHemisphere op;
op.shape.center = center;
op.shape.flat_direction = flat_direction;
op.shape.plane_d = flat_direction.dot(center);
op.shape.radius = radius;
op.shape.sdf_scale = get_sdf_scale();
op.box = op.shape.get_box().clipped(_terrain->get_voxel_bounds());
op.mode = ops::Mode(get_mode());
if (!is_area_editable(op.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, op.box, _terrain->get_generator().ptr(), !_terrain->is_full_load_mode_enabled());
{
RWLockRead rlock(data_lod.map_lock);
op.blocks.reference_area(data_lod.map, op.box);
op();
}
_post_edit(op.box);
}
template <typename Op_T>
@ -297,11 +270,15 @@ private:
void VoxelToolLodTerrain::do_sphere_async(Vector3 center, float radius) {
ERR_FAIL_COND(_terrain == nullptr);
const Box3i box = Box3i(math::floor_to_int(center) - Vector3iUtil::create(Math::floor(radius)),
Vector3iUtil::create(Math::ceil(radius) * 2))
.clipped(_terrain->get_voxel_bounds());
ops::DoSphere op;
op.shape.center = center;
op.shape.radius = radius;
op.shape.sdf_scale = get_sdf_scale();
op.box = op.shape.get_box().clipped(_terrain->get_voxel_bounds());
op.mode = ops::Mode(get_mode());
op.texture_params = _texture_params;
if (!is_area_editable(box)) {
if (!is_area_editable(op.box)) {
ZN_PRINT_VERBOSE("Area not editable");
return;
}
@ -309,17 +286,6 @@ void VoxelToolLodTerrain::do_sphere_async(Vector3 center, float radius) {
std::shared_ptr<VoxelDataLodMap> data = _terrain->get_storage();
ERR_FAIL_COND(data == nullptr);
ops::DoSphere op;
op.box = box;
op.center = center;
op.mode = get_mode();
op.radius = radius;
op.sdf_scale = get_sdf_scale();
op.texture_params = _texture_params;
// TODO How do I use unique_ptr with Godot's memnew/memdelete instead?
// (without having to mention it everywhere I pass this around)
VoxelToolAsyncEdit<ops::DoSphere> *task = memnew(VoxelToolAsyncEdit<ops::DoSphere>(op, data));
_terrain->push_async_edit(task, op.box, task->get_tracker());
}
@ -809,6 +775,8 @@ 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_hemisphere", "center", "radius", "flat_direction", "smoothness"),
&VoxelToolLodTerrain::do_hemisphere, DEFVAL(0.0));
}
} // namespace zylann::voxel

View File

@ -19,17 +19,15 @@ public:
bool is_area_editable(const Box3i &box) const override;
Ref<VoxelRaycastResult> raycast(Vector3 pos, Vector3 dir, float max_distance, uint32_t collision_mask) override;
int get_raycast_binary_search_iterations() const;
void set_raycast_binary_search_iterations(int iterations);
void do_sphere(Vector3 center, float radius) override;
void do_sphere_async(Vector3 center, float radius);
void copy(Vector3i pos, Ref<gd::VoxelBuffer> dst, uint8_t channels_mask) const override;
// Specialized API
int get_raycast_binary_search_iterations() const;
void set_raycast_binary_search_iterations(int iterations);
void do_sphere_async(Vector3 center, float radius);
void do_hemisphere(Vector3 center, float radius, Vector3 flat_direction, float smoothness);
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);

View File

@ -159,27 +159,58 @@ void VoxelToolTerrain::paste(
}
void VoxelToolTerrain::do_sphere(Vector3 center, float radius) {
ZN_PROFILE_SCOPE();
ERR_FAIL_COND(_terrain == nullptr);
if (_mode != MODE_TEXTURE_PAINT) {
VoxelTool::do_sphere(center, radius);
return;
}
ops::DoSphere op;
op.shape.center = center;
op.shape.radius = radius;
op.shape.sdf_scale = get_sdf_scale();
op.box = op.shape.get_box().clipped(_terrain->get_bounds());
op.mode = ops::Mode(get_mode());
op.texture_params = _texture_params;
op.blocky_value = _value;
ZN_PROFILE_SCOPE();
const Box3i box(math::floor_to_int(center) - Vector3iUtil::create(Math::floor(radius)),
Vector3iUtil::create(Math::ceil(radius) * 2));
if (!is_area_editable(box)) {
if (!is_area_editable(op.box)) {
ZN_PRINT_VERBOSE("Area not editable");
return;
}
_terrain->get_storage().write_box_2(box, VoxelBufferInternal::CHANNEL_INDICES, VoxelBufferInternal::CHANNEL_WEIGHTS,
ops::TextureBlendSphereOp{ center, radius, _texture_params });
VoxelDataMap &data = _terrain->get_storage();
_post_edit(box);
op.blocks.reference_area(data, op.box);
op();
_post_edit(op.box);
}
void VoxelToolTerrain::do_hemisphere(Vector3 center, float radius, Vector3 flat_direction, float smoothness) {
ZN_PROFILE_SCOPE();
ERR_FAIL_COND(_terrain == nullptr);
ops::DoHemisphere op;
op.shape.center = center;
op.shape.radius = radius;
op.shape.flat_direction = flat_direction;
op.shape.plane_d = flat_direction.dot(center);
op.shape.smoothness = smoothness;
op.shape.sdf_scale = get_sdf_scale();
op.box = op.shape.get_box().clipped(_terrain->get_bounds());
op.mode = ops::Mode(get_mode());
op.texture_params = _texture_params;
op.blocky_value = _value;
if (!is_area_editable(op.box)) {
ZN_PRINT_VERBOSE("Area not editable");
return;
}
VoxelDataMap &data = _terrain->get_storage();
op.blocks.reference_area(data, op.box);
op();
_post_edit(op.box);
}
uint64_t VoxelToolTerrain::_get_voxel(Vector3i pos) const {
@ -432,6 +463,8 @@ void VoxelToolTerrain::_bind_methods() {
&VoxelToolTerrain::run_blocky_random_tick, DEFVAL(16));
ClassDB::bind_method(D_METHOD("for_each_voxel_metadata_in_area", "voxel_area", "callback"),
&VoxelToolTerrain::for_each_voxel_metadata_in_area);
ClassDB::bind_method(D_METHOD("do_hemisphere", "center", "radius", "flat_direction", "smoothness"),
&VoxelToolTerrain::do_hemisphere, DEFVAL(0.0));
}
} // namespace zylann::voxel

View File

@ -30,6 +30,8 @@ public:
// Specialized API
void do_hemisphere(Vector3 center, float radius, Vector3 flat_direction, float smoothness);
void run_blocky_random_tick(
AABB voxel_area, int voxel_count, const Callable &callback, int block_batch_count) const;

View File

@ -94,7 +94,8 @@ public:
// Flat array, in order [z][x][y] because it allows faster vertical-wise access (the engine is Y-up).
uint8_t *data = nullptr;
// Default value when data is null
// Default value when data is null.
// This is an encoded value, so non-integer values may be obtained by converting it.
uint64_t defval = 0;
Depth depth = DEFAULT_CHANNEL_DEPTH;
@ -279,6 +280,7 @@ public:
Span<Data_T> data = Span<uint8_t>(channel.data, channel.size_in_bytes).reinterpret_cast_to<Data_T>();
// `&` is required because lambda captures are `const` by default and `mutable` can be used only from C++23
for_each_index_and_pos(box, [&data, action_func, offset](size_t i, Vector3i pos) {
// This does not require the action to use the exact type, conversion can occur here.
data.set(i, action_func(pos + offset, data[i]));
});
compress_if_uniform(channel);

View File

@ -120,6 +120,7 @@ private:
}
// Flat grid indexed in ZXY order
// TODO Ability to use thread-local/stack pool allocator? Such grids are often temporary
std::vector<std::shared_ptr<VoxelBufferInternal>> _blocks;
// Size of the grid in blocks
Vector3i _size_in_blocks;

View File

@ -10,6 +10,8 @@ namespace zylann::math {
// Signed-distance-field functions.
// For more, see https://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm
// TODO Use `float`, or templatize SDF values. Doubles may prevent some SIMD optimizations.
inline real_t sdf_box(const Vector3 pos, const Vector3 extents) {
const Vector3 d = pos.abs() - extents;
return minf(maxf(d.x, maxf(d.y, d.z)), 0) + Vector3(maxf(d.x, 0), maxf(d.y, 0), maxf(d.z, 0)).length();
@ -39,6 +41,14 @@ inline Interval sdf_torus(
return get_length(qx, y) - r1;
}
// Note: calculate `plane_d` as `dot(plane_normal, point_in_plane)`
inline real_t sdf_plane(Vector3 pos, Vector3 plane_normal, real_t plane_d) {
// On Inigo's website it's a `+h`, but it seems to be backward because then if a plane has normal (0,1,0) and height
// 1, a point at (0,1,0) will give a dot of 1, + height will be 2, which is wrong because the expected SDF here
// would be 0.
return pos.dot(plane_normal) - plane_d;
}
inline real_t sdf_union(real_t a, real_t b) {
return min(a, b);
}