diff --git a/src/terrain_3d.cpp b/src/terrain_3d.cpp index babb88c8d..e79f15c05 100644 --- a/src/terrain_3d.cpp +++ b/src/terrain_3d.cpp @@ -127,6 +127,9 @@ void Terrain3D::__physics_process(const double p_delta) { if (_collision && _collision->is_dynamic_mode()) { _collision->update(); } + if (_collision && _collision->is_instance_collision_enabled()) { + _collision->update_instance_collision(); + } } /** @@ -175,6 +178,7 @@ void Terrain3D::_destroy_collision(const bool p_final) { LOG(INFO, "Destroying Collision"); if (_collision) { _collision->destroy(); + _collision->destroy_instance_collision(); } if (p_final) { memdelete_safely(_collision); @@ -683,6 +687,7 @@ void Terrain3D::set_vertex_spacing(const real_t p_spacing) { _material->update(); _collision->destroy(); _collision->build(); + _collision->set_instance_collision_dirty(true); _update_displacement_buffer(); } if (IS_EDITOR && _editor_plugin) { @@ -1099,6 +1104,7 @@ void Terrain3D::_notification(const int p_what) { set_physics_process(false); _destroy_mesher(); _destroy_instancer(); + _destroy_collision(); _destroy_mouse_picking(); _destroy_displacement_buffer(); if (_assets.is_valid()) { @@ -1219,6 +1225,12 @@ void Terrain3D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_physics_material", "material"), &Terrain3D::set_physics_material); ClassDB::bind_method(D_METHOD("get_physics_material"), &Terrain3D::get_physics_material); + // Instance Collision + ClassDB::bind_method(D_METHOD("set_instance_collision_mode", "mode"), &Terrain3D::set_instance_collision_mode); + ClassDB::bind_method(D_METHOD("get_instance_collision_mode"), &Terrain3D::get_instance_collision_mode); + ClassDB::bind_method(D_METHOD("set_instance_collision_radius", "radius"), &Terrain3D::set_instance_collision_radius); + ClassDB::bind_method(D_METHOD("get_instance_collision_radius"), &Terrain3D::get_instance_collision_radius); + // Terrain Mesh ClassDB::bind_method(D_METHOD("set_mesh_lods", "count"), &Terrain3D::set_mesh_lods); ClassDB::bind_method(D_METHOD("get_mesh_lods"), &Terrain3D::get_mesh_lods); @@ -1337,6 +1349,9 @@ void Terrain3D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "collision_mask", PROPERTY_HINT_LAYERS_3D_PHYSICS), "set_collision_mask", "get_collision_mask"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "collision_priority", PROPERTY_HINT_RANGE, "0.1,256,.1"), "set_collision_priority", "get_collision_priority"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "physics_material", PROPERTY_HINT_RESOURCE_TYPE, "PhysicsMaterial"), "set_physics_material", "get_physics_material"); + ADD_SUBGROUP("Instance Collision", ""); + ADD_PROPERTY(PropertyInfo(Variant::INT, "instance_collision_mode", PROPERTY_HINT_ENUM, "Disabled, Dynamic / Game, Dynamic / Editor"), "set_instance_collision_mode", "get_instance_collision_mode"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "instance_collision_radius", PROPERTY_HINT_RANGE, "1.,256.,1."), "set_instance_collision_radius", "get_instance_collision_radius"); ADD_GROUP("Terrain Mesh", ""); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "clipmap_target", PROPERTY_HINT_NODE_TYPE, "Node3D", PROPERTY_USAGE_DEFAULT, "Node3D"), "set_clipmap_target", "get_clipmap_target"); diff --git a/src/terrain_3d.h b/src/terrain_3d.h index 664468377..2c23921dd 100644 --- a/src/terrain_3d.h +++ b/src/terrain_3d.h @@ -236,6 +236,12 @@ class Terrain3D : public Node3D { void set_physics_material(const Ref &p_mat) { _collision ? _collision->set_physics_material(p_mat) : void(); } Ref get_physics_material() const { return _collision ? _collision->get_physics_material() : Ref(); } + // Instance Collision Aliases + void set_instance_collision_mode(const InstanceCollisionMode p_mode) { _collision ? _collision->set_instance_collision_mode(p_mode) : void(); } + InstanceCollisionMode get_instance_collision_mode() const { return _collision ? _collision->get_instance_collision_mode() : InstanceCollisionMode::INSTANCE_COLLISION_DYNAMIC_GAME; } + void set_instance_collision_radius(const real_t p_radius) { _collision ? _collision->set_instance_collision_radius(p_radius) : void(); } + real_t get_instance_collision_radius() const { return _collision ? _collision->get_instance_collision_radius() : 64.f; } + // Instancer Aliases void set_instancer_mode(const InstancerMode p_mode) { _instancer ? _instancer->set_mode(p_mode) : void(); } InstancerMode get_instancer_mode() const { return _instancer ? _instancer->get_mode() : InstancerMode::NORMAL; } diff --git a/src/terrain_3d_collision.cpp b/src/terrain_3d_collision.cpp index f4f2c0c22..edd70120b 100644 --- a/src/terrain_3d_collision.cpp +++ b/src/terrain_3d_collision.cpp @@ -217,6 +217,735 @@ void Terrain3DCollision::_reload_physics_material() { } } +TypedArray Terrain3DCollision::_get_instance_cells_to_build(const Vector2i &p_snapped_pos, const real_t p_radius, const int p_region_size, const int p_cell_size, const real_t p_vertex_spacing) { + LOG(INFO, "Building list of instance cells within the radius (descaled units)"); + TypedArray instance_cells_to_build; + const int cells_per_region = p_region_size / p_cell_size; + + // p_snapped_pos is descaled (vertex units), p_radius_descaled is descaled (vertex units) + const real_t radius_sq = p_radius * p_radius; + // number of cell steps to cover radius + const int cell_radius = int(Math::ceil(p_radius / real_t(p_cell_size))); + + for (int cx = -cell_radius; cx <= cell_radius; ++cx) { + for (int cy = -cell_radius; cy <= cell_radius; ++cy) { + const Vector2i grid_pos = p_snapped_pos + Vector2i(cx * p_cell_size, cy * p_cell_size); + const Vector2i region_loc = V2I_DIVIDE_FLOOR(grid_pos, p_region_size); + + // If the region for this cell is not loaded, skip it. + const Terrain3DRegion *region = _terrain->get_data()->get_region_ptr(region_loc); + if (!region || region->is_deleted()) { + continue; + } + + // region-local grid coordinate (vertex/grid units) + const Vector2i region_local_coord = grid_pos - region_loc * p_region_size; + const Vector2i local_cell = V2I_DIVIDE_FLOOR(region_local_coord, p_cell_size); + const Vector2i cell_loc = region_loc * cells_per_region + local_cell; // global cell index + + // Compute cell bounds in descaled (vertex) units + const Vector2 cell_min = Vector2(cell_loc) * real_t(p_cell_size); + const Vector2 cell_max = cell_min + Vector2(real_t(p_cell_size), real_t(p_cell_size)); + + // Closest point on AABB to the p_snapped_pos + + Vector2 closest; + closest.x = CLAMP(p_snapped_pos.x, cell_min.x, cell_max.x); + closest.y = CLAMP(p_snapped_pos.y, cell_min.y, cell_max.y); + + // Distance compare in descaled space using closest point (AABB-circle test) + if ((closest - p_snapped_pos).length_squared() > radius_sq) { + continue; + } + + // Only build cells we don't already have active + if (_active_instance_cells.has(cell_loc)) { + continue; + } + + instance_cells_to_build.push_back(cell_loc); + } + } + + return instance_cells_to_build; +} + +Dictionary Terrain3DCollision::_get_recyclable_instances(const Vector2i &p_snapped_pos, const real_t p_radius_descaled, const int p_cell_size, const real_t p_vertex_spacing) { + Dictionary recyclable_mesh_instance_shapes; + LOG(INFO, "Decomposing cells beyond (descaled radius): ", p_radius_descaled, " of ", p_snapped_pos); + const TypedArray instance_cells = _active_instance_cells.keys(); + const real_t radius_sq = p_radius_descaled * p_radius_descaled; + + for (const Vector2i cell_origin : instance_cells) { + // cell_origin is global cell index (cell units) + const Vector2 cell_min = Vector2(cell_origin) * real_t(p_cell_size); + const Vector2 cell_max = cell_min + Vector2(real_t(p_cell_size), real_t(p_cell_size)); + + // Closest point on AABB to the brush center + Vector2 closest; + closest.x = CLAMP(p_snapped_pos.x, cell_min.x, cell_max.x); + closest.y = CLAMP(p_snapped_pos.y, cell_min.y, cell_max.y); + + // If closest point is within radius, keep the cell + if ((closest - p_snapped_pos).length_squared() <= radius_sq) { + continue; + } + + // This cell is outside the desired radius and should be decomposed. + LOG(EXTREME, "Decomposing at ", cell_origin); + const Dictionary active_instances_dict = _active_instance_cells[cell_origin]; + const TypedArray mesh_asset_keys = active_instances_dict.keys(); + for (const int mesh_asset_id : mesh_asset_keys) { + const Array active_instances_arr = active_instances_dict[mesh_asset_id]; + // Grab existing array of unused assets for this mesh type and append + Array unused_assets = recyclable_mesh_instance_shapes[mesh_asset_id]; + unused_assets.append_array(active_instances_arr); + recyclable_mesh_instance_shapes[mesh_asset_id] = unused_assets; + LOG(EXTREME, "Stashed ", active_instances_arr.size(), " * mesh asset ID ", mesh_asset_id); + } + _active_instance_cells.erase(cell_origin); + } + return recyclable_mesh_instance_shapes; +} + +Dictionary Terrain3DCollision::_get_instance_build_data(const TypedArray &p_instance_cells_to_build, const int p_region_size, const int p_cell_size, const real_t p_vertex_spacing) { + Dictionary mesh_instance_build_data; + const int cells_per_region = p_region_size / p_cell_size; + LOG(INFO, "Building instance data"); + + const int mesh_count = _terrain->get_assets()->get_mesh_count(); + for (int mesh_id = 0; mesh_id < mesh_count; mesh_id++) { + // Verify mesh id is valid and has some meshes + LOG(EXTREME, "Checking mesh id ", mesh_id); + const Ref ma = _terrain->get_assets()->get_mesh_asset(mesh_id); + + if (!ma.is_valid()) { + LOG(WARN, "MeshAsset ", mesh_id, " is null, skipping"); + continue; + } + + if (!ma->is_enabled()) { + continue; + } + + if (ma->get_shape_count() == 0) { + LOG(EXTREME, "MeshAsset ", mesh_id, " valid but has no collision shapes, skipping"); + continue; + } + + Dictionary cells; + + for (const Vector2i cell_position : p_instance_cells_to_build) { + const Vector2i region_loc = Vector2i((Vector2(cell_position) / real_t(cells_per_region)).floor()); + const Terrain3DRegion *region = _terrain->get_data()->get_region_ptr(region_loc); + + if (!region) { + LOG(WARN, "Could not get region ", region_loc, " for cell at ", cell_position); + continue; + } + + const Vector2i cell_loc = cell_position - (region_loc * cells_per_region); + const Dictionary mesh_inst_dict = region->get_instances(); + + // This could be checked once per region, if the layout was {region: { cell }} instead + // But right now it may be tested many times for the same region + if (!mesh_inst_dict.has(mesh_id)) { + continue; + } + + const Dictionary cell_inst_dict = mesh_inst_dict[mesh_id]; + if (!cell_inst_dict.has(cell_loc)) { + // no instances in this cell + continue; + } + + const Array triple = cell_inst_dict[cell_loc]; + if (triple.is_empty()) { + LOG(WARN, "Triple is empty"); + continue; + } + + TypedArray xforms = triple[0]; + if (xforms.is_empty()) { + // no instances to add + continue; + } + + LOG(DEBUG, xforms.size(), " instances of ", mesh_id, " to build in ", cell_position); + + // Compute region global offset once (Vector3) to avoid integer/implicit op errors. + const Vector3 region_global_offset = v2iv3(region_loc) * real_t(p_region_size) * p_vertex_spacing; + + // Make an explicit local copy of region_space transforms before mutating to global. + TypedArray transformed_xforms; + transformed_xforms.resize(xforms.size()); + for (int xi = 0; xi < xforms.size(); xi++) { + Transform3D t = Transform3D(xforms[xi]); // region-local + t.origin += region_global_offset; // global transform + transformed_xforms[xi] = t; + } + + cells[cell_position] = transformed_xforms; + + // Next cell + } + + if (!cells.is_empty()) { + mesh_instance_build_data[mesh_id] = cells; + } + + // Next mesh type + } + return mesh_instance_build_data; +} + +Dictionary Terrain3DCollision::_get_unused_instance_shapes(const Dictionary &p_instance_build_data, Dictionary &p_recyclable_instance_shapes) { + Dictionary unused_instance_shapes; + LOG(INFO, "Decomposing spare assets"); + const TypedArray spare_instance_keys = p_recyclable_instance_shapes.keys(); + LOG(DEBUG, spare_instance_keys.size(), " types of instance to decompose"); + for (const int mesh_id : spare_instance_keys) { + LOG(EXTREME, "Decomposing spare mesh id ", mesh_id); + + Array ma_arr = p_recyclable_instance_shapes[mesh_id]; + if (ma_arr.is_empty()) { + LOG(ERROR, "Unexpectedly found no more assets to decompose"); + continue; + } + + int keep_count = 0; + if (p_instance_build_data.has(mesh_id)) { + const Dictionary cell_data = p_instance_build_data[mesh_id]; + const TypedArray cell_keys = cell_data.keys(); + for (const Vector2i cell_position : cell_keys) { + const TypedArray cell_xforms = cell_data[cell_position]; + keep_count += cell_xforms.size(); + } + } + + LOG(DEBUG, "Decomposing all but ", keep_count, " assets of type ", mesh_id); + + // Number of instances we can fully decompose into shapes while leaving + // enough instances to match those we will rebuild + const int nb_instances_to_decompose = MAX(0, ma_arr.size() - keep_count); + for (int i = 0; i < nb_instances_to_decompose; i++) { + const TypedArray ma_instance = ma_arr.pop_back(); + if (ma_arr.is_empty()) { + p_recyclable_instance_shapes.erase(mesh_id); + } else { + p_recyclable_instance_shapes[mesh_id] = ma_arr; + } + for (const RID rid : ma_instance) { + // Determine shape type to place it in the unused pool keyed by type + const int shape_type = PS->shape_get_type(rid); + if (!rid.is_valid()) { + LOG(WARN, "Tried to decompose shape with invalid RID"); + continue; + } + TypedArray unused_shapes = unused_instance_shapes[shape_type]; + unused_shapes.push_back(rid); + unused_instance_shapes[shape_type] = unused_shapes; + LOG(EXTREME, "Stored shape ", rid); + } + } + } + + return unused_instance_shapes; +} + +void Terrain3DCollision::_destroy_remaining_instance_shapes(Dictionary &p_unused_instance_shapes) { + LOG(INFO, "Destroying unused shapes"); + // This tracks whether we destroyed any shapes, if so we will update our RID/index map + bool is_dirty = false; + const TypedArray shape_types = p_unused_instance_shapes.keys(); + for (const int shape_type : shape_types) { + const TypedArray inactive_shapes = p_unused_instance_shapes[shape_type]; + LOG(DEBUG, " Shape type: ", shape_type, " Found ", inactive_shapes.size(), " shapes"); + for (const RID shape_rid : inactive_shapes) { + if (!shape_rid.is_valid()) { + LOG(WARN, "Attempted to destroy an invalid shape"); + continue; + } + _queue_debug_mesh_update(shape_rid, Transform3D(), Ref(), DebugMeshInstanceData::Action::DESTROY); + PS->free_rid(shape_rid); + is_dirty = true; + LOG(EXTREME, "Destroyed ", shape_rid); + } + p_unused_instance_shapes.erase(shape_type); + } + // If we destroyed shapes the body_shape indices will need to update the RID/index map. + // + if (is_dirty) { + LOG(INFO, "Rebuilding shape indices"); + // Rebuild indices for all instance bodies (_instance_static_body_rid + per-MA bodies) + // Clear existing map + _RID_index_map.clear(); + // Per-mesh bodies + const TypedArray keys = _instance_body_rids.keys(); + for (const int mesh_id : keys) { + const RID body_rid = _instance_body_rids[mesh_id]; + if (!body_rid.is_valid()) { + continue; + } + for (int i = 0; i < PS->body_get_shape_count(body_rid); i++) { + _RID_index_map[PS->body_get_shape(body_rid, i)] = Array::make(body_rid, i); + } + } + } +} + +void Terrain3DCollision::_generate_instances(const Dictionary &p_instance_build_data, Dictionary &p_recyclable_instances, Dictionary &p_unused_instance_shapes) { + LOG(INFO, "Generating instances"); + const TypedArray mesh_instance_keys = p_instance_build_data.keys(); + + // Ensure per-mesh bodies exist and have proper settings + for (const int mesh_id : mesh_instance_keys) { + const Ref ma = _terrain->get_assets()->get_mesh_asset(mesh_id); + if (!ma.is_valid()) { + continue; + } + RID body_rid; + if (_instance_body_rids.has(mesh_id)) { + body_rid = _instance_body_rids[mesh_id]; + } + if (!body_rid.is_valid()) { + body_rid = PS->body_create(); + PS->body_set_mode(body_rid, PhysicsServer3D::BODY_MODE_STATIC); + PS->body_set_space(body_rid, _terrain->get_world_3d()->get_space()); + PS->body_attach_object_instance_id(body_rid, _terrain->get_instance_id()); + _instance_body_rids[mesh_id] = body_rid; + } + // Set collision layers/mask/priority for this mesh body + PS->body_set_collision_mask(body_rid, ma->get_instance_collision_mask()); + PS->body_set_collision_layer(body_rid, ma->get_instance_collision_layers()); + PS->body_set_collision_priority(body_rid, _priority); + // Set physics material params for body + if (!ma->get_instance_physics_material().is_valid()) { + PS->body_set_param(body_rid, PhysicsServer3D::BODY_PARAM_BOUNCE, 0.f); + PS->body_set_param(body_rid, PhysicsServer3D::BODY_PARAM_FRICTION, 1.f); + } else { + real_t computed_bounce = ma->get_instance_physics_material()->get_bounce() * (ma->get_instance_physics_material()->is_absorbent() ? -1.f : 1.f); + real_t computed_friction = ma->get_instance_physics_material()->get_friction() * (ma->get_instance_physics_material()->is_rough() ? -1.f : 1.f); + PS->body_set_param(body_rid, PhysicsServer3D::BODY_PARAM_BOUNCE, computed_bounce); + PS->body_set_param(body_rid, PhysicsServer3D::BODY_PARAM_FRICTION, computed_friction); + } + } + + for (const int mesh_id : mesh_instance_keys) { + // Verify mesh id is valid and has some meshes + const Ref ma = _terrain->get_assets()->get_mesh_asset(mesh_id); + if (ma.is_valid()) { + if (!ma->is_enabled()) { + LOG(ERROR, mesh_id, " is not enabled. This shouldn't happen."); + continue; + } + if (ma->get_shapes().is_empty()) { + LOG(ERROR, "MeshAsset ", mesh_id, " valid but has no collision shapes, skipping. This shouldn't happen."); + continue; + } + } else { + LOG(ERROR, "MeshAsset ", mesh_id, " is null, skipping. This shouldn't happen."); + continue; + } + + const Dictionary cell_data = p_instance_build_data[mesh_id]; + if (cell_data.is_empty()) { + // No new instances of this type are needed + continue; + } + + const RID target_body = _instance_body_rids[mesh_id]; + + const TypedArray cell_positions = cell_data.keys(); + for (const Vector2i cell_pos : cell_positions) { + const TypedArray xforms = cell_data[cell_pos]; + + if (xforms.is_empty()) { + LOG(ERROR, "No instances of type ", mesh_id, " in cell ", cell_pos, " to create. This shouldn't happen."); + continue; + } + + for (int x = 0; x < xforms.size(); x++) { + const Transform3D xform = xforms[x]; + TypedArray shapes; + Dictionary active_instances_dict = _active_instance_cells[cell_pos]; + Array active_instances_arr = active_instances_dict[mesh_id]; + Array reusable_assets; + if (p_recyclable_instances.has(mesh_id)) { + reusable_assets = Array(p_recyclable_instances[mesh_id]); + } + if (!reusable_assets.is_empty()) { + // If we have a fully formed instance (array of RIDs) available then + // pop one and reposition its member shapes rather than creating new shapes. + const TypedArray reusable_shapes = TypedArray(reusable_assets.pop_back()); + if (reusable_assets.is_empty()) { + p_recyclable_instances.erase(mesh_id); + } else { + p_recyclable_instances[mesh_id] = reusable_assets; + } + for (int s = 0; s < reusable_shapes.size(); s++) { + const Transform3D shape_transform = ma->get_shape_transforms()[s]; + const Transform3D this_transform = xform * shape_transform; + const RID shape_rid = reusable_shapes[s]; + // Get the shape index and body from the map if present + if (!_RID_index_map.has(shape_rid)) { + LOG(ERROR, shape_rid, " does not have an entry in RID_index_map. This shouldn't happen."); + continue; + } + Variant map_entry = _RID_index_map[shape_rid]; + RID current_body = RID(); + int shape_id = -1; + + const Array me = map_entry; + current_body = me[0]; + shape_id = int(me[1]); + + // If the shape is on a different body, move it to target body + if (current_body != target_body) { + if (current_body.is_valid()) { + PS->body_remove_shape(current_body, shape_id); + } + // Add to target body + PS->body_add_shape(target_body, shape_rid, this_transform); + LOG(EXTREME, "Reparented shape ", shape_rid, " to body for mesh ", mesh_id); + } else { + // Same body: just update transform + PS->body_set_shape_transform(target_body, shape_id, this_transform); + } + if (is_instance_collision_editor_mode()) { + const Ref &ma_shape = cast_to(ma->get_shapes()[s]); + _queue_debug_mesh_update(shape_rid, this_transform, ma_shape->get_debug_mesh(), DebugMeshInstanceData::Action::UPDATE); + } + } + active_instances_arr.push_back(reusable_shapes); + } else { + // No shapes to recycle, create new ones or reuse spare shapes + LOG(DEBUG, "No instances of ", mesh_id, " to recycle"); + for (int i = 0; i < ma->get_shape_count(); i++) { + const Ref ma_shape = cast_to(ma->get_shapes()[i]); + const Transform3D shape_transforms = ma->get_shape_transforms()[i]; + const int shape_type = PS->shape_get_type(ma_shape->get_rid()); + const Transform3D this_transform = xform * shape_transforms; + RID shape_rid = RID(); + // Check for a shape in the unused shapes pool + TypedArray unused_shapes; + if (p_unused_instance_shapes.has(shape_type)) { + unused_shapes = TypedArray(p_unused_instance_shapes[shape_type]); + } + // If no unused shapes available, attempt to decompose one recyclable full-instance + if (unused_shapes.is_empty()) { + if (p_recyclable_instances.has(mesh_id)) { + Array recyclable_instances = Array(p_recyclable_instances[mesh_id]); + if (!recyclable_instances.is_empty()) { + // Pop one full instance and distribute its member RIDs into the unused pool + TypedArray recyclable_instance = TypedArray(recyclable_instances.pop_back()); + if (recyclable_instances.is_empty()) { + p_recyclable_instances.erase(mesh_id); + } else { + p_recyclable_instances[mesh_id] = recyclable_instances; + } + for (const RID &recyclable_shape_rid : recyclable_instance) { + if (!recyclable_shape_rid.is_valid()) { + continue; + } + const int recyclable_shape_type = PS->shape_get_type(recyclable_shape_rid); + TypedArray pool = p_unused_instance_shapes[recyclable_shape_type]; + pool.push_back(recyclable_shape_rid); + p_unused_instance_shapes[recyclable_shape_type] = pool; + LOG(EXTREME, "Decomposed recyclable instance, stored shape ", recyclable_shape_rid); + } + // refresh local unused_shapes for this type + if (p_unused_instance_shapes.has(shape_type)) { + unused_shapes = TypedArray(p_unused_instance_shapes[shape_type]); + } + } + } + } + + if (!unused_shapes.is_empty()) { + shape_rid = unused_shapes.pop_back(); + if (unused_shapes.is_empty()) { + p_unused_instance_shapes.erase(shape_type); + } else { + p_unused_instance_shapes[shape_type] = unused_shapes; + } + // If shape belonged to another body, reparent it + if (_RID_index_map.has(shape_rid)) { + Variant me = _RID_index_map[shape_rid]; + RID current_body = RID(); + int current_idx = -1; + + Array arr = me; + current_body = arr[0]; + current_idx = int(arr[1]); + + if (current_body != target_body) { + if (current_body.is_valid()) { + PS->body_remove_shape(current_body, current_idx); + } + PS->body_add_shape(target_body, shape_rid, this_transform); + LOG(EXTREME, "Reparented shape ", shape_rid, " to body for mesh ", mesh_id); + } else { + // same body: update transform + PS->body_set_shape_transform(target_body, current_idx, this_transform); + } + } + // Copy geometry and set transform if newly created or moved + PS->shape_set_data(shape_rid, PS->shape_get_data(ma_shape->get_rid())); + // If shape was newly added to body, we need to set map entry later when rebuilding + shapes.push_back(shape_rid); + if (is_instance_collision_editor_mode()) { + _queue_debug_mesh_update(shape_rid, this_transform, ma_shape->get_debug_mesh(), DebugMeshInstanceData::Action::UPDATE); + } + } + // If we didn't find a shape to recycle, create a new one + if (!shape_rid.is_valid()) { + LOG(DEBUG, "No shapes to recycle. Creating new shape for ", ma_shape->get_name(), " type: ", shape_type); + // Create shape using PS + // Different methods are required to create different shapes + switch (shape_type) { + case PhysicsServer3D::ShapeType::SHAPE_SPHERE: + shape_rid = PS->sphere_shape_create(); + break; + case PhysicsServer3D::ShapeType::SHAPE_BOX: + shape_rid = PS->box_shape_create(); + break; + case PhysicsServer3D::ShapeType::SHAPE_CAPSULE: + shape_rid = PS->capsule_shape_create(); + break; + case PhysicsServer3D::ShapeType::SHAPE_CYLINDER: + shape_rid = PS->cylinder_shape_create(); + break; + case PhysicsServer3D::ShapeType::SHAPE_CONVEX_POLYGON: + shape_rid = PS->convex_polygon_shape_create(); + break; + case PhysicsServer3D::ShapeType::SHAPE_CONCAVE_POLYGON: + shape_rid = PS->concave_polygon_shape_create(); + break; + default: + LOG(WARN, "Tried to use unsupported shape type : ", shape_type); + break; + } + if (!shape_rid.is_valid()) { + LOG(ERROR, "Failed to create shape type : ", shape_type); + continue; + } + // Add the shape to the target body + PS->shape_set_data(shape_rid, PS->shape_get_data(ma_shape->get_rid())); + PS->body_add_shape(target_body, shape_rid, this_transform); + shapes.push_back(shape_rid); + if (is_instance_collision_editor_mode()) { + _queue_debug_mesh_update(shape_rid, this_transform, ma_shape->get_debug_mesh(), DebugMeshInstanceData::Action::CREATE); + } + } + // next shape++ + } + active_instances_arr.push_back(shapes); + } + active_instances_dict[mesh_id] = active_instances_arr; + _active_instance_cells[cell_pos] = active_instances_dict; + } + } + } + + // Rebuild RID -> [body, index] mapping for all instance bodies + _RID_index_map.clear(); + const TypedArray body_keys = _instance_body_rids.keys(); + for (const int mid : body_keys) { + const RID bid = _instance_body_rids[mid]; + if (!bid.is_valid()) + continue; + for (int i = 0; i < PS->body_get_shape_count(bid); i++) { + _RID_index_map[PS->body_get_shape(bid, i)] = Array::make(bid, i); + } + } +} + +void Terrain3DCollision::update_instance_collision() { + IS_INIT(VOID); + if (!is_instance_collision_enabled() || (IS_EDITOR && !is_instance_collision_editor_mode())) { + return; + } + const int time = Time::get_singleton()->get_ticks_usec(); + const int region_size = _terrain->get_region_size(); + const real_t vertex_spacing = _terrain->get_vertex_spacing(); + const real_t radius_world = _instance_collision_radius; + const int cell_size = _terrain->get_instancer()->CELL_SIZE; + // snapped_pos is in descaled (vertex) units + const Vector2i snapped_pos = _snap_to_grid(_terrain->get_collision_target_position() / vertex_spacing); + + // Convert radius from world units to descaled (vertex) units for cell selection and recycling + const real_t radius_descaled = radius_world / vertex_spacing; + + // Skip if location hasn't moved to next step + if (!_instance_collision_is_dirty && (_last_snapped_pos_instance_collision - snapped_pos).length_squared() < (_shape_size * _shape_size)) { + return; + } + if (_instance_collision_is_dirty) { + _instance_collision_is_dirty = false; + destroy_instance_collision(); + } + if (!_terrain->get_data()->get_regionp(v2v3(snapped_pos)).is_valid()) { + return; + } + LOG(EXTREME, "Updating instance collision at ", snapped_pos); + + // Determine which cells need to be built (snapped_pos and radius_descaled in descaled units) + const TypedArray instance_cells_to_build = _get_instance_cells_to_build(snapped_pos, radius_descaled, region_size, cell_size, vertex_spacing); + + // Decompose cells outside of radius (snapped_pos and radius_descaled in descaled units) + Dictionary recyclable_instances = _get_recyclable_instances(snapped_pos, radius_descaled, cell_size, vertex_spacing); + + // Build a list of instances to create + Dictionary instance_build_data = _get_instance_build_data(instance_cells_to_build, region_size, cell_size, vertex_spacing); + + // Decompose assets which will not be recycled in full + Dictionary unused_instance_shapes = _get_unused_instance_shapes(instance_build_data, recyclable_instances); + + // Do the instancing + _generate_instances(instance_build_data, recyclable_instances, unused_instance_shapes); + + // Destroy any remaining unused shapes + _destroy_remaining_instance_shapes(unused_instance_shapes); + + _last_snapped_pos_instance_collision = snapped_pos; + + LOG(EXTREME, "Active instance collision cell count : ", _active_instance_cells.size()); + LOG(EXTREME, "Instance collision update time: ", Time::get_singleton()->get_ticks_usec() - time, " us"); +} + +void Terrain3DCollision::destroy_instance_collision() { + LOG(INFO, "Destroying instance collision"); + const int time = Time::get_singleton()->get_ticks_usec(); + _destroy_debug_mesh_instances(); + for (const RID &body_rid : _instance_body_rids.values()) { + while (PS->body_get_shape_count(body_rid) > 0) { + const RID shape_rid = PS->body_get_shape(body_rid, 0); + PS->body_remove_shape(body_rid, 0); + PS->free_rid(shape_rid); + LOG(EXTREME, "Destroyed shape ", shape_rid); + } + } + _instance_body_rids.clear(); + _active_instance_cells.clear(); + _last_snapped_pos_instance_collision = V2I_MAX; + LOG(EXTREME, "Destroy instance collision update time: ", Time::get_singleton()->get_ticks_usec() - time, " us"); +} + +void Terrain3DCollision::_create_debug_mesh_instance(const RID &p_shape_rid, const Transform3D &p_xform, const Ref &p_debug_mesh) { + if (!is_instance_collision_editor_mode()) { + return; + } + const int time = Time::get_singleton()->get_ticks_usec(); + if (!p_xform.is_finite()) { + LOG(WARN, "Transform invalid for shape ", p_shape_rid); + LOG(WARN, "xform: ", p_xform); + return; + } + if (!p_debug_mesh.is_valid()) { + LOG(WARN, "Invalid debug mesh for shape ", p_shape_rid); + return; + } + if (_shape_debug_mesh_pairs.has(p_shape_rid)) { + LOG(WARN, "Debug mesh instance already exists for shape ", p_shape_rid); + return; + } + const RID visual_rid = RS->instance_create(); + RS->instance_set_scenario(visual_rid, _terrain->get_world_3d()->get_scenario()); + RS->instance_set_base(visual_rid, p_debug_mesh->get_rid()); + RS->instance_set_transform(visual_rid, p_xform); + _shape_debug_mesh_pairs[p_shape_rid] = visual_rid; + LOG(EXTREME, "Created visual rid ", visual_rid, "to pair with ", p_shape_rid, " at ", p_xform.origin, " in ", Time::get_singleton()->get_ticks_usec() - time, " us"); +} + +void Terrain3DCollision::_update_debug_mesh_instance(const RID &p_shape_rid, const Transform3D &p_xform, const Ref &p_debug_mesh) { + if (!is_instance_collision_editor_mode()) { + return; + } + const int time = Time::get_singleton()->get_ticks_usec(); + if (!p_xform.is_finite()) { + LOG(WARN, "Transform invalid for shape ", p_shape_rid); + LOG(WARN, "xform: ", p_xform); + return; + } + if (!_shape_debug_mesh_pairs.has(p_shape_rid)) { + LOG(EXTREME, "Shape RID not paired with a debug mesh instance, skipping"); + return; + } + const RID visual_rid = _shape_debug_mesh_pairs[p_shape_rid]; + if (!visual_rid.is_valid()) { + LOG(WARN, "Debug mesh instance RID for shape ", p_shape_rid, " was invalid, skipping"); + return; + } + RS->instance_set_transform(visual_rid, p_xform); + RS->instance_set_base(visual_rid, p_debug_mesh->get_rid()); + LOG(EXTREME, "Updated debug mesh instance in : ", Time::get_singleton()->get_ticks_usec() - time, " us"); +} + +void Terrain3DCollision::_destroy_debug_mesh_instance(const RID &p_shape_rid) { + const int time = Time::get_singleton()->get_ticks_usec(); + if (!_shape_debug_mesh_pairs.has(p_shape_rid)) { + LOG(EXTREME, "Shape RID not paired with a visual instance, skipping"); + return; + } + const RID &visual_rid = _shape_debug_mesh_pairs[p_shape_rid]; + _shape_debug_mesh_pairs.erase(p_shape_rid); + if (!visual_rid.is_valid()) { + LOG(EXTREME, "Debug mesh instance RID invalid, skipping"); + return; + } + RS->free_rid(visual_rid); + LOG(EXTREME, "Destroyed debug mesh instance ", visual_rid, " which was paired with shape ", p_shape_rid, "in : ", Time::get_singleton()->get_ticks_usec() - time, " us"); +} + +void Terrain3DCollision::_destroy_debug_mesh_instances() { + LOG(INFO, "Destroying visual instances"); + const int time = Time::get_singleton()->get_ticks_usec(); + const Array keys = _shape_debug_mesh_pairs.keys(); + for (const RID &shape_rid : keys) { + _queue_debug_mesh_update(shape_rid, Transform3D(), Ref(), DebugMeshInstanceData::Action::DESTROY); + } + // Process these updates immediately + _process_debug_mesh_updates(); + LOG(EXTREME, "Destroyed all debug mesh instances in : ", Time::get_singleton()->get_ticks_usec() - time, " us"); +} + +void Terrain3DCollision::_queue_debug_mesh_update(const RID &p_shape_rid, const Transform3D &p_xform, const Ref &p_debug_mesh, const DebugMeshInstanceData::Action &p_action) { + DebugMeshInstanceData vi; + vi.shape_rid = p_shape_rid; + vi.xform = p_xform; + vi.debug_mesh = p_debug_mesh; + vi.action = p_action; + _debug_visual_instance_queue.push_back(vi); + if (!RS->is_connected("frame_pre_draw", callable_mp(this, &Terrain3DCollision::_process_debug_mesh_updates))) { + LOG(DEBUG, "Connect to RS::frame_pre_draw signal"); + RS->connect("frame_pre_draw", callable_mp(this, &Terrain3DCollision::_process_debug_mesh_updates)); + } +} + +void Terrain3DCollision::_process_debug_mesh_updates() { + if (_debug_visual_instance_queue.empty()) { + if (RS->is_connected("frame_pre_draw", callable_mp(this, &Terrain3DCollision::_process_debug_mesh_updates))) { + LOG(DEBUG, "Disconnect from RS::frame_pre_draw signal"); + RS->disconnect("frame_pre_draw", callable_mp(this, &Terrain3DCollision::_process_debug_mesh_updates)); + } + return; + } + if (!_terrain->is_inside_tree()) { + return; + } + for (const DebugMeshInstanceData &vi : _debug_visual_instance_queue) { + if (vi.action == DebugMeshInstanceData::Action::CREATE) { + _create_debug_mesh_instance(vi.shape_rid, vi.xform, vi.debug_mesh); + } else if (vi.action == DebugMeshInstanceData::Action::UPDATE) { + _update_debug_mesh_instance(vi.shape_rid, vi.xform, vi.debug_mesh); + } else if (vi.action == DebugMeshInstanceData::Action::DESTROY) { + _destroy_debug_mesh_instance(vi.shape_rid); + } + } + _debug_visual_instance_queue.clear(); +} + /////////////////////////// // Public Functions /////////////////////////// @@ -310,10 +1039,9 @@ void Terrain3DCollision::build() { } else { RID shape_rid = PS->heightmap_shape_create(); PS->body_add_shape(_static_body_rid, shape_rid, xform, true); - LOG(DEBUG, "Adding shape: ", i, ", rid: ", shape_rid.get_id(), " pos: ", _shape_get_position(i)); + LOG(EXTREME, "Adding shape: ", i, ", rid: ", shape_rid.get_id(), " pos: ", _shape_get_position(i)); } } - _initialized = true; update(); } @@ -327,6 +1055,7 @@ void Terrain3DCollision::update(const bool p_rebuild) { build(); return; } + int time = Time::get_singleton()->get_ticks_usec(); real_t spacing = _terrain->get_vertex_spacing(); @@ -425,7 +1154,7 @@ void Terrain3DCollision::update(const bool p_rebuild) { _last_snapped_pos = snapped_pos; LOG(EXTREME, "Setting _last_snapped_pos: ", _last_snapped_pos); LOG(EXTREME, "inactive_shape_ids size: ", inactive_shape_ids.size()); - + LOG(EXTREME, "Terrain collision update time: ", Time::get_singleton()->get_ticks_usec() - time, " us"); } else { // Full collision int shape_count = _terrain->get_data()->get_region_count(); @@ -445,8 +1174,8 @@ void Terrain3DCollision::update(const bool p_rebuild) { _shape_set_disabled(i, false); _shape_set_data(i, shape_data); } + LOG(EXTREME, "Terrain collision update time: ", Time::get_singleton()->get_ticks_usec() - time, " us"); } - LOG(EXTREME, "Collision update time: ", Time::get_singleton()->get_ticks_usec() - time, " us"); } void Terrain3DCollision::destroy() { @@ -492,6 +1221,20 @@ void Terrain3DCollision::set_mode(const CollisionMode p_mode) { } } +void Terrain3DCollision::set_instance_collision_mode(const InstanceCollisionMode p_mode) { + SET_IF_DIFF(_instance_collision_mode, p_mode); + LOG(INFO, "Setting instance collision mode: ", p_mode); + destroy_instance_collision(); +} + +void Terrain3DCollision::set_instance_collision_radius(const real_t p_radius) { + SET_IF_DIFF(_instance_collision_radius, p_radius); + LOG(INFO, "Setting instance collision redius: ", p_radius); + if (is_instance_collision_enabled()) { + set_instance_collision_dirty(); + } +} + void Terrain3DCollision::set_shape_size(const uint16_t p_size) { uint16_t size = CLAMP(p_size, 8, 64); size = int_round_mult(size, uint16_t(8)); @@ -597,11 +1340,15 @@ RID Terrain3DCollision::get_rid() const { /////////////////////////// void Terrain3DCollision::_bind_methods() { - BIND_ENUM_CONSTANT(DISABLED); - BIND_ENUM_CONSTANT(DYNAMIC_GAME); - BIND_ENUM_CONSTANT(DYNAMIC_EDITOR); - BIND_ENUM_CONSTANT(FULL_GAME); - BIND_ENUM_CONSTANT(FULL_EDITOR); + BIND_ENUM_CONSTANT(CollisionMode::DISABLED); + BIND_ENUM_CONSTANT(CollisionMode::DYNAMIC_GAME); + BIND_ENUM_CONSTANT(CollisionMode::DYNAMIC_EDITOR); + BIND_ENUM_CONSTANT(CollisionMode::FULL_GAME); + BIND_ENUM_CONSTANT(CollisionMode::FULL_EDITOR); + + BIND_ENUM_CONSTANT(InstanceCollisionMode::INSTANCE_COLLISION_DISABLED); + BIND_ENUM_CONSTANT(InstanceCollisionMode::INSTANCE_COLLISION_DYNAMIC_GAME); + BIND_ENUM_CONSTANT(InstanceCollisionMode::INSTANCE_COLLISION_DYNAMIC_EDITOR); ClassDB::bind_method(D_METHOD("build"), &Terrain3DCollision::build); ClassDB::bind_method(D_METHOD("update", "rebuild"), &Terrain3DCollision::update, DEFVAL(false)); @@ -612,6 +1359,16 @@ void Terrain3DCollision::_bind_methods() { ClassDB::bind_method(D_METHOD("is_editor_mode"), &Terrain3DCollision::is_editor_mode); ClassDB::bind_method(D_METHOD("is_dynamic_mode"), &Terrain3DCollision::is_dynamic_mode); + ClassDB::bind_method(D_METHOD("update_instance_collision"), &Terrain3DCollision::update_instance_collision); + ClassDB::bind_method(D_METHOD("destroy_instance_collision"), &Terrain3DCollision::destroy_instance_collision); + ClassDB::bind_method(D_METHOD("set_instance_collision_mode", "mode"), &Terrain3DCollision::set_instance_collision_mode); + ClassDB::bind_method(D_METHOD("get_instance_collision_mode"), &Terrain3DCollision::get_instance_collision_mode); + ClassDB::bind_method(D_METHOD("is_instance_collision_enabled"), &Terrain3DCollision::is_instance_collision_enabled); + ClassDB::bind_method(D_METHOD("is_instance_collision_editor_mode"), &Terrain3DCollision::is_instance_collision_editor_mode); + ClassDB::bind_method(D_METHOD("is_instance_collision_dynamic_mode"), &Terrain3DCollision::is_instance_collision_dynamic_mode); + ClassDB::bind_method(D_METHOD("set_instance_collision_radius", "radius"), &Terrain3DCollision::set_instance_collision_radius); + ClassDB::bind_method(D_METHOD("get_instance_collision_radius"), &Terrain3DCollision::get_instance_collision_radius); + ClassDB::bind_method(D_METHOD("set_shape_size", "size"), &Terrain3DCollision::set_shape_size); ClassDB::bind_method(D_METHOD("get_shape_size"), &Terrain3DCollision::get_shape_size); ClassDB::bind_method(D_METHOD("set_radius", "radius"), &Terrain3DCollision::set_radius); @@ -633,4 +1390,6 @@ void Terrain3DCollision::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "mask", PROPERTY_HINT_LAYERS_3D_PHYSICS), "set_mask", "get_mask"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "priority", PROPERTY_HINT_RANGE, "0.1,256,.1"), "set_priority", "get_priority"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "physics_material", PROPERTY_HINT_RESOURCE_TYPE, "PhysicsMaterial"), "set_physics_material", "get_physics_material"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "instance_collision_mode", PROPERTY_HINT_ENUM, "Disabled,Dynamic / Game,Dynamic / Editor"), "set_instance_collision_mode", "get_instance_collision_mode"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "instance_collision_radius", PROPERTY_HINT_RANGE, "1.,256.,1."), "set_instance_collision_radius", "get_instance_collision_radius"); } diff --git a/src/terrain_3d_collision.h b/src/terrain_3d_collision.h index bcfe38559..58e0b5b01 100644 --- a/src/terrain_3d_collision.h +++ b/src/terrain_3d_collision.h @@ -25,12 +25,17 @@ class Terrain3DCollision : public Object { FULL_GAME, FULL_EDITOR, }; + enum InstanceCollisionMode { + INSTANCE_COLLISION_DISABLED, + INSTANCE_COLLISION_DYNAMIC_GAME, + INSTANCE_COLLISION_DYNAMIC_EDITOR, + }; private: Terrain3D *_terrain = nullptr; // Public settings - CollisionMode _mode = DYNAMIC_GAME; + CollisionMode _mode = CollisionMode::DYNAMIC_GAME; uint16_t _shape_size = 16; uint16_t _radius = 64; uint32_t _layer = 1; @@ -46,6 +51,40 @@ class Terrain3DCollision : public Object { bool _initialized = false; Vector2i _last_snapped_pos = V2I_MAX; + // Instance collision data + InstanceCollisionMode _instance_collision_mode = InstanceCollisionMode::INSTANCE_COLLISION_DYNAMIC_GAME; + Vector2i _last_snapped_pos_instance_collision = V2I_MAX; + real_t _instance_collision_radius = 64.f; + bool _instance_collision_is_dirty = false; + + // Per-mesh instance PhysicsServer bodies: {mesh_id:int -> body_rid:RID} + Dictionary _instance_body_rids = {}; + + // Stored as {rid:RID} -> [body_rid:RID, shape_index:int] + Dictionary _RID_index_map = {}; + + // Debug visualisation queue + + struct DebugMeshInstanceData { + enum class Action { + CREATE, + UPDATE, + DESTROY, + }; + RID shape_rid; + Action action = Action::CREATE; + Transform3D xform; + Ref debug_mesh; + }; + + std::vector _debug_visual_instance_queue; + + // Stored as {PS_rid:RID} -> RS_rid:RID + Dictionary _shape_debug_mesh_pairs = {}; + + // Stored as {cell_loc:v2} -> {mesh_asset_id:int} -> [instances [shapes [RID]]] + Dictionary _active_instance_cells = {}; + Vector2i _snap_to_grid(const Vector2i &p_pos) const; Vector2i _snap_to_grid(const Vector3 &p_pos) const; Dictionary _get_shape_data(const Vector2i &p_position, const int p_size); @@ -57,9 +96,24 @@ class Terrain3DCollision : public Object { void _reload_physics_material(); + TypedArray _get_instance_cells_to_build(const Vector2i &p_snapped_pos, const real_t p_radius, const int p_region_size, const int p_cell_size, const real_t p_vertex_spacing); + Dictionary _get_recyclable_instances(const Vector2i &p_snapped_pos, const real_t p_radius, const int p_cell_size, const real_t p_vertex_spacing); + Dictionary _get_instance_build_data(const TypedArray &p_instance_cells_to_build, const int p_region_size, const int p_cell_size, const real_t p_vertex_spacing); + Dictionary _get_unused_instance_shapes(const Dictionary &p_mesh_instance_build_data, Dictionary &p_recyclable_mesh_instance_shapes); + + void _destroy_remaining_instance_shapes(Dictionary &p_unused_instance_shapes); + void _generate_instances(const Dictionary &p_instance_build_data, Dictionary &p_recyclable_instances, Dictionary &p_unused_instance_shapes); + void _create_debug_mesh_instance(const RID &p_shape_rid, const Transform3D &p_xform, const Ref &p_debug_mesh); + void _update_debug_mesh_instance(const RID &p_shape_rid, const Transform3D &p_xform, const Ref &p_debug_mesh); + void _destroy_debug_mesh_instance(const RID &p_shape_rid); + void _destroy_debug_mesh_instances(); + void _queue_debug_mesh_update(const RID &p_shape_rid, const Transform3D &p_xform, const Ref &p_debug_mesh, const DebugMeshInstanceData::Action &p_action); + void _process_debug_mesh_updates(); + public: Terrain3DCollision() {} ~Terrain3DCollision() { destroy(); } + void initialize(Terrain3D *p_terrain); void build(); @@ -73,6 +127,17 @@ class Terrain3DCollision : public Object { bool is_editor_mode() const { return _mode == DYNAMIC_EDITOR || _mode == FULL_EDITOR; } bool is_dynamic_mode() const { return _mode == DYNAMIC_GAME || _mode == DYNAMIC_EDITOR; } + void update_instance_collision(); + void destroy_instance_collision(); + void set_instance_collision_mode(const InstanceCollisionMode p_mode); + InstanceCollisionMode get_instance_collision_mode() const { return _instance_collision_mode; } + bool is_instance_collision_enabled() const { return _instance_collision_mode > InstanceCollisionMode::INSTANCE_COLLISION_DISABLED; } + bool is_instance_collision_editor_mode() const { return _instance_collision_mode == InstanceCollisionMode::INSTANCE_COLLISION_DYNAMIC_EDITOR; } + bool is_instance_collision_dynamic_mode() const { return _instance_collision_mode == InstanceCollisionMode::INSTANCE_COLLISION_DYNAMIC_GAME || _instance_collision_mode == InstanceCollisionMode::INSTANCE_COLLISION_DYNAMIC_EDITOR; } + void set_instance_collision_radius(const real_t p_radius); + real_t get_instance_collision_radius() const { return _instance_collision_radius; } + void set_instance_collision_dirty(const bool p_dirty = true) { _instance_collision_is_dirty = p_dirty; } + void set_shape_size(const uint16_t p_size); uint16_t get_shape_size() const { return _shape_size; } void set_radius(const uint16_t p_radius); @@ -94,6 +159,9 @@ class Terrain3DCollision : public Object { using CollisionMode = Terrain3DCollision::CollisionMode; VARIANT_ENUM_CAST(Terrain3DCollision::CollisionMode); +using InstanceCollisionMode = Terrain3DCollision::InstanceCollisionMode; +VARIANT_ENUM_CAST(Terrain3DCollision::InstanceCollisionMode); + inline Vector2i Terrain3DCollision::_snap_to_grid(const Vector2i &p_pos) const { return Vector2i(int_round_mult(p_pos.x, int32_t(_shape_size)), int_round_mult(p_pos.y, int32_t(_shape_size))); @@ -105,4 +173,4 @@ inline Vector2i Terrain3DCollision::_snap_to_grid(const Vector3 &p_pos) const { _shape_size; } -#endif // TERRAIN3D_COLLISION_CLASS_H \ No newline at end of file +#endif // TERRAIN3D_COLLISION_CLASS_H diff --git a/src/terrain_3d_instancer.cpp b/src/terrain_3d_instancer.cpp index ddfe8cd2a..9ad83043e 100644 --- a/src/terrain_3d_instancer.cpp +++ b/src/terrain_3d_instancer.cpp @@ -56,6 +56,7 @@ void Terrain3DInstancer::_process_updates() { } _queued_updates.clear(); _terrain->get_assets()->load_pending_meshes(); + _terrain->get_collision() ? _terrain->get_collision()->set_instance_collision_dirty(true) : void(); return; } @@ -102,6 +103,7 @@ void Terrain3DInstancer::_process_updates() { } _queued_updates.clear(); _terrain->get_assets()->load_pending_meshes(); + _terrain->get_collision() ? _terrain->get_collision()->set_instance_collision_dirty(true) : void(); } void Terrain3DInstancer::_update_mmi_by_region(const Terrain3DRegion *p_region, const int p_mesh_id) { @@ -113,6 +115,7 @@ void Terrain3DInstancer::_update_mmi_by_region(const Terrain3DRegion *p_region, LOG(ERROR, "p_mesh_id is out of bounds"); return; } + Vector2i region_loc = p_region->get_location(); Dictionary mesh_inst_dict = p_region->get_instances(); @@ -503,15 +506,6 @@ RID Terrain3DInstancer::_create_multimesh(const int p_mesh_id, const int p_lod, return mm; } -Vector2i Terrain3DInstancer::_get_cell(const Vector3 &p_global_position, const int p_region_size) const { - IS_INIT(V2I_ZERO); - real_t vertex_spacing = _terrain->get_vertex_spacing(); - Vector2i cell; - cell.x = UtilityFunctions::posmod(UtilityFunctions::floori(p_global_position.x / vertex_spacing), p_region_size) / CELL_SIZE; - cell.y = UtilityFunctions::posmod(UtilityFunctions::floori(p_global_position.z / vertex_spacing), p_region_size) / CELL_SIZE; - return cell; -} - // Get appropriate terrain height. Could find terrain (excluding slope or holes) or optional collision Array Terrain3DInstancer::_get_usable_height(const Vector3 &p_global_position, const Vector2 &p_slope_range, const bool p_on_collision, const real_t p_raycast_start) const { IS_DATA_INIT(Array()); @@ -982,7 +976,7 @@ void Terrain3DInstancer::append_region(const Ref &p_region, con for (int i = 0; i < p_xforms.size(); i++) { Transform3D xform = p_xforms[i]; Color col = p_colors[i]; - Vector2i cell = _get_cell(xform.origin, region_size); + Vector2i cell = Util::get_cell(xform.origin, region_size, p_region->get_vertex_spacing(), CELL_SIZE); // Get current instance arrays or create if none Array triple = cell_locations[cell]; @@ -1144,7 +1138,7 @@ int Terrain3DInstancer::get_closest_mesh_id(const Vector3 &p_global_position) co } int region_size = region->get_region_size(); Vector3 region_global_pos = v2iv3(region_loc) * real_t(region_size) * _terrain->get_vertex_spacing(); - Vector2i cell = _get_cell(p_global_position, region_size); + Vector2i cell = Util::get_cell(p_global_position, region_size, region->get_vertex_spacing(), CELL_SIZE); Dictionary mesh_inst_dict = region->get_instances(); if (mesh_inst_dict.is_empty()) { return -1; // No meshes found diff --git a/src/terrain_3d_instancer.h b/src/terrain_3d_instancer.h index 0b5d91358..0f8a731d0 100644 --- a/src/terrain_3d_instancer.h +++ b/src/terrain_3d_instancer.h @@ -63,7 +63,6 @@ class Terrain3DInstancer : public Object { void _destroy_mmi_by_cell(const Vector2i &p_region_loc, const int p_mesh_id, const Vector2i p_cell, const int p_lod = INT32_MAX); void _backup_region(const Ref &p_region); RID _create_multimesh(const int p_mesh_id, const int p_lod, const TypedArray &p_xforms = TypedArray(), const PackedColorArray &p_colors = PackedColorArray()) const; - Vector2i _get_cell(const Vector3 &p_global_position, const int p_region_size) const; Array _get_usable_height(const Vector3 &p_global_position, const Vector2 &p_slope_range, const bool p_on_collision, const real_t p_raycast_start) const; public: diff --git a/src/terrain_3d_mesh_asset.cpp b/src/terrain_3d_mesh_asset.cpp index 2b4a06d79..b917a6155 100644 --- a/src/terrain_3d_mesh_asset.cpp +++ b/src/terrain_3d_mesh_asset.cpp @@ -160,6 +160,8 @@ void Terrain3DMeshAsset::clear() { _generated_type = TYPE_NONE; _generated_faces = 2; _generated_size = V2(1.f); + _shapes.clear(); + _shape_transforms.clear(); _height_offset = 0.f; _density = 0.f; _cast_shadows = SHADOWS_ON; @@ -172,6 +174,11 @@ void Terrain3DMeshAsset::clear() { _clear_lod_ranges(); _fade_margin = 0.f; _thumbnail.unref(); + // instance collision defaults + _instance_collision_layers = 1; + _instance_physics_material.unref(); + _instance_collision_mask = 1; + _instance_collision_enabled = true; } void Terrain3DMeshAsset::set_name(const String &p_name) { @@ -209,6 +216,33 @@ void Terrain3DMeshAsset::set_highlighted(const bool p_highlighted) { emit_signal("instancer_setting_changed", _id); } +void Terrain3DMeshAsset::set_instance_collision_enabled(const bool p_enabled) { + SET_IF_DIFF(_instance_collision_enabled, p_enabled); + LOG(INFO, "MeshAsset ", _id, ": Setting instance collision enabled: ", _instance_collision_enabled); + emit_signal("instancer_setting_changed", _id); +} + +void Terrain3DMeshAsset::set_instance_collision_layers(const uint32_t p_layers) { + SET_IF_DIFF(_instance_collision_layers, p_layers); + LOG(INFO, "MeshAsset ", _id, ": Setting instance collision layers: ", _instance_collision_layers); + emit_signal("instancer_setting_changed", _id); +} + +void Terrain3DMeshAsset::set_instance_collision_mask(const uint32_t p_mask) { + SET_IF_DIFF(_instance_collision_mask, p_mask); + LOG(INFO, "MeshAsset ", _id, ": Setting instance collision mask: ", _instance_collision_mask); + emit_signal("instancer_setting_changed", _id); +} + +void Terrain3DMeshAsset::set_instance_physics_material(const Ref &p_mat) { + if (_instance_physics_material == p_mat) { + return; + } + _instance_physics_material = p_mat; + LOG(INFO, "MeshAsset ", _id, ": Setting instance physics material"); + emit_signal("instancer_setting_changed", _id); +} + Color Terrain3DMeshAsset::get_highlight_color() const { StandardMaterial3D *mat = cast_to(_highlight_mat.ptr()); if (_highlighted && mat) { @@ -311,6 +345,19 @@ void Terrain3DMeshAsset::set_scene_file(const Ref &p_scene_file) { } _pending_meshes.push_back(mesh); } + + // Read and store phyics shapes + TypedArray collision_shapes = node->find_children("*", "CollisionShape3D"); + if (collision_shapes.size() > 0) { + LOG(INFO, "Found ", collision_shapes.size(), " CollisionShapes in scene file"); + for (int i = 0; i < collision_shapes.size(); i++) { + CollisionShape3D *collision_shape = cast_to(collision_shapes[i]); + Ref shape = collision_shape->get_shape(); + Transform3D xform = collision_shape->get_transform(); + _shapes.push_back(shape); + _shape_transforms.push_back(xform); + } + } node->queue_free(); } if (_pending_meshes.size() > 0) { @@ -363,6 +410,17 @@ Ref Terrain3DMeshAsset::get_mesh(const int p_lod) const { return Ref(); } +TypedArray Terrain3DMeshAsset::get_shapes() const { + return _shapes; +} + +int Terrain3DMeshAsset::get_shape_count() const { + return _shapes.size(); +} + +TypedArray Terrain3DMeshAsset::get_shape_transforms() const { + return _shape_transforms; +} void Terrain3DMeshAsset::set_height_offset(const real_t p_offset) { SET_IF_DIFF(_height_offset, CLAMP(p_offset, -50.f, 50.f)); LOG(INFO, "ID ", _id, ", ", _name, ": Setting height offset: ", _height_offset); @@ -663,6 +721,13 @@ void Terrain3DMeshAsset::_bind_methods() { ClassDB::bind_method(D_METHOD("get_instance_count"), &Terrain3DMeshAsset::get_instance_count); + ClassDB::bind_method(D_METHOD("set_instance_collision_layers", "layers"), &Terrain3DMeshAsset::set_instance_collision_layers); + ClassDB::bind_method(D_METHOD("get_instance_collision_layers"), &Terrain3DMeshAsset::get_instance_collision_layers); + ClassDB::bind_method(D_METHOD("set_instance_collision_mask", "mask"), &Terrain3DMeshAsset::set_instance_collision_mask); + ClassDB::bind_method(D_METHOD("get_instance_collision_mask"), &Terrain3DMeshAsset::get_instance_collision_mask); + ClassDB::bind_method(D_METHOD("set_instance_physics_material", "material"), &Terrain3DMeshAsset::set_instance_physics_material); + ClassDB::bind_method(D_METHOD("get_instance_physics_material"), &Terrain3DMeshAsset::get_instance_physics_material); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "name", PROPERTY_HINT_NONE), "set_name", "get_name"); ADD_PROPERTY(PropertyInfo(Variant::INT, "id", PROPERTY_HINT_NONE), "set_id", "get_id"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "enabled", PROPERTY_HINT_NONE), "set_enabled", "is_enabled"); @@ -696,4 +761,9 @@ void Terrain3DMeshAsset::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lod9_range", PROPERTY_HINT_RANGE, "0.,4096.0,.05,or_greater"), "set_lod9_range", "get_lod9_range"); // Fade disabled until https://github.com/godotengine/godot/issues/102799 is fixed ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "fade_margin", PROPERTY_HINT_RANGE, "0.,64.0,.05,or_greater", PROPERTY_USAGE_NO_EDITOR), "set_fade_margin", "get_fade_margin"); + + ADD_GROUP("Instance Collision", ""); + ADD_PROPERTY(PropertyInfo(Variant::INT, "collision_layers", PROPERTY_HINT_LAYERS_3D_PHYSICS, "PhysicsLayers"), "set_instance_collision_layers", "get_instance_collision_layers"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "collision_mask", PROPERTY_HINT_LAYERS_3D_PHYSICS, "PhysicsLayers"), "set_instance_collision_mask", "get_instance_collision_mask"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "physics_material", PROPERTY_HINT_RESOURCE_TYPE, "PhysicsMaterial"), "set_instance_physics_material", "get_instance_physics_material"); } diff --git a/src/terrain_3d_mesh_asset.h b/src/terrain_3d_mesh_asset.h index 000f14c17..8e4d001ae 100644 --- a/src/terrain_3d_mesh_asset.h +++ b/src/terrain_3d_mesh_asset.h @@ -6,7 +6,9 @@ #include #include #include +#include #include +#include #include #include "constants.h" @@ -68,10 +70,19 @@ class Terrain3DMeshAsset : public Terrain3DAssetResource { PackedFloat32Array _lod_ranges; real_t _fade_margin = 0.f; + // Instance collision settings + bool _instance_collision_enabled = true; + uint32_t _instance_collision_layers = 1; + uint32_t _instance_collision_mask = 1; + Ref _instance_physics_material; + // Working data Ref _highlight_mat; TypedArray _meshes; TypedArray _pending_meshes; // Queue to avoid warnings from RS on mesh swap + TypedArray _shapes; + TypedArray _shape_transforms; + Ref _thumbnail; uint32_t _instance_count = 0; void _clear_lod_ranges(); @@ -95,8 +106,8 @@ class Terrain3DMeshAsset : public Terrain3DAssetResource { bool is_highlighted() const override { return _highlighted; } Ref get_highlight_material() const { return _highlighted ? _highlight_mat : Ref(); } Color get_highlight_color() const override; + void set_thumbnail(Ref p_tex) { _thumbnail = p_tex; } Ref get_thumbnail() const override { return _thumbnail; } - void set_enabled(const bool p_enabled); bool is_enabled() const { return _enabled; } @@ -111,7 +122,9 @@ class Terrain3DMeshAsset : public Terrain3DAssetResource { void set_generated_type(const GenType p_type); GenType get_generated_type() const { return _generated_type; } Ref get_mesh(const int p_lod = 0) const; - void set_thumbnail(Ref p_tex) { _thumbnail = p_tex; } + TypedArray get_shapes() const; + int get_shape_count() const; + TypedArray get_shape_transforms() const; void set_height_offset(const real_t p_offset); real_t get_height_offset() const { return _height_offset; } void set_density(const real_t p_density); @@ -126,6 +139,16 @@ class Terrain3DMeshAsset : public Terrain3DAssetResource { void set_material_overlay(const Ref &p_material); Ref get_material_overlay() const { return _material_overlay; } + // Instance collision settings + void set_instance_collision_enabled(const bool p_enabled); + bool is_instance_collision_enabled() const { return _instance_collision_enabled; } + void set_instance_collision_layers(const uint32_t p_layers); + uint32_t get_instance_collision_layers() const { return _instance_collision_layers; } + void set_instance_collision_mask(const uint32_t p_mask); + uint32_t get_instance_collision_mask() const { return _instance_collision_mask; } + void set_instance_physics_material(const Ref &p_mat); + Ref get_instance_physics_material() const { return _instance_physics_material; } + void set_generated_faces(const int p_count); int get_generated_faces() const { return _generated_faces; } void set_generated_size(const Vector2 &p_size); diff --git a/src/terrain_3d_util.cpp b/src/terrain_3d_util.cpp index 6c4a429ec..9d25c992d 100644 --- a/src/terrain_3d_util.cpp +++ b/src/terrain_3d_util.cpp @@ -519,6 +519,13 @@ void Terrain3DUtil::benchmark(Terrain3D *p_terrain) { } } +Vector2i Terrain3DUtil::get_cell(const Vector3 &p_global_position, const int p_region_size, const real_t p_vertex_spacing, const int p_cell_size) { + Vector2i cell; + cell.x = UtilityFunctions::posmod(UtilityFunctions::floori(p_global_position.x / p_vertex_spacing), p_region_size) / p_cell_size; + cell.y = UtilityFunctions::posmod(UtilityFunctions::floori(p_global_position.z / p_vertex_spacing), p_region_size) / p_cell_size; + return cell; +} + /////////////////////////// // Protected Functions /////////////////////////// diff --git a/src/terrain_3d_util.h b/src/terrain_3d_util.h index 448635bab..5c3c61209 100644 --- a/src/terrain_3d_util.h +++ b/src/terrain_3d_util.h @@ -57,6 +57,8 @@ class Terrain3DUtil : public Object { static Ref luminance_to_height(const Ref &p_src_rgb); static void benchmark(Terrain3D *p_terrain); + static Vector2i get_cell(const Vector3 &p_global_position, const int p_region_size, const real_t p_vertex_spacing, const int p_cell_size); + protected: static void _bind_methods(); };