Bullet's vanilla Kinematic Character Controller is far from finished and polished compared to the KCCs that ship with commercial engines. I've had to give up on it and roll my own (using the Bullet GJK and manually resolving collisions against a capsule shape) in my current Bullet project.
I keep promising to submit mine to Bullet but the problem is it is so tied up into my own wrapper system around Bullet it has hard to break it out, and it is only okay for my particular domain (Mario-style 3D platformer) and probably not of any use in any other context.
My advice - learn enough to implement your own KCC, based on Bullet's collision and ray test functionality so you can tweak it to suit your particular requirements. Not easy though - I had already implemented my own (very poor) GJK before I felt comfortable handing that over to Bullet for a stable result.
Some pointers on the way mine works - I have a capsule shape representing the character, and also constantly cast a ray down from its base to measure the distance to the floor, only applying fake gravity if the controller is above a certain threshold above the "floor" and snapping it to a fixed distance above the floor if within the threshold. This avoids the need to use any kind of friction to prevent slope sliding. I use the Bullet broadphase then the GJK pair test to get separation vectors against the world geometry for other collisions. There are some additional complications when penetrating into steep slopes that require a bit of vector math to figure out.
Massive code dump ahoy...
#include "Kcc.h"
#include "maths/Vec2.h"
#include "maths/Vec3.h"
#include "maths/Matrix.h"
#include "maths/Quaternion.h"
#include "maths/Ray.h"
#include "physics/Physics.h"
#include "physics/components/Body.h"
#include "debug/DebugRender.h"
namespace
{
float minFloorDistance(float radius, const Vec3 &normal, float margin)
{
return sphericalDistanceToNormal(radius, normal) + (margin * 2);
}
RayResult findFloor(Physics &physics, float radius, const Vec3 &base)
{
RayResult r = physics.rayCast(Ray(base, Vec3(0, -1, 0)), 100);
if(r.valid() && dotVectors(r.normal(), Vec3(0, 1, 0)) >= 0.8f && r.distance() < minFloorDistance(radius, r.normal(), 0.15f))
{
return r;
}
return RayResult();
}
Vec3 alignToFloor(const Vec3 &velocity, const Vec3 &normal)
{
return transformNormal(velocity, rotationToQuaternion(Vec3(0, 1, 0), normal));
}
float lockedY(const RayResult &r, float radius, float height)
{
return r.worldPoint().y + minFloorDistance(radius, r.normal(), 0.05f) + ((height / 2) - radius);
}
bool lockToFloor(Physics &physics, Vec3 &pos, float radius, float height)
{
Vec3 base(pos.x, pos.y - ((height / 2) - radius), pos.z);
RayResult r = findFloor(physics, radius, base);
if(r.valid())
{
pos.y = lockedY(r, radius, height);
return true;
}
return false;
}
Vec3 slopeCorrection(const Vec3 &separation)
{
float d = dotVectors(normalizeVector(separation), Vec3(0, 1, 0));
if(d >= 0 && d < 0.8f)
{
return getDownVector(separation) * vectorLength(separation);
}
return Vec3(0, 0, 0);
}
Vec3 separatingVector(Physics &physics, const Shape &shape, const Vec3 &at)
{
Vec3 result = at;
Matrix to = translationMatrix(result);
BroadphaseResult br = physics.broadphaseAabb(shape.aabb(to).expanded(2));
if(br.valid())
{
int it = 0;
bool loop = true;
while(it < 5 && loop)
{
loop = false;
for(auto &b: br.bodies)
{
ConvexResult r = physics.convexIntersection(shape, to, b->shape(), b->transform());
if(r.valid())
{
result += r.separatingVector();
to = translationMatrix(result);
loop = true;
}
}
++it;
}
}
return result - at;
}
}
Kcc::Kcc(float radius, float height, const Vec3 &position) : pos(position), prev(position), shape(radius, height)
{
}
void Kcc::setPosition(const Vec3 &value)
{
pos = value;
prev = pos;
}
void Kcc::move(Physics &physics, MoveFlags flags, const Vec3 &step, float epsilon)
{
Vec3 tp = pos;
prev = pos;
bool tg = gr;
implementMove(tp, tg, physics, flags, step);
if(vectorLength(Vec2(tp.x, tp.z) - Vec2(pos.x, pos.z)) >= epsilon)
{
pos.x = tp.x;
pos.z = tp.z;
}
pos.y = tp.y;
gr = tg;
}
bool Kcc::canMove(Physics &physics, MoveFlags flags, const Vec3 &step, float epsilon) const
{
Vec3 tp = pos;
bool tg = gr;
implementMove(tp, tg, physics, flags, step);
return vectorLength(Vec2(tp.x, tp.z) - Vec2(pos.x, pos.z)) >= epsilon;
}
float Kcc::distanceToFloor(Physics &physics) const
{
RayResult r = physics.rayCast(Ray(pos + Vec3(0, -shape.height() / 2, 0), Vec3(0, -1, 0)), 100);
if(r.valid())
{
return r.distance() + (shape.radius() - minFloorDistance(shape.radius(), r.normal(), 0.01f));
}
return -1.0f;
}
bool Kcc::booleanIntersection(Physics &physics, const Matrix &transform, const BodyFilter &filter) const
{
return physics.booleanIntersection(shape, transform, filter);
}
void Kcc::implementMove(Vec3 &position, bool &grounded, Physics &physics, MoveFlags flags, const Vec3 &step) const
{
bool flying = step.y > 0;
float hh = shape.height() / 2;
RayResult floor = findFloor(physics, shape.radius(), position + step + Vec3(0, -(hh - shape.radius()), 0));
Vec3 dv(step.x, 0, step.z);
Vec3 mv = floor.valid() && !flying ? alignToFloor(dv, floor.normal()) : dv;
mv.y = step.y;
Vec3 sep = separatingVector(physics, shape, position + mv);
if(!flying)
{
mv += slopeCorrection(sep);
}
position += mv + sep;
grounded = false;
if(step.y <= 0)
{
grounded = lockToFloor(physics, position, shape.radius(), shape.height());
}
}