Alright...I've been trying to figure this out for about a week now to no avail, so I'm hoping someone on here can lend a hand or point me in the right direction. I have implemented a collision detection / response algorithm as discussed in the famous paper: Improved Collision detection and Response, and when I say "implemented" what I really did was convert the source code provided by the following tutorial: Sliding Camera Collision Detection to work with my opengl engine (which uses glm library). I was able to get everything working as expected, however for the life of me I cannot figure out how to limit sliding.
For example, If I move my character object up an incline and then stop, it will slide backwards until it reaches a flat surface. This is the behavior I have been attempting to rectify. I know why it's happening, gravity is being applied to the object on the Y-axis pushing it down into the plane, then the position is corrected moving the object outside of the plane which is now a lower position than it was before the collision, then this process repeats until a flat surface is reached.
Things I have tried so far include:
- Only applying gravity if my initial velocity does not equal (0,0,0), so if player is providing input. I quickly realized how naive this was when I stopped moving while being "in-air".
- Ray casting from the position of the character object downwards on the Y axis and returning the closest intersection point, then only applying gravity if the difference between the character object and intersection point was greater than a specific value. This sort of worked, but was inconsistent and caused a horrible stutter when running down a slope.
- Finding the collision intersection point, a `min_y`, and a `max_y` value. `min_y` and `max_y` denoted a range of y values that if the intersection point was between then we knew we were "grounded". This produced the same results as my attempt with the ray cast.
If anyone knows how I should go about implementing this, please, for the love of god let me know as my sanity is slowly slipping away! Any and all tips, suggestions, links to articles, etc greatly appreciated.
Below is my code relevant to the question (let me know if I missed something or if you want to see something else). Also if anyone is interested I can provide a link to a .zip of the project. Just let me know.
void PlayerController::updateVelocity() {
float speed = player_move_speed * delta;
player_velocity = glm::vec3(0.0f, 0.0f, 0.0f);
if (input_state->key_w)
player_velocity += speed * player_direction;
if (input_state->key_s)
player_velocity -= speed * player_direction;
if (input_state->key_a)
player_velocity -= glm::normalize(glm::cross(player_direction, up)) * speed;
if (input_state->key_d)
player_velocity += glm::normalize(glm::cross(player_direction, up)) * speed;
if (input_state->key_space)
player_velocity += speed * glm::vec3(0.0f, 10.0f, 0.0f);
}
void PlayerController::updatePosition() {
CollisionPacket cp;
cp.ellipsoid_space = glm::vec3(0.5f, 1.0f, 0.5f); // dimension of player ellipsoid collision object (radius)
cp.w_position = player_position;
cp.w_velocity = player_velocity;
player_position = collisionSlide(cp);
view_position = player_position + view_offset;
}
glm::vec3 PlayerController::collisionSlide(CollisionPacket cp) {
cp.e_velocity = cp.w_velocity / cp.ellipsoid_space;
cp.e_position = cp.w_position / cp.ellipsoid_space;
cp.collision_recursion_depth = 0;
glm::vec3 final_position = collideWithWorld(cp);
cp.e_velocity = gravity / cp.ellipsoid_space;
cp.e_position = final_position;
cp.collision_recursion_depth = 0;
final_position = collideWithWorld(cp);
final_position = final_position * cp.ellipsoid_space;
return final_position;
}
glm::vec3 PlayerController::collideWithWorld(CollisionPacket& cp) {
float unit_scale = 1.0f; // 1 unit per meter
float very_close_distance = 0.005f * unit_scale;
if (cp.collision_recursion_depth > 5) {
return cp.e_position;
}
cp.e_normalized_velocity = glm::normalize(cp.e_velocity);
cp.found_collision = false;
cp.nearest_distance = 0.0f;
glm::vec3 p0, p1, p2;
for (int tri_counter = 0; tri_counter < collision_soup.indices.size() / 3; tri_counter++) {
p0 = collision_soup.positions[collision_soup.indices[3 * tri_counter]];
p1 = collision_soup.positions[collision_soup.indices[3 * tri_counter + 1]];
p2 = collision_soup.positions[collision_soup.indices[3 * tri_counter + 2]];
p0 = p0 / cp.ellipsoid_space;
p1 = p1 / cp.ellipsoid_space;
p2 = p2 / cp.ellipsoid_space;
glm::vec3 tri_normal = glm::normalize(glm::cross((p1 - p0), (p2 - p0)));
sphereCollidingWithTriangle(cp, p0, p1, p2, tri_normal);
}
if (cp.found_collision == false) {
return cp.e_position + cp.e_velocity;
}
glm::vec3 destination_point = cp.e_position + cp.e_velocity;
glm::vec3 new_position = cp.e_position;
if (cp.nearest_distance >= very_close_distance) {
glm::vec3 V = cp.e_velocity;
V = glm::normalize(V);
V = V * (cp.nearest_distance - very_close_distance);
new_position = cp.e_position + V;
V = glm::normalize(V);
cp.intersection_point -= very_close_distance * V;
}
glm::vec3 slide_plane_origin = cp.intersection_point;
glm::vec3 slide_plane_normal = new_position - cp.intersection_point;
slide_plane_normal = glm::normalize(slide_plane_normal);
float x = slide_plane_origin.x;
float y = slide_plane_origin.y;
float z = slide_plane_origin.z;
float A = slide_plane_normal.x;
float B = slide_plane_normal.y;
float C = slide_plane_normal.z;
float D = -((A*x) + (B*y) + (C*z));
float plane_constant = D;
float signed_distance_from_destination_point_to_sliding_plane = glm::dot(destination_point, slide_plane_normal) + plane_constant;
glm::vec3 new_destination_point = destination_point - signed_distance_from_destination_point_to_sliding_plane * slide_plane_normal;
glm::vec3 new_velocity_vector = new_destination_point - cp.intersection_point;
if (glm::length(new_velocity_vector) < very_close_distance) {
return new_position;
}
cp.collision_recursion_depth++;
cp.e_position = new_position;
cp.e_velocity = new_velocity_vector;
return collideWithWorld(cp);
}
bool PlayerController::sphereCollidingWithTriangle(CollisionPacket& cp, glm::vec3& p0,
glm::vec3& p1, glm::vec3& p2, glm::vec3& tri_normal) {
float facing = glm::dot(tri_normal, cp.e_normalized_velocity);
if (facing <= 0) {
glm::vec3 velocity = cp.e_velocity;
glm::vec3 position = cp.e_position;
float t0, t1;
bool sphere_in_plane = false;
// First the point in the plane
float x = p0.x;
float y = p0.y;
float z = p0.z;
// Next the planes normal
float A = tri_normal.x;
float B = tri_normal.y;
float C = tri_normal.z;
// Lets solve for D
// step 1: 0 = Ax + By + Cz + D
// step 2: subtract D from both sides
// -D = Ax + By + Cz
// setp 3: multiply both sides by -1
// -D*-1 = -1 * (Ax + By + Cz)
// final answer: D = -(Ax + By + Cz)
float D = -((A*x) + (B*y) + (C*z));
// To keep the variable names clear, we will rename D to plane_constant
float plane_constant = D;
float signed_distance_from_position_to_tri_plane = glm::dot(position, tri_normal) + plane_constant;
float plane_normal_dot_velocity = glm::dot(tri_normal, velocity);
if (plane_normal_dot_velocity == 0.0f) {
if (fabs(signed_distance_from_position_to_tri_plane) >= 1.0f) {
return false;
}
else {
sphere_in_plane = true;
}
}
else {
t0 = (1.0f - signed_distance_from_position_to_tri_plane) / plane_normal_dot_velocity;
t1 = (-1.0f - signed_distance_from_position_to_tri_plane) / plane_normal_dot_velocity;
if (t0 > t1) {
float temp = t0;
t0 = t1;
t1 = temp;
}
if (t0 > 1.0f || t1 < 0.0f) {
return false;
}
if (t0 < 0.0) t0 = 0.0;
if (t1 > 1.0) t1 = 1.0;
glm::vec3 collision_point;
bool colliding_with_tri = false;
float t = 1.0f;
if (!sphere_in_plane) {
glm::vec3 plane_intersection_point = (position + t0 * velocity - tri_normal);
if (checkPointInTriangle(plane_intersection_point, p0, p1, p2)) {
colliding_with_tri = true;
t = t0;
collision_point = plane_intersection_point;
}
}
if (colliding_with_tri == false) {
float a, b, c; // Equation Parameters
float velocity_length_squared = glm::length(velocity);
velocity_length_squared *= velocity_length_squared;
a = velocity_length_squared;
float new_t;
// P0 - Collision test with sphere and p0
b = 2.0f * glm::dot(velocity, position - p0);
c = glm::length((p0 - position));
c = (c*c) - 1.0f;
if (getLowestRoot(a, b, c, t, &new_t)) {
t = new_t;
colliding_with_tri = true;
collision_point = p0;
}
// P1 - Collision test with sphere and p1
b = 2.0f * glm::dot(velocity, position - p1);
c = glm::length((p1 - position));
c = (c*c) - 1.0f;
if (getLowestRoot(a, b, c, t, &new_t)) {
t = new_t;
colliding_with_tri = true;
collision_point = p1;
}
// P2 - Collision test with sphere and p2
b = 2.0f * glm::dot(velocity, position - p2);
c = glm::length((p2 - position));
c = (c*c) - 1.0f;
if (getLowestRoot(a, b, c, t, &new_t)) {
t = new_t;
colliding_with_tri = true;
collision_point = p2;
}
// Edge (p0, p1):
glm::vec3 edge = p1 - p0;
glm::vec3 sphere_position_to_vertex = p0 - position;
float edge_length_squared = glm::length(edge);
edge_length_squared *= edge_length_squared;
float edge_dot_velocity = glm::dot(edge, velocity);
float edge_dot_sphere_position_to_vertex = glm::dot(edge, sphere_position_to_vertex);
float sphere_position_to_vertex_length_squared = glm::length(sphere_position_to_vertex);
sphere_position_to_vertex_length_squared = sphere_position_to_vertex_length_squared * sphere_position_to_vertex_length_squared;
// Equation parameters
a = edge_length_squared * -velocity_length_squared + (edge_dot_velocity * edge_dot_velocity);
b = edge_length_squared * (2.0f * glm::dot(velocity, sphere_position_to_vertex)) - (2.0f * edge_dot_velocity * edge_dot_sphere_position_to_vertex);
c = edge_length_squared * (1.0f - sphere_position_to_vertex_length_squared) + (edge_dot_sphere_position_to_vertex * edge_dot_sphere_position_to_vertex);
if (getLowestRoot(a, b, c, t, &new_t)) {
float f = (edge_dot_velocity * new_t - edge_dot_sphere_position_to_vertex) / edge_length_squared;
if (f >= 0.0f && f <= 1.0f) {
t = new_t;
colliding_with_tri = true;
collision_point = p0 + f * edge;
}
}
// Edge (p1, p2):
edge = p2 - p1;
sphere_position_to_vertex = p1 - position;
edge_length_squared = glm::length(edge);
edge_length_squared *= edge_length_squared;
edge_dot_velocity = glm::dot(edge, velocity);
edge_dot_sphere_position_to_vertex = glm::dot(edge, sphere_position_to_vertex);
sphere_position_to_vertex_length_squared = glm::length(sphere_position_to_vertex);
sphere_position_to_vertex_length_squared = sphere_position_to_vertex_length_squared * sphere_position_to_vertex_length_squared;
// Equation parameters
a = edge_length_squared * -velocity_length_squared + (edge_dot_velocity * edge_dot_velocity);
b = edge_length_squared * (2.0f * glm::dot(velocity, sphere_position_to_vertex)) - (2.0f * edge_dot_velocity * edge_dot_sphere_position_to_vertex);
c = edge_length_squared * (1.0f - sphere_position_to_vertex_length_squared) + (edge_dot_sphere_position_to_vertex * edge_dot_sphere_position_to_vertex);
if (getLowestRoot(a, b, c, t, &new_t)) {
float f = (edge_dot_velocity * new_t - edge_dot_sphere_position_to_vertex) / edge_length_squared;
if (f >= 0.0f && f <= 1.0f) {
t = new_t;
colliding_with_tri = true;
collision_point = p1 + f * edge;
}
}
// Edge (p2, p0):
edge = p0 - p2;
sphere_position_to_vertex = p2 - position;
edge_length_squared = glm::length(edge);
edge_length_squared *= edge_length_squared;
edge_dot_velocity = glm::dot(edge, velocity);
edge_dot_sphere_position_to_vertex = glm::dot(edge, sphere_position_to_vertex);
sphere_position_to_vertex_length_squared = glm::length(sphere_position_to_vertex);
sphere_position_to_vertex_length_squared = sphere_position_to_vertex_length_squared * sphere_position_to_vertex_length_squared;
// Equation parameters
a = edge_length_squared * -velocity_length_squared + (edge_dot_velocity * edge_dot_velocity);
b = edge_length_squared * (2.0f * glm::dot(velocity, sphere_position_to_vertex)) - (2.0f * edge_dot_velocity * edge_dot_sphere_position_to_vertex);
c = edge_length_squared * (1.0f - sphere_position_to_vertex_length_squared) + (edge_dot_sphere_position_to_vertex * edge_dot_sphere_position_to_vertex);
if (getLowestRoot(a, b, c, t, &new_t)) {
float f = (edge_dot_velocity * new_t - edge_dot_sphere_position_to_vertex) / edge_length_squared;
if (f >= 0.0f && f <= 1.0f) {
t = new_t;
colliding_with_tri = true;
collision_point = p2 + f * edge;
}
}
}
if (colliding_with_tri == true) {
float dist_to_collision = t * glm::length(velocity);
if (cp.found_collision == false || dist_to_collision < cp.nearest_distance) {
cp.nearest_distance = dist_to_collision;
cp.intersection_point = collision_point;
cp.found_collision = true;
return true;
}
}
}
return false;
}
/* End sphereCollidingWithTriangle
--------------------------------------------------------------*/
}
bool PlayerController::checkPointInTriangle(const glm::vec3& point, const glm::vec3& triV1, const glm::vec3& triV2, const glm::vec3& triV3) {
glm::vec3 cp1 = glm::cross((triV3 - triV2), (point - triV2));
glm::vec3 cp2 = glm::cross((triV3 - triV2), (triV1 - triV2));
if (glm::dot(cp1, cp2) >= 0) {
cp1 = glm::cross((triV3 - triV1), (point - triV1));
cp2 = glm::cross((triV3 - triV1), (triV2 - triV1));
if (glm::dot(cp1, cp2) >= 0) {
cp1 = glm::cross((triV2 - triV1), (point - triV1));
cp2 = glm::cross((triV2 - triV1), (triV3 - triV1));
if (glm::dot(cp1, cp2) >= 0) {
return true;
}
}
}
return false;
}
bool PlayerController::getLowestRoot(float a, float b, float c, float maxR, float* root) {
float determinant = b * b - 4.0f*a*c;
if (determinant < 0.0f) return false;
float sqrtD = sqrt(determinant);
float r1 = (-b - sqrtD) / (2 * a);
float r2 = (-b + sqrtD) / (2 * a);
if (r1 > r2) {
float temp = r2;
r2 = r1;
r1 = temp;
}
if (r1 > 0 && r1 < maxR) {
*root = r1;
return true;
}
if (r2 > 0 && r2 < maxR) {
*root = r2;
return true;
}
return false;
}