Optimize raycast with large Heightmap shape data

Port raycast accelerator from Bullet's btHeightfieldTerrainShape.
This commit is contained in:
PouleyKetchoupp 2021-05-11 18:40:34 -07:00
parent b277c88280
commit 8d56354f7c
2 changed files with 319 additions and 133 deletions

View file

@ -1676,6 +1676,17 @@ struct _HeightmapSegmentCullParams {
FaceShape3DSW *face = nullptr;
};
struct _HeightmapGridCullState {
real_t length = 0.0;
real_t length_flat = 0.0;
real_t dist = 0.0;
real_t prev_dist = 0.0;
int x = 0;
int z = 0;
};
_FORCE_INLINE_ bool _heightmap_face_cull_segment(_HeightmapSegmentCullParams &p_params) {
Vector3 res;
Vector3 normal;
@ -1688,11 +1699,11 @@ _FORCE_INLINE_ bool _heightmap_face_cull_segment(_HeightmapSegmentCullParams &p_
return false;
}
_FORCE_INLINE_ bool _heightmap_cell_cull_segment(_HeightmapSegmentCullParams &p_params, int p_x, int p_z) {
_FORCE_INLINE_ bool _heightmap_cell_cull_segment(_HeightmapSegmentCullParams &p_params, const _HeightmapGridCullState &p_state) {
// First triangle.
p_params.heightmap->_get_point(p_x, p_z, p_params.face->vertex[0]);
p_params.heightmap->_get_point(p_x + 1, p_z, p_params.face->vertex[1]);
p_params.heightmap->_get_point(p_x, p_z + 1, p_params.face->vertex[2]);
p_params.heightmap->_get_point(p_state.x, p_state.z, p_params.face->vertex[0]);
p_params.heightmap->_get_point(p_state.x + 1, p_state.z, p_params.face->vertex[1]);
p_params.heightmap->_get_point(p_state.x, p_state.z + 1, p_params.face->vertex[2]);
p_params.face->normal = Plane(p_params.face->vertex[0], p_params.face->vertex[1], p_params.face->vertex[2]).normal;
if (_heightmap_face_cull_segment(p_params)) {
return true;
@ -1700,7 +1711,7 @@ _FORCE_INLINE_ bool _heightmap_cell_cull_segment(_HeightmapSegmentCullParams &p_
// Second triangle.
p_params.face->vertex[0] = p_params.face->vertex[1];
p_params.heightmap->_get_point(p_x + 1, p_z + 1, p_params.face->vertex[1]);
p_params.heightmap->_get_point(p_state.x + 1, p_state.z + 1, p_params.face->vertex[1]);
p_params.face->normal = Plane(p_params.face->vertex[0], p_params.face->vertex[1], p_params.face->vertex[2]).normal;
if (_heightmap_face_cull_segment(p_params)) {
return true;
@ -1709,6 +1720,182 @@ _FORCE_INLINE_ bool _heightmap_cell_cull_segment(_HeightmapSegmentCullParams &p_
return false;
}
_FORCE_INLINE_ bool _heightmap_chunk_cull_segment(_HeightmapSegmentCullParams &p_params, const _HeightmapGridCullState &p_state) {
const HeightMapShape3DSW::Range &chunk = p_params.heightmap->_get_bounds_chunk(p_state.x, p_state.z);
Vector3 enter_pos;
Vector3 exit_pos;
if (p_state.length_flat > CMP_EPSILON) {
real_t flat_to_3d = p_state.length / p_state.length_flat;
real_t enter_param = p_state.prev_dist * flat_to_3d;
real_t exit_param = p_state.dist * flat_to_3d;
enter_pos = p_params.from + p_params.dir * enter_param;
exit_pos = p_params.from + p_params.dir * exit_param;
} else {
// Consider the ray vertical.
// (though we shouldn't reach this often because there is an early check up-front)
enter_pos = p_params.from;
exit_pos = p_params.to;
}
// Transform positions to heightmap space.
enter_pos *= HeightMapShape3DSW::BOUNDS_CHUNK_SIZE;
exit_pos *= HeightMapShape3DSW::BOUNDS_CHUNK_SIZE;
// We did enter the flat projection of the AABB,
// but we have to check if we intersect it on the vertical axis.
if ((enter_pos.y > chunk.max) && (exit_pos.y > chunk.max)) {
return false;
}
if ((enter_pos.y < chunk.min) && (exit_pos.y < chunk.min)) {
return false;
}
return p_params.heightmap->_intersect_grid_segment(_heightmap_cell_cull_segment, enter_pos, exit_pos, p_params.heightmap->width, p_params.heightmap->depth, p_params.heightmap->local_origin, p_params.result, p_params.normal);
}
template <typename ProcessFunction>
bool HeightMapShape3DSW::_intersect_grid_segment(ProcessFunction &p_process, const Vector3 &p_begin, const Vector3 &p_end, int p_width, int p_depth, const Vector3 &offset, Vector3 &r_point, Vector3 &r_normal) const {
Vector3 delta = (p_end - p_begin);
real_t length = delta.length();
if (length < CMP_EPSILON) {
return false;
}
Vector3 local_begin = p_begin + offset;
FaceShape3DSW face;
face.backface_collision = false;
_HeightmapSegmentCullParams params;
params.from = p_begin;
params.to = p_end;
params.dir = delta / length;
params.heightmap = this;
params.face = &face;
_HeightmapGridCullState state;
// Perform grid query from projected ray.
Vector2 ray_dir_flat(delta.x, delta.z);
state.length = length;
state.length_flat = ray_dir_flat.length();
if (state.length_flat < CMP_EPSILON) {
ray_dir_flat = Vector2();
} else {
ray_dir_flat /= state.length_flat;
}
const int x_step = (ray_dir_flat.x > CMP_EPSILON) ? 1 : ((ray_dir_flat.x < -CMP_EPSILON) ? -1 : 0);
const int z_step = (ray_dir_flat.y > CMP_EPSILON) ? 1 : ((ray_dir_flat.y < -CMP_EPSILON) ? -1 : 0);
const real_t infinite = 1e20;
const real_t delta_x = (x_step != 0) ? 1.f / Math::abs(ray_dir_flat.x) : infinite;
const real_t delta_z = (z_step != 0) ? 1.f / Math::abs(ray_dir_flat.y) : infinite;
real_t cross_x; // At which value of `param` we will cross a x-axis lane?
real_t cross_z; // At which value of `param` we will cross a z-axis lane?
// X initialization.
if (x_step != 0) {
if (x_step == 1) {
cross_x = (Math::ceil(local_begin.x) - local_begin.x) * delta_x;
} else {
cross_x = (local_begin.x - Math::floor(local_begin.x)) * delta_x;
}
} else {
cross_x = infinite; // Will never cross on X.
}
// Z initialization.
if (z_step != 0) {
if (z_step == 1) {
cross_z = (Math::ceil(local_begin.z) - local_begin.z) * delta_z;
} else {
cross_z = (local_begin.z - Math::floor(local_begin.z)) * delta_z;
}
} else {
cross_z = infinite; // Will never cross on Z.
}
int x = Math::floor(local_begin.x);
int z = Math::floor(local_begin.z);
// Workaround cases where the ray starts at an integer position.
if (Math::is_zero_approx(cross_x)) {
cross_x += delta_x;
// If going backwards, we should ignore the position we would get by the above flooring,
// because the ray is not heading in that direction.
if (x_step == -1) {
x -= 1;
}
}
if (Math::is_zero_approx(cross_z)) {
cross_z += delta_z;
if (z_step == -1) {
z -= 1;
}
}
// Start inside the grid.
int x_start = MAX(MIN(x, p_width - 2), 0);
int z_start = MAX(MIN(z, p_depth - 2), 0);
// Adjust initial cross values.
cross_x += delta_x * x_step * (x_start - x);
cross_z += delta_z * z_step * (z_start - z);
x = x_start;
z = z_start;
while (true) {
state.prev_dist = state.dist;
state.x = x;
state.z = z;
if (cross_x < cross_z) {
// X lane.
x += x_step;
// Assign before advancing the param,
// to be in sync with the initialization step.
state.dist = cross_x;
cross_x += delta_x;
} else {
// Z lane.
z += z_step;
state.dist = cross_z;
cross_z += delta_z;
}
if (state.dist > state.length_flat) {
state.dist = state.length_flat;
if (p_process(params, state)) {
r_point = params.result;
r_normal = params.normal;
return true;
}
break;
}
if (p_process(params, state)) {
r_point = params.result;
r_normal = params.normal;
return true;
}
// Stop when outside the grid.
if ((x < 0) || (z < 0) || (x >= p_width - 1) || (z >= p_depth - 1)) {
break;
}
}
return false;
}
bool HeightMapShape3DSW::intersect_segment(const Vector3 &p_begin, const Vector3 &p_end, Vector3 &r_point, Vector3 &r_normal) const {
if (heights.is_empty()) {
return false;
@ -1717,142 +1904,49 @@ bool HeightMapShape3DSW::intersect_segment(const Vector3 &p_begin, const Vector3
Vector3 local_begin = p_begin + local_origin;
Vector3 local_end = p_end + local_origin;
FaceShape3DSW face;
face.backface_collision = false;
_HeightmapSegmentCullParams params;
params.from = p_begin;
params.to = p_end;
params.dir = (p_end - p_begin).normalized();
params.heightmap = this;
params.face = &face;
// Quantize the ray begin/end.
int begin_x = floor(local_begin.x);
int begin_z = floor(local_begin.z);
int end_x = floor(local_end.x);
int end_z = floor(local_end.z);
int begin_x = Math::floor(local_begin.x);
int begin_z = Math::floor(local_begin.z);
int end_x = Math::floor(local_end.x);
int end_z = Math::floor(local_end.z);
if ((begin_x == end_x) && (begin_z == end_z)) {
// Simple case for rays that don't traverse the grid horizontally.
// Just perform a test on the given cell.
int x = CLAMP(begin_x, 0, width - 2);
int z = CLAMP(begin_z, 0, depth - 2);
if (_heightmap_cell_cull_segment(params, x, z)) {
FaceShape3DSW face;
face.backface_collision = false;
_HeightmapSegmentCullParams params;
params.from = p_begin;
params.to = p_end;
params.dir = (p_end - p_begin).normalized();
params.heightmap = this;
params.face = &face;
_HeightmapGridCullState state;
state.x = MAX(MIN(begin_x, width - 2), 0);
state.z = MAX(MIN(begin_z, depth - 2), 0);
if (_heightmap_cell_cull_segment(params, state)) {
r_point = params.result;
r_normal = params.normal;
return true;
}
} else if (bounds_grid.is_empty()) {
// Process all cells intersecting the flat projection of the ray.
return _intersect_grid_segment(_heightmap_cell_cull_segment, p_begin, p_end, width, depth, local_origin, r_point, r_normal);
} else {
// Perform grid query from projected ray.
Vector2 ray_dir_proj(local_end.x - local_begin.x, local_end.z - local_begin.z);
real_t ray_dist_proj = ray_dir_proj.length();
if (ray_dist_proj < CMP_EPSILON) {
ray_dir_proj = Vector2();
Vector3 ray_diff = (p_end - p_begin);
real_t length_flat_sqr = ray_diff.x * ray_diff.x + ray_diff.z * ray_diff.z;
if (length_flat_sqr < BOUNDS_CHUNK_SIZE * BOUNDS_CHUNK_SIZE) {
// Don't use chunks, the ray is too short in the plane.
return _intersect_grid_segment(_heightmap_cell_cull_segment, p_begin, p_end, width, depth, local_origin, r_point, r_normal);
} else {
ray_dir_proj /= ray_dist_proj;
}
const int x_step = (ray_dir_proj.x > CMP_EPSILON) ? 1 : ((ray_dir_proj.x < -CMP_EPSILON) ? -1 : 0);
const int z_step = (ray_dir_proj.y > CMP_EPSILON) ? 1 : ((ray_dir_proj.y < -CMP_EPSILON) ? -1 : 0);
const real_t infinite = 1e20;
const real_t delta_x = (x_step != 0) ? 1.f / Math::abs(ray_dir_proj.x) : infinite;
const real_t delta_z = (z_step != 0) ? 1.f / Math::abs(ray_dir_proj.y) : infinite;
real_t cross_x; // At which value of `param` we will cross a x-axis lane?
real_t cross_z; // At which value of `param` we will cross a z-axis lane?
// X initialization.
if (x_step != 0) {
if (x_step == 1) {
cross_x = (ceil(local_begin.x) - local_begin.x) * delta_x;
} else {
cross_x = (local_begin.x - floor(local_begin.x)) * delta_x;
}
} else {
cross_x = infinite; // Will never cross on X.
}
// Z initialization.
if (z_step != 0) {
if (z_step == 1) {
cross_z = (ceil(local_begin.z) - local_begin.z) * delta_z;
} else {
cross_z = (local_begin.z - floor(local_begin.z)) * delta_z;
}
} else {
cross_z = infinite; // Will never cross on Z.
}
int x = floor(local_begin.x);
int z = floor(local_begin.z);
// Workaround cases where the ray starts at an integer position.
if (Math::is_zero_approx(cross_x)) {
cross_x += delta_x;
// If going backwards, we should ignore the position we would get by the above flooring,
// because the ray is not heading in that direction.
if (x_step == -1) {
x -= 1;
}
}
if (Math::is_zero_approx(cross_z)) {
cross_z += delta_z;
if (z_step == -1) {
z -= 1;
}
}
// Start inside the grid.
int x_start = CLAMP(x, 0, width - 2);
int z_start = CLAMP(z, 0, depth - 2);
// Adjust initial cross values.
cross_x += delta_x * x_step * (x_start - x);
cross_z += delta_z * z_step * (z_start - z);
x = x_start;
z = z_start;
if (_heightmap_cell_cull_segment(params, x, z)) {
r_point = params.result;
r_normal = params.normal;
return true;
}
real_t dist = 0.0;
while (true) {
if (cross_x < cross_z) {
// X lane.
x += x_step;
// Assign before advancing the param,
// to be in sync with the initialization step.
dist = cross_x;
cross_x += delta_x;
} else {
// Z lane.
z += z_step;
dist = cross_z;
cross_z += delta_z;
}
// Stop when outside the grid.
if ((x < 0) || (z < 0) || (x >= width - 1) || (z >= depth - 1)) {
break;
}
if (_heightmap_cell_cull_segment(params, x, z)) {
r_point = params.result;
r_normal = params.normal;
return true;
}
if (dist > ray_dist_proj) {
break;
}
// The ray is long, run raycast on a higher-level grid.
Vector3 bounds_from = p_begin / BOUNDS_CHUNK_SIZE;
Vector3 bounds_to = p_end / BOUNDS_CHUNK_SIZE;
Vector3 bounds_offset = local_origin / BOUNDS_CHUNK_SIZE;
return _intersect_grid_segment(_heightmap_chunk_cull_segment, bounds_from, bounds_to, bounds_grid_width, bounds_grid_depth, bounds_offset, r_point, r_normal);
}
}
@ -1917,7 +2011,7 @@ void HeightMapShape3DSW::cull(const AABB &p_local_aabb, QueryCallback p_callback
_get_point(x, z, face.vertex[0]);
_get_point(x + 1, z, face.vertex[1]);
_get_point(x, z + 1, face.vertex[2]);
face.normal = Plane(face.vertex[0], face.vertex[2], face.vertex[1]).normal;
face.normal = Plane(face.vertex[0], face.vertex[1], face.vertex[2]).normal;
if (p_callback(p_userdata, &face)) {
return;
}
@ -1925,7 +2019,7 @@ void HeightMapShape3DSW::cull(const AABB &p_local_aabb, QueryCallback p_callback
// Second triangle.
face.vertex[0] = face.vertex[1];
_get_point(x + 1, z + 1, face.vertex[1]);
face.normal = Plane(face.vertex[0], face.vertex[2], face.vertex[1]).normal;
face.normal = Plane(face.vertex[0], face.vertex[1], face.vertex[2]).normal;
if (p_callback(p_userdata, &face)) {
return;
}
@ -1943,6 +2037,75 @@ Vector3 HeightMapShape3DSW::get_moment_of_inertia(real_t p_mass) const {
(p_mass / 3.0) * (extents.x * extents.x + extents.y * extents.y));
}
void HeightMapShape3DSW::_build_accelerator() {
bounds_grid.clear();
bounds_grid_width = width / BOUNDS_CHUNK_SIZE;
bounds_grid_depth = depth / BOUNDS_CHUNK_SIZE;
if (width % BOUNDS_CHUNK_SIZE > 0) {
++bounds_grid_width; // In case terrain size isn't dividable by chunk size.
}
if (depth % BOUNDS_CHUNK_SIZE > 0) {
++bounds_grid_depth;
}
uint32_t bound_grid_size = (uint32_t)(bounds_grid_width * bounds_grid_depth);
if (bound_grid_size < 2) {
// Grid is empty or just one chunk.
return;
}
bounds_grid.resize(bound_grid_size);
// Compute min and max height for all chunks.
for (int cz = 0; cz < bounds_grid_depth; ++cz) {
int z0 = cz * BOUNDS_CHUNK_SIZE;
for (int cx = 0; cx < bounds_grid_width; ++cx) {
int x0 = cx * BOUNDS_CHUNK_SIZE;
Range r;
r.min = _get_height(x0, z0);
r.max = r.min;
// Compute min and max height for this chunk.
// We have to include one extra cell to account for neighbors.
// Here is why:
// Say we have a flat terrain, and a plateau that fits a chunk perfectly.
//
// Left Right
// 0---0---0---1---1---1
// | | | | | |
// 0---0---0---1---1---1
// | | | | | |
// 0---0---0---1---1---1
// x
//
// If the AABB for the Left chunk did not share vertices with the Right,
// then we would fail collision tests at x due to a gap.
//
int z_max = MIN(z0 + BOUNDS_CHUNK_SIZE + 1, depth);
int x_max = MIN(x0 + BOUNDS_CHUNK_SIZE + 1, width);
for (int z = z0; z < z_max; ++z) {
for (int x = x0; x < x_max; ++x) {
real_t height = _get_height(x, z);
if (height < r.min) {
r.min = height;
} else if (height > r.max) {
r.max = height;
}
}
}
bounds_grid[cx + cz * bounds_grid_width] = r;
}
}
}
void HeightMapShape3DSW::_setup(const Vector<real_t> &p_heights, int p_width, int p_depth, real_t p_min_height, real_t p_max_height) {
heights = p_heights;
width = p_width;
@ -1959,6 +2122,8 @@ void HeightMapShape3DSW::_setup(const Vector<real_t> &p_heights, int p_width, in
aabb.position -= local_origin;
_build_accelerator();
configure(aabb);
}
@ -2017,7 +2182,7 @@ void HeightMapShape3DSW::set_data(const Variant &p_data) {
} else {
int heights_size = heights.size();
for (int i = 0; i < heights_size; ++i) {
float h = heights[i];
real_t h = heights[i];
if (h < min_height) {
min_height = h;
} else if (h > max_height) {

View file

@ -32,6 +32,7 @@
#define SHAPE_SW_H
#include "core/math/geometry_3d.h"
#include "core/templates/local_vector.h"
#include "servers/physics_server_3d.h"
class Shape3DSW;
@ -385,6 +386,21 @@ struct HeightMapShape3DSW : public ConcaveShape3DSW {
int depth = 0;
Vector3 local_origin;
// Accelerator.
struct Range {
real_t min = 0.0;
real_t max = 0.0;
};
LocalVector<Range> bounds_grid;
int bounds_grid_width = 0;
int bounds_grid_depth = 0;
static const int BOUNDS_CHUNK_SIZE = 16;
_FORCE_INLINE_ const Range &_get_bounds_chunk(int p_x, int p_z) const {
return bounds_grid[(p_z * bounds_grid_width) + p_x];
}
_FORCE_INLINE_ real_t _get_height(int p_x, int p_z) const {
return heights[(p_z * width) + p_x];
}
@ -397,6 +413,11 @@ struct HeightMapShape3DSW : public ConcaveShape3DSW {
void _get_cell(const Vector3 &p_point, int &r_x, int &r_y, int &r_z) const;
void _build_accelerator();
template <typename ProcessFunction>
bool _intersect_grid_segment(ProcessFunction &p_process, const Vector3 &p_begin, const Vector3 &p_end, int p_width, int p_depth, const Vector3 &offset, Vector3 &r_point, Vector3 &r_normal) const;
void _setup(const Vector<real_t> &p_heights, int p_width, int p_depth, real_t p_min_height, real_t p_max_height);
public: