Initial WIP for SDF non-destructive voxel modifiers. Support is incomplete.

Some voxel queries and operations will not take modifiers into account.
It needs to be worked on, might involve some refactoring.
master
Marc Gilleron 2022-06-09 20:35:29 +01:00
parent f5a77a8f2b
commit 0e12e6dbf1
17 changed files with 1083 additions and 40 deletions

View File

@ -3,9 +3,10 @@
#include "../storage/funcs.h"
#include "../util/fixed_array.h"
#include "../util/math/conv.h"
#include "../util/math/sdf.h"
#include "../util/math/vector3.h"
#include "../util/math/vector3f.h"
#include <core/math/transform_3d.h>
namespace zylann::voxel {
@ -178,6 +179,25 @@ struct SdfSphere {
}
};
struct SdfBufferShape {
Span<const float> buffer;
Vector3i buffer_size;
Transform3D world_to_buffer;
float isolevel;
float sdf_scale;
inline real_t operator()(const Vector3 &wpos) const {
// Transform terrain-space position to buffer-space
const Vector3f lpos = to_vec3f(world_to_buffer.xform(wpos));
if (lpos.x < 0 || lpos.y < 0 || lpos.z < 0 || lpos.x >= buffer_size.x || lpos.y >= buffer_size.y ||
lpos.z >= buffer_size.z) {
// Outside the buffer
return 100;
}
return interpolate_trilinear(buffer, buffer_size, lpos) * sdf_scale - isolevel;
}
};
struct TextureParams {
float opacity = 1.f;
float sharpness = 2.f;

View File

@ -276,6 +276,7 @@ public:
RWLockRead rlock(data_lod.map_lock);
// TODO May want to fail if not all blocks were found
_op.blocks.reference_area(data_lod.map, _op.box);
// TODO Need to apply modifiers
_op();
}
_tracker->post_complete();
@ -774,31 +775,12 @@ void VoxelToolLodTerrain::stamp_sdf(
// TODO Maybe more efficient to "rasterize" the box? We're going to iterate voxels the box doesnt intersect
// TODO Maybe we should scale SDF values based on the scale of the transform too
struct SdfBufferShape {
Span<const float> buffer;
Vector3i buffer_size;
Transform3D world_to_buffer;
float isolevel;
float sdf_scale;
inline real_t operator()(const Vector3 &wpos) const {
// Transform terrain-space position to buffer-space
const Vector3f lpos = to_vec3f(world_to_buffer.xform(wpos));
if (lpos.x < 0 || lpos.y < 0 || lpos.z < 0 || lpos.x >= buffer_size.x || lpos.y >= buffer_size.y ||
lpos.z >= buffer_size.z) {
// Outside the buffer
return 100;
}
return interpolate_trilinear(buffer, buffer_size, lpos) * sdf_scale - isolevel;
}
};
const Transform3D buffer_to_box =
Transform3D(Basis().scaled(Vector3(local_aabb.size / buffer.get_size())), local_aabb.position);
const Transform3D buffer_to_world = box_to_world * buffer_to_box;
// TODO Support other depths, format should be accessible from the volume
ops::SdfOperation16bit<ops::SdfUnion, SdfBufferShape> op;
ops::SdfOperation16bit<ops::SdfUnion, ops::SdfBufferShape> op;
op.shape.world_to_buffer = buffer_to_world.affine_inverse();
op.shape.buffer_size = buffer.get_size();
op.shape.isolevel = isolevel;

View File

@ -1,5 +1,6 @@
#include "voxel_terrain_editor_plugin.h"
#include "../../generators/voxel_generator.h"
#include "../../storage/modifiers_gd.h"
#include "../../terrain/fixed_lod/voxel_terrain.h"
#include "../../terrain/variable_lod/voxel_lod_terrain.h"
#include "../about_window.h"
@ -102,6 +103,10 @@ static bool is_side_handled(Object *p_object) {
if (wrapper != nullptr) {
return true;
}
gd::VoxelModifier *modifier = Object::cast_to<gd::VoxelModifier>(p_object);
if (modifier != nullptr) {
return true;
}
return false;
}

View File

@ -20,6 +20,7 @@
#include "meshers/dmc/voxel_mesher_dmc.h"
#include "meshers/transvoxel/voxel_mesher_transvoxel.h"
#include "server/voxel_server_gd.h"
#include "storage/modifiers_gd.h"
#include "storage/voxel_buffer_gd.h"
#include "storage/voxel_memory_pool.h"
#include "storage/voxel_metadata_variant.h"
@ -160,6 +161,9 @@ void initialize_voxel_module(ModuleInitializationLevel p_level) {
ClassDB::register_class<VoxelInstanceGenerator>();
ClassDB::register_class<VoxelInstancer>();
ClassDB::register_class<VoxelInstanceComponent>();
ClassDB::register_abstract_class<gd::VoxelModifier>();
ClassDB::register_class<gd::VoxelModifierSphere>();
ClassDB::register_class<gd::VoxelModifierMesh>();
// Streams
ClassDB::register_abstract_class<VoxelStream>();

View File

@ -1,4 +1,5 @@
#include "mesh_block_task.h"
#include "../storage/voxel_data_map.h"
#include "../util/dstack.h"
#include "../util/log.h"
#include "../util/profiling.h"
@ -179,6 +180,10 @@ void MeshBlockTask::run(zylann::ThreadedTaskContext ctx) {
const Vector3i origin_in_voxels = position * (int(data_block_size) << lod);
if (data != nullptr) {
data->modifiers.apply(voxels, AABB(origin_in_voxels, voxels.get_size() << lod));
}
const VoxelMesher::Input input = { voxels, meshing_dependency->generator.ptr(), data.get(), origin_in_voxels, lod,
collision_hint };
mesher->build(_surfaces_output, input);

347
storage/modifiers.cpp Normal file
View File

@ -0,0 +1,347 @@
#include "modifiers.h"
#include "../edition/funcs.h"
#include "../util/dstack.h"
#include "../util/math/conv.h"
#include "../util/profiling.h"
namespace zylann::voxel {
namespace {
thread_local std::vector<float> tls_sdf;
thread_local std::vector<Vector3> tls_positions;
Span<const Vector3> get_positions_temporary(Vector3i buffer_size, Vector3 origin, Vector3 size) {
tls_positions.resize(Vector3iUtil::get_volume(buffer_size));
Span<Vector3> positions = to_span(tls_positions);
const Vector3 end = origin + size;
const Vector3 bsf = buffer_size;
unsigned int i = 0;
for (int z = 0; z < buffer_size.z; ++z) {
for (int x = 0; x < buffer_size.x; ++x) {
for (int y = 0; y < buffer_size.y; ++y) {
positions[i] = math::lerp(origin, end, Vector3(x / bsf.x, y / bsf.y, z / bsf.z));
++i;
}
}
}
return positions;
}
Span<float> decompress_sdf_to_temporary(VoxelBufferInternal &voxels) {
ZN_DSTACK();
const Vector3i bs = voxels.get_size();
tls_sdf.resize(Vector3iUtil::get_volume(bs));
Span<float> sdf = to_span(tls_sdf);
const VoxelBufferInternal::ChannelId channel = VoxelBufferInternal::CHANNEL_SDF;
voxels.decompress_channel(channel);
const VoxelBufferInternal::Depth depth = voxels.get_channel_depth(channel);
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();
}
const float inv_scale = 1.0f / VoxelBufferInternal::get_sdf_quantization_scale(depth);
for (unsigned int i = 0; i < sdf.size(); ++i) {
sdf[i] *= inv_scale;
}
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
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
uint32_t VoxelModifierStack::allocate_id() {
return ++_next_id;
}
void VoxelModifierStack::remove_modifier(uint32_t id) {
RWLockWrite lock(_stack_lock);
auto map_it = _modifiers.find(id);
ZN_ASSERT_RETURN(map_it != _modifiers.end());
const VoxelModifier *ptr = map_it->second.get();
for (auto stack_it = _stack.begin(); stack_it != _stack.end(); ++stack_it) {
if (*stack_it == ptr) {
_stack.erase(stack_it);
break;
}
}
_modifiers.erase(map_it);
}
bool VoxelModifierStack::has_modifier(uint32_t id) const {
return _modifiers.find(id) != _modifiers.end();
}
VoxelModifier *VoxelModifierStack::get_modifier(uint32_t id) const {
auto it = _modifiers.find(id);
if (it != _modifiers.end()) {
return it->second.get();
}
return nullptr;
}
void VoxelModifierStack::apply(VoxelBufferInternal &voxels, AABB aabb) const {
ZN_PROFILE_SCOPE();
RWLockRead lock(_stack_lock);
if (_stack.size() == 0) {
return;
}
VoxelModifierContext ctx;
bool any_intersection = false;
for (unsigned int i = 0; i < _stack.size(); ++i) {
const VoxelModifier *modifier = _stack[i];
ZN_ASSERT(modifier != nullptr);
if (modifier->get_aabb().intersects(aabb)) {
if (any_intersection == false) {
any_intersection = true;
ctx.positions = get_positions_temporary(voxels.get_size(), aabb.position, aabb.size);
ctx.sdf = decompress_sdf_to_temporary(voxels);
}
modifier->apply(ctx);
}
}
if (any_intersection) {
store_sdf(voxels, ctx.sdf);
voxels.compress_uniform_channels();
}
}
void VoxelModifierStack::apply(float &sdf, Vector3 position) const {
ZN_PROFILE_SCOPE();
RWLockRead lock(_stack_lock);
if (_stack.size() == 0) {
return;
}
VoxelModifierContext ctx;
ctx.positions = Span<Vector3>(&position, 1);
ctx.sdf = Span<float>(&sdf, 1);
const AABB aabb(position, Vector3(1, 1, 1));
for (unsigned int i = 0; i < _stack.size(); ++i) {
const VoxelModifier *modifier = _stack[i];
ZN_ASSERT(modifier != nullptr);
if (modifier->get_aabb().intersects(aabb)) {
modifier->apply(ctx);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void VoxelModifierSphere::set_radius(real_t radius) {
RWLockWrite wlock(_rwlock);
if (radius == _radius) {
return;
}
_radius = radius;
update_aabb();
}
real_t VoxelModifierSphere::get_radius() const {
return _radius;
}
void VoxelModifierSphere::update_aabb() {
const float extent = _radius * 1.25f + get_smoothness();
const float extent2 = 2.0 * extent;
_aabb = AABB(get_transform().origin - Vector3(extent, extent, extent), Vector3(extent2, extent2, extent2));
}
void VoxelModifierSphere::apply(VoxelModifierContext ctx) const {
ZN_PROFILE_SCOPE();
RWLockRead rlock(_rwlock);
const float smoothness = get_smoothness();
const Vector3 center = get_transform().origin;
const float sdf_scale = 1.0f;
// TODO Support transform scale
switch (get_operation()) {
case OP_ADD:
for (unsigned int i = 0; i < ctx.sdf.size(); ++i) {
const float sd = sdf_scale * math::sdf_sphere(ctx.positions[i], center, _radius);
ctx.sdf[i] = math::sdf_smooth_union(ctx.sdf[i], sd, smoothness);
}
break;
case OP_SUBTRACT:
for (unsigned int i = 0; i < ctx.sdf.size(); ++i) {
const float sd = sdf_scale * math::sdf_sphere(ctx.positions[i], center, _radius);
ctx.sdf[i] = math::sdf_smooth_subtract(ctx.sdf[i], sd, smoothness);
}
break;
default:
ZN_CRASH();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void VoxelModifierBuffer::set_buffer(std::shared_ptr<VoxelBufferInternal> buffer, Vector3f min_pos, Vector3f max_pos) {
//ZN_ASSERT_RETURN(buffer != nullptr);
RWLockWrite wlock(_rwlock);
_buffer = buffer;
_min_pos = min_pos;
_max_pos = max_pos;
update_aabb();
}
void VoxelModifierBuffer::set_isolevel(float isolevel) {
RWLockWrite wlock(_rwlock);
if (isolevel == isolevel) {
return;
}
_isolevel = isolevel;
}
float get_largest_coord(Vector3 v) {
return math::max(math::max(v.x, v.y), v.z);
}
void VoxelModifierBuffer::apply(VoxelModifierContext ctx) const {
RWLockRead rlock(_rwlock);
if (_buffer == nullptr) {
return;
}
const Transform3D model_to_world = get_transform();
const Transform3D buffer_to_model =
Transform3D(Basis().scaled(to_vec3(_max_pos - _min_pos) / to_vec3(_buffer->get_size())), to_vec3(_min_pos));
const Transform3D buffer_to_world = model_to_world * buffer_to_model;
Span<const float> buffer_sdf;
ZN_ASSERT_RETURN(_buffer->get_channel_data(VoxelBufferInternal::CHANNEL_SDF, buffer_sdf));
const float smoothness = get_smoothness();
ops::SdfBufferShape shape;
shape.buffer = buffer_sdf;
shape.buffer_size = _buffer->get_size();
shape.isolevel = _isolevel;
shape.sdf_scale = get_largest_coord(model_to_world.get_basis().get_scale());
shape.world_to_buffer = buffer_to_world.affine_inverse();
switch (get_operation()) {
case OP_ADD:
for (unsigned int i = 0; i < ctx.sdf.size(); ++i) {
const float sd = shape(ctx.positions[i]);
ctx.sdf[i] = math::sdf_smooth_union(ctx.sdf[i], sd, smoothness);
}
break;
case OP_SUBTRACT:
for (unsigned int i = 0; i < ctx.sdf.size(); ++i) {
const float sd = shape(ctx.positions[i]);
ctx.sdf[i] = math::sdf_smooth_subtract(ctx.sdf[i], sd, smoothness);
}
break;
default:
ZN_CRASH();
}
}
void VoxelModifierBuffer::update_aabb() {
const Transform3D &model_to_world = get_transform();
_aabb = model_to_world.xform(AABB(to_vec3(_min_pos), to_vec3(_max_pos - _min_pos)));
}
} // namespace zylann::voxel

176
storage/modifiers.h Normal file
View File

@ -0,0 +1,176 @@
#ifndef VOXEL_MODIFIERS_H
#define VOXEL_MODIFIERS_H
#include "../util/math/sdf.h"
#include "../util/math/vector3.h"
#include "../util/math/vector3f.h"
#include "../util/thread/rw_lock.h"
#include "voxel_buffer_internal.h"
#include <core/math/transform_3d.h>
#include <unordered_map>
namespace zylann::voxel {
struct VoxelModifierContext {
Span<float> sdf;
Span<const Vector3> positions;
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class VoxelModifier {
public:
enum Type { TYPE_SPHERE, TYPE_BUFFER };
virtual ~VoxelModifier() {}
virtual void apply(VoxelModifierContext ctx) const = 0;
const Transform3D &get_transform() const {
return _transform;
}
void set_transform(Transform3D t) {
if (t == _transform) {
return;
}
_transform = t;
update_aabb();
}
const AABB &get_aabb() const {
return _aabb;
}
virtual Type get_type() const = 0;
virtual bool is_sdf() const = 0;
protected:
virtual void update_aabb() = 0;
AABB _aabb;
private:
Transform3D _transform;
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class VoxelModifierSdf : public VoxelModifier {
public:
enum Operation { //
OP_ADD,
OP_SUBTRACT
};
inline Operation get_operation() const {
return _operation;
}
void set_operation(Operation op) {
RWLockWrite wlock(_rwlock);
_operation = op;
}
inline float get_smoothness() const {
return _smoothness;
}
void set_smoothness(float p_smoothness) {
RWLockWrite wlock(_rwlock);
const float smoothness = math::max(p_smoothness, 0.f);
if (smoothness == _smoothness) {
return;
}
_smoothness = smoothness;
update_aabb();
}
bool is_sdf() const override {
return true;
}
protected:
RWLock _rwlock;
private:
Operation _operation = OP_ADD;
float _smoothness = 0.f;
//float _margin = 0.f;
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class VoxelModifierSphere : public VoxelModifierSdf {
public:
Type get_type() const override {
return TYPE_SPHERE;
};
void set_radius(real_t radius);
real_t get_radius() const;
void apply(VoxelModifierContext ctx) const override;
protected:
void update_aabb() override;
private:
real_t _radius = 10.f;
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class VoxelModifierBuffer : public VoxelModifierSdf {
public:
Type get_type() const override {
return TYPE_BUFFER;
};
void set_buffer(std::shared_ptr<VoxelBufferInternal> buffer, Vector3f min_pos, Vector3f max_pos);
void set_isolevel(float isolevel);
void apply(VoxelModifierContext ctx) const override;
protected:
void update_aabb() override;
private:
std::shared_ptr<VoxelBufferInternal> _buffer;
Vector3f _min_pos;
Vector3f _max_pos;
float _isolevel;
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class VoxelModifierStack {
public:
uint32_t allocate_id();
template <typename T>
T *add_modifier(uint32_t id) {
ZN_ASSERT(!has_modifier(id));
UniquePtr<VoxelModifier> &uptr = _modifiers[id];
uptr = make_unique_instance<T>();
VoxelModifier *ptr = uptr.get();
RWLockWrite lock(_stack_lock);
_stack.push_back(ptr);
return static_cast<T *>(ptr);
}
void remove_modifier(uint32_t id);
bool has_modifier(uint32_t id) const;
VoxelModifier *get_modifier(uint32_t id) const;
void apply(VoxelBufferInternal &voxels, AABB aabb) const;
void apply(float &sdf, Vector3 position) const;
private:
std::unordered_map<uint32_t, UniquePtr<VoxelModifier>> _modifiers;
uint32_t _next_id = 1;
// TODO Later, replace this with a spatial acceleration structure based on AABBs, like BVH
std::vector<VoxelModifier *> _stack;
RWLock _stack_lock;
};
} // namespace zylann::voxel
#endif // VOXEL_MODIFIERS_H

307
storage/modifiers_gd.cpp Normal file
View File

@ -0,0 +1,307 @@
#include "modifiers_gd.h"
#include "../terrain/variable_lod/voxel_lod_terrain.h"
#include "../util/errors.h"
#include "../util/math/conv.h"
namespace zylann::voxel::gd {
void post_edit_modifier(VoxelLodTerrain &volume, AABB aabb) {
volume.post_edit_modifiers(Box3i(math::floor_to_int(aabb.position), math::floor_to_int(aabb.size)));
}
// template <typename Modifier_T, typename F>
// void edit_modifier(VoxelLodTerrain &volume, Modifier_T &modifier, F action) {
// const AABB prev_aabb = modifier->get_aabb();
// action(modifier);
// const AABB new_aabb = modifier->get_aabb();
// post_edit_modifier(volume, prev_aabb);
// post_edit_modifier(volume, new_aabb);
// }
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
VoxelModifier::VoxelModifier() {
set_notify_local_transform(true);
}
zylann::voxel::VoxelModifier *VoxelModifier::create(zylann::voxel::VoxelModifierStack &modifiers, uint32_t id) {
ZN_PRINT_ERROR("Not implemented");
return nullptr;
}
zylann::voxel::VoxelModifierSdf::Operation to_op(VoxelModifier::Operation op) {
return zylann::voxel::VoxelModifierSdf::Operation(op);
}
void VoxelModifier::set_operation(Operation op) {
ZN_ASSERT_RETURN(op >= 0 && op < OPERATION_COUNT);
if (op == _operation) {
return;
}
_operation = op;
if (_volume == nullptr) {
return;
}
std::shared_ptr<VoxelDataLodMap> data = _volume->get_storage();
VoxelModifierStack &modifiers = data->modifiers;
zylann::voxel::VoxelModifier *modifier = modifiers.get_modifier(_modifier_id);
ZN_ASSERT_RETURN(modifier != nullptr);
ZN_ASSERT_RETURN(modifier->is_sdf());
zylann::voxel::VoxelModifierSdf *sdf_modifier = static_cast<zylann::voxel::VoxelModifierSdf *>(modifier);
sdf_modifier->set_operation(to_op(_operation));
post_edit_modifier(*_volume, modifier->get_aabb());
}
VoxelModifier::Operation VoxelModifier::get_operation() const {
return _operation;
}
void VoxelModifier::set_smoothness(float s) {
if (s == _smoothness) {
return;
}
_smoothness = math::max(s, 0.f);
if (_volume == nullptr) {
return;
}
std::shared_ptr<VoxelDataLodMap> data = _volume->get_storage();
VoxelModifierStack &modifiers = data->modifiers;
zylann::voxel::VoxelModifier *modifier = modifiers.get_modifier(_modifier_id);
ZN_ASSERT_RETURN(modifier != nullptr);
ZN_ASSERT_RETURN(modifier->is_sdf());
zylann::voxel::VoxelModifierSdf *sdf_modifier = static_cast<zylann::voxel::VoxelModifierSdf *>(modifier);
const AABB prev_aabb = modifier->get_aabb();
sdf_modifier->set_smoothness(_smoothness);
const AABB new_aabb = modifier->get_aabb();
post_edit_modifier(*_volume, prev_aabb);
post_edit_modifier(*_volume, new_aabb);
}
float VoxelModifier::get_smoothness() const {
return _smoothness;
}
void VoxelModifier::_notification(int p_what) {
switch (p_what) {
case Node::NOTIFICATION_PARENTED: {
Node *parent = get_parent();
ZN_ASSERT_RETURN(parent != nullptr);
ZN_ASSERT_RETURN(_volume == nullptr);
VoxelLodTerrain *volume = Object::cast_to<VoxelLodTerrain>(parent);
_volume = volume;
if (_volume != nullptr) {
std::shared_ptr<VoxelDataLodMap> data = _volume->get_storage();
VoxelModifierStack &modifiers = data->modifiers;
const uint32_t id = modifiers.allocate_id();
zylann::voxel::VoxelModifier *modifier = create(modifiers, id);
if (modifier->is_sdf()) {
zylann::voxel::VoxelModifierSdf *sdf_modifier =
static_cast<zylann::voxel::VoxelModifierSdf *>(modifier);
sdf_modifier->set_operation(to_op(_operation));
sdf_modifier->set_smoothness(_smoothness);
}
modifier->set_transform(get_transform());
_modifier_id = id;
// TODO Optimize: on loading of a scene, this could be very bad for performance because there could be,
// a lot of modifiers on the map, but there is no distinction possible in Godot at the moment...
post_edit_modifier(*_volume, modifier->get_aabb());
}
} break;
case Node::NOTIFICATION_UNPARENTED: {
if (_volume != nullptr) {
std::shared_ptr<VoxelDataLodMap> data = _volume->get_storage();
VoxelModifierStack &modifiers = data->modifiers;
zylann::voxel::VoxelModifier *modifier = modifiers.get_modifier(_modifier_id);
ZN_ASSERT_RETURN(modifier != nullptr);
post_edit_modifier(*_volume, modifier->get_aabb());
modifiers.remove_modifier(_modifier_id);
_volume = nullptr;
_modifier_id = 0;
}
} break;
case Node3D::NOTIFICATION_LOCAL_TRANSFORM_CHANGED: {
if (_volume != nullptr && is_inside_tree()) {
std::shared_ptr<VoxelDataLodMap> data = _volume->get_storage();
VoxelModifierStack &modifiers = data->modifiers;
zylann::voxel::VoxelModifier *modifier = modifiers.get_modifier(_modifier_id);
ZN_ASSERT_RETURN(modifier != nullptr);
const AABB prev_aabb = modifier->get_aabb();
modifier->set_transform(get_transform());
const AABB aabb = modifier->get_aabb();
post_edit_modifier(*_volume, prev_aabb);
post_edit_modifier(*_volume, aabb);
// TODO Handle nesting properly, though it's a pain in the ass
// When the terrain is moved, the local transform of modifiers technically changes too.
// However it did not change relative to the terrain. But because we don't have a way to check that,
// all modifiers will trigger updates at the same time...
}
} break;
}
}
void VoxelModifier::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_operation", "op"), &VoxelModifier::set_operation);
ClassDB::bind_method(D_METHOD("get_operation"), &VoxelModifier::get_operation);
ClassDB::bind_method(D_METHOD("set_smoothness", "smoothness"), &VoxelModifier::set_smoothness);
ClassDB::bind_method(D_METHOD("get_smoothness"), &VoxelModifier::get_smoothness);
ADD_PROPERTY(PropertyInfo(Variant::INT, "operation", PROPERTY_HINT_ENUM, "Add,Remove"), "set_operation",
"get_operation");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "smoothness", PROPERTY_HINT_RANGE, "0.0, 100.0, 0.1"), "set_smoothness",
"get_smoothness");
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
template <typename T>
T *get_modifier(VoxelLodTerrain &volume, uint32_t id, zylann::voxel::VoxelModifier::Type type) {
std::shared_ptr<VoxelDataLodMap> data = volume.get_storage();
VoxelModifierStack &modifiers = data->modifiers;
zylann::voxel::VoxelModifier *modifier = modifiers.get_modifier(id);
ZN_ASSERT_RETURN_V(modifier != nullptr, nullptr);
ZN_ASSERT_RETURN_V(modifier->get_type() == type, nullptr);
return static_cast<T *>(modifier);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
zylann::voxel::VoxelModifierSphere *get_sphere(VoxelLodTerrain &volume, uint32_t id) {
return get_modifier<zylann::voxel::VoxelModifierSphere>(volume, id, zylann::voxel::VoxelModifier::TYPE_SPHERE);
}
float VoxelModifierSphere::get_radius() const {
return _radius;
}
void VoxelModifierSphere::set_radius(float r) {
_radius = math::max(r, 0.f);
if (_volume == nullptr) {
return;
}
zylann::voxel::VoxelModifierSphere *sphere = get_sphere(*_volume, _modifier_id);
ZN_ASSERT_RETURN(sphere != nullptr);
const AABB prev_aabb = sphere->get_aabb();
sphere->set_radius(r);
const AABB new_aabb = sphere->get_aabb();
post_edit_modifier(*_volume, prev_aabb);
post_edit_modifier(*_volume, new_aabb);
}
zylann::voxel::VoxelModifier *VoxelModifierSphere::create(zylann::voxel::VoxelModifierStack &modifiers, uint32_t id) {
zylann::voxel::VoxelModifierSphere *sphere = modifiers.add_modifier<zylann::voxel::VoxelModifierSphere>(id);
sphere->set_radius(_radius);
return sphere;
}
void VoxelModifierSphere::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_radius", "r"), &VoxelModifierSphere::set_radius);
ClassDB::bind_method(D_METHOD("get_radius"), &VoxelModifierSphere::get_radius);
ADD_PROPERTY(
PropertyInfo(Variant::FLOAT, "radius", PROPERTY_HINT_RANGE, "0.0, 100.0, 0.1"), "set_radius", "get_radius");
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
zylann::voxel::VoxelModifierBuffer *get_buffer_modifier(VoxelLodTerrain &volume, uint32_t id) {
return get_modifier<zylann::voxel::VoxelModifierBuffer>(volume, id, zylann::voxel::VoxelModifier::TYPE_BUFFER);
}
static void set_buffer(zylann::voxel::VoxelModifierBuffer &bmod, Ref<VoxelMeshSDF> mesh_sdf) {
if (mesh_sdf.is_null() || mesh_sdf->get_voxel_buffer() == nullptr) {
bmod.set_buffer(nullptr, Vector3f(), Vector3f());
} else {
const AABB aabb = mesh_sdf->get_aabb();
bmod.set_buffer(mesh_sdf->get_voxel_buffer()->get_buffer_shared(), to_vec3f(aabb.position),
to_vec3f(aabb.position + aabb.size));
}
}
void VoxelModifierMesh::set_mesh_sdf(Ref<VoxelMeshSDF> mesh_sdf) {
if (mesh_sdf == _mesh_sdf) {
return;
}
if (_mesh_sdf.is_valid()) {
_mesh_sdf->disconnect("baked", callable_mp(this, &VoxelModifierMesh::_on_mesh_sdf_baked));
}
_mesh_sdf = mesh_sdf;
if (_mesh_sdf.is_valid()) {
_mesh_sdf->connect("baked", callable_mp(this, &VoxelModifierMesh::_on_mesh_sdf_baked));
}
if (_volume == nullptr) {
return;
}
zylann::voxel::VoxelModifierBuffer *bmod = get_buffer_modifier(*_volume, _modifier_id);
ZN_ASSERT_RETURN(bmod != nullptr);
const AABB prev_aabb = bmod->get_aabb();
set_buffer(*bmod, _mesh_sdf);
const AABB new_aabb = bmod->get_aabb();
post_edit_modifier(*_volume, prev_aabb);
post_edit_modifier(*_volume, new_aabb);
}
Ref<VoxelMeshSDF> VoxelModifierMesh::get_mesh_sdf() const {
return _mesh_sdf;
}
void VoxelModifierMesh::set_isolevel(float isolevel) {
if (isolevel == _isolevel) {
return;
}
_isolevel = isolevel;
if (_volume == nullptr) {
return;
}
zylann::voxel::VoxelModifierBuffer *bmod = get_buffer_modifier(*_volume, _modifier_id);
ZN_ASSERT_RETURN(bmod != nullptr);
bmod->set_isolevel(_isolevel);
post_edit_modifier(*_volume, bmod->get_aabb());
}
float VoxelModifierMesh::get_isolevel() const {
return _isolevel;
}
zylann::voxel::VoxelModifier *VoxelModifierMesh::create(zylann::voxel::VoxelModifierStack &modifiers, uint32_t id) {
zylann::voxel::VoxelModifierBuffer *bmod = modifiers.add_modifier<zylann::voxel::VoxelModifierBuffer>(id);
set_buffer(*bmod, _mesh_sdf);
bmod->set_isolevel(_isolevel);
return bmod;
}
void VoxelModifierMesh::_on_mesh_sdf_baked() {
if (_volume == nullptr) {
return;
}
zylann::voxel::VoxelModifierBuffer *bmod = get_buffer_modifier(*_volume, _modifier_id);
ZN_ASSERT_RETURN(bmod != nullptr);
const AABB prev_aabb = bmod->get_aabb();
set_buffer(*bmod, _mesh_sdf);
const AABB new_aabb = bmod->get_aabb();
post_edit_modifier(*_volume, prev_aabb);
post_edit_modifier(*_volume, new_aabb);
}
void VoxelModifierMesh::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_mesh_sdf", "mesh_sdf"), &VoxelModifierMesh::set_mesh_sdf);
ClassDB::bind_method(D_METHOD("get_mesh_sdf"), &VoxelModifierMesh::get_mesh_sdf);
ClassDB::bind_method(D_METHOD("set_isolevel", "isolevel"), &VoxelModifierMesh::set_isolevel);
ClassDB::bind_method(D_METHOD("get_isolevel"), &VoxelModifierMesh::get_isolevel);
ADD_PROPERTY(
PropertyInfo(Variant::OBJECT, "mesh_sdf", PROPERTY_HINT_RESOURCE_TYPE, VoxelMeshSDF::get_class_static()),
"set_mesh_sdf", "get_mesh_sdf");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "isolevel", PROPERTY_HINT_RANGE, "-100.0, 100.0, 0.01"), "set_isolevel",
"get_isolevel");
}
} // namespace zylann::voxel::gd

92
storage/modifiers_gd.h Normal file
View File

@ -0,0 +1,92 @@
#ifndef VOXEL_MODIFIERS_GD_H
#define VOXEL_MODIFIERS_GD_H
#include "../edition/voxel_mesh_sdf_gd.h"
#include <scene/3d/node_3d.h>
namespace zylann::voxel {
class VoxelLodTerrain;
class VoxelModifier;
class VoxelModifierStack;
namespace gd {
class VoxelModifier : public Node3D {
GDCLASS(VoxelModifier, Node3D)
public:
enum Operation { //
OPERATION_ADD,
OPERATION_REMOVE,
OPERATION_COUNT
};
VoxelModifier();
void set_operation(Operation op);
Operation get_operation() const;
void set_smoothness(float s);
float get_smoothness() const;
protected:
virtual zylann::voxel::VoxelModifier *create(zylann::voxel::VoxelModifierStack &modifiers, uint32_t id);
void _notification(int p_what);
VoxelLodTerrain *_volume = nullptr;
uint32_t _modifier_id = 0;
private:
static void _bind_methods();
Operation _operation = OPERATION_ADD;
float _smoothness = 0.f;
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class VoxelModifierSphere : public VoxelModifier {
GDCLASS(VoxelModifierSphere, VoxelModifier);
public:
float get_radius() const;
void set_radius(float r);
protected:
zylann::voxel::VoxelModifier *create(zylann::voxel::VoxelModifierStack &modifiers, uint32_t id) override;
private:
static void _bind_methods();
float _radius = 10.f;
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class VoxelModifierMesh : public VoxelModifier {
GDCLASS(VoxelModifierMesh, VoxelModifier);
public:
void set_mesh_sdf(Ref<VoxelMeshSDF> mesh_sdf);
Ref<VoxelMeshSDF> get_mesh_sdf() const;
void set_isolevel(float isolevel);
float get_isolevel() const;
protected:
zylann::voxel::VoxelModifier *create(zylann::voxel::VoxelModifierStack &modifiers, uint32_t id) override;
private:
void _on_mesh_sdf_baked();
static void _bind_methods();
Ref<VoxelMeshSDF> _mesh_sdf;
float _isolevel = 0.0f;
};
} // namespace gd
} // namespace zylann::voxel
VARIANT_ENUM_CAST(zylann::voxel::gd::VoxelModifier::Operation);
#endif // VOXEL_MODIFIERS_GD_H

View File

@ -356,6 +356,7 @@ void preload_box(VoxelDataLodMap &data, Box3i voxel_box, VoxelGenerator *generat
VoxelGenerator::VoxelQueryData q{ *task.voxels, task.block_pos * (data_block_size << task.lod_index),
task.lod_index };
generator->generate_block(q);
data.modifiers.apply(q.voxel_buffer, AABB(q.origin_in_voxels, q.voxel_buffer.get_size() << q.lod));
}
}

View File

@ -3,6 +3,7 @@
#include "../util/fixed_array.h"
#include "../util/profiling.h"
#include "modifiers.h"
#include "voxel_data_block.h"
#include <unordered_map>
@ -210,6 +211,7 @@ struct VoxelDataLodMap {
// Each LOD works in a set of coordinates spanning 2x more voxels the higher their index is
FixedArray<Lod, constants::MAX_LOD> lods;
unsigned int lod_count = 1;
VoxelModifierStack modifiers;
};
// Generates all non-present blocks in preparation for an edit.

View File

@ -126,6 +126,29 @@ static inline uint64_t get_ticks_msec() {
} // namespace
void VoxelLodTerrain::ApplyMeshUpdateTask::run(TimeSpreadTaskContext &ctx) {
if (!VoxelServer::get_singleton().is_volume_valid(volume_id)) {
// The node can have been destroyed while this task was still pending
ZN_PRINT_VERBOSE("Cancelling ApplyMeshUpdateTask, volume_id is invalid");
return;
}
std::unordered_map<Vector3i, RefCount> &queued_tasks_in_lod = self->_queued_main_thread_mesh_updates[data.lod];
auto it = queued_tasks_in_lod.find(data.position);
if (it != queued_tasks_in_lod.end()) {
RefCount &count = it->second;
count.remove();
if (count.get() > 0) {
// This is not the only main thread task queued for this block.
// Cancel it to avoid buildup.
return;
}
queued_tasks_in_lod.erase(it);
}
self->apply_mesh_update(data);
}
VoxelLodTerrain::VoxelLodTerrain() {
// Note: don't do anything heavy in the constructor.
// Godot may create and destroy dozens of instances of all node types on startup,
@ -148,20 +171,6 @@ VoxelLodTerrain::VoxelLodTerrain() {
_update_data->settings.bounds_in_voxels =
Box3i::from_center_extents(Vector3i(), Vector3iUtil::create(constants::MAX_VOLUME_EXTENT));
struct ApplyMeshUpdateTask : public ITimeSpreadTask {
void run(TimeSpreadTaskContext &ctx) override {
if (!VoxelServer::get_singleton().is_volume_valid(volume_id)) {
// The node can have been destroyed while this task was still pending
ZN_PRINT_VERBOSE("Cancelling ApplyMeshUpdateTask, volume_id is invalid");
return;
}
self->apply_mesh_update(data);
}
uint32_t volume_id = 0;
VoxelLodTerrain *self = nullptr;
VoxelServer::BlockMeshOutput data;
};
// Mesh updates are spread over frames by scheduling them in a task runner of VoxelServer,
// but instead of using a reception buffer we use a callback,
// because this kind of task scheduling would otherwise delay the update by 1 frame
@ -174,6 +183,16 @@ VoxelLodTerrain::VoxelLodTerrain() {
task->self = self;
task->data = ob;
VoxelServer::get_singleton().push_main_thread_time_spread_task(task);
// If two tasks are queued for the same mesh, cancel the old ones.
// This is for cases where creating the mesh is slower than the speed at which it is generated,
// which can cause a buildup that never seems to stop.
// This is at the expense of holes appearing until all tasks are done.
std::unordered_map<Vector3i, RefCount> &queued_tasks_in_lod = self->_queued_main_thread_mesh_updates[ob.lod];
auto p = queued_tasks_in_lod.insert({ ob.position, RefCount(1) });
if (!p.second) {
p.first->second.add();
}
};
callbacks.data_output_callback = [](void *cb_data, VoxelServer::BlockDataOutput &ob) {
VoxelLodTerrain *self = reinterpret_cast<VoxelLodTerrain *>(cb_data);
@ -515,7 +534,13 @@ VoxelSingleValue VoxelLodTerrain::get_voxel(Vector3i pos, unsigned int channel,
std::shared_ptr<VoxelBufferInternal> voxels = try_get_voxel_buffer_with_lock(data_lod0, block_pos);
if (voxels == nullptr) {
if (_generator.is_valid()) {
return _generator->generate_single(pos, channel);
VoxelSingleValue value = _generator->generate_single(pos, channel);
if (channel == VoxelBufferInternal::CHANNEL_SDF) {
float sdf = value.f;
_data->modifiers.apply(sdf, to_vec3(pos));
value.f = sdf;
}
return value;
}
} else {
const Vector3i rpos = data_lod0.map.to_local(pos);
@ -560,6 +585,7 @@ bool VoxelLodTerrain::try_set_voxel_without_update(Vector3i pos, unsigned int ch
voxels->create(Vector3iUtil::create(get_data_block_size()));
VoxelGenerator::VoxelQueryData q{ *voxels, pos, 0 };
_generator->generate_block(q);
_data->modifiers.apply(q.voxel_buffer, AABB(pos, q.voxel_buffer.get_size()));
RWLockWrite wlock(data_lod0.map_lock);
if (data_lod0.map.has_block(block_pos_lod0)) {
// A block was loaded by another thread, cancel our edit.
@ -576,16 +602,27 @@ 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) {
const VoxelDataLodMap::Lod &data_lod0 = _data->lods[0];
VoxelModifierStack &modifiers = _data->modifiers;
if (_update_data->settings.full_load_mode && _generator.is_valid()) {
struct GenContext {
VoxelGenerator &generator;
const VoxelModifierStack &modifiers;
};
GenContext gctx{ **_generator, modifiers };
RWLockRead rlock(data_lod0.map_lock);
data_lod0.map.copy(p_origin_voxels, dst_buffer, channels_mask, *_generator,
data_lod0.map.copy(p_origin_voxels, dst_buffer, channels_mask, &gctx,
[](void *callback_data, VoxelBufferInternal &voxels, Vector3i pos) {
VoxelGenerator *generator = reinterpret_cast<VoxelGenerator *>(callback_data);
GenContext *gctx = reinterpret_cast<GenContext *>(callback_data);
VoxelGenerator::VoxelQueryData q{ voxels, pos, 0 };
generator->generate_block(q);
gctx->generator.generate_block(q);
gctx->modifiers.apply(voxels, AABB(pos, voxels.get_size()));
});
} else {
RWLockRead rlock(data_lod0.map_lock);
// TODO Apply modifiers
data_lod0.map.copy(p_origin_voxels, dst_buffer, channels_mask);
}
}
@ -622,6 +659,7 @@ void VoxelLodTerrain::post_edit_area(Box3i p_box) {
// TODO That boolean is also modified by the threaded update task (always set to false)
if (!block->get_needs_lodding()) {
block->set_needs_lodding(true);
// This is what indirectly causes remeshing
_update_data->state.blocks_pending_lodding_lod0.push_back(block_pos_lod0);
}
});
@ -632,6 +670,11 @@ void VoxelLodTerrain::post_edit_area(Box3i p_box) {
}
}
void VoxelLodTerrain::post_edit_modifiers(Box3i p_box) {
MutexLock lock(_update_data->state.remesh_requests_mutex);
_update_data->state.remesh_requests.push_back(p_box);
}
void VoxelLodTerrain::push_async_edit(IThreadedTask *task, Box3i box, std::shared_ptr<AsyncDependencyTracker> tracker) {
CRASH_COND(task == nullptr);
CRASH_COND(tracker == nullptr);
@ -788,6 +831,10 @@ void VoxelLodTerrain::_set_lod_count(int p_lod_count) {
item.octree.create(p_lod_count, nda);
}
for (unsigned int i = 0; i < _queued_main_thread_mesh_updates.size(); ++i) {
_queued_main_thread_mesh_updates[i].clear();
}
// Not entirely required, but changing LOD count at runtime is rarely needed
reset_maps();
}

View File

@ -143,6 +143,7 @@ public:
// These must be called after an edit
void post_edit_area(Box3i p_box);
void post_edit_modifiers(Box3i p_box);
// TODO This still sucks atm cuz the edit will still run on the main thread
void push_async_edit(IThreadedTask *task, Box3i box, std::shared_ptr<AsyncDependencyTracker> tracker);
@ -339,6 +340,16 @@ private:
std::shared_ptr<StreamingDependency> _streaming_dependency;
std::shared_ptr<MeshingDependency> _meshing_dependency;
struct ApplyMeshUpdateTask : public ITimeSpreadTask {
void run(TimeSpreadTaskContext &ctx) override;
uint32_t volume_id = 0;
VoxelLodTerrain *self = nullptr;
VoxelServer::BlockMeshOutput data;
};
FixedArray<std::unordered_map<Vector3i, RefCount>, constants::MAX_LOD> _queued_main_thread_mesh_updates;
#ifdef TOOLS_ENABLED
bool _show_gizmos_enabled = false;
bool _show_octree_bounds_gizmos = true;

View File

@ -159,6 +159,9 @@ struct VoxelLodTerrainUpdateData {
BinaryMutex pending_async_edits_mutex;
std::vector<RunningAsyncEdit> running_async_edits;
std::vector<Box3i> remesh_requests;
BinaryMutex remesh_requests_mutex;
Stats stats;
};

View File

@ -1340,6 +1340,36 @@ static void process_async_edits(VoxelLodTerrainUpdateData::State &state,
}
}
static void process_remesh_requests(
VoxelLodTerrainUpdateData::State &state, const VoxelLodTerrainUpdateData::Settings &settings) {
const unsigned int mesh_block_size = 1 << settings.mesh_block_size_po2;
MutexLock lock(state.remesh_requests_mutex);
if (state.remesh_requests.size() == 0) {
return;
}
for (unsigned int lod_index = 0; lod_index < settings.lod_count; ++lod_index) {
VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index];
for (auto box_it = state.remesh_requests.begin(); box_it != state.remesh_requests.end(); ++box_it) {
const Box3i &voxel_box = *box_it;
const Box3i bbox = voxel_box.padded(1).downscaled(mesh_block_size << lod_index);
RWLockRead rlock(lod.mesh_map_state.map_lock);
bbox.for_each_cell_zxy([&lod](const Vector3i bpos) {
auto block_it = lod.mesh_map_state.map.find(bpos);
if (block_it != lod.mesh_map_state.map.end()) {
VoxelLodTerrainUpdateTask::schedule_mesh_update(block_it->second, bpos, lod.blocks_pending_update);
}
});
}
}
state.remesh_requests.clear();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void VoxelLodTerrainUpdateTask::run(ThreadedTaskContext ctx) {
@ -1388,8 +1418,12 @@ void VoxelLodTerrainUpdateTask::run(ThreadedTaskContext ctx) {
// Update pending LOD data modifications due to edits.
// These are deferred from edits so we can batch them.
// It has to happen first because blocks can be unloaded afterwards.
// This is also what causes meshes to update after edits.
flush_pending_lod_edits(state, data, generator, settings.full_load_mode, 1 << settings.mesh_block_size_po2);
// Other mesh updates
process_remesh_requests(state, settings);
static thread_local std::vector<VoxelLodTerrainUpdateData::BlockToSave> data_blocks_to_save;
static thread_local std::vector<VoxelLodTerrainUpdateData::BlockLocation> data_blocks_to_load;
data_blocks_to_load.clear();

View File

@ -22,6 +22,10 @@ inline bool is_normalized(const Vector3 &v) {
return v.is_normalized();
}
inline Vector3 lerp(const Vector3 a, const Vector3 b, const Vector3 alpha) {
return Vector3(Math::lerp(a.x, b.x, alpha.x), Math::lerp(a.y, b.y, alpha.y), Math::lerp(a.z, b.z, alpha.z));
}
} // namespace zylann::math
#endif // ZN_VECTOR3_H

View File

@ -9,6 +9,9 @@ namespace zylann {
// This one is not thread-safe.
class RefCount {
public:
RefCount() {}
RefCount(unsigned int initial_count): _count(initial_count) {}
inline void add() {
++_count;
}