Why can a physic body fall through a terrain?

Hello,

I have two projects. In both I use JME3. Sometimes my character falls thought other physics bodies.
The character can be a capsule or a car.
I publish a video in my closed channel and a screenshot of one of my test projects. Maybe I should change something (physic calculations quality or something else).

In video and screenshot I use Minnie for the physics simulation.

What can be reason of this? If the code is important it is bellow:

public class Main extends SimpleApplication implements ActionListener {
    
    private Joystick viewedJoystick;
    private Node joystickInfo;
    private float yInfo = 0;
    private JoystickButton lastButton;
    
    private BulletAppState bulletAppState;
    private Spatial autoModel;
    private VehicleControl vehicle;
    private final float accelerationForce = 1000.0f;
    private final float brakeForce = 100.0f;
    private float steeringValue = 0;
    private float accelerationValue = 0;
    final private Vector3f jumpForce = new Vector3f(0, 3000, 0);
    private Node scene;
    private PointLight red, blue;
    private final Vector3f redPos = new Vector3f();
    private final Vector3f bluePos = new Vector3f();
    private Vector3f redDir = new Vector3f();
    private  Vector3f blueDir = new Vector3f();
    private final Vector3f LIGHT_OFFSET_TO_BLUE = new Vector3f(0.5f,-2,0f);
    
    private final float lightsAlpha = 0.75f;
    
    
    private boolean throttle;
    
    public static void main(String[] args) {
        Main app = new Main();
        AppSettings settings = new AppSettings(true);
        settings.setUseJoysticks(true);
        app.setSettings(settings);
        app.start();
    }

    @Override
    public void simpleInitApp() {      
        scene = (Node)assetManager.loadModel("Scenes/Scene_2.j3o");        
        rootNode.attachChild(scene);
        flyCam.setMoveSpeed(flyCam.getMoveSpeed()*30f);
        loadPhysic();
        Node terrain = (Node)scene.getChild("Terrain");
       
        terrain.addControl(new RigidBodyControl(0));
        bulletAppState.getPhysicsSpace().addAll(terrain);
        bulletAppState.getPhysicsSpace().setGravity(bulletAppState.getPhysicsSpace().getGravity(redPos).mult(4));
        setupKeys();
        buildPlayer();
        loadLights();
        terrain.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
        loadShield();
        loadGamepad();
    }
    
    private void setupKeys() {
        inputManager.addMapping("Lefts", new KeyTrigger(KeyInput.KEY_LEFT));
        inputManager.addMapping("Rights", new KeyTrigger(KeyInput.KEY_RIGHT));
        inputManager.addMapping("Ups", new KeyTrigger(KeyInput.KEY_UP));
        inputManager.addMapping("Downs", new KeyTrigger(KeyInput.KEY_DOWN));
        inputManager.addMapping("Space", new KeyTrigger(KeyInput.KEY_SPACE));
        inputManager.addMapping("Reset", new KeyTrigger(KeyInput.KEY_RETURN));
        inputManager.addListener(this, "Lefts");
        inputManager.addListener(this, "Rights");
        inputManager.addListener(this, "Ups");
        inputManager.addListener(this, "Downs");
        inputManager.addListener(this, "Space");
        inputManager.addListener(this, "Reset");
    }

    @Override
    public void simpleUpdate(float tpf) {
        
        Spatial child = autoModel;
        child.setLocalTranslation(vehicle.getPhysicsLocation());
        child.setLocalRotation(vehicle.getPhysicsRotation());  
        
        redPos.x = child.getLocalTranslation().x-LIGHT_OFFSET_TO_BLUE.x;
        redPos.y = child.getLocalTranslation().y+LIGHT_OFFSET_TO_BLUE.y;
        redPos.z = child.getLocalTranslation().z+LIGHT_OFFSET_TO_BLUE.z;
        
        bluePos.x = child.getLocalTranslation().x+LIGHT_OFFSET_TO_BLUE.x;
        bluePos.y = child.getLocalTranslation().y+LIGHT_OFFSET_TO_BLUE.y;
        bluePos.z = child.getLocalTranslation().z+LIGHT_OFFSET_TO_BLUE.z;
        
        red.setPosition(redPos);
        blue.setPosition(bluePos);
        flyCam.setEnabled(false);
    }

    @Override
    public void simpleRender(RenderManager rm) {
        //TODO: add render code
    }

