Physics shapes collide on flat surface's triangle boundaries

I’ve recently started tackling a problem in my game where my entities would sometimes bounce when moving somewhat fast on a flat terrain. After doing some testing, I’ve figured out that my entities’ collision shapes collide with the triangle boundaries of the terrain, which is completely inaccurate. After looking this up, I came upon this thread on the bullet forums that describes my problem: http://bulletphysics.org/Bullet/phpBB3/viewtopic.php?f=9&t=7539&view=next.

On the thread, they talk about how you can fix this problem by using the functions in btInternalEdgeUtility. Now I’m not a bullet expert, but how viable is this solution? Should I make changes in JME’s native bullet implementation and then make a PR?

After taking a bit of a break, I decided to try and fix this annoying problem. Like written in my previous post, I tried using the functions in btInternalEdgeUtility in order to fix the internal edge collisions, but with little to no success. What I’ve done currently is the following: when creating a mesh collision shape, generate the internal edge info using

btTriangleInfoMap* triangleInfoMap = new btTriangleInfoMap();
btGenerateInternalEdgeInfo(shape, triangleInfoMap);

When creating a rigid body, set the custom material callback collision flag

if (shape->getShapeType() == TRIANGLE_SHAPE_PROXYTYPE) {
    body->setCollisionFlags(body->getCollisionFlags() | btCollisionObject::CF_CUSTOM_MATERIAL_CALLBACK);
}

Set the following callback as the contact added callback (mostly taken from bullet’s InternalEdgeDemo)

gContactAddedCallback = customMaterialCombinerCallback;
...
bool customMaterialCombinerCallback(btManifoldPoint& cp, const btCollisionObjectWrapper* colObj0Wrap, int partId0, int index0, const btCollisionObjectWrapper* colObj1Wrap, int partId1, int index1) {
    // Find the triangle mesh object and the other object
    const btCollisionObjectWrapper *trimesh = colObj0Wrap, *other = colObj1Wrap;
    int trimesh_pid = partId0, other_pid = partId1;
    int trimesh_id = index0, other_id = index1;
    // Make sure we're giving btAdjustInternalEdgeContacts the right arguments 
    if ( colObj1Wrap->getCollisionShape()->getShapeType() == TRIANGLE_SHAPE_PROXYTYPE ) {
        trimesh = colObj1Wrap;
        trimesh_pid = partId1;
        trimesh_id = index1;

        other = colObj0Wrap;
        other_pid = partId0;
        other_id = index0;
    }

    btAdjustInternalEdgeContacts(cp, trimesh, other, trimesh_pid, trimesh_id);

    float friction0 = other->getCollisionObject()->getFriction();
    float friction1 = trimesh->getCollisionObject()->getFriction();
    float restitution0 = other->getCollisionObject()->getRestitution();
    float restitution1 = trimesh->getCollisionObject()->getRestitution();

    if (other->getCollisionObject()->getCollisionFlags() & btCollisionObject::CF_CUSTOM_MATERIAL_CALLBACK) {
        friction0 = 1.0; //partId0,index0
        restitution0 = 0.f;
    }
    if (trimesh->getCollisionObject()->getCollisionFlags() & btCollisionObject::CF_CUSTOM_MATERIAL_CALLBACK) {
        if (trimesh_pid & 1) {
            friction1 = 1.0f; //partId1,index1
        } else {
            friction1 = 0.f;
        }
        restitution1 = 0.f;
    }

    cp.m_combinedFriction = calculateCombinedFriction(friction0, friction1);
    cp.m_combinedRestitution = calculateCombinedRestitution(restitution0, restitution1);
    
    return true;
}

Here is the test case I am using that demonstrates the problem.

package test;

import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.MeshCollisionShape;
import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.math.Vector3f;
import com.jme3.scene.shape.Box;
import com.jme3.terrain.geomipmap.TerrainQuad;

public class CharacterControlTest extends SimpleApplication implements ActionListener{

    private BetterCharacterControl characterControl;
    
    public static void main(String[] args){
        new CharacterControlTest().start();
    }
    
