Added basic stair climbing to VoxelBoxMover
parent
b64532c0ea
commit
7bb683552d
|
@ -49,6 +49,7 @@ Godot 4 is required from this version.
|
|||
- `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 (aka surfaces)
|
||||
- `VoxelMesherBlocky`: mesh collisions: added support for specifying which surfaces have collision
|
||||
- `VoxelBoxMover`: added basic support for stair climbing
|
||||
|
||||
- Fixes
|
||||
- `VoxelBuffer`: frequently creating buffers with always different sizes no longer wastes memory
|
||||
|
|
|
@ -30,7 +30,7 @@ static AABB expand_with_vector(AABB box, Vector3 v) {
|
|||
return box;
|
||||
}
|
||||
|
||||
static real_t calculate_i_offset(AABB box, AABB other, real_t motion, int i, int j, int k) {
|
||||
static real_t calculate_i_offset(const AABB &box, AABB other, real_t motion, int i, int j, int k) {
|
||||
const real_t EPSILON = 0.001;
|
||||
|
||||
Vector3 other_end = other.position + other.size;
|
||||
|
@ -79,7 +79,7 @@ static real_t calculate_i_offset(AABB box, AABB other, real_t motion, int i, int
|
|||
//
|
||||
// TODO one way to fix this would be to try a "hot side" projection instead
|
||||
//
|
||||
static Vector3 get_motion(AABB box, Vector3 motion, const std::vector<AABB> &environment_boxes) {
|
||||
static Vector3 get_motion(AABB box, Vector3 motion, Span<const AABB> environment_boxes) {
|
||||
// The bounding box is expanded to include it's estimated version at next update.
|
||||
// This also makes the algorithm tunnelling-free
|
||||
const AABB expanded_box = expand_with_vector(box, motion);
|
||||
|
@ -118,46 +118,89 @@ static Vector3 get_motion(AABB box, Vector3 motion, const std::vector<AABB> &env
|
|||
return new_motion;
|
||||
}
|
||||
|
||||
Vector3 VoxelBoxMover::get_motion(Vector3 p_pos, Vector3 p_motion, AABB p_aabb, VoxelTerrain *p_terrain) {
|
||||
ERR_FAIL_COND_V(p_terrain == nullptr, Vector3());
|
||||
// The mesher is required to know how collisions should be processed
|
||||
ERR_FAIL_COND_V(p_terrain->get_mesher().is_null(), Vector3());
|
||||
// static bool raycast_down(Span<const AABB> aabbs, Vector3 ray_origin, real_t &out_hit_y) {
|
||||
// if (aabbs.size() == 0) {
|
||||
// return false;
|
||||
// }
|
||||
// bool hit = false;
|
||||
// real_t max_y = -9999999;
|
||||
// for (unsigned int i = 0; i < aabbs.size(); ++i) {
|
||||
// const AABB &aabb = aabbs[i];
|
||||
// if ( //
|
||||
// ray_origin.x >= aabb.position.x && //
|
||||
// ray_origin.z >= aabb.position.z && //
|
||||
// ray_origin.x < aabb.position.x + aabb.size.x && //
|
||||
// ray_origin.z < aabb.position.z + aabb.size.z) {
|
||||
// //
|
||||
// const real_t box_top = aabb.position.y + aabb.size.y;
|
||||
// if (hit) {
|
||||
// max_y = math::max(box_top, max_y);
|
||||
// } else {
|
||||
// max_y = box_top;
|
||||
// }
|
||||
// hit = true;
|
||||
// }
|
||||
// }
|
||||
// out_hit_y = max_y;
|
||||
// return hit;
|
||||
// }
|
||||
|
||||
// Transform to local in case the volume is transformed
|
||||
const Transform3D to_world = p_terrain->get_global_transform();
|
||||
const Transform3D to_local = to_world.affine_inverse();
|
||||
const Vector3 pos = to_local.xform(p_pos);
|
||||
const Vector3 motion = to_local.basis.xform(p_motion);
|
||||
const AABB aabb = Transform3D(to_local.basis, Vector3()).xform(p_aabb);
|
||||
inline Vector2 get_xz(Vector3 v) {
|
||||
return Vector2(v.x, v.z);
|
||||
}
|
||||
|
||||
const AABB box(aabb.position + pos, aabb.size);
|
||||
const AABB expanded_box = expand_with_vector(box, motion);
|
||||
static bool boxcast_down(Span<const AABB> aabbs, Vector2 box_pos, Vector2 box_size, real_t &out_hit_y) {
|
||||
if (aabbs.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
bool hit = false;
|
||||
real_t max_y = -9999999;
|
||||
for (unsigned int i = 0; i < aabbs.size(); ++i) {
|
||||
const AABB &aabb = aabbs[i];
|
||||
if (Rect2(box_pos, box_size).intersects(Rect2(get_xz(aabb.position), get_xz(aabb.size)))) {
|
||||
const real_t box_top = aabb.position.y + aabb.size.y;
|
||||
if (hit) {
|
||||
max_y = math::max(box_top, max_y);
|
||||
} else {
|
||||
max_y = box_top;
|
||||
}
|
||||
hit = true;
|
||||
}
|
||||
}
|
||||
out_hit_y = max_y;
|
||||
return hit;
|
||||
}
|
||||
|
||||
static thread_local std::vector<AABB> s_colliding_boxes;
|
||||
std::vector<AABB> &potential_boxes = s_colliding_boxes;
|
||||
potential_boxes.clear();
|
||||
static bool intersects(Span<const AABB> aabbs, const AABB &box) {
|
||||
for (unsigned int i = 0; i < aabbs.size(); ++i) {
|
||||
if (aabbs[i].intersects(box)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Collect potential collisions with the terrain (broad phase)
|
||||
static void collect_boxes(
|
||||
VoxelTerrain &p_terrain, AABB query_box, uint32_t collision_nask, std::vector<AABB> &potential_boxes) {
|
||||
const VoxelDataMap &voxels = p_terrain.get_storage();
|
||||
|
||||
const VoxelDataMap &voxels = p_terrain->get_storage();
|
||||
const int min_x = int(Math::floor(query_box.position.x));
|
||||
const int min_y = int(Math::floor(query_box.position.y));
|
||||
const int min_z = int(Math::floor(query_box.position.z));
|
||||
|
||||
const int min_x = int(Math::floor(expanded_box.position.x));
|
||||
const int min_y = int(Math::floor(expanded_box.position.y));
|
||||
const int min_z = int(Math::floor(expanded_box.position.z));
|
||||
|
||||
const Vector3 expanded_box_end = expanded_box.position + expanded_box.size;
|
||||
const int max_x = int(Math::ceil(expanded_box_end.x));
|
||||
const int max_y = int(Math::ceil(expanded_box_end.y));
|
||||
const int max_z = int(Math::ceil(expanded_box_end.z));
|
||||
const Vector3 query_box_end = query_box.position + query_box.size;
|
||||
const int max_x = int(Math::ceil(query_box_end.x));
|
||||
const int max_y = int(Math::ceil(query_box_end.y));
|
||||
const int max_z = int(Math::ceil(query_box_end.z));
|
||||
|
||||
Vector3i i(min_x, min_y, min_z);
|
||||
|
||||
Ref<VoxelMesherBlocky> mesher_blocky;
|
||||
Ref<VoxelMesherCubes> mesher_cubes;
|
||||
|
||||
if (try_get_as(p_terrain->get_mesher(), mesher_blocky)) {
|
||||
if (try_get_as(p_terrain.get_mesher(), mesher_blocky)) {
|
||||
Ref<VoxelBlockyLibrary> library_ref = mesher_blocky->get_library();
|
||||
ERR_FAIL_COND_V_MSG(library_ref.is_null(), Vector3(), "VoxelMesherBlocky has no library assigned");
|
||||
ERR_FAIL_COND_MSG(library_ref.is_null(), "VoxelMesherBlocky has no library assigned");
|
||||
VoxelBlockyLibrary &library = **library_ref;
|
||||
const int channel = VoxelBufferInternal::CHANNEL_TYPE;
|
||||
|
||||
|
@ -169,7 +212,7 @@ Vector3 VoxelBoxMover::get_motion(Vector3 p_pos, Vector3 p_motion, AABB p_aabb,
|
|||
if (library.has_voxel(type_id)) {
|
||||
const VoxelBlockyModel &voxel_type = library.get_voxel_const(type_id);
|
||||
|
||||
if ((voxel_type.get_collision_mask() & _collision_mask) == 0) {
|
||||
if ((voxel_type.get_collision_mask() & collision_nask) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -185,7 +228,7 @@ Vector3 VoxelBoxMover::get_motion(Vector3 p_pos, Vector3 p_motion, AABB p_aabb,
|
|||
}
|
||||
}
|
||||
|
||||
} else if (try_get_as(p_terrain->get_mesher(), mesher_cubes)) {
|
||||
} else if (try_get_as(p_terrain.get_mesher(), mesher_cubes)) {
|
||||
const int channel = VoxelBufferInternal::CHANNEL_COLOR;
|
||||
|
||||
for (i.z = min_z; i.z < max_z; ++i.z) {
|
||||
|
@ -199,9 +242,59 @@ Vector3 VoxelBoxMover::get_motion(Vector3 p_pos, Vector3 p_motion, AABB p_aabb,
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 VoxelBoxMover::get_motion(Vector3 p_pos, Vector3 p_motion, AABB p_aabb, VoxelTerrain &p_terrain) {
|
||||
// The mesher is required to know how collisions should be processed
|
||||
ERR_FAIL_COND_V(p_terrain.get_mesher().is_null(), Vector3());
|
||||
|
||||
// Transform to local in case the volume is transformed
|
||||
const Transform3D to_world = p_terrain.get_global_transform();
|
||||
const Transform3D to_local = to_world.affine_inverse();
|
||||
const Vector3 pos = to_local.xform(p_pos);
|
||||
const Vector3 motion = to_local.basis.xform(p_motion);
|
||||
const AABB aabb = Transform3D(to_local.basis, Vector3()).xform(p_aabb);
|
||||
|
||||
const AABB box(aabb.position + pos, aabb.size);
|
||||
const AABB expanded_box = expand_with_vector(box, motion);
|
||||
|
||||
static thread_local std::vector<AABB> s_colliding_boxes;
|
||||
std::vector<AABB> &potential_boxes = s_colliding_boxes;
|
||||
potential_boxes.clear();
|
||||
|
||||
// Collect potential collisions with the terrain (broad phase)
|
||||
// TODO If motion is really big, we may want something more optimal or reject it
|
||||
collect_boxes(p_terrain, expanded_box, _collision_mask, potential_boxes);
|
||||
|
||||
// Calculate collisions (narrow phase)
|
||||
const Vector3 slided_motion = zylann::voxel::get_motion(box, motion, potential_boxes);
|
||||
Vector3 slided_motion = zylann::voxel::get_motion(box, motion, to_span(potential_boxes));
|
||||
|
||||
// Minecraft-style stair climbing:
|
||||
// If we were moving, changed horizontal direction due to collision, and resulting motion is about horizontal
|
||||
_has_stepped_up = false;
|
||||
if (_step_climbing_enabled && Math::abs(slided_motion.y) < 0.001 &&
|
||||
Vector2(motion.x, motion.z).length_squared() > 0.0001 &&
|
||||
Vector2(motion.x, motion.z).normalized().dot(Vector2(slided_motion.x, slided_motion.z).normalized()) <
|
||||
0.99) {
|
||||
real_t hit_y;
|
||||
// Find out the height of the step
|
||||
if (boxcast_down(to_span(potential_boxes), get_xz(expanded_box.position), get_xz(expanded_box.size), hit_y)) {
|
||||
// If the step is up and not too high
|
||||
if (hit_y > box.position.y && (hit_y - box.position.y) <= _max_step_height) {
|
||||
const AABB hyp_box(Vector3(box.position.x + motion.x, hit_y, box.position.z + motion.z), box.size);
|
||||
|
||||
potential_boxes.clear();
|
||||
collect_boxes(p_terrain, hyp_box, _collision_mask, potential_boxes);
|
||||
|
||||
// If the box fits on top of the step
|
||||
if (!intersects(to_span(potential_boxes), hyp_box)) {
|
||||
// Change motion so that it brings the box on top of the step
|
||||
slided_motion = hyp_box.position - box.position;
|
||||
_has_stepped_up = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Switch back to world
|
||||
const Vector3 world_slided_motion = to_world.basis.xform(slided_motion);
|
||||
|
@ -213,17 +306,46 @@ void VoxelBoxMover::set_collision_mask(uint32_t mask) {
|
|||
_collision_mask = mask;
|
||||
}
|
||||
|
||||
void VoxelBoxMover::set_step_climbing_enabled(bool enable) {
|
||||
_step_climbing_enabled = enable;
|
||||
}
|
||||
|
||||
bool VoxelBoxMover::is_step_climbing_enabled() const {
|
||||
return _step_climbing_enabled;
|
||||
}
|
||||
|
||||
bool VoxelBoxMover::has_stepped_up() const {
|
||||
return _has_stepped_up;
|
||||
}
|
||||
|
||||
void VoxelBoxMover::set_max_step_height(float height) {
|
||||
_max_step_height = height;
|
||||
}
|
||||
|
||||
float VoxelBoxMover::get_max_step_height() const {
|
||||
return _max_step_height;
|
||||
}
|
||||
|
||||
Vector3 VoxelBoxMover::_b_get_motion(Vector3 pos, Vector3 motion, AABB aabb, Node *terrain_node) {
|
||||
ERR_FAIL_COND_V(terrain_node == nullptr, Vector3());
|
||||
VoxelTerrain *terrain = Object::cast_to<VoxelTerrain>(terrain_node);
|
||||
ERR_FAIL_COND_V(terrain == nullptr, Vector3());
|
||||
return get_motion(pos, motion, aabb, terrain);
|
||||
return get_motion(pos, motion, aabb, *terrain);
|
||||
}
|
||||
|
||||
void VoxelBoxMover::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("get_motion", "pos", "motion", "aabb", "terrain"), &VoxelBoxMover::_b_get_motion);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("set_collision_mask", "mask"), &VoxelBoxMover::set_collision_mask);
|
||||
ClassDB::bind_method(D_METHOD("get_collision_mask"), &VoxelBoxMover::get_collision_mask);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("set_step_climbing_enabled", "enabled"), &VoxelBoxMover::set_step_climbing_enabled);
|
||||
ClassDB::bind_method(D_METHOD("is_step_climbing_enabled"), &VoxelBoxMover::is_step_climbing_enabled);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("set_max_step_height", "height"), &VoxelBoxMover::set_max_step_height);
|
||||
ClassDB::bind_method(D_METHOD("get_max_step_height"), &VoxelBoxMover::get_max_step_height);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("has_stepped_up"), &VoxelBoxMover::has_stepped_up);
|
||||
}
|
||||
|
||||
} // namespace zylann::voxel
|
||||
|
|
|
@ -10,19 +10,33 @@ namespace zylann::voxel {
|
|||
class VoxelBoxMover : public RefCounted {
|
||||
GDCLASS(VoxelBoxMover, RefCounted)
|
||||
public:
|
||||
Vector3 get_motion(Vector3 pos, Vector3 motion, AABB aabb, VoxelTerrain *terrain);
|
||||
Vector3 get_motion(Vector3 pos, Vector3 motion, AABB aabb, VoxelTerrain &terrain);
|
||||
|
||||
void set_collision_mask(uint32_t mask);
|
||||
inline uint32_t get_collision_mask() const {
|
||||
return _collision_mask;
|
||||
}
|
||||
|
||||
void set_step_climbing_enabled(bool enable);
|
||||
bool is_step_climbing_enabled() const;
|
||||
|
||||
void set_max_step_height(float height);
|
||||
float get_max_step_height() const;
|
||||
|
||||
bool has_stepped_up() const;
|
||||
|
||||
private:
|
||||
Vector3 _b_get_motion(Vector3 p_pos, Vector3 p_motion, AABB p_aabb, Node *p_terrain_node);
|
||||
|
||||
static void _bind_methods();
|
||||
|
||||
// Config
|
||||
uint32_t _collision_mask = 0xffffffff; // Everything
|
||||
bool _step_climbing_enabled = false;
|
||||
real_t _max_step_height = 0.5;
|
||||
|
||||
// States
|
||||
bool _has_stepped_up = false;
|
||||
};
|
||||
|
||||
} // namespace zylann::voxel
|
||||
|
|
Loading…
Reference in New Issue