    private void loadPhysic() {
        bulletAppState = new BulletAppState();
        bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);
        stateManager.attach(bulletAppState);
        bulletAppState.getPhysicsSpace().setAccuracy(1f/60f);
       
    }
    
    private void buildPlayer() {
        Material mat = new Material(getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");      
        mat.getAdditionalRenderState().setWireframe(true);
        mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
        mat.setTransparent(true);
        mat.setColor("Color", ColorRGBA.Red);
        CompoundCollisionShape compoundShape = new CompoundCollisionShape();
        BoxCollisionShape box = new BoxCollisionShape(new Vector3f(1.0f, 0.5f, 2.4f));
        compoundShape.addChildShape(box, new Vector3f(0, 1, 0));
        //create vehicle node
        Node vehicleNode =new Node("vehicleNode");
        vehicle = new VehicleControl(compoundShape, 150);
        vehicleNode.addControl(vehicle);

        float stiffness = 60.0f;//200=f1 car
        float compValue = .3f; //(should be lower than damp)
        float dampValue = .4f;
        vehicle.setSuspensionCompression(compValue * 2.0f * FastMath.sqrt(stiffness));
        vehicle.setSuspensionDamping(dampValue * 2.0f * FastMath.sqrt(stiffness));
        vehicle.setSuspensionStiffness(stiffness);
        vehicle.setMaxSuspensionForce(10000.0f);

        //Create four wheels and add them at their locations
        Vector3f wheelDirection = new Vector3f(0, -1, 0); // was 0, -1, 0
        Vector3f wheelAxle = new Vector3f(-1, 0, 0); // was -1, 0, 0
        float radius = 0.55f;
        float restLength = 0.40f;   //dist to the wheel from the body
        float yOff = 0.65f;
        float xOff = 2.095f;
        float zOff = 2f;

        Cylinder wheelMesh = new Cylinder(16, 16, radius, radius * 2.6f, true);
        wheelMesh.setMode(Mesh.Mode.Points);

        Node node1 = new Node("wheel 1 node");
        Geometry wheels1 = new Geometry("wheel 1", wheelMesh);
        node1.attachChild(wheels1);
        wheels1.rotate(0, FastMath.HALF_PI, 0);
        wheels1.setMaterial(mat);
        //wheels1.
        vehicle.addWheel(node1, new Vector3f(-xOff, yOff, zOff),
                wheelDirection, wheelAxle, restLength, radius, true);

        Node node2 = new Node("wheel 2 node");
        Geometry wheels2 = new Geometry("wheel 2", wheelMesh);
        node2.attachChild(wheels2);
        wheels2.rotate(0, FastMath.HALF_PI, 0);
        wheels2.setMaterial(mat);
        vehicle.addWheel(node2, new Vector3f(xOff, yOff, zOff),
                wheelDirection, wheelAxle, restLength, radius, true);

        Node node3 = new Node("wheel 3 node");
        Geometry wheels3 = new Geometry("wheel 3", wheelMesh);
        node3.attachChild(wheels3);
        wheels3.rotate(0, FastMath.HALF_PI, 0);
        wheels3.setMaterial(mat);
        vehicle.addWheel(node3, new Vector3f(-xOff, yOff, -zOff),
                wheelDirection, wheelAxle, restLength, radius, false);

        Node node4 = new Node("wheel 4 node");
        Geometry wheels4 = new Geometry("wheel 4", wheelMesh);
        node4.attachChild(wheels4);
        wheels4.rotate(0, FastMath.HALF_PI, 0);
        wheels4.setMaterial(mat);
        vehicle.addWheel(node4, new Vector3f(xOff, yOff, -zOff),
                wheelDirection, wheelAxle, restLength, radius, false);

        vehicleNode.attachChild(node1);
        vehicleNode.attachChild(node2);
        vehicleNode.attachChild(node3);
        vehicleNode.attachChild(node4);
        rootNode.attachChild(vehicleNode);

        getPhysicsSpace().add(vehicle);     
        
        autoModel = (Spatial)scene.getChild("Auto_2");   
        autoModel.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
        vehicleNode.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
      
        vehicle.setPhysicsLocation(autoModel.getLocalTranslation());  
        
        ChaseCamera chaseCam = new ChaseCamera(cam, autoModel, inputManager);
        chaseCam.setSmoothMotion(true);
        chaseCam.setDefaultVerticalRotation(3.14f/12f);
    }
    
    private PhysicsSpace getPhysicsSpace(){
        return bulletAppState.getPhysicsSpace();
    }
    
    @Override
    public void onAction(String binding, boolean value, float tpf) {
        if (binding.equals("Lefts")) {
            if (value) {
                steeringValue += .25f;
            } else {
                steeringValue += -.25f;
            }
            vehicle.steer(steeringValue);
        } else if (binding.equals("Rights")) {
            if (value) {
                steeringValue += -.25f;
            } else {
                steeringValue += .25f;
            }
            vehicle.steer(steeringValue);
        } else if (binding.equals("Ups")) {
            if (value) {
                accelerationValue += accelerationForce;
            } else {
                accelerationValue -= accelerationForce;
            }
            vehicle.accelerate(accelerationValue);
        } else if (binding.equals("Downs")) {
            if (value) {
                vehicle.brake(brakeForce);
            } else {
                vehicle.brake(0f);
            }
        } else if (binding.equals("Space")) {
            if (value) {
                vehicle.applyImpulse(jumpForce, Vector3f.ZERO);
            }
        } else if (binding.equals("Reset")) {
            if (value) {
                System.out.println("Reset");
                vehicle.setPhysicsLocation(Vector3f.ZERO);
                vehicle.setPhysicsRotation(new Matrix3f());
                vehicle.setLinearVelocity(Vector3f.ZERO);
                vehicle.setAngularVelocity(Vector3f.ZERO);
                vehicle.resetSuspension();
            } else {
            }
        }
    }

    private void loadLights() {
        red = new PointLight(new Vector3f(), new ColorRGBA(1,0,0,lightsAlpha));
        blue = new PointLight(new Vector3f(), new ColorRGBA(0,0,1,lightsAlpha));
        float LIGHTS_RADIUS = 3000;
        red.setRadius(LIGHTS_RADIUS);
        blue.setRadius(LIGHTS_RADIUS);
        
        
            try{
        System.out.println(" Start to load lights ");
        var lights = scene.getLocalLightList();
        for (int i = 0; i < lights.size(); i++){
             if (lights.get(i) instanceof PointLight point){
                PointLightShadowRenderer sunShadowRenderer = new PointLightShadowRenderer(getAssetManager(), 1024*2);
                sunShadowRenderer.setLight(point);
                viewPort.addProcessor(sunShadowRenderer);
                System.out.println("Light added");
            }
             else if (lights.get(i) instanceof DirectionalLight point){
                DirectionalLightShadowRenderer sunShadowRenderer = new DirectionalLightShadowRenderer(getAssetManager(), 1024*2,2);
                sunShadowRenderer.setLight(point);
                viewPort.addProcessor(sunShadowRenderer);
                System.out.println("Dir Light added");
            }
        }

            }
            catch (Exception e){
                System.out.println("Can not load shadows. " + e.getLocalizedMessage());
            }
    }

    private void loadShield() {
        scene.getChild("Shield").setShadowMode(RenderQueue.ShadowMode.Cast);
    }

    private void loadGamepad() {
        Joystick[] joysticks = inputManager.getJoysticks();
        if (joysticks == null)
            throw new IllegalStateException("Cannot find any joysticks!");

        try {
            PrintWriter out = new PrintWriter( new FileWriter( "joysticks-" + System.currentTimeMillis() + ".txt" ) );
            dumpJoysticks( joysticks, out );
            out.close();
        } catch( IOException e ) {
            throw new RuntimeException( "Error writing joystick dump", e );
        }   


        int gamepadSize = cam.getHeight() / 2;
        float scale = gamepadSize / 512.0f;        
        
        joystickInfo = new Node( "joystickInfo" );
        joystickInfo.setLocalTranslation( 0, cam.getHeight(), 0 );
        
        // Add a raw listener because it's easier to get all joystick events
        // this way.
        inputManager.addRawInputListener( new JoystickEventListener() );
        
        // add action listener for mouse click 
        // to all easier custom mapping
        inputManager.addMapping("mouseClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
        inputManager.addListener(new ActionListener() {
            @Override
            public void onAction(String name, boolean isPressed, float tpf) {
                if(isPressed){
                    
                }
            }
        }, "mouseClick");
    }
    
    
    protected void dumpJoysticks( Joystick[] joysticks, PrintWriter out ) {
        
    }
           
    
    protected void addInfo( String info, int column ) {
    
        BitmapText t = new BitmapText(guiFont);
        t.setText( info );
        t.setLocalTranslation( column * 200, yInfo, 0 );
        //joystickInfo.attachChild(t);
        yInfo -= t.getHeight();
    }
    
    protected void setViewedJoystick( Joystick stick ) {
        if( this.viewedJoystick == stick )
            return;
 
        if( this.viewedJoystick != null ) {
            joystickInfo.detachAllChildren();
        }
                   
        this.viewedJoystick = stick;
 
        if( this.viewedJoystick != null ) {       
            // Draw the hud
            yInfo = 0;
 
            addInfo(  "Joystick:\"" + stick.getName() + "\"  id:" + stick.getJoyId(), 0 );
 
            yInfo -= 5;
                       
            float ySave = yInfo;
            
            // Column one for the buttons
            addInfo( "Buttons:", 0 );
            for( JoystickButton b : stick.getButtons() ) {
                addInfo( " '" + b.getName() + "' id:'" + b.getLogicalId() + "'", 0 );
            }
            yInfo = ySave;
            
            // Column two for the axes
            addInfo( "Axes:", 1 );
            for( JoystickAxis a : stick.getAxes() ) {
                addInfo( " '" + a.getName() + "' id:'" + a.getLogicalId() + "' analog:" + a.isAnalog(), 1 );
            }
            
        } 
    }
    
    private void log(String log){
        System.out.println("DATA: " + log);
    }
    
    protected class JoystickEventListener implements RawInputListener {

        final private Map<JoystickAxis, Float> lastValues = new HashMap<>();

        @Override
        public void onJoyAxisEvent(JoyAxisEvent evt) {
            Float last = lastValues.remove(evt.getAxis());
            float value = evt.getValue();
                    
            // Check the axis dead zone.  InputManager normally does this
            // by default but not for raw events like we get here.
            float effectiveDeadZone = Math.max(inputManager.getAxisDeadZone(), evt.getAxis().getDeadZone());
            if( Math.abs(value) < effectiveDeadZone ) {
                if( last == null ) {
                    // Just skip the event
                    return;
                }
                // Else set the value to 0
                lastValues.remove(evt.getAxis());
                value = 0;
            }         
            setViewedJoystick( evt.getAxis().getJoystick() );            
           //gamepad.setAxisValue( evt.getAxis(), value );
            log("Axis event: " + value + "; Axis: " + evt.getAxis().getName());
            if (evt.getAxis().getName().equals("0")){
               log("Axis!");
                 if (value>(-0.1f) && value < 0.1f) {
                    steeringValue= 0f;
                } else {
                    steeringValue= value/4f;
                }
            vehicle.steer(steeringValue);
            }
            if( value != 0 ) {
                lastValues.put(evt.getAxis(), value);
            } 
        }

        @Override
        public void onJoyButtonEvent(JoyButtonEvent evt) {
            setViewedJoystick( evt.getButton().getJoystick() );
        }

        @Override
        public void beginInput() {}
        @Override
        public void endInput() {}
        @Override
        public void onMouseMotionEvent(MouseMotionEvent evt) {}
        @Override
        public void onMouseButtonEvent(MouseButtonEvent evt) {}
        @Override
        public void onKeyEvent(KeyInputEvent evt) {}
        @Override
        public void onTouchEvent(TouchEvent evt) {}        
    }
}

I have enabled the debug mode and recorded a new video.

1 Like

Thanks for providing your code. I tried to execute it, but didn’t get far because I lack the “Scene_2.j3o” file.

One possibility is that the character’s collision shape doesn’t match the character model.

To troubleshoot a physics issue like this, one of the first steps is to enable debug visualization:

bulletAppState.setDebugEnabled(true);

In your app, the logical place to add this code would be in the loadPhysic() method.

Additional troubleshooting hints can be found in the Minie tutorial:

Thanks, I have enabled the debug mode and recorded a new video. I made the model (not the physic body) of the car smaller.
What do you think about it now?

1 Like

Good video: brief and to the point.

I think the first thing to try is enabling continuous collision detection on that fast-moving vehicle:

6 posts were split to a new topic: Rounded object falls through terrain

I split off some of the discussion to a new topic: Rounded object falls through terrain

@MGDSStudio Did continuous collision detection resolve the issue?

Yes, it happens not more so regularly. Only once. But maybe I should apply:

    rigidBody.setCcdMotionThreshold(radius);
    rigidBody.setCcdSweptSphereRadius(radius);

not only for the body of the auto but also for the wheels and everything will be OK. Thanks, my son will be happy.

1 Like

The wheels of a VehicleControl are raycasters—not real collision objects—so you can’t apply continuous collision detection (CCD) to them.

If the vehicle still passes through the terrain occasionally, even with CCD enabled on the chassis, the next thing to try would be disabling contact filtering on the terrain’s collision shape:

RigidBodyControl rbc1 = new RigidBodyControl(0f);
rbc1.getCollisionShape().setContactFilterEnabled(false);
terrain.addControl(rbc1);

I’m currently investigating why contact filtering sometimes malfunctions.

1 Like

@sgold
Maybe you wanted to write:

        RigidBodyControl rbc1 = new RigidBodyControl(0f);                  
        terrain.addControl(rbc1);
        rbc1.getCollisionShape().setContactFilterEnabled(false); 

?

Your version of the code throws the NullPointerException, because:

value of "com.jme3.bullet.control.RigidBodyControl.getCollisionShape()" is null
1 Like

Correct. I forgot one must add rbc1 to a Spatial before you can access its collision shape.