    @Override
    public void simpleInitApp(){
        characterControl = new BetterCharacterControl(0.1f, 1, 50);
        cam.setLocation(new Vector3f(0, 1, 10));
        flyCam.setEnabled(false);
        
        BulletAppState bulletAppState = new BulletAppState();
        //bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);
        getStateManager().attach(bulletAppState);
        bulletAppState.setDebugEnabled(true);
        
        bulletAppState.getPhysicsSpace().add(characterControl);
        
        RigidBodyControl floorPhysics = new RigidBodyControl(new MeshCollisionShape(new Box(Vector3f.ZERO, 100f, 0.2f, 100f)), 0);
        floorPhysics.setPhysicsLocation(new Vector3f(0f, 0, 0f));
        floorPhysics.setKinematic(false);
        //bulletAppState.getPhysicsSpace().add(floorPhysics);
        
        TerrainQuad terrain = new TerrainQuad("terrain", 32, 129, null);
        RigidBodyControl terrainPhysics = new RigidBodyControl(0);
        terrain.addControl(terrainPhysics);
//        terrainPhysics = new RigidBodyControl(new MeshCollisionShape(DebugShapeFactory.getDebugMesh(terrainPhysics.getCollisionShape())), 0);
//        terrain.removeControl(RigidBodyControl.class);
//        terrain.addControl(terrainPhysics);
        bulletAppState.getPhysicsSpace().add(terrainPhysics);
        
        inputManager.addMapping("Forward", new KeyTrigger(KeyInput.KEY_W));
        inputManager.addMapping("Backward", new KeyTrigger(KeyInput.KEY_S));
        inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_A));
        inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_D));
        inputManager.addListener(this, "Forward", "Backward", "Left", "Right", "Jump");
    }

    @Override
    public void onAction(String name, boolean isPressed, float f){
        Vector3f movementDirection = characterControl.getWalkDirection();
        if(name.equals("Forward")){
            if(isPressed)
                movementDirection.addLocal(Vector3f.UNIT_Z.negate().normalize().multLocal(5));
            else
                movementDirection.subtractLocal(Vector3f.UNIT_Z.negate().normalize().multLocal(5));
        }
        if(name.equals("Backward")){
            if(isPressed)
                movementDirection.addLocal(Vector3f.UNIT_Z.normalize().multLocal(5));
            else
                movementDirection.subtractLocal(Vector3f.UNIT_Z.normalize().multLocal(5));
        }
        if(name.equals("Left")){
            if(isPressed)
                movementDirection.addLocal(Vector3f.UNIT_X.negate().normalize().multLocal(5));
            else
                movementDirection.subtractLocal(Vector3f.UNIT_X.negate().normalize().multLocal(5));
        }
        if(name.equals("Right")){
            if(isPressed)
                movementDirection.addLocal(Vector3f.UNIT_X.normalize().multLocal(5));
            else
                movementDirection.subtractLocal(Vector3f.UNIT_X.normalize().multLocal(5));
        }
    }
}

By moving using WASD, you can see the character control bouncing off the internal edges of the floor, which is a flat terrain. If instead of using the terrain you use a simple box collision shape for the floor and you aim for the one internal edge, the character gets a slight jump (more noticeable sometimes then others) almost as if like the edge was a ramp.

Some bullet forum discussions that I’ve found regarding the issue:
http://bulletphysics.org/Bullet/phpBB3/viewtopic.php?f=9&t=6662&p=24566&hilit=internal+edge#p24566
https://www.bulletphysics.org/Bullet/phpBB3/viewtopic.php?f=9&t=7794
http://bulletphysics.org/Bullet/phpBB3/viewtopic.php?f=9&t=7539&view=next
http://bulletphysics.org/Bullet/phpBB3/viewtopic.php?t=10946
http://bulletphysics.org/Bullet/phpBB3/viewtopic.php?f=9&t=4603&hilit=btAdjustInternalEdgeContacts

Most discussions I’ve found describe the exact issue and say that using the utility fixes the problem. However I’ve also seen some posts where some people had problems integrating it. At this point I’m just wondering what the best approach is to try and fix this issue.