Implement LOD transitions in VoxelLodTerrain

- VoxelBlock contains 6 optional transition meshes
- VoxelLodTerrain calculates transition masks when block visibility changes
- VoxelMesher can now specify different min and max paddings
- Fix Cube::SIDE_POSITIVE/NEGATIVE_X/Y/Z enum not matching Cube::g_side_normals
- Convert transition masks to make up for the Cube:: inconsistency for now
This commit is contained in:
Marc Gilleron 2019-12-31 16:48:46 +00:00
parent 44f84a9422
commit ebc6be35d1
19 changed files with 446 additions and 175 deletions

View File

@ -54,10 +54,14 @@ const int g_side_quad_triangles[SIDE_COUNT][6] = {
//const unsigned int g_side_sign[SIDE_COUNT] = { 0, 1, 0, 1, 0, 1 };
const Vector3i g_side_normals[SIDE_COUNT] = {
// TODO Wrong! Left should be -X
Vector3i(1, 0, 0), // LEFT
Vector3i(-1, 0, 0), // RIGHT
Vector3i(0, -1, 0), // BOTTOM
Vector3i(0, 1, 0), // TOP
// TODO Wrong! Front should be -Z
Vector3i(0, 0, -1), // BACK
Vector3i(0, 0, 1), // FRONT
};
@ -161,5 +165,4 @@ const Vector3i g_moore_neighboring_3d[MOORE_NEIGHBORING_3D_COUNT] = {
Vector3i(1, 1, 1),
};
} // namespace CubeTables
} // namespace Cube

View File

@ -18,14 +18,14 @@ enum Side {
SIDE_COUNT
};
// Alias to the above for clarity, using OpenGL axis convention
// Alias to the above for clarity, fixing some interpretation problems regarding the side_normals table...
enum SideAxis {
SIDE_NEGATIVE_X = 0,
SIDE_POSITIVE_X,
SIDE_POSITIVE_X = 0,
SIDE_NEGATIVE_X,
SIDE_NEGATIVE_Y,
SIDE_POSITIVE_Y,
SIDE_POSITIVE_Z,
SIDE_NEGATIVE_Z
SIDE_NEGATIVE_Z,
SIDE_POSITIVE_Z
};
// Index convention used in some lookup tables

View File

