Allow to specify which surfaces have collision

master
Marc Gilleron 2022-05-17 22:12:54 +01:00
parent fef56aad36
commit fa027cbe4e
16 changed files with 206 additions and 32 deletions

View File

@ -45,7 +45,8 @@ Godot 4 is required from this version.
- Blocky voxels
- `VoxelMesherBlocky`: materials are now unlimited and specified in each model, either as overrides or directly from mesh (You still need to consider draw calls when using many materials)
- `VoxelMesherBlocky`: each model can have up to 2 materials
- `VoxelMesherBlocky`: each model can have up to 2 materials (aka surfaces)
- `VoxelMesherBlocky`: mesh collisions: added support for specifying which surfaces have collision
- Fixes
- `VoxelBuffer`: frequently creating buffers with always different sizes no longer wastes memory

View File

@ -30,7 +30,7 @@ Ref<Mesh> build_mesh(const VoxelBufferInternal &voxels, VoxelMesher &mesher,
std::vector<unsigned int> &surface_index_to_material, Ref<Image> &out_atlas, float p_scale, Vector3 p_offset) {
//
VoxelMesher::Output output;
VoxelMesher::Input input = { voxels, 0 };
VoxelMesher::Input input = { voxels, nullptr, nullptr, Vector3i(), 0, false };
mesher.build(output, input);
if (output.surfaces.size() == 0) {

View File

@ -51,6 +51,11 @@ bool VoxelBlockyModel::_set(const StringName &p_name, const Variant &p_value) {
const int index = name.substr(ZN_ARRAY_LENGTH("material_override_")).to_int();
set_material_override(index, p_value);
return true;
} else if (name.begins_with("collision_enabled_")) {
const int index = name.substr(ZN_ARRAY_LENGTH("collision_enabled_")).to_int();
set_mesh_collision_enabled(index, p_value);
return true;
}
return false;
@ -72,6 +77,11 @@ bool VoxelBlockyModel::_get(const StringName &p_name, Variant &r_ret) const {
const int index = name.substr(ZN_ARRAY_LENGTH("material_override_")).to_int();
r_ret = get_material_override(index);
return true;
} else if (name.begins_with("collision_enabled_")) {
const int index = name.substr(ZN_ARRAY_LENGTH("collision_enabled_")).to_int();
r_ret = is_mesh_collision_enabled(index);
return true;
}
return false;
@ -103,13 +113,13 @@ void VoxelBlockyModel::_get_property_list(List<PropertyInfo> *p_list) const {
PROPERTY_HINT_RESOURCE_TYPE, Material::get_class_static()));
}
// p_list->push_back(
// PropertyInfo(Variant::NIL, "Surface collision", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_GROUP));
p_list->push_back(PropertyInfo(
Variant::NIL, "Mesh collision", PROPERTY_HINT_NONE, "collision_enabled_", PROPERTY_USAGE_GROUP));
// for (unsigned int i = 0; i < _surface_count; ++i) {
// p_list->push_back(PropertyInfo(Variant::OBJECT, String("collision_enabled_{0}").format(varray(i)),
// PROPERTY_HINT_RESOURCE_TYPE, Material::get_class_static()));
// }
for (unsigned int i = 0; i < _surface_count; ++i) {
p_list->push_back(PropertyInfo(Variant::OBJECT, String("collision_enabled_{0}").format(varray(i)),
PROPERTY_HINT_RESOURCE_TYPE, Material::get_class_static()));
}
}
}
@ -129,15 +139,33 @@ void VoxelBlockyModel::set_color(Color color) {
}
void VoxelBlockyModel::set_material_override(int index, Ref<Material> material) {
ERR_FAIL_INDEX(index, _surface_count);
// TODO Can't check for `_surface_count` instead, because there is no guarantee about the order in which Godot will
// set properties when loading the resource. The mesh could be set later, so we can't know the number of surfaces.
ERR_FAIL_INDEX(index, int(_surface_params.size()));
_surface_params[index].material_override = material;
}
Ref<Material> VoxelBlockyModel::get_material_override(int index) const {
ERR_FAIL_INDEX_V(index, _surface_count, Ref<Material>());
// TODO Can't check for `_surface_count` instead, because there is no guarantee about the order in which Godot will
// set properties when loading the resource. The mesh could be set later, so we can't know the number of surfaces.
ERR_FAIL_INDEX_V(index, int(_surface_params.size()), Ref<Material>());
return _surface_params[index].material_override;
}
void VoxelBlockyModel::set_mesh_collision_enabled(int surface_index, bool enabled) {
// TODO Can't check for `_surface_count` instead, because there is no guarantee about the order in which Godot will
// set properties when loading the resource. The mesh could be set later, so we can't know the number of surfaces.
ERR_FAIL_INDEX(surface_index, int(_surface_params.size()));
_surface_params[surface_index].collision_enabled = enabled;
}
bool VoxelBlockyModel::is_mesh_collision_enabled(int surface_index) const {
// TODO Can't check for `_surface_count` instead, because there is no guarantee about the order in which Godot will
// set properties when loading the resource. The mesh could be set later, so we can't know the number of surfaces.
ERR_FAIL_INDEX_V(surface_index, int(_surface_params.size()), false);
return _surface_params[surface_index].collision_enabled;
}
void VoxelBlockyModel::set_transparent(bool t) {
if (t) {
if (_transparency_index == 0) {
@ -616,6 +644,11 @@ void VoxelBlockyModel::_b_set_collision_aabbs(Array array) {
}
}
// void ortho_simplify(Span<const Vector3f> vertices, Span<const int> indices, std::vector<int> &output) {
// TODO Optimization: implement mesh simplification based on axis-aligned triangles.
// It could be very effective on mesh collisions with the blocky mesher.
// }
void VoxelBlockyModel::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_voxel_name", "name"), &VoxelBlockyModel::set_voxel_name);
ClassDB::bind_method(D_METHOD("get_voxel_name"), &VoxelBlockyModel::get_voxel_name);
@ -646,6 +679,10 @@ void VoxelBlockyModel::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_custom_mesh", "type"), &VoxelBlockyModel::set_custom_mesh);
ClassDB::bind_method(D_METHOD("get_custom_mesh"), &VoxelBlockyModel::get_custom_mesh);
ClassDB::bind_method(D_METHOD("set_mesh_collision_enabled", "surface_index", "enabled"),
&VoxelBlockyModel::set_mesh_collision_enabled);
ClassDB::bind_method(D_METHOD("is_mesh_collision_enabled"), &VoxelBlockyModel::is_mesh_collision_enabled);
ClassDB::bind_method(D_METHOD("set_collision_aabbs", "aabbs"), &VoxelBlockyModel::_b_set_collision_aabbs);
ClassDB::bind_method(D_METHOD("get_collision_aabbs"), &VoxelBlockyModel::_b_get_collision_aabbs);

View File

@ -44,7 +44,7 @@ public:
FixedArray<std::vector<float>, Cube::SIDE_COUNT> side_tangents;
int material_id = -1;
//bool collision_enabled = true;
bool collision_enabled = true;
void clear() {
positions.clear();
@ -127,6 +127,9 @@ public:
void set_material_override(int index, Ref<Material> material);
Ref<Material> get_material_override(int index) const;
void set_mesh_collision_enabled(int surface_index, bool enabled);
bool is_mesh_collision_enabled(int surface_index) const;
// TODO Might become obsoleted by transparency index
void set_transparent(bool t = true);
_FORCE_INLINE_ bool is_transparent() const {
@ -227,7 +230,7 @@ private:
// If assigned, these materials override those present on the mesh itself.
Ref<Material> material_override;
// If true and classic mesh physics are enabled, the surface will be present in the collider.
//bool collision_enabled = true;
bool collision_enabled = true;
};
FixedArray<SurfaceParams, BakedData::Model::MAX_SURFACES> _surface_params;

View File

@ -49,8 +49,13 @@ static thread_local std::vector<int> tls_index_offsets;
template <typename Type_T>
void generate_blocky_mesh(std::vector<VoxelMesherBlocky::Arrays> &out_arrays_per_material,
const Span<Type_T> type_buffer, const Vector3i block_size, const VoxelBlockyLibrary::BakedData &library,
bool bake_occlusion, float baked_occlusion_darkness) {
VoxelMesher::Output::CollisionSurface *collision_surface, const Span<Type_T> type_buffer,
const Vector3i block_size, const VoxelBlockyLibrary::BakedData &library, bool bake_occlusion,
float baked_occlusion_darkness) {
// TODO Optimization: not sure if this mandates a template function. There is so much more happening in this
// function other than reading voxels, although reading is on the hottest path. It needs to be profiled. If
// changing makes no difference, we could use a function pointer or switch inside instead to reduce executable size.
ERR_FAIL_COND(block_size.x < static_cast<int>(2 * VoxelMesherBlocky::PADDING) ||
block_size.y < static_cast<int>(2 * VoxelMesherBlocky::PADDING) ||
block_size.z < static_cast<int>(2 * VoxelMesherBlocky::PADDING));
@ -69,6 +74,8 @@ void generate_blocky_mesh(std::vector<VoxelMesherBlocky::Arrays> &out_arrays_per
index_offsets.clear();
index_offsets.resize(out_arrays_per_material.size(), 0);
int collision_surface_index_offset = 0;
FixedArray<int, Cube::SIDE_COUNT> side_neighbor_lut;
side_neighbor_lut[Cube::SIDE_LEFT] = row_size;
side_neighbor_lut[Cube::SIDE_RIGHT] = -row_size;
@ -142,7 +149,7 @@ void generate_blocky_mesh(std::vector<VoxelMesherBlocky::Arrays> &out_arrays_per
const VoxelBlockyModel::BakedData::Model &model = voxel.model;
// Hybrid approach: extract cube faces and decimate those that aren't visible,
// and still allow voxels to have geometry that is not a cube
// and still allow voxels to have geometry that is not a cube.
// Sides
for (unsigned int side = 0; side < Cube::SIDE_COUNT; ++side) {
@ -297,6 +304,31 @@ void generate_blocky_mesh(std::vector<VoxelMesherBlocky::Arrays> &out_arrays_per
}
}
if (collision_surface != nullptr && surface.collision_enabled) {
std::vector<Vector3f> &dst_positions = collision_surface->positions;
std::vector<int> &dst_indices = collision_surface->indices;
{
const unsigned int append_index = dst_positions.size();
dst_positions.resize(dst_positions.size() + vertex_count);
Vector3f *w = dst_positions.data() + append_index;
for (unsigned int i = 0; i < vertex_count; ++i) {
w[i] = side_positions[i] + pos;
}
}
{
int i = dst_indices.size();
dst_indices.resize(dst_indices.size() + index_count);
int *w = dst_indices.data();
for (unsigned int j = 0; j < index_count; ++j) {
w[i++] = collision_surface_index_offset + side_indices[j];
}
}
collision_surface_index_offset += vertex_count;
}
index_offset += vertex_count;
}
}
@ -346,6 +378,20 @@ void generate_blocky_mesh(std::vector<VoxelMesherBlocky::Arrays> &out_arrays_per
arrays.indices.push_back(index_offset + indices[i]);
}
if (collision_surface != nullptr && surface.collision_enabled) {
std::vector<Vector3f> &dst_positions = collision_surface->positions;
std::vector<int> &dst_indices = collision_surface->indices;
for (unsigned int i = 0; i < vertex_count; ++i) {
dst_positions.push_back(positions[i] + pos);
}
for (unsigned int i = 0; i < index_count; ++i) {
dst_indices.push_back(collision_surface_index_offset + indices[i]);
}
collision_surface_index_offset += vertex_count;
}
index_offset += vertex_count;
}
}
@ -472,6 +518,11 @@ void VoxelMesherBlocky::build(VoxelMesher::Output &output, const VoxelMesher::In
const Vector3i block_size = voxels.get_size();
const VoxelBufferInternal::Depth channel_depth = voxels.get_channel_depth(channel);
VoxelMesher::Output::CollisionSurface *collision_surface = nullptr;
if (input.collision_hint) {
collision_surface = &output.collision_surface;
}
unsigned int material_count = 0;
{
// We can only access baked data. Only this data is made for multithreaded access.
@ -486,13 +537,14 @@ void VoxelMesherBlocky::build(VoxelMesher::Output &output, const VoxelMesher::In
switch (channel_depth) {
case VoxelBufferInternal::DEPTH_8_BIT:
generate_blocky_mesh(arrays_per_material, raw_channel, block_size, library_baked_data,
params.bake_occlusion, baked_occlusion_darkness);
generate_blocky_mesh(arrays_per_material, collision_surface, raw_channel, block_size,
library_baked_data, params.bake_occlusion, baked_occlusion_darkness);
break;
case VoxelBufferInternal::DEPTH_16_BIT:
generate_blocky_mesh(arrays_per_material, raw_channel.reinterpret_cast_to<uint16_t>(), block_size,
library_baked_data, params.bake_occlusion, baked_occlusion_darkness);
generate_blocky_mesh(arrays_per_material, collision_surface,
raw_channel.reinterpret_cast_to<uint16_t>(), block_size, library_baked_data,
params.bake_occlusion, baked_occlusion_darkness);
break;
default:

View File

@ -31,7 +31,11 @@ public:
Vector3i origin_in_voxels;
// LOD index. 0 means highest detail. 1 means half detail etc.
// Not initialized because it confused GCC.
int lod; // = 0;
uint8_t lod; // = 0;
// If true, collision information is required.
// Sometimes it doesn't change anything as the rendering mesh can be used as collider,
// but in other setups it can be different and will be returned in `collision_surface`.
bool collision_hint = false;
};
struct Output {
@ -42,7 +46,14 @@ public:
std::vector<Surface> surfaces;
FixedArray<std::vector<Surface>, Cube::SIDE_COUNT> transition_surfaces;
Mesh::PrimitiveType primitive_type = Mesh::PRIMITIVE_TRIANGLES;
unsigned int mesh_flags = 0;
uint32_t mesh_flags = 0;
struct CollisionSurface {
std::vector<Vector3f> positions;
std::vector<int> indices;
};
CollisionSurface collision_surface;
Ref<Image> atlas_image;
};

View File

@ -176,7 +176,8 @@ void MeshBlockTask::run(zylann::ThreadedTaskContext ctx) {
const Vector3i origin_in_voxels = position * (int(data_block_size) << lod);
const VoxelMesher::Input input = { voxels, meshing_dependency->generator.ptr(), data.get(), origin_in_voxels, lod };
const VoxelMesher::Input input = { voxels, meshing_dependency->generator.ptr(), data.get(), origin_in_voxels, lod,
collision_hint };
mesher->build(_surfaces_output, input);
_has_run = true;

View File

@ -31,6 +31,7 @@ public:
uint8_t lod;
uint8_t blocks_count;
uint8_t data_block_size;
bool collision_hint;
PriorityDependency priority_dependency;
std::shared_ptr<MeshingDependency> meshing_dependency;
std::shared_ptr<VoxelDataLodMap> data;

View File

@ -276,6 +276,7 @@ void VoxelServer::request_block_mesh(uint32_t volume_id, const BlockMeshInput &i
task->blocks_count = input.data_blocks_count;
task->position = input.render_block_position;
task->lod = input.lod;
task->collision_hint = input.collision_hint;
task->meshing_dependency = volume.meshing_dependency;
task->data_block_size = volume.data_block_size;

View File

@ -62,6 +62,7 @@ public:
unsigned int data_blocks_count = 0;
Vector3i render_block_position;
uint8_t lod = 0;
bool collision_hint = false;
};
struct VolumeCallbacks {

View File

@ -1378,6 +1378,7 @@ void VoxelTerrain::process_meshing() {
VoxelServer::BlockMeshInput mesh_request;
mesh_request.render_block_position = mesh_block_pos;
mesh_request.lod = 0;
mesh_request.collision_hint = _generate_collisions;
//mesh_request.data_blocks_count = data_box.size.volume();
// This iteration order is specifically chosen to match VoxelServer and threaded access
@ -1439,7 +1440,9 @@ void VoxelTerrain::apply_mesh_update(const VoxelServer::BlockMeshOutput &ob) {
Ref<ArrayMesh> mesh;
std::vector<Array> collidable_surfaces;
const bool gen_collisions = _generate_collisions && block->collision_viewers.get() > 0;
const bool use_render_mesh_as_collider = gen_collisions && ob.surfaces.collision_surface.positions.size() == 0;
std::vector<Array> render_surfaces;
int gd_surface_index = 0;
for (unsigned int surface_index = 0; surface_index < ob.surfaces.surfaces.size(); ++surface_index) {
@ -1454,7 +1457,9 @@ void VoxelTerrain::apply_mesh_update(const VoxelServer::BlockMeshOutput &ob) {
continue;
}
collidable_surfaces.push_back(arrays);
if (use_render_mesh_as_collider) {
render_surfaces.push_back(arrays);
}
if (mesh.is_null()) {
mesh.instantiate();
@ -1470,7 +1475,7 @@ void VoxelTerrain::apply_mesh_update(const VoxelServer::BlockMeshOutput &ob) {
if (mesh.is_valid() && is_mesh_empty(**mesh)) {
mesh = Ref<Mesh>();
collidable_surfaces.clear();
render_surfaces.clear();
}
if (_instancer != nullptr) {
@ -1486,18 +1491,28 @@ void VoxelTerrain::apply_mesh_update(const VoxelServer::BlockMeshOutput &ob) {
}
}
const bool gen_collisions = _generate_collisions && block->collision_viewers.get() > 0;
block->set_mesh(mesh, DirectMeshInstance::GIMode(get_gi_mode()));
if (_material_override.is_valid()) {
block->set_material_override(_material_override);
}
if (gen_collisions) {
block->set_collision_mesh(to_span_const(collidable_surfaces), get_tree()->is_debugging_collisions_hint(), this,
_collision_margin);
const bool debug_collisions = get_tree()->is_debugging_collisions_hint();
const VoxelMesher::Output::CollisionSurface &collision_surface = ob.surfaces.collision_surface;
if (use_render_mesh_as_collider) {
block->set_collision_mesh(to_span(render_surfaces), debug_collisions, this, _collision_margin);
} else {
block->set_collision_mesh(to_span(collision_surface.positions), to_span(collision_surface.indices),
debug_collisions, this, _collision_margin);
}
block->set_collision_layer(_collision_layer);
block->set_collision_mask(_collision_mask);
}
block->set_visible(true);
block->set_parent_visible(is_visible());
block->set_parent_transform(get_global_transform());

View File

@ -134,11 +134,24 @@ void VoxelMeshBlock::set_collision_mesh(
drop_collision();
return;
}
Ref<Shape3D> shape = create_concave_polygon_shape(surface_arrays);
set_collision_shape(shape, debug_collision, node, margin);
}
void VoxelMeshBlock::set_collision_mesh(
Span<const Vector3f> positions, Span<const int> indices, bool debug_collision, Node3D *node, float margin) {
if (positions.size() == 0) {
drop_collision();
return;
}
Ref<Shape3D> shape = create_concave_polygon_shape(positions, indices);
set_collision_shape(shape, debug_collision, node, margin);
}
void VoxelMeshBlock::set_collision_shape(Ref<Shape3D> shape, bool debug_collision, Node3D *node, float margin) {
ERR_FAIL_COND(node == nullptr);
ERR_FAIL_COND_MSG(node->get_world_3d() != _world, "Physics body and attached node must be from the same world");
Ref<Shape3D> shape = create_concave_polygon_shape(surface_arrays);
if (shape.is_null()) {
drop_collision();
return;

View File

@ -47,6 +47,9 @@ public:
// Collisions
void set_collision_mesh(Span<const Array> surface_arrays, bool debug_collision, Node3D *node, float margin);
void set_collision_mesh(
Span<const Vector3f> positions, Span<const int> indices, bool debug_collision, Node3D *node, float margin);
void set_collision_shape(Ref<Shape3D> shape, bool debug_collision, Node3D *node, float margin);
void set_collision_layer(int layer);
void set_collision_mask(int mask);
void set_collision_margin(float margin);

View File

@ -2063,7 +2063,7 @@ void test_voxel_mesher_cubes() {
mesher.instantiate();
mesher->set_color_mode(VoxelMesherCubes::COLOR_RAW);
VoxelMesher::Input input{ vb, nullptr, nullptr, Vector3i(), 0 };
VoxelMesher::Input input{ vb, nullptr, nullptr, Vector3i(), 0, false };
VoxelMesher::Output output;
mesher->build(output, input);

View File

@ -1,4 +1,5 @@
#include "funcs.h"
#include "../math/conv.h"
#include "../profiling.h"
#include <core/config/engine.h>
@ -55,7 +56,7 @@ Ref<ConcavePolygonShape3D> create_concave_polygon_shape(Span<const Array> surfac
return Ref<ConcavePolygonShape3D>();
}
//copy the points into it
// Deindex surfaces into a single one
unsigned int face_points_offset = 0;
for (unsigned int i = 0; i < surfaces.size(); i++) {
const Array &surface_arrays = surfaces[i];
@ -101,6 +102,39 @@ Ref<ConcavePolygonShape3D> create_concave_polygon_shape(Span<const Array> surfac
return shape;
}
Ref<ConcavePolygonShape3D> create_concave_polygon_shape(Span<const Vector3f> positions, Span<const int> indices) {
ZN_PROFILE_SCOPE();
PackedVector3Array face_points;
if (indices.size() < 3) {
return Ref<ConcavePolygonShape3D>();
}
face_points.resize(indices.size());
ERR_FAIL_COND_V(positions.size() < 3, Ref<ConcavePolygonShape3D>());
ERR_FAIL_COND_V(indices.size() < 3, Ref<ConcavePolygonShape3D>());
ERR_FAIL_COND_V(indices.size() % 3 != 0, Ref<ConcavePolygonShape3D>());
// Deindex mesh
{
Vector3 *w = face_points.ptrw();
for (unsigned int ii = 0; ii < indices.size(); ++ii) {
const int index = indices[ii];
w[ii] = to_vec3(positions[index]);
}
}
Ref<ConcavePolygonShape3D> shape;
{
ZN_PROFILE_SCOPE_NAMED("Godot shape");
shape.instantiate();
shape->set_faces(face_points);
}
return shape;
}
int get_visible_instance_count(const MultiMesh &mm) {
int visible_count = mm.get_visible_instance_count();
if (visible_count == -1) {

View File

@ -22,6 +22,7 @@ bool is_surface_triangulated(Array surface);
bool is_mesh_empty(const Mesh &mesh);
Ref<ConcavePolygonShape3D> create_concave_polygon_shape(Span<const Array> surfaces);
Ref<ConcavePolygonShape3D> create_concave_polygon_shape(Span<const Vector3f> positions, Span<const int> indices);
// This API can be confusing so I made a wrapper
int get_visible_instance_count(const MultiMesh &mm);