738 lines
24 KiB
C++
738 lines
24 KiB
C++
#include "voxel_tool_lod_terrain.h"
|
|
#include "../constants/voxel_string_names.h"
|
|
#include "../storage/voxel_buffer_gd.h"
|
|
#include "../storage/voxel_data_grid.h"
|
|
#include "../terrain/variable_lod/voxel_lod_terrain.h"
|
|
#include "../util/godot/funcs.h"
|
|
#include "../util/island_finder.h"
|
|
#include "../util/tasks/async_dependency_tracker.h"
|
|
#include "../util/voxel_raycast.h"
|
|
#include "funcs.h"
|
|
|
|
#include <scene/3d/collision_shape_3d.h>
|
|
#include <scene/3d/mesh_instance_3d.h>
|
|
#include <scene/3d/physics_body_3d.h>
|
|
#include <scene/main/timer.h>
|
|
|
|
namespace zylann::voxel {
|
|
|
|
VoxelToolLodTerrain::VoxelToolLodTerrain(VoxelLodTerrain *terrain) : _terrain(terrain) {
|
|
ERR_FAIL_COND(terrain == nullptr);
|
|
// At the moment, only LOD0 is supported.
|
|
// Don't destroy the terrain while a voxel tool still references it
|
|
}
|
|
|
|
bool VoxelToolLodTerrain::is_area_editable(const Box3i &box) const {
|
|
ERR_FAIL_COND_V(_terrain == nullptr, false);
|
|
return _terrain->is_area_editable(box);
|
|
}
|
|
|
|
template <typename Volume_F>
|
|
float get_sdf_interpolated(const Volume_F &f, Vector3 pos) {
|
|
const Vector3i c = Vector3iUtil::from_floored(pos);
|
|
|
|
const float s000 = f(Vector3i(c.x, c.y, c.z));
|
|
const float s100 = f(Vector3i(c.x + 1, c.y, c.z));
|
|
const float s010 = f(Vector3i(c.x, c.y + 1, c.z));
|
|
const float s110 = f(Vector3i(c.x + 1, c.y + 1, c.z));
|
|
const float s001 = f(Vector3i(c.x, c.y, c.z + 1));
|
|
const float s101 = f(Vector3i(c.x + 1, c.y, c.z + 1));
|
|
const float s011 = f(Vector3i(c.x, c.y + 1, c.z + 1));
|
|
const float s111 = f(Vector3i(c.x + 1, c.y + 1, c.z + 1));
|
|
|
|
return math::interpolate(s000, s100, s101, s001, s010, s110, s111, s011, to_vec3f(math::fract(pos)));
|
|
}
|
|
|
|
// Binary search can be more accurate than linear regression because the SDF can be inaccurate in the first place.
|
|
// An alternative would be to polygonize a tiny area around the middle-phase hit position.
|
|
// `d1` is how far from `pos0` along `dir` the binary search will take place.
|
|
// The segment may be adjusted internally if it does not contain a zero-crossing of the
|
|
template <typename Volume_F>
|
|
float approximate_distance_to_isosurface_binary_search(
|
|
const Volume_F &f, Vector3 pos0, Vector3 dir, float d1, int iterations) {
|
|
float d0 = 0.f;
|
|
float sdf0 = get_sdf_interpolated(f, pos0);
|
|
// The position given as argument may be a rough approximation coming from the middle-phase,
|
|
// so it can be slightly below the surface. We can adjust it a little so it is above.
|
|
for (int i = 0; i < 4 && sdf0 < 0.f; ++i) {
|
|
d0 -= 0.5f;
|
|
sdf0 = get_sdf_interpolated(f, pos0 + dir * d0);
|
|
}
|
|
|
|
float sdf1 = get_sdf_interpolated(f, pos0 + dir * d1);
|
|
for (int i = 0; i < 4 && sdf1 > 0.f; ++i) {
|
|
d1 += 0.5f;
|
|
sdf1 = get_sdf_interpolated(f, pos0 + dir * d1);
|
|
}
|
|
|
|
if ((sdf0 > 0) != (sdf1 > 0)) {
|
|
// Binary search
|
|
for (int i = 0; i < iterations; ++i) {
|
|
const float dm = 0.5f * (d0 + d1);
|
|
const float sdf_mid = get_sdf_interpolated(f, pos0 + dir * dm);
|
|
|
|
if ((sdf_mid > 0) != (sdf0 > 0)) {
|
|
sdf1 = sdf_mid;
|
|
d1 = dm;
|
|
} else {
|
|
sdf0 = sdf_mid;
|
|
d0 = dm;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pick distance closest to the surface
|
|
if (Math::abs(sdf0) < Math::abs(sdf1)) {
|
|
return d0;
|
|
} else {
|
|
return d1;
|
|
}
|
|
}
|
|
|
|
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 Implement reverse raycast? (going from inside ground to air, could be useful for undigging)
|
|
|
|
struct RaycastPredicate {
|
|
VoxelLodTerrain *terrain;
|
|
|
|
bool operator()(Vector3i pos) {
|
|
// This is not particularly optimized, but runs fast enough for player raycasts
|
|
VoxelSingleValue defval;
|
|
defval.f = 1.f;
|
|
const VoxelSingleValue v = terrain->get_voxel(pos, VoxelBufferInternal::CHANNEL_SDF, defval);
|
|
return v.f < 0;
|
|
}
|
|
};
|
|
|
|
Ref<VoxelRaycastResult> res;
|
|
|
|
// We use grid-raycast as a middle-phase to roughly detect where the hit will be
|
|
RaycastPredicate predicate = { _terrain };
|
|
Vector3i hit_pos;
|
|
Vector3i prev_pos;
|
|
float hit_distance;
|
|
float hit_distance_prev;
|
|
// Voxels polygonized using marching cubes influence a region centered on their lower corner,
|
|
// and extend up to 0.5 units in all directions.
|
|
//
|
|
// o--------o--------o
|
|
// | A | B | Here voxel B is full, voxels A, C and D are empty.
|
|
// | xxx | Matter will show up at the lower corner of B due to interpolation.
|
|
// | xxxxxxx |
|
|
// o---xxxxxoxxxxx---o
|
|
// | xxxxxxx |
|
|
// | xxx |
|
|
// | C | D |
|
|
// o--------o--------o
|
|
//
|
|
// `voxel_raycast` operates on a discrete grid of cubic voxels, so to account for the smooth interpolation,
|
|
// we may offset the ray so that cubes act as if they were centered on the filtered result.
|
|
const Vector3 offset(0.5, 0.5, 0.5);
|
|
if (voxel_raycast(pos + offset, dir, predicate, max_distance, hit_pos, prev_pos, hit_distance, hit_distance_prev)) {
|
|
// Approximate surface
|
|
|
|
float d = hit_distance;
|
|
|
|
if (_raycast_binary_search_iterations > 0) {
|
|
// This is not particularly optimized, but runs fast enough for player raycasts
|
|
struct VolumeSampler {
|
|
VoxelLodTerrain *terrain;
|
|
|
|
inline float operator()(const Vector3i &pos) const {
|
|
VoxelSingleValue defval;
|
|
defval.f = 1.f;
|
|
const VoxelSingleValue value = terrain->get_voxel(pos, VoxelBufferInternal::CHANNEL_SDF, defval);
|
|
return value.f;
|
|
}
|
|
};
|
|
|
|
VolumeSampler sampler{ _terrain };
|
|
d = hit_distance_prev +
|
|
approximate_distance_to_isosurface_binary_search(sampler, pos + dir * hit_distance_prev, dir,
|
|
hit_distance - hit_distance_prev, _raycast_binary_search_iterations);
|
|
}
|
|
|
|
res.instantiate();
|
|
res->position = hit_pos;
|
|
res->previous_position = prev_pos;
|
|
res->distance_along_ray = d;
|
|
}
|
|
|
|
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(Vector3iUtil::from_floored(center) - Vector3iUtil::create(Math::floor(radius)),
|
|
Vector3iUtil::create(Math::ceil(radius) * 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];
|
|
|
|
if (_terrain->is_full_load_mode_enabled()) {
|
|
preload_box(*data, box, _terrain->get_generator().ptr());
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
_post_edit(box);
|
|
}
|
|
|
|
template <typename Op_T>
|
|
class VoxelToolAsyncEdit : public IThreadedTask {
|
|
public:
|
|
VoxelToolAsyncEdit(Op_T op, std::shared_ptr<VoxelDataLodMap> data) : _op(op), _data(data) {
|
|
_tracker = make_shared_instance<AsyncDependencyTracker>(1);
|
|
}
|
|
|
|
void run(ThreadedTaskContext ctx) override {
|
|
ZN_PROFILE_SCOPE();
|
|
CRASH_COND(_data == nullptr);
|
|
VoxelDataLodMap::Lod &data_lod = _data->lods[0];
|
|
{
|
|
// TODO Prefer a spatial lock?
|
|
// We want blocks inside the edited area to not be accessed by other threads,
|
|
// but this locks the entire map, not just our area. If we used a spatial lock we would only need to lock
|
|
// the map for the duration of `reference_area`.
|
|
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);
|
|
_op();
|
|
}
|
|
_tracker->post_complete();
|
|
}
|
|
|
|
void apply_result() override {}
|
|
std::shared_ptr<AsyncDependencyTracker> get_tracker() {
|
|
return _tracker;
|
|
}
|
|
|
|
private:
|
|
Op_T _op;
|
|
// We reference this just to keep map pointers alive
|
|
std::shared_ptr<VoxelDataLodMap> _data;
|
|
std::shared_ptr<AsyncDependencyTracker> _tracker;
|
|
};
|
|
|
|
void VoxelToolLodTerrain::do_sphere_async(Vector3 center, float radius) {
|
|
ERR_FAIL_COND(_terrain == nullptr);
|
|
|
|
const Box3i box = Box3i(Vector3iUtil::from_floored(center) - Vector3iUtil::create(Math::floor(radius)),
|
|
Vector3iUtil::create(Math::ceil(radius) * 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);
|
|
|
|
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());
|
|
}
|
|
|
|
void VoxelToolLodTerrain::copy(Vector3i pos, Ref<gd::VoxelBuffer> dst, uint8_t channels_mask) const {
|
|
ERR_FAIL_COND(_terrain == nullptr);
|
|
ERR_FAIL_COND(dst.is_null());
|
|
if (channels_mask == 0) {
|
|
channels_mask = (1 << _channel);
|
|
}
|
|
_terrain->copy(pos, dst->get_buffer(), channels_mask);
|
|
}
|
|
|
|
float VoxelToolLodTerrain::get_voxel_f_interpolated(Vector3 position) const {
|
|
ERR_FAIL_COND_V(_terrain == nullptr, 0);
|
|
const int channel = get_channel();
|
|
VoxelLodTerrain *terrain = _terrain;
|
|
// TODO Optimization: is it worth a making a fast-path for this?
|
|
return get_sdf_interpolated(
|
|
[terrain, channel](Vector3i ipos) {
|
|
VoxelSingleValue defval;
|
|
defval.f = 1.f;
|
|
VoxelSingleValue value = terrain->get_voxel(ipos, VoxelBufferInternal::CHANNEL_SDF, defval);
|
|
return value.f;
|
|
},
|
|
position);
|
|
}
|
|
|
|
uint64_t VoxelToolLodTerrain::_get_voxel(Vector3i pos) const {
|
|
ERR_FAIL_COND_V(_terrain == nullptr, 0);
|
|
VoxelSingleValue defval;
|
|
defval.i = 0;
|
|
return _terrain->get_voxel(pos, _channel, defval).i;
|
|
}
|
|
|
|
float VoxelToolLodTerrain::_get_voxel_f(Vector3i pos) const {
|
|
ERR_FAIL_COND_V(_terrain == nullptr, 0);
|
|
VoxelSingleValue defval;
|
|
defval.f = 1.f;
|
|
return _terrain->get_voxel(pos, _channel, defval).f;
|
|
}
|
|
|
|
void VoxelToolLodTerrain::_set_voxel(Vector3i pos, uint64_t v) {
|
|
ERR_FAIL_COND(_terrain == nullptr);
|
|
_terrain->try_set_voxel_without_update(pos, _channel, v);
|
|
}
|
|
|
|
void VoxelToolLodTerrain::_set_voxel_f(Vector3i pos, float v) {
|
|
ERR_FAIL_COND(_terrain == nullptr);
|
|
// TODO Format should be accessible from terrain
|
|
_terrain->try_set_voxel_without_update(pos, _channel, snorm_to_s16(v));
|
|
}
|
|
|
|
void VoxelToolLodTerrain::_post_edit(const Box3i &box) {
|
|
ERR_FAIL_COND(_terrain == nullptr);
|
|
_terrain->post_edit_area(box);
|
|
}
|
|
|
|
int VoxelToolLodTerrain::get_raycast_binary_search_iterations() const {
|
|
return _raycast_binary_search_iterations;
|
|
}
|
|
|
|
void VoxelToolLodTerrain::set_raycast_binary_search_iterations(int iterations) {
|
|
_raycast_binary_search_iterations = math::clamp(iterations, 0, 16);
|
|
}
|
|
|
|
// Turns floating chunks of voxels into rigidbodies:
|
|
// Detects separate groups of connected voxels within a box. Each group fully contained in the box is removed from
|
|
// the source volume, and turned into a rigidbody.
|
|
// This is one way of doing it, I don't know if it's the best way (there is rarely a best way)
|
|
// so there are probably other approaches that could be explored in the future, if they have better performance
|
|
Array separate_floating_chunks(VoxelTool &voxel_tool, Box3i world_box, Node *parent_node, Transform3D transform,
|
|
Ref<VoxelMesher> mesher, Array materials) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
// Checks
|
|
ERR_FAIL_COND_V(mesher.is_null(), Array());
|
|
ERR_FAIL_COND_V(parent_node == nullptr, Array());
|
|
|
|
// Copy source data
|
|
|
|
// TODO Do not assume channel, at the moment it's hardcoded for smooth terrain
|
|
static const int channels_mask = (1 << VoxelBufferInternal::CHANNEL_SDF);
|
|
static const int main_channel = VoxelBufferInternal::CHANNEL_SDF;
|
|
|
|
// TODO We should be able to use `VoxelBufferInternal`, just needs some things exposed
|
|
Ref<gd::VoxelBuffer> source_copy_buffer_ref;
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Copy");
|
|
source_copy_buffer_ref.instantiate();
|
|
source_copy_buffer_ref->create(world_box.size.x, world_box.size.y, world_box.size.z);
|
|
voxel_tool.copy(world_box.pos, source_copy_buffer_ref, channels_mask);
|
|
}
|
|
VoxelBufferInternal &source_copy_buffer = source_copy_buffer_ref->get_buffer();
|
|
|
|
// Label distinct voxel groups
|
|
|
|
static thread_local std::vector<uint8_t> ccl_output;
|
|
ccl_output.resize(Vector3iUtil::get_volume(world_box.size));
|
|
|
|
unsigned int label_count = 0;
|
|
|
|
{
|
|
// TODO Allow to run the algorithm at a different LOD, to trade precision for speed
|
|
ZN_PROFILE_SCOPE_NAMED("CCL scan");
|
|
IslandFinder island_finder;
|
|
island_finder.scan_3d(
|
|
Box3i(Vector3i(), world_box.size),
|
|
[&source_copy_buffer](Vector3i pos) {
|
|
// TODO Can be optimized further with direct access
|
|
return source_copy_buffer.get_voxel_f(pos.x, pos.y, pos.z, main_channel) < 0.f;
|
|
},
|
|
to_span(ccl_output), &label_count);
|
|
}
|
|
|
|
struct Bounds {
|
|
Vector3i min_pos;
|
|
Vector3i max_pos; // inclusive
|
|
bool valid = false;
|
|
};
|
|
|
|
// Compute bounds of each group
|
|
|
|
std::vector<Bounds> bounds_per_label;
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Bounds calculation");
|
|
|
|
// Adding 1 because label 0 is the index for "no label"
|
|
bounds_per_label.resize(label_count + 1);
|
|
|
|
unsigned int ccl_index = 0;
|
|
for (int z = 0; z < world_box.size.z; ++z) {
|
|
for (int x = 0; x < world_box.size.x; ++x) {
|
|
for (int y = 0; y < world_box.size.y; ++y) {
|
|
CRASH_COND(ccl_index >= ccl_output.size());
|
|
const uint8_t label = ccl_output[ccl_index];
|
|
++ccl_index;
|
|
|
|
if (label == 0) {
|
|
continue;
|
|
}
|
|
|
|
CRASH_COND(label >= bounds_per_label.size());
|
|
Bounds &bounds = bounds_per_label[label];
|
|
|
|
if (bounds.valid == false) {
|
|
bounds.min_pos = Vector3i(x, y, z);
|
|
bounds.max_pos = bounds.min_pos;
|
|
bounds.valid = true;
|
|
|
|
} else {
|
|
if (x < bounds.min_pos.x) {
|
|
bounds.min_pos.x = x;
|
|
} else if (x > bounds.max_pos.x) {
|
|
bounds.max_pos.x = x;
|
|
}
|
|
|
|
if (y < bounds.min_pos.y) {
|
|
bounds.min_pos.y = y;
|
|
} else if (y > bounds.max_pos.y) {
|
|
bounds.max_pos.y = y;
|
|
}
|
|
|
|
if (z < bounds.min_pos.z) {
|
|
bounds.min_pos.z = z;
|
|
} else if (z > bounds.max_pos.z) {
|
|
bounds.max_pos.z = z;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Eliminate groups that touch the box border,
|
|
// because that means we can't tell if they are truly hanging in the air or attached to land further away
|
|
|
|
const Vector3i lbmax = world_box.size - Vector3i(1, 1, 1);
|
|
for (unsigned int label = 1; label < bounds_per_label.size(); ++label) {
|
|
CRASH_COND(label >= bounds_per_label.size());
|
|
Bounds &local_bounds = bounds_per_label[label];
|
|
ERR_CONTINUE(!local_bounds.valid);
|
|
|
|
if (local_bounds.min_pos.x == 0 || local_bounds.min_pos.y == 0 || local_bounds.min_pos.z == 0 ||
|
|
local_bounds.max_pos.x == lbmax.x || local_bounds.max_pos.y == lbmax.y ||
|
|
local_bounds.max_pos.z == lbmax.z) {
|
|
//
|
|
local_bounds.valid = false;
|
|
}
|
|
}
|
|
|
|
// Create voxel buffer for each group
|
|
|
|
struct InstanceInfo {
|
|
Ref<gd::VoxelBuffer> voxels;
|
|
Vector3i world_pos;
|
|
unsigned int label;
|
|
};
|
|
std::vector<InstanceInfo> instances_info;
|
|
|
|
const int min_padding = 2; //mesher->get_minimum_padding();
|
|
const int max_padding = 2; //mesher->get_maximum_padding();
|
|
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Extraction");
|
|
|
|
for (unsigned int label = 1; label < bounds_per_label.size(); ++label) {
|
|
CRASH_COND(label >= bounds_per_label.size());
|
|
const Bounds local_bounds = bounds_per_label[label];
|
|
|
|
if (!local_bounds.valid) {
|
|
continue;
|
|
}
|
|
|
|
const Vector3i world_pos = world_box.pos + local_bounds.min_pos - Vector3iUtil::create(min_padding);
|
|
const Vector3i size =
|
|
local_bounds.max_pos - local_bounds.min_pos + Vector3iUtil::create(1 + max_padding + min_padding);
|
|
|
|
// TODO We should be able to use `VoxelBufferInternal`, just needs some things exposed
|
|
Ref<gd::VoxelBuffer> buffer_ref;
|
|
buffer_ref.instantiate();
|
|
buffer_ref->create(size.x, size.y, size.z);
|
|
|
|
// Read voxels from the source volume
|
|
voxel_tool.copy(world_pos, buffer_ref, channels_mask);
|
|
|
|
VoxelBufferInternal &buffer = buffer_ref->get_buffer();
|
|
|
|
// Cleanup padding borders
|
|
const Box3i inner_box(Vector3iUtil::create(min_padding),
|
|
buffer.get_size() - Vector3iUtil::create(min_padding + max_padding));
|
|
Box3i(Vector3i(), buffer.get_size()).difference(inner_box, [&buffer](Box3i box) {
|
|
buffer.fill_area_f(1.f, box.pos, box.pos + box.size, main_channel);
|
|
});
|
|
|
|
// Filter out voxels that don't belong to this label
|
|
for (int z = local_bounds.min_pos.z; z <= local_bounds.max_pos.z; ++z) {
|
|
for (int x = local_bounds.min_pos.x; x <= local_bounds.max_pos.x; ++x) {
|
|
for (int y = local_bounds.min_pos.y; y <= local_bounds.max_pos.y; ++y) {
|
|
const unsigned int ccl_index = Vector3iUtil::get_zxy_index(Vector3i(x, y, z), world_box.size);
|
|
CRASH_COND(ccl_index >= ccl_output.size());
|
|
const uint8_t label2 = ccl_output[ccl_index];
|
|
|
|
if (label2 != 0 && label != label2) {
|
|
buffer.set_voxel_f(1.f, min_padding + x - local_bounds.min_pos.x,
|
|
min_padding + y - local_bounds.min_pos.y, min_padding + z - local_bounds.min_pos.z,
|
|
main_channel);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
instances_info.push_back(InstanceInfo{ buffer_ref, world_pos, label });
|
|
}
|
|
}
|
|
|
|
// Erase voxels from source volume.
|
|
// Must be done after we copied voxels from it.
|
|
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Erasing");
|
|
|
|
voxel_tool.set_channel(main_channel);
|
|
|
|
for (unsigned int instance_index = 0; instance_index < instances_info.size(); ++instance_index) {
|
|
CRASH_COND(instance_index >= instances_info.size());
|
|
const InstanceInfo info = instances_info[instance_index];
|
|
ERR_CONTINUE(info.voxels.is_null());
|
|
|
|
voxel_tool.sdf_stamp_erase(info.voxels, info.world_pos);
|
|
}
|
|
}
|
|
|
|
// Create instances
|
|
|
|
Array nodes;
|
|
|
|
{
|
|
ZN_PROFILE_SCOPE_NAMED("Remeshing and instancing");
|
|
|
|
for (unsigned int instance_index = 0; instance_index < instances_info.size(); ++instance_index) {
|
|
CRASH_COND(instance_index >= instances_info.size());
|
|
const InstanceInfo info = instances_info[instance_index];
|
|
ERR_CONTINUE(info.voxels.is_null());
|
|
|
|
CRASH_COND(info.label >= bounds_per_label.size());
|
|
const Bounds local_bounds = bounds_per_label[info.label];
|
|
ERR_CONTINUE(!local_bounds.valid);
|
|
|
|
// DEBUG
|
|
// print_line(String("--- Instance {0}").format(varray(instance_index)));
|
|
// for (int z = 0; z < info.voxels->get_size().z; ++z) {
|
|
// for (int x = 0; x < info.voxels->get_size().x; ++x) {
|
|
// String s;
|
|
// for (int y = 0; y < info.voxels->get_size().y; ++y) {
|
|
// float sdf = info.voxels->get_voxel_f(x, y, z, VoxelBuffer::CHANNEL_SDF);
|
|
// if (sdf < -0.1f) {
|
|
// s += "X ";
|
|
// } else if (sdf < 0.f) {
|
|
// s += "x ";
|
|
// } else {
|
|
// s += "- ";
|
|
// }
|
|
// }
|
|
// print_line(s);
|
|
// }
|
|
// print_line("//");
|
|
// }
|
|
|
|
const Transform3D local_transform(Basis(), info.world_pos);
|
|
|
|
for (int i = 0; i < materials.size(); ++i) {
|
|
Ref<ShaderMaterial> sm = materials[i];
|
|
if (sm.is_valid() && sm->get_shader().is_valid() &&
|
|
sm->get_shader()->has_param(VoxelStringNames::get_singleton().u_block_local_transform)) {
|
|
// That parameter should have a valid default value matching the local transform relative to the
|
|
// volume, which is usually per-instance, but in Godot 3 we have no such feature, so we have to
|
|
// duplicate.
|
|
sm = sm->duplicate(false);
|
|
sm->set_shader_param(VoxelStringNames::get_singleton().u_block_local_transform, local_transform);
|
|
materials[i] = sm;
|
|
}
|
|
}
|
|
|
|
Ref<Mesh> mesh = mesher->build_mesh(info.voxels, materials);
|
|
// The mesh is not supposed to be null,
|
|
// because we build these buffers from connected groups that had negative SDF.
|
|
ERR_CONTINUE(mesh.is_null());
|
|
|
|
if (is_mesh_empty(**mesh)) {
|
|
continue;
|
|
}
|
|
|
|
// DEBUG
|
|
// {
|
|
// Ref<VoxelBlockSerializer> serializer;
|
|
// serializer.instance();
|
|
// Ref<StreamPeerBuffer> peer;
|
|
// peer.instance();
|
|
// serializer->serialize(peer, info.voxels, false);
|
|
// String fpath = String("debug_data/split_dump_{0}.bin").format(varray(instance_index));
|
|
// FileAccess *f = FileAccess::open(fpath, FileAccess::WRITE);
|
|
// PoolByteArray bytes = peer->get_data_array();
|
|
// PoolByteArray::Read bytes_read = bytes.read();
|
|
// f->store_buffer(bytes_read.ptr(), bytes.size());
|
|
// f->close();
|
|
// memdelete(f);
|
|
// }
|
|
|
|
// TODO Option to make multiple convex shapes
|
|
// TODO Use the fast way. This is slow because of the internal TriangleMesh thing.
|
|
// TODO Don't create a body if the mesh has no triangles
|
|
Ref<Shape3D> shape = mesh->create_convex_shape();
|
|
ERR_CONTINUE(shape.is_null());
|
|
CollisionShape3D *collision_shape = memnew(CollisionShape3D);
|
|
collision_shape->set_shape(shape);
|
|
// Center the shape somewhat, because Godot is confusing node origin with center of mass
|
|
const Vector3i size =
|
|
local_bounds.max_pos - local_bounds.min_pos + Vector3iUtil::create(1 + max_padding + min_padding);
|
|
const Vector3 offset = -Vector3(size) * 0.5f;
|
|
collision_shape->set_position(offset);
|
|
|
|
RigidDynamicBody3D *rigid_body = memnew(RigidDynamicBody3D);
|
|
rigid_body->set_transform(transform * local_transform.translated(-offset));
|
|
rigid_body->add_child(collision_shape);
|
|
rigid_body->set_freeze_mode(RigidDynamicBody3D::FREEZE_MODE_KINEMATIC);
|
|
rigid_body->set_freeze_enabled(true);
|
|
|
|
// Switch to rigid after a short time to workaround clipping with terrain,
|
|
// because colliders are updated asynchronously
|
|
Timer *timer = memnew(Timer);
|
|
timer->set_wait_time(0.2);
|
|
timer->set_one_shot(true);
|
|
timer->connect("timeout", callable_mp(rigid_body, &RigidDynamicBody3D::set_freeze_enabled), varray(false));
|
|
// Cannot use start() here because it requires to be inside the SceneTree,
|
|
// and we don't know if it will be after we add to the parent.
|
|
timer->set_autostart(true);
|
|
rigid_body->add_child(timer);
|
|
|
|
MeshInstance3D *mesh_instance = memnew(MeshInstance3D);
|
|
mesh_instance->set_mesh(mesh);
|
|
mesh_instance->set_position(offset);
|
|
rigid_body->add_child(mesh_instance);
|
|
|
|
parent_node->add_child(rigid_body);
|
|
|
|
nodes.append(rigid_body);
|
|
}
|
|
}
|
|
|
|
return nodes;
|
|
}
|
|
|
|
Array VoxelToolLodTerrain::separate_floating_chunks(AABB world_box, Node *parent_node) {
|
|
ERR_FAIL_COND_V(_terrain == nullptr, Array());
|
|
ERR_FAIL_COND_V(!math::is_valid_size(world_box.size), Array());
|
|
Ref<VoxelMesher> mesher = _terrain->get_mesher();
|
|
Array materials;
|
|
materials.append(_terrain->get_material());
|
|
const Box3i int_world_box(
|
|
Vector3iUtil::from_floored(world_box.position), Vector3iUtil::from_ceiled(world_box.size));
|
|
return zylann::voxel::separate_floating_chunks(
|
|
*this, int_world_box, parent_node, _terrain->get_global_transform(), mesher, materials);
|
|
}
|
|
|
|
void VoxelToolLodTerrain::_bind_methods() {
|
|
ClassDB::bind_method(D_METHOD("set_raycast_binary_search_iterations", "iterations"),
|
|
&VoxelToolLodTerrain::set_raycast_binary_search_iterations);
|
|
ClassDB::bind_method(D_METHOD("get_raycast_binary_search_iterations"),
|
|
&VoxelToolLodTerrain::get_raycast_binary_search_iterations);
|
|
ClassDB::bind_method(
|
|
D_METHOD("get_voxel_f_interpolated", "position"), &VoxelToolLodTerrain::get_voxel_f_interpolated);
|
|
ClassDB::bind_method(
|
|
D_METHOD("separate_floating_chunks", "box", "parent_node"), &VoxelToolLodTerrain::separate_floating_chunks);
|
|
ClassDB::bind_method(D_METHOD("do_sphere_async", "center", "radius"), &VoxelToolLodTerrain::do_sphere_async);
|
|
}
|
|
|
|
} // namespace zylann::voxel
|