@ -35,7 +35,9 @@ inline bool is_transparent(const VoxelLibrary &lib, int voxel_id) {
VoxelMesherBlocky::VoxelMesherBlocky() :
_baked_occlusion_darkness(0.8),
_bake_occlusion(true) {}
_bake_occlusion(true) {
set_padding(PADDING, PADDING);
}
void VoxelMesherBlocky::set_library(Ref<VoxelLibrary> library) {
_library = library;
@ -54,14 +56,12 @@ void VoxelMesherBlocky::set_occlusion_enabled(bool enable) {
_bake_occlusion = enable;
}
void VoxelMesherBlocky::build(VoxelMesher::Output &output, const VoxelBuffer &buffer, int padding) {
void VoxelMesherBlocky::build(VoxelMesher::Output &output, const VoxelBuffer &buffer) {
//uint64_t time_before = OS::get_singleton()->get_ticks_usec();
ERR_FAIL_COND(_library.is_null());
ERR_FAIL_COND(padding < MINIMUM_PADDING);
const int channel = VoxelBuffer::CHANNEL_TYPE;
ERR_FAIL_COND(_library.is_null());
const VoxelLibrary &library = **_library;
for (unsigned int i = 0; i < MAX_MATERIALS; ++i) {
@ -87,8 +87,8 @@ void VoxelMesherBlocky::build(VoxelMesher::Output &output, const VoxelBuffer &bu
// => Could be implemented in a separate class?
// Data must be padded, hence the off-by-one
Vector3i min = Vector3i(padding);
Vector3i max = buffer.get_size() - Vector3i(padding);
Vector3i min = Vector3i(get_minimum_padding());
Vector3i max = buffer.get_size() - Vector3i(get_maximum_padding());
int index_offsets[MAX_MATERIALS] = { 0 };
@ -408,10 +408,6 @@ void VoxelMesherBlocky::build(VoxelMesher::Output &output, const VoxelBuffer &bu
//print_line(String("P: {0}, M: {1}, C: {2}").format(varray(time_prep, time_meshing, time_commit)));
}
int VoxelMesherBlocky::get_minimum_padding() const {
return MINIMUM_PADDING;
}
VoxelMesher *VoxelMesherBlocky::clone() {
VoxelMesherBlocky *c = memnew(VoxelMesherBlocky);
c->set_library(_library);

View File

@ -14,7 +14,7 @@ class VoxelMesherBlocky : public VoxelMesher {
public:
static const unsigned int MAX_MATERIALS = 8; // Arbitrary. Tweak if needed.
static const int MINIMUM_PADDING = 1;
static const int PADDING = 1;
VoxelMesherBlocky();
@ -27,8 +27,7 @@ public:
void set_occlusion_enabled(bool enable);
bool get_occlusion_enabled() const { return _bake_occlusion; }
void build(VoxelMesher::Output &output, const VoxelBuffer &voxels, int padding) override;
int get_minimum_padding() const override;
void build(VoxelMesher::Output &output, const VoxelBuffer &voxels) override;
VoxelMesher *clone() override;

View File

@ -1429,6 +1429,7 @@ void polygonize_volume_directly(const VoxelBuffer &voxels, Vector3i min, Vector3
#define BUILD_OCTREE_BOTTOM_UP
VoxelMesherDMC::VoxelMesherDMC() {
set_padding(PADDING, PADDING);
}
void VoxelMesherDMC::set_mesh_mode(MeshMode mode) {
@ -1463,7 +1464,7 @@ VoxelMesherDMC::SeamMode VoxelMesherDMC::get_seam_mode() const {
return _seam_mode;
}
void VoxelMesherDMC::build(VoxelMesher::Output &output, const VoxelBuffer &voxels, int padding) {
void VoxelMesherDMC::build(VoxelMesher::Output &output, const VoxelBuffer &voxels) {
// Requirements:
// - Voxel data must be padded
@ -1476,15 +1477,13 @@ void VoxelMesherDMC::build(VoxelMesher::Output &output, const VoxelBuffer &voxel
return;
}
ERR_FAIL_COND(padding < MINIMUM_PADDING);
const Vector3i buffer_size = voxels.get_size();
// Taking previous power of two because the algorithm uses an integer cubic octree, and data should be padded
int chunk_size = previous_power_of_2(MIN(MIN(buffer_size.x, buffer_size.y), buffer_size.z));
ERR_FAIL_COND(voxels.get_size().x < chunk_size + padding * 2);
ERR_FAIL_COND(voxels.get_size().y < chunk_size + padding * 2);
ERR_FAIL_COND(voxels.get_size().z < chunk_size + padding * 2);
ERR_FAIL_COND(voxels.get_size().x < chunk_size + PADDING * 2);
ERR_FAIL_COND(voxels.get_size().y < chunk_size + PADDING * 2);
ERR_FAIL_COND(voxels.get_size().z < chunk_size + PADDING * 2);
// TODO Option for this in case LOD is not used
bool skirts_enabled = _seam_mode == SEAM_MARCHING_SQUARE_SKIRTS;
@ -1498,7 +1497,7 @@ void VoxelMesherDMC::build(VoxelMesher::Output &output, const VoxelBuffer &voxel
// So we can't improve this further until Godot's API gives us that possibility, or other approaches like skirts need to be taken.
// Construct an intermediate to handle padding transparently
dmc::VoxelAccess voxels_access(voxels, Vector3i(padding));
dmc::VoxelAccess voxels_access(voxels, Vector3i(PADDING));
real_t time_before = OS::get_singleton()->get_ticks_usec();
@ -1571,7 +1570,7 @@ void VoxelMesherDMC::build(VoxelMesher::Output &output, const VoxelBuffer &voxel
// This is essentially regular marching cubes.
time_before = OS::get_singleton()->get_ticks_usec();
dmc::polygonize_volume_directly(voxels, Vector3i(padding), Vector3i(chunk_size), _mesh_builder, skirts_enabled);
dmc::polygonize_volume_directly(voxels, Vector3i(PADDING), Vector3i(chunk_size), _mesh_builder, skirts_enabled);
_stats.meshing_time = OS::get_singleton()->get_ticks_usec() - time_before;
}
@ -1591,10 +1590,6 @@ void VoxelMesherDMC::build(VoxelMesher::Output &output, const VoxelBuffer &voxel
}
}
int VoxelMesherDMC::get_minimum_padding() const {
return MINIMUM_PADDING;
}
VoxelMesher *VoxelMesherDMC::clone() {
VoxelMesherDMC *c = memnew(VoxelMesherDMC);
c->set_mesh_mode(_mesh_mode);

View File

@ -65,7 +65,7 @@ struct DualGrid {
class VoxelMesherDMC : public VoxelMesher {
GDCLASS(VoxelMesherDMC, VoxelMesher)
public:
static const int MINIMUM_PADDING = 2;
static const int PADDING = 2;
enum MeshMode {
MESH_NORMAL,
@ -101,8 +101,7 @@ public:
void set_seam_mode(SeamMode mode);
SeamMode get_seam_mode() const;
void build(VoxelMesher::Output &output, const VoxelBuffer &voxels, int padding) override;
int get_minimum_padding() const override;
void build(VoxelMesher::Output &output, const VoxelBuffer &voxels) override;
Dictionary get_statistics() const;

View File

@ -69,13 +69,11 @@ inline int8_t increase(int8_t v, int8_t a) {
} // namespace
int VoxelMesherMC::get_minimum_padding() const {
return MINIMUM_PADDING;
VoxelMesherMC::VoxelMesherMC() {
set_padding(MIN_PADDING, MAX_PADDING);
}
void VoxelMesherMC::build(VoxelMesher::Output &output, const VoxelBuffer &voxels, int padding) {
ERR_FAIL_COND(padding < MINIMUM_PADDING);
void VoxelMesherMC::build(VoxelMesher::Output &output, const VoxelBuffer &voxels) {
int channel = VoxelBuffer::CHANNEL_ISOLEVEL;
@ -141,9 +139,8 @@ void VoxelMesherMC::build_internal(const VoxelBuffer &voxels, unsigned int chann
// The algorithm works with a 2x2 kernel and needs extra neighbors for normals,
// so it looks 1 voxel away in negative axes, and 2 voxels away in positive axes.
Vector3i pos;
// Could have been offset by 1, but using 2 instead because the VoxelMesher API expects a symetric padding at the moment
Vector3i min_pos(2);
Vector3i max_pos(block_size - Vector3i(2));
Vector3i min_pos(get_minimum_padding());
Vector3i max_pos(block_size - Vector3i(get_maximum_padding()));
if (_seam_mode == SEAM_OVERLAP) {
// When this is enabled, the algorithm may detect if it's on a border,
@ -440,8 +437,7 @@ VoxelMesherMC::ReuseCell &VoxelMesherMC::get_reuse_cell(Vector3i pos) {
}
void VoxelMesherMC::emit_vertex(Vector3 primary, Vector3 normal) {
// Could have been offset by 1, but using 2 instead because the VoxelMesher API expects a symetric padding at the moment
_output_vertices.push_back(primary - Vector3(MINIMUM_PADDING, MINIMUM_PADDING, MINIMUM_PADDING));
_output_vertices.push_back(primary - Vector3(MIN_PADDING, MIN_PADDING, MIN_PADDING));
_output_normals.push_back(normal);
}

View File

@ -4,20 +4,22 @@
#include "../voxel_mesher.h"
// Simple marching cubes.
// Implementation is simplified from Transvoxel.
// Implementation is simplified from old Transvoxel code.
class VoxelMesherMC : public VoxelMesher {
GDCLASS(VoxelMesherMC, VoxelMesher)
public:
static const int MINIMUM_PADDING = 2;
static const int MIN_PADDING = 1;
static const int MAX_PADDING = 2;
enum SeamMode {
SEAM_NONE,
SEAM_OVERLAP
};
void build(VoxelMesher::Output &output, const VoxelBuffer &voxels, int padding) override;
int get_minimum_padding() const override;
VoxelMesherMC();
void build(VoxelMesher::Output &output, const VoxelBuffer &voxels) override;
void set_seam_mode(SeamMode mode);
SeamMode get_seam_mode() const;

View File

@ -94,28 +94,6 @@ inline Vector3 get_secondary_position(Vector3 primary, Vector3 normal, int lod,
return primary + delta;
}
// TODO Use this to prevent some cases of null normals
inline Vector3 get_gradient_normal(uint8_t left, uint8_t right, uint8_t bottom, uint8_t top, uint8_t back, uint8_t front, uint8_t middle) {
float gx, gy, gz;
if (left == right && bottom == top && back == front) {
// Sided gradient, but can't be zero
float m = tof(tos(middle));
gx = tof(tos(left)) - m;
gy = tof(tos(bottom)) - m;
gz = tof(tos(back)) - m;
} else {
// Symetric gradient
gx = tof(tos(left)) - tof(tos(right));
gy = tof(tos(bottom)) - tof(tos(top));
gz = tof(tos(back)) - tof(tos(front));
}
return Vector3(gx, gy, gz).normalized();
}
inline uint8_t get_border_mask(const Vector3i &pos, const Vector3i &min_pos, const Vector3i &max_pos) {
uint8_t mask = 0;
@ -143,8 +121,8 @@ inline uint8_t get_border_mask(const Vector3i &pos, const Vector3i &min_pos, con
} // namespace
int VoxelMesherTransvoxel::get_minimum_padding() const {
return MINIMUM_PADDING;
VoxelMesherTransvoxel::VoxelMesherTransvoxel() {
set_padding(MIN_PADDING, MAX_PADDING);
}
void VoxelMesherTransvoxel::clear_output() {
@ -177,7 +155,7 @@ void VoxelMesherTransvoxel::fill_surface_arrays(Array &arrays) {
arrays[Mesh::ARRAY_INDEX] = indices;
}
void VoxelMesherTransvoxel::build(VoxelMesher::Output &output, const VoxelBuffer &voxels, int padding) {
void VoxelMesherTransvoxel::build(VoxelMesher::Output &output, const VoxelBuffer &voxels) {
int channel = VoxelBuffer::CHANNEL_ISOLEVEL;
@ -187,23 +165,32 @@ void VoxelMesherTransvoxel::build(VoxelMesher::Output &output, const VoxelBuffer
// Once capacity is big enough, no more memory should be allocated
clear_output();
ERR_FAIL_COND(padding < MINIMUM_PADDING);
build_internal(voxels, channel);
// OS::get_singleton()->print("vertices: %i, normals: %i, indices: %i\n",
// m_output_vertices.size(),
// m_output_normals.size(),
// m_output_indices.size());
if (_output_vertices.size() == 0) {
// The mesh can be empty
return;
}
Array arrays;
fill_surface_arrays(arrays);
Array regular_arrays;
fill_surface_arrays(regular_arrays);
output.surfaces.push_back(regular_arrays);
for (int dir = 0; dir < Cube::SIDE_COUNT; ++dir) {
clear_output();
build_transition(voxels, channel, dir);
if (_output_vertices.size() == 0) {
continue;
}
Array transition_arrays;
fill_surface_arrays(transition_arrays);
output.transition_surfaces[dir].push_back(transition_arrays);
}
output.surfaces.push_back(arrays);
output.primitive_type = Mesh::PRIMITIVE_TRIANGLES;
output.compression_flags = MESH_COMPRESSION_FLAGS;
}
@ -270,15 +257,15 @@ void VoxelMesherTransvoxel::build_internal(const VoxelBuffer &voxels, unsigned i
}
const Vector3i block_size = voxels.get_size();
const Vector3i block_size_without_padding = block_size - 3 * PAD;
const Vector3i block_size_without_padding = block_size - 3 * Vector3i(MIN_PADDING);
// Prepare vertex reuse cache
reset_reuse_cells(block_size);
// We iterate 2x2 voxel groups, which the paper calls "cells".
// We also reach one voxel further to compute normals, so we adjust the iterated area
const Vector3i min_pos = PAD;
const Vector3i max_pos = block_size - Vector3i(1) - PAD;
const Vector3i min_pos = Vector3i(MIN_PADDING);
const Vector3i max_pos = block_size - Vector3i(MAX_PADDING);
const Vector3i max_pos_c = max_pos - Vector3i(1);
// TODO Change the Mesher API to allow different min and max paddings. Here 1 on min, 2 on max
// TODO Also abstract positions with padding, it can get quite confusing when one is used but not the other...
@ -433,10 +420,11 @@ void VoxelMesherTransvoxel::build_internal(const VoxelBuffer &voxels, unsigned i
// TODO Implement surface shifting interpolation (see other places we interpolate too).
// See issue https://github.com/Zylann/godot_voxel/issues/60
// Seen in the paper, it fixes "steps" between LODs on flat surfaces.
// It is using a binary search through higher lods to find the zero-crossing edge.
// I did not do it here, because our data model is such that when we have low-resolution voxels,
// we cannot just have a look at the high-res ones, because they are not in memory.
// However, it might be possible on low-res blocks bordering high-res ones due to neighboring rules,
// or with a different storage strategy.
// or by falling back on the generator that was used to produce the volume.
Vector3 primary = p0.to_vec3() * t0 + p1.to_vec3() * t1;
Vector3 normal = corner_normals[v0] * t0 + corner_normals[v1] * t1;
@ -655,7 +643,7 @@ void VoxelMesherTransvoxel::build_transition(const VoxelBuffer &p_voxels, unsign
}
const Vector3i block_size = p_voxels.get_size();
const Vector3i block_size_without_padding = block_size - 3 * PAD;
const Vector3i block_size_without_padding = block_size - Vector3i(MIN_PADDING + MAX_PADDING);
//const Vector3i half_block_size = block_size / 2;
ERR_FAIL_COND(block_size.x < 3);
@ -676,8 +664,8 @@ void VoxelMesherTransvoxel::build_transition(const VoxelBuffer &p_voxels, unsign
// This represents the actual box of voxels we are working on.
// It also represents positions of the minimum and maximum vertices that can be generated.
// Padding is present to allow reaching 1 voxel further for calculating normals
const Vector3i min_pos = PAD;
const Vector3i max_pos = block_size - Vector3i(1) - PAD;
const Vector3i min_pos = Vector3i(MIN_PADDING);
const Vector3i max_pos = block_size - Vector3i(MAX_PADDING);
int axis_x, axis_y;
L::get_face_axes(axis_x, axis_y, direction);
@ -694,7 +682,7 @@ void VoxelMesherTransvoxel::build_transition(const VoxelBuffer &p_voxels, unsign
for (int fy = min_fpos_y; fy < max_fpos_y; fy += 2) {
for (int fx = min_fpos_x; fx < max_fpos_x; fx += 2) {
const int fz = PAD.x;
const int fz = MIN_PADDING;
const VoxelBuffer &fvoxels = p_voxels;
@ -719,8 +707,6 @@ void VoxelMesherTransvoxel::build_transition(const VoxelBuffer &p_voxels, unsign
// | | |
// 0---1---2
// TODO Double-check positions and transforms. I understand what's needed (contrary to what's next ._.) but it's boring to do, so I botched it
// Full-resolution samples 0..8
for (unsigned int i = 0; i < 9; ++i) {
cell_samples[i] = tos(get_voxel(fvoxels, cell_positions[i], channel));
@ -991,8 +977,8 @@ int VoxelMesherTransvoxel::emit_vertex(Vector3 primary, Vector3 normal, uint16_t
int vi = _output_vertices.size();
// TODO Unpad positions in calling code, as it may simplify border offset
primary -= PAD.to_vec3();
secondary -= PAD.to_vec3();
primary -= Vector3(MIN_PADDING, MIN_PADDING, MIN_PADDING);
secondary -= Vector3(MIN_PADDING, MIN_PADDING, MIN_PADDING);
_output_vertices.push_back(primary);
_output_normals.push_back(normal);

View File

@ -10,10 +10,12 @@ class VoxelMesherTransvoxel : public VoxelMesher {
GDCLASS(VoxelMesherTransvoxel, VoxelMesher)
public:
static const int MINIMUM_PADDING = 2;
static const int MIN_PADDING = 1;
static const int MAX_PADDING = 2;
void build(VoxelMesher::Output &output, const VoxelBuffer &voxels, int padding) override;
int get_minimum_padding() const override;
VoxelMesherTransvoxel();
void build(VoxelMesher::Output &output, const VoxelBuffer &voxels) override;
VoxelMesher *clone() override;
@ -54,8 +56,6 @@ private:
void fill_surface_arrays(Array &arrays);
private:
const Vector3i PAD = Vector3i(1, 1, 1);
FixedArray<std::vector<ReuseCell>, 2> _cache;
FixedArray<std::vector<ReuseTransitionCell>, 2> _cache_2d;
Vector3i _block_size;

View File

@ -5,7 +5,7 @@ Ref<Mesh> VoxelMesher::build_mesh(Ref<VoxelBuffer> voxels) {
ERR_FAIL_COND_V(voxels.is_null(), Ref<ArrayMesh>());
Output output;
build(output, **voxels, get_minimum_padding());
build(output, **voxels);
if (output.surfaces.empty()) {
return Ref<ArrayMesh>();
@ -21,11 +21,24 @@ Ref<Mesh> VoxelMesher::build_mesh(Ref<VoxelBuffer> voxels) {
return mesh;
}
void VoxelMesher::build(Output &output, const VoxelBuffer &voxels, int padding) {
void VoxelMesher::build(Output &output, const VoxelBuffer &voxels) {
ERR_PRINT("Not implemented");
}
int VoxelMesher::get_minimum_padding() const {
return 0;
return _minimum_padding;
}
int VoxelMesher::get_maximum_padding() const {
return _maximum_padding;
}
void VoxelMesher::set_padding(int minimum, int maximum) {
CRASH_COND(minimum < 0);
CRASH_COND(maximum < 0);
_minimum_padding = minimum;
_maximum_padding = maximum;
}
VoxelMesher *VoxelMesher::clone() {
@ -38,4 +51,5 @@ void VoxelMesher::_bind_methods() {
// Useful for testing the different meshers.
ClassDB::bind_method(D_METHOD("build_mesh", "voxel_buffer"), &VoxelMesher::build_mesh);
ClassDB::bind_method(D_METHOD("get_minimum_padding"), &VoxelMesher::get_minimum_padding);
ClassDB::bind_method(D_METHOD("get_maximum_padding"), &VoxelMesher::get_maximum_padding);
}

View File

@ -1,6 +1,8 @@
#ifndef VOXEL_MESHER_H
#define VOXEL_MESHER_H
#include "../cube_tables.h"
#include "../util/fixed_array.h"
#include "../voxel_buffer.h"
#include <scene/resources/mesh.h>
@ -10,20 +12,31 @@ public:
struct Output {
// Each surface correspond to a different material
Vector<Array> surfaces;
FixedArray<Vector<Array>, Cube::SIDE_COUNT> transition_surfaces;
Mesh::PrimitiveType primitive_type = Mesh::PRIMITIVE_TRIANGLES;
unsigned int compression_flags = Mesh::ARRAY_COMPRESS_DEFAULT;
};
virtual void build(Output &output, const VoxelBuffer &voxels, int padding);
virtual int get_minimum_padding() const;
virtual void build(Output &output, const VoxelBuffer &voxels);
// Must be cloneable so can be used by more than one thread
// Get how many neighbor voxels need to be accessed around the meshed area.
// If this is not respected, the mesher might produce seams at the edges, or an error
int get_minimum_padding() const;
int get_maximum_padding() const;
// Must be cloneable so can be duplicated for use by more than one thread
virtual VoxelMesher *clone();
Ref<Mesh> build_mesh(Ref<VoxelBuffer> voxels);
protected:
static void _bind_methods();
void set_padding(int minimum, int maximum);
private:
int _minimum_padding = 0;
int _maximum_padding = 0;
};
#endif // VOXEL_MESHER_H

View File

@ -47,6 +47,21 @@ VoxelBlock *VoxelBlock::create(Vector3i bpos, Ref<VoxelBuffer> buffer, unsigned
block->_position_in_voxels = bpos * (size << p_lod_index);
block->voxels = buffer;
#ifdef VOXEL_DEBUG_LOD_MATERIALS
Ref<SpatialMaterial> debug_material;
debug_material.instance();
int checker = (bpos.x + bpos.y + bpos.z) & 1;
Color debug_color = Color(0.8, 0.4, 0.8).linear_interpolate(Color(0.0, 0.0, 0.5), static_cast<float>(p_lod_index) / 8.f);
debug_color = debug_color.lightened(checker * 0.1f);
debug_material->set_albedo(debug_color);
block->_debug_material = debug_material;
Ref<SpatialMaterial> debug_transition_material;
debug_transition_material.instance();
debug_transition_material->set_albedo(Color(1, 1, 0));
block->_debug_transition_material = debug_transition_material;
#endif
return block;
}
@ -68,6 +83,8 @@ void VoxelBlock::set_mesh(Ref<Mesh> mesh, Spatial *node, bool generate_collision
Ref<World> world = node->get_world();
ERR_FAIL_COND(world.is_null());
Transform transform(Basis(), _position_in_voxels.to_vec3());
if (!_mesh_instance.is_valid()) {
// Create instance if it doesn't exist
_mesh_instance.create();
@ -75,12 +92,17 @@ void VoxelBlock::set_mesh(Ref<Mesh> mesh, Spatial *node, bool generate_collision
_mesh_instance.set_visible(_visible);
}
Transform transform(Basis(), _position_in_voxels.to_vec3());
_mesh_instance.set_mesh(mesh);
_mesh_instance.set_transform(transform);
// TODO The day VoxelTerrain becomes a Spatial, this transform will need to be updatable separately
if (_shader_material.is_valid()) {
_mesh_instance.set_material_override(_shader_material);
}
#ifdef VOXEL_DEBUG_LOD_MATERIALS
_mesh_instance.set_material_override(_debug_material);
#endif
if (generate_collision) {
Ref<Shape> shape = create_concave_polygon_shape(surface_arrays);
@ -116,6 +138,42 @@ void VoxelBlock::set_mesh(Ref<Mesh> mesh, Spatial *node, bool generate_collision
// }
}
void VoxelBlock::set_transition_mesh(Ref<Mesh> mesh, int side, Ref<World> world) {
DirectMeshInstance &mesh_instance = _transition_mesh_instances[side];
if (mesh.is_valid()) {
ERR_FAIL_COND(world.is_null());
if (!mesh_instance.is_valid()) {
// Create instance if it doesn't exist
mesh_instance.create();
mesh_instance.set_world(*world);
mesh_instance.set_visible(_visible && _is_transition_visible(side));
}
Transform transform(Basis(), _position_in_voxels.to_vec3());
mesh_instance.set_mesh(mesh);
mesh_instance.set_transform(transform);
if (_shader_material.is_valid()) {
mesh_instance.set_material_override(_shader_material);
}
#ifdef VOXEL_DEBUG_LOD_MATERIALS
mesh_instance.set_material_override(_debug_transition_material);
#endif
} else {
if (mesh_instance.is_valid()) {
// Delete instance if it exists
mesh_instance.destroy();
}
}
}
bool VoxelBlock::has_mesh() const {
return _mesh_instance.get_mesh().is_valid();
}
@ -132,6 +190,11 @@ void VoxelBlock::set_world(World *world) {
if (_mesh_instance.is_valid()) {
_mesh_instance.set_world(world);
}
for (int i = 0; i < _transition_mesh_instances.size(); ++i) {
if (_transition_mesh_instances[i].is_valid()) {
_transition_mesh_instances[i].set_world(world);
}
}
if (_static_body.is_valid()) {
_static_body.set_world(world);
}
@ -153,11 +216,63 @@ void VoxelBlock::_set_visible(bool visible) {
if (_mesh_instance.is_valid()) {
_mesh_instance.set_visible(visible);
}
for (int dir = 0; dir < _transition_mesh_instances.size(); ++dir) {
if (_transition_mesh_instances[dir].is_valid()) {
_transition_mesh_instances[dir].set_visible(visible & _is_transition_visible(dir));
}
}
if (_static_body.is_valid()) {
_static_body.set_shape_enabled(0, visible);
}
}
void VoxelBlock::set_shader_material(Ref<ShaderMaterial> material) {
_shader_material = material;
}
//void VoxelBlock::set_transition_bit(uint8_t side, bool value) {
// CRASH_COND(side >= Cube::SIDE_COUNT);
// uint32_t m = _transition_mask;
// if (value) {
// m |= (1 << side);
// } else {
// m &= ~(1 << side);
// }
// set_transition_mask(m);
//}
void VoxelBlock::set_transition_mask(uint8_t m) {
CRASH_COND(m >= (1 << Cube::SIDE_COUNT));
uint8_t diff = _transition_mask ^ m;
if (diff == 0) {
return;
}
_transition_mask = m;
if (_shader_material.is_valid()) {
// TODO Needs translation here, because Cube:: tables use slightly different order...
// We may get rid of this once cube tables respects -x+x-y+y-z+z order
uint8_t bits[Cube::SIDE_COUNT];
for (unsigned int dir = 0; dir < Cube::SIDE_COUNT; ++dir) {
bits[dir] = (m >> dir) & 1;
}
uint8_t tm = bits[Cube::SIDE_NEGATIVE_X];
tm |= bits[Cube::SIDE_POSITIVE_X] << 1;
tm |= bits[Cube::SIDE_NEGATIVE_Y] << 2;
tm |= bits[Cube::SIDE_POSITIVE_Y] << 3;
tm |= bits[Cube::SIDE_NEGATIVE_Z] << 4;
tm |= bits[Cube::SIDE_POSITIVE_Z] << 5;
_shader_material->set_shader_param("u_transition_mask", tm);
}
for (int dir = 0; dir < Cube::SIDE_COUNT; ++dir) {
DirectMeshInstance &mi = _transition_mesh_instances[dir];
if (diff & (1 << dir) && mi.is_valid()) {
mi.set_visible(_visible & _is_transition_visible(dir));
}
}
}
void VoxelBlock::set_parent_visible(bool parent_visible) {
if (_parent_visible && parent_visible) {
return;

View File

@ -1,10 +1,14 @@
#ifndef VOXEL_BLOCK_H
#define VOXEL_BLOCK_H
#include "../cube_tables.h"
#include "../util/direct_mesh_instance.h"
#include "../util/direct_static_body.h"
#include "../util/fixed_array.h"
#include "../voxel_buffer.h"
//#define VOXEL_DEBUG_LOD_MATERIALS
class Spatial;
// Internal structure holding a reference to mesh visuals, physics and a block of voxel data.
@ -21,20 +25,24 @@ public:
Ref<VoxelBuffer> voxels;
Vector3i position;
unsigned int lod_index = 0;
bool pending_transition_update = false;
static VoxelBlock *create(Vector3i bpos, Ref<VoxelBuffer> buffer, unsigned int size, unsigned int p_lod_index);
~VoxelBlock();
// Visuals and physics
void set_mesh(Ref<Mesh> mesh, Spatial *node, bool generate_collision, Array surface_arrays, bool debug_collision);
void set_transition_mesh(Ref<Mesh> mesh, int side, Ref<World> world);
bool has_mesh() const;
void set_shader_material(Ref<ShaderMaterial> material);
inline Ref<ShaderMaterial> get_shader_material() const { return _shader_material; }
void set_mesh_state(MeshState ms);
MeshState get_mesh_state() const;
void set_needs_lodding(bool need_lodding);
inline bool get_needs_lodding() const { return _needs_lodding; }
void set_world(World *world);
void set_visible(bool visible);
@ -42,6 +50,15 @@ public:
void set_parent_visible(bool parent_visible);
void set_transition_mask(uint8_t m);
//void set_transition_bit(uint8_t side, bool value);
inline uint8_t get_transition_mask() const { return _transition_mask; }
// Voxel data
void set_needs_lodding(bool need_lodding);
inline bool get_needs_lodding() const { return _needs_lodding; }
bool is_modified() const;
void set_modified(bool modified);
@ -49,17 +66,27 @@ private:
VoxelBlock();
void _set_visible(bool visible);
void _update_transition_visibility();
inline bool _is_transition_visible(int side) const { return _transition_mask & (1 << side); }
private:
Vector3i _position_in_voxels;
Ref<ShaderMaterial> _shader_material;
DirectMeshInstance _mesh_instance;
FixedArray<DirectMeshInstance, Cube::SIDE_COUNT> _transition_mesh_instances;
DirectStaticBody _static_body;
#ifdef VOXEL_DEBUG_LOD_MATERIALS
Ref<Material> _debug_material;
Ref<Material> _debug_transition_material;
#endif
int _mesh_update_count = 0;
bool _visible = true;
bool _parent_visible = true;
MeshState _mesh_state = MESH_NEVER_UPDATED;
uint8_t _transition_mask = 0;
// The block was edited, which requires its LOD counterparts to be recomputed
bool _needs_lodding = false;

View File

@ -13,6 +13,46 @@
const uint32_t MAIN_THREAD_MESHING_BUDGET_MS = 8;
namespace {
Ref<ArrayMesh> build_mesh(const Vector<Array> surfaces, Mesh::PrimitiveType primitive, int compression_flags,
Ref<Material> material, Array *collidable_surface) {
Ref<ArrayMesh> mesh;
mesh.instance();
unsigned int surface_index = 0;
for (int i = 0; i < surfaces.size(); ++i) {
Array surface = surfaces[i];
if (surface.empty()) {
continue;
}
CRASH_COND(surface.size() != Mesh::ARRAY_MAX);
if (!is_surface_triangulated(surface)) {
continue;
}
if (collidable_surface != nullptr && collidable_surface->empty()) {
*collidable_surface = surface;
}
mesh->add_surface_from_arrays(primitive, surface, Array(), compression_flags);
mesh->surface_set_material(surface_index, material);
// No multi-material supported yet
++surface_index;
}
if (is_mesh_empty(mesh)) {
mesh = Ref<Mesh>();
}
return mesh;
}
} // namespace
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,
@ -817,6 +857,8 @@ void VoxelLodTerrain::_process() {
_last_octree_region_box = new_box;
}
CRASH_COND(_blocks_pending_transition_update.size() != 0);
// Find which blocks we need to load and see, within each octree
{
// TODO Maintain a vector to make iteration faster?
@ -874,6 +916,8 @@ void VoxelLodTerrain::_process() {
CRASH_COND(block->get_mesh_state() != VoxelBlock::MESH_UP_TO_DATE);
block->set_visible(true);
self->add_transition_update(block);
self->add_transition_updates_around(bpos, lod_index);
return true;
}
};
@ -906,6 +950,7 @@ void VoxelLodTerrain::_process() {
VoxelBlock *block = lod.map->get_block(bpos);
if (block) {
block->set_visible(false);
self->add_transition_updates_around(bpos, lod_index);
}
}
};
@ -925,8 +970,12 @@ void VoxelLodTerrain::_process() {
// If not, something in block management prevents LODs to properly show up and should be fixed.
_stats.blocked_lods += subdivide_action.blocked_count + unsubdivide_action.blocked_count;
}
process_transition_updates();
}
CRASH_COND(_blocks_pending_transition_update.size() != 0);
_stats.time_detect_required_blocks = profiling_clock.restart();
send_block_data_requests();
@ -992,6 +1041,12 @@ void VoxelLodTerrain::_process() {
// The block will be made visible and meshed only by LodOctree
block->set_visible(false);
block->set_parent_visible(is_visible());
Ref<ShaderMaterial> shader_material = _material;
if (shader_material.is_valid() && block->get_shader_material().is_null()) {
Ref<ShaderMaterial> sm = shader_material->duplicate(false);
block->set_shader_material(sm);
}
}
}
@ -1023,16 +1078,14 @@ void VoxelLodTerrain::_process() {
Ref<VoxelBuffer> nbuffer;
nbuffer.instance();
// TODO Make the buffer re-usable
// TODO Make the buffer re-usable, or pool memory
unsigned int block_size = lod.map->get_block_size();
unsigned int padding = _block_updater->get_required_padding();
nbuffer->create(
block_size + 2 * padding,
block_size + 2 * padding,
block_size + 2 * padding);
unsigned int min_padding = _block_updater->get_minimum_padding();
unsigned int max_padding = _block_updater->get_maximum_padding();
nbuffer->create(Vector3i(block_size + min_padding + max_padding));
unsigned int channels_mask = (1 << VoxelBuffer::CHANNEL_ISOLEVEL);
lod.map->get_buffer_copy(lod.map->block_to_voxel(block_pos) - Vector3i(padding), **nbuffer, channels_mask);
lod.map->get_buffer_copy(lod.map->block_to_voxel(block_pos) - Vector3i(min_padding), **nbuffer, channels_mask);
VoxelMeshUpdater::InputBlock iblock;
iblock.data.voxels = nbuffer;
@ -1112,39 +1165,15 @@ void VoxelLodTerrain::_process() {
block->set_mesh_state(VoxelBlock::MESH_UP_TO_DATE);
}
Ref<ArrayMesh> mesh;
mesh.instance();
const VoxelMesher::Output mesh_data = ob.data.smooth_surfaces;
// TODO Allow multiple collision surfaces
Array collidable_surface;
unsigned int surface_index = 0;
const VoxelMeshUpdater::OutputBlockData &data = ob.data;
for (int i = 0; i < data.smooth_surfaces.surfaces.size(); ++i) {
Array surface = data.smooth_surfaces.surfaces[i];
if (surface.empty()) {
continue;
}
CRASH_COND(surface.size() != Mesh::ARRAY_MAX);
if (!is_surface_triangulated(surface)) {
continue;
}
if (collidable_surface.empty()) {
collidable_surface = surface;
}
mesh->add_surface_from_arrays(data.smooth_surfaces.primitive_type, surface, Array(), data.smooth_surfaces.compression_flags);
mesh->surface_set_material(surface_index, _material);
// No multi-material supported yet
++surface_index;
}
if (is_mesh_empty(mesh)) {
mesh = Ref<Mesh>();
}
Ref<ArrayMesh> mesh = build_mesh(
mesh_data.surfaces,
mesh_data.primitive_type,
mesh_data.compression_flags,
_material, &collidable_surface);
bool has_collision = _generate_collisions;
if (has_collision && _collision_lod_count != -1) {
@ -1152,6 +1181,17 @@ void VoxelLodTerrain::_process() {
}
block->set_mesh(mesh, this, has_collision, collidable_surface, get_tree()->is_debugging_collisions_hint());
for (int dir = 0; dir < mesh_data.transition_surfaces.size(); ++dir) {
Ref<ArrayMesh> transition_mesh = build_mesh(
mesh_data.transition_surfaces[dir],
mesh_data.primitive_type,
mesh_data.compression_flags,
_material, nullptr);
block->set_transition_mesh(transition_mesh, dir, get_world());
}
}
shift_up(_blocks_pending_main_thread_update, queue_index);
@ -1299,6 +1339,79 @@ void VoxelLodTerrain::save_all_modified_blocks(bool with_copy) {
send_block_data_requests();
}
void VoxelLodTerrain::add_transition_update(VoxelBlock *block) {
if (!block->pending_transition_update) {
_blocks_pending_transition_update.push_back(block);
block->pending_transition_update = true;
}
}
void VoxelLodTerrain::add_transition_updates_around(Vector3i block_pos, int lod_index) {
Lod &lod = _lods[lod_index];
CRASH_COND(lod.map.is_null());
for (int dir = 0; dir < Cube::SIDE_COUNT; ++dir) {
Vector3i npos = block_pos + Cube::g_side_normals[dir];
VoxelBlock *nblock = lod.map->get_block(npos);
if (nblock != nullptr && nblock->is_visible()) {
add_transition_update(nblock);
}
}
}
void VoxelLodTerrain::process_transition_updates() {
for (unsigned int i = 0; i < _blocks_pending_transition_update.size(); ++i) {
VoxelBlock *block = _blocks_pending_transition_update[i];
CRASH_COND(block == nullptr);
if (block->is_visible()) {
block->set_transition_mask(get_transition_mask(block->position, block->lod_index));
}
block->pending_transition_update = false;
}
_blocks_pending_transition_update.clear();
}
uint8_t VoxelLodTerrain::get_transition_mask(Vector3i block_pos, int lod_index) {
uint8_t transition_mask = 0;
if (lod_index + 1 >= _lods.size()) {
return transition_mask;
}
Lod &lower_lod = _lods[lod_index + 1];
if (!lower_lod.map.is_valid()) {
return transition_mask;
}
Vector3i lpos = block_pos >> 1;
// Check for lower-LOD blocks around
for (int dir = 0; dir < Cube::SIDE_COUNT; ++dir) {
Vector3i lnpos = (block_pos + Cube::g_side_normals[dir]) >> 1;
if (lnpos != lpos) {
VoxelBlock *nblock = lower_lod.map->get_block(lnpos);
if (nblock != nullptr && nblock->is_visible()) {
// The block has a visible neighbor of lower LOD
transition_mask |= (1 << dir);
}
}
}
return transition_mask;
}
Dictionary VoxelLodTerrain::get_statistics() const {
Dictionary d;

View File

@ -111,6 +111,11 @@ private:
void save_all_modified_blocks(bool with_copy);
void send_block_data_requests();
void add_transition_update(VoxelBlock *block);
void add_transition_updates_around(Vector3i block_pos, int lod_index);
void process_transition_updates();
uint8_t get_transition_mask(Vector3i block_pos, int lod_index);
struct OctreeItem {
LodOctree<bool> octree;
#ifdef VOXEL_DEBUG_BOXES
@ -137,6 +142,9 @@ private:
std::vector<VoxelMeshUpdater::OutputBlock> _blocks_pending_main_thread_update;
std::vector<VoxelDataLoader::InputBlock> _blocks_to_save;
// Only populated and then cleared inside _process, so lifetime of pointers should be valid
std::vector<VoxelBlock *> _blocks_pending_transition_update;
Ref<Material> _material;
bool _generate_collisions = true;
@ -164,7 +172,7 @@ private:
#endif
};
Lod _lods[MAX_LOD];
FixedArray<Lod, MAX_LOD> _lods;
int _lod_count = 0;
float _lod_split_scale = 0.f;
unsigned int _view_distance_voxels = 512;

View File

@ -1,13 +1,13 @@
#include "voxel_mesh_updater.h"
#include "../meshers/dmc/voxel_mesher_dmc.h"
#include "../meshers/transvoxel/voxel_mesher_transvoxel.h"
#include "../util/utility.h"
#include "voxel_lod_terrain.h"
#include <core/os/os.h>
static void scale_mesh_data(VoxelMesher::Output &data, float factor) {
static void scale_mesh_data(Vector<Array> &surfaces, float factor) {
for (int i = 0; i < data.surfaces.size(); ++i) {
Array &surface = data.surfaces.write[i]; // There is COW here too but should not happen, hopefully
for (int i = 0; i < surfaces.size(); ++i) {
Array &surface = surfaces.write[i]; // There is COW here too but should not happen, hopefully
if (surface.empty()) {
continue;
@ -37,24 +37,24 @@ VoxelMeshUpdater::VoxelMeshUpdater(unsigned int thread_count, MeshingParams para
print_line("Constructing VoxelMeshUpdater");
Ref<VoxelMesherBlocky> blocky_mesher;
Ref<VoxelMesherDMC> smooth_mesher;
Ref<VoxelMesherTransvoxel> smooth_mesher;
_required_padding = 0;
_minimum_padding = 0;
_maximum_padding = 0;
if (params.library.is_valid()) {
blocky_mesher.instance();
blocky_mesher->set_library(params.library);
blocky_mesher->set_occlusion_enabled(params.baked_ao);
blocky_mesher->set_occlusion_darkness(params.baked_ao_darkness);
_required_padding = max(_required_padding, blocky_mesher->get_minimum_padding());
_minimum_padding = max(_minimum_padding, blocky_mesher->get_minimum_padding());
_maximum_padding = max(_maximum_padding, blocky_mesher->get_maximum_padding());
}
if (params.smooth_surface) {
smooth_mesher.instance();
smooth_mesher->set_geometric_error(0.05);
smooth_mesher->set_simplify_mode(VoxelMesherDMC::SIMPLIFY_NONE);
smooth_mesher->set_seam_mode(VoxelMesherDMC::SEAM_MARCHING_SQUARE_SKIRTS);
_required_padding = max(_required_padding, smooth_mesher->get_minimum_padding());
_minimum_padding = max(_minimum_padding, smooth_mesher->get_minimum_padding());
_maximum_padding = max(_maximum_padding, smooth_mesher->get_maximum_padding());
}
Mgr::BlockProcessingFunc processors[Mgr::MAX_LOD];
@ -73,7 +73,7 @@ VoxelMeshUpdater::VoxelMeshUpdater(unsigned int thread_count, MeshingParams para
}
processors[i] = [this, blocky_mesher, smooth_mesher](const ArraySlice<InputBlock> inputs, ArraySlice<OutputBlock> outputs, Mgr::ProcessorStats &_) {
this->process_blocks_thread_func(inputs, outputs, blocky_mesher, smooth_mesher, this->_required_padding);
this->process_blocks_thread_func(inputs, outputs, blocky_mesher, smooth_mesher);
};
}
@ -91,8 +91,7 @@ void VoxelMeshUpdater::process_blocks_thread_func(
const ArraySlice<InputBlock> inputs,
ArraySlice<OutputBlock> outputs,
Ref<VoxelMesher> blocky_mesher,
Ref<VoxelMesher> smooth_mesher,
int padding) {
Ref<VoxelMesher> smooth_mesher) {
CRASH_COND(inputs.size() != outputs.size());
@ -105,16 +104,20 @@ void VoxelMeshUpdater::process_blocks_thread_func(
CRASH_COND(block.voxels.is_null());
if (blocky_mesher.is_valid()) {
blocky_mesher->build(output.blocky_surfaces, **block.voxels, padding);
blocky_mesher->build(output.blocky_surfaces, **block.voxels);
}
if (smooth_mesher.is_valid()) {
smooth_mesher->build(output.smooth_surfaces, **block.voxels, padding);
smooth_mesher->build(output.smooth_surfaces, **block.voxels);
}
if (ib.lod > 0) {
// TODO Make this optional if the mesher can factor in the upscale already
float factor = 1 << ib.lod;
scale_mesh_data(output.blocky_surfaces, factor);
scale_mesh_data(output.smooth_surfaces, factor);
scale_mesh_data(output.blocky_surfaces.surfaces, factor);
scale_mesh_data(output.smooth_surfaces.surfaces, factor);
for (int i = 0; i < output.smooth_surfaces.transition_surfaces.size(); ++i) {
scale_mesh_data(output.smooth_surfaces.transition_surfaces[i], factor);
}
}
}
}

View File

@ -41,17 +41,18 @@ public:
void push(const Input &input) { _mgr->push(input); }
void pop(Output &output) { _mgr->pop(output); }
int get_required_padding() const { return _required_padding; }
int get_minimum_padding() const { return _minimum_padding; }
int get_maximum_padding() const { return _maximum_padding; }
private:
void process_blocks_thread_func(const ArraySlice<InputBlock> inputs,
ArraySlice<OutputBlock> outputs,
Ref<VoxelMesher> blocky_mesher,
Ref<VoxelMesher> smooth_mesher,
int padding);
Ref<VoxelMesher> smooth_mesher);
Mgr *_mgr = nullptr;
int _required_padding = 0;
int _minimum_padding = 0;
int _maximum_padding = 0;
};
#endif // VOXEL_MESH_UPDATER_H

View File

@ -934,11 +934,12 @@ void VoxelTerrain::_process() {
// TODO Make the buffer re-usable
unsigned int block_size = _map->get_block_size();
unsigned int padding = _block_updater->get_required_padding();
nbuffer->create(Vector3i(block_size + 2 * padding));
unsigned int min_padding = _block_updater->get_minimum_padding();
unsigned int max_padding = _block_updater->get_maximum_padding();
nbuffer->create(Vector3i(block_size + min_padding + max_padding));
unsigned int channels_mask = (1 << VoxelBuffer::CHANNEL_TYPE) | (1 << VoxelBuffer::CHANNEL_ISOLEVEL);
_map->get_buffer_copy(_map->block_to_voxel(block_pos) - Vector3i(padding), **nbuffer, channels_mask);
_map->get_buffer_copy(_map->block_to_voxel(block_pos) - Vector3i(min_padding), **nbuffer, channels_mask);
VoxelMeshUpdater::InputBlock iblock;
iblock.data.voxels = nbuffer;