[SOLVED] Chase camera collision with terrain (heightmap)

Hello there!
I search a lot on internet and youtube videos about camera collision, and im experiencing a little problem when converting the code logical into my jme3 application.

I was trying to extend ChaseCamera and check for collision before super.update()
I have a QuadTerrain attached to my root, and my player character is attached to terrain.

The way im checking collision is, raycast the camera target to camera position.
Then set maxZoom to the distance between target and hitpoint

But i dont have a cool result… it does zoom so much!!

Has any body an example of chase camera with collision?

package com.mmo.client.desktop.camera;

import java.util.Optional;

import com.jme3.collision.CollisionResults;
import com.jme3.input.ChaseCamera;
import com.jme3.input.InputManager;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;
import com.jme3.terrain.geomipmap.TerrainQuad;

public class TerrainChaseCamera extends ChaseCamera {

    private final TerrainQuad terrain;
    private final Ray ray;
    private final CollisionResults collisionResults;
    private final float maxDistance;

    public TerrainChaseCamera(Camera camera, Spatial target, InputManager inputManager, TerrainQuad terrain) {
        super(camera, target, inputManager);
        setMinDistance(10);
        this.maxDistance = getMaxDistance();
        this.terrain = terrain;
        this.ray = new Ray();
        this.collisionResults = new CollisionResults();
    }

    @Override
    public void update(float tpf) {
        checkCollision().ifPresentOrElse(this::setMaxDistance, this::resetMaxDistance);

        super.update(tpf);
    }

    private Optional<Vector3f> checkCollision() {
        collisionResults.clear();

        ray.setOrigin(targetLocation);
        ray.setDirection(cam.getLocation());

        terrain.collideWith(ray, collisionResults);

        if (collisionResults.size() == 0) {
            return Optional.empty();
        }

        Vector3f contactPoint = collisionResults.getClosestCollision().getContactPoint();

        return Optional.of(contactPoint);
    }

    private void setMaxDistance(Vector3f contactPoint) {
        float maxDistance = Math.abs(contactPoint.distance(cam.getLocation()));
        System.out.println(maxDistance + " | " + getDistanceToTarget());
        setMaxDistance(getMinDistance());
    }

    private void resetMaxDistance() {
        setMaxDistance(maxDistance);
    }
}
2 Likes

This line of code looks incorrect. Instead of a location vector, it is looking for a normalized directional vector.

So to set the direction pointing back towards your camera, you’d instead do something like

Vector3f dirToCam = cam.getLocation().subtract(target.getLocation());
dirToCam.normalizeLocal();
ray.setDirection(dirToCam);
2 Likes

Still did not work =(
I got an idea… check for collision with terrain forward, backward, left and right from camera.
If collide stop rotating and zooming
I began working on rotation collision, but if you move the mouse fast, it will bug =(
Anyone can help?

here is the code


public class TerrainChaseCamera extends ChaseCamera {

    private static final float COLLISION_MIN_DISTANCE = 10;

    private final TerrainQuad terrain;
    private final Ray ray;
    private final CollisionResults collisionResults;
    private boolean collided;

    public TerrainChaseCamera(Camera camera, Spatial target, InputManager inputManager, TerrainQuad terrain) {
        super(camera, target, inputManager);

        setMinDistance(10);

        this.maxDistance = getMaxDistance();
        this.terrain = terrain;
        this.ray = new Ray();
        this.collisionResults = new CollisionResults();
    }

    @Override
    public void update(float tpf) {
        collided = checkForwardCollision() || checkBackwardCollision()
                || checkLeftCollision() || checkRightCollision();

        if (collided) {
            System.out.println("is collided");
        }

        super.update(tpf);
    }

    @Override
    protected void rotateCamera(float value) {
        float last = targetRotation;

        super.rotateCamera(value);

        if (collided) {
            rotating = false;
            targetRotation = last;
            super.rotateCamera(-value);
        }
    }

    @Override
    protected void vRotateCamera(float value) {
        float last = targetVRotation;

        super.vRotateCamera(value);

        if (collided) {
            vRotating = false;
            targetVRotation = last;
            super.rotateCamera(-value * 3);
        }
    }

    private boolean checkForwardCollision() {
        return checkCollision(cam.getDirection());
    }

    private boolean checkBackwardCollision() {
        return checkCollision(cam.getDirection());
    }

    private boolean checkLeftCollision() {
        return checkCollision(cam.getLeft());
    }

    private boolean checkRightCollision() {
        return checkCollision(cam.getLeft().negate());
    }

    private boolean checkCollision(Vector3f direction) {
        collisionResults.clear();

        ray.setOrigin(cam.getLocation());
        ray.setDirection(direction);

        terrain.collideWith(ray, collisionResults);

        if (collisionResults.size() == 0) {
            return false;
        }

        Vector3f contactPoint = collisionResults.getClosestCollision().getContactPoint();

        return cam.getLocation().distance(contactPoint) <= COLLISION_MIN_DISTANCE;
    }
}

I believe you also want the ray origin to be the location of the target that is being chased/orbitted by the camera. This way, you will just be casting ray backwards from the player aiming at the camera, and can then place the camera at the collided point if there’s any collision before within the max distance.

And then I don’t think you would need to do a left/right/forward collision either, only one collision back from the player to the camera.

I’ve done the same thing in my own project to confirm the concept works, but another user @capdevon has coincidentally just posted a great video displaying the same thing to the monthly WIP thread with nice debugging arrows to show what exactly is going on, hopefully the video in his post can help as well.

I see your point, this also was my 1st approach.
The problem in this approach is that my base terrain is a height map, with curves, valleys. And when i cast a ray from target to the cam location direction, it always hit a very close point because of height map curves. Maybe this approach, works well in a flat floor scene.

After many tests, i was wondering only prevening camera getting inside heightmap “curves”, like testing collision when rotation and zoom int/out.

But if you guys have a sample or maybe have you ever see this camera colusion with heightmap would be very nice to know!

this was my trying zoom approach

public class TerrainChaseCamera extends ChaseCamera {

    private final TerrainQuad terrain;
    private final Ray ray;
    private final CollisionResults collisionResults;

    public TerrainChaseCamera(Camera camera, Spatial target, InputManager inputManager, TerrainQuad terrain) {
        super(camera, target, inputManager);

        setMinDistance(10);

        this.maxDistance = getMaxDistance();
        this.terrain = terrain;
        this.ray = new Ray();
        this.collisionResults = new CollisionResults();
    }

    @Override
    public void update(float tpf) {
        if (checkCollision()) {
            Vector3f contactPoint = collisionResults.getClosestCollision().getContactPoint();
            float distance = cam.getLocation().distance(contactPoint);
            System.out.println("collided " + distance);
            zoomCamera(-distance);
        }

        super.update(tpf);
    }

    private boolean checkCollision() {
        collisionResults.clear();

        ray.setOrigin(targetLocation);
        ray.setDirection(cam.getLocation().subtract(targetLocation).normalize());

        terrain.collideWith(ray, collisionResults);

        if (collisionResults.size() == 0) {
            return false;
        }

        return true;
    }
}

Are you casting the ray from the targets feet or from their head? If you cast from the head then it should work.

My code is actually almost identical to yours and it works with terrains (although I’m on my phone atm so I can’t check but will soon). The only difference is that i call a getHeadLoc() method for my Agent class that returns the location of a node that’s at the Agents eye level.

The default height of the camera in relation to the player is important as well. In my case I don’t use chase cam, but I have the camera floating at eye level following behind the player, then looking down raises the camera, and looking up lowers he camera, and this helps the camsra angle stay adjusted well so that the back-ray check for collisions doesn’t clip too close to the player.

1st of all, thanks for helping me!
I tried to cast a ray from player eyes and it got better results at all!!!
But i think i will need to rotate the camera, can you confirm that?
Because when i set the zoom, it goes trough “walls”

And another thing i need to do, is to reset zoom position when has no collision.
I am extending chase camera and it is a little complicate to keep things working hehe

My results right now


1 Like

This is caused from the camera being positioned exactly at the contact point - so to fix it you could add a tiny offset to the contact point with the inverse value of the back-facing ray, then the camera should sit on top of the terrain, rather than within the terrain (or you could use contact normal as well, I think both work but haven’t experimented with which is better)

So something like

Vector3f offsetVec = ray.getDirection().negate();
//or
offsetVec = collisionResults.getClosestCollision().getContactNormal();

offsetVec = offsetVec.normalize();
offsetVec.multLocal(offsetLength);  //offsetLength should be a tiny value, like between 0.5 - 2.0, depending on the scale of your world

I can’t see what happens in the zoomCamera method to see if that’s working as i’d expect, and my way might be a bit different since I don’t use chase cam and instead manage the camera location manually.

But the way I do it is to just force the camera location to the maximum zoom value when there is not a collision detected any closer, so something like

@Override
public void update(float tpf) {
    if (checkCollision()) {
        Vector3f contactPoint = collisionResults.getClosestCollision().getContactPoint();
        float distance = cam.getLocation().distance(contactPoint);
        System.out.println("collided " + distance);

        zoomCamera(-distance);
    } 
   else{
          Vector3f maxOffsetVec = ray.getDirection().normalize();
          maxOffsetVec .multLocal(maxZoomDistance); 

          Vector3f newCamLoc = ray.getOrigin().add(maxOffsetVec ); 
          app.getCamera().setLocation(newCamLoc);


   }

    super.update(tpf);
}

So I think part of my suggestion might be way over-complicating things, since I don’t use chase cam to know how it works.

But I think you could also just fix the issue of the camera floating through walls by adding a small value to the value you send into the zoomCamera method, and that will act as a small offset and would be a lot easier than doing it how I suggested in first half of my last post. The code in my last post might be irrelevent if ChaseCam doesn’t let you force-set the camera location and instead relies on the zoomCamera mehod, but the idea is still the same.

zoomCamera(-distance + 0.5f);

And I think my suggestion of doing this when there’s no collision:

else{
      Vector3f maxOffsetVec = ray.getDirection().normalize();
      maxOffsetVec .multLocal(maxZoomDistance); 

      Vector3f newCamLoc = ray.getOrigin().add(maxOffsetVec ); 
      app.getCamera().setLocation(newCamLoc);

}

should be able to be simplified to

 else{
      zoomCamera( - maxZoomOutDistnace);
 }

if I’m understanding how chase camera works, of course. My apologies if I’m getting anything wrong since i don’t use that class myself to know for sure.

I got working the “reset” zoom, thanks a lot again!
The last is not working is the floating through walls, i got the idea of adding/subtracting offset
But it still allows crazy rotation like this picture

Maybe is it allowing too much rotation?

Anyway, this is my last code version, with zoom reset!

public class TerrainChaseCamera extends ChaseCamera {

    private static final Vector3f TARGET_HEAD_OFFSET = new Vector3f(0, 2, 0);

    private final TerrainQuad terrain;
    private final Ray ray;
    private final CollisionResults collisionResults;
    private Float realDistance;

    public TerrainChaseCamera(Camera camera, Spatial target, InputManager inputManager, TerrainQuad terrain) {
        super(camera, target, inputManager);

        setMinDistance(3);

        this.maxDistance = getMaxDistance();
        this.terrain = terrain;
        this.ray = new Ray();
        this.collisionResults = new CollisionResults();
    }

    @Override
    public void update(float tpf) {
        if (checkCollision()) {
            if (Objects.isNull(realDistance)) {
                realDistance = getDistanceToTarget();
            }

            // first attempt fixing through walls
            Vector3f offset = collisionResults.getClosestCollision().getContactNormal()
                    .normalize()
                    .multLocal(2);

            // second attempt fixing through walls
            offset = ray.getDirection().negate();

            Vector3f contactPoint = collisionResults.getClosestCollision().getContactPoint()
                    .add(offset);

            float distance = cam.getLocation().distance(contactPoint);

            // third attemp fixing through walls
            //distance += 0.5f;
            
            zoomCamera(-distance);
        } else if (Objects.nonNull(realDistance)) {
            // reset zoom to original before collision
            zoomCamera(realDistance - getDistanceToTarget());
            realDistance = null;
        }

        super.update(tpf);
    }

    private boolean checkCollision() {
        collisionResults.clear();

        ray.setOrigin(targetLocation.add(TARGET_HEAD_OFFSET));
        ray.setDirection(cam.getLocation().subtract(targetLocation).normalize());

        terrain.collideWith(ray, collisionResults);

        return collisionResults.size() != 0;
    }
}

The distance value needs calculated from the location where the ray was castfrom. As it is with this code, distance will be a value representing the difference between the collision point and the camera’s last location, but what you really want is the distance between the collision, and the location where the ray was cast from / the target player.

float distance = ray.getOrigin().distance(contactPoint);

I got your point, but in this case i think the distance is corrected because zoomCamera() method is expecting a value to ADD to the current camera distance,
So if my camera is 17 unit distance from contact point, I should add -17 to get closer

Oh I see, I was mistakenly thinking it wanted the new total zoom value.

In this case, I would try storing the totalZoom value as a variable and updating it every frame, and then you can compare this to the new total zoom distance to get the increment value that the method wants.

            float distance = ray.getOrigin().distance(contactPoint);
            // third attemp fixing through walls
            //distance += 0.5f;

            float incrimentDistance = lastDistance - distance; // might need subtracted in opposite order 

            lastDistance = distance;

            
            
            zoomCamera(incrimentDistance );

The reason that checking the distance between the camera and collision point won’t work is because it is giving you a value greater than zero when you rotate the camera, which alters the zoom even though no zooming has occurred. So it is important to not mistake the camera being rotated for a change in zoom distance. Changing the rotation could end up causing a change in zoom, but only if it is due to the back-facing ray catching a collision at a closer point after the rotation has occurred.

Sorry the late, I got some problem and could not open my personal notebook these days.
I agree with you, if rotate, the zoom might change.

The zoom “reset” is working, and the problem is that zoom keep trough “walls” yet, even multiplying by 1.1 or 0.9 to get 90% or 110%
(If I zoom in manually and slowly, without any customization in update method, it goes exactly like the last print screen - seems like impossible)
I also tried your suggestion of incrementDistance, but maybe i did not got the point

this is my full code (if you prefer, I can share with you guys the full project -it is small)


public class TerrainChaseCamera extends ChaseCamera {

    private static final Vector3f TARGET_HEAD_OFFSET = new Vector3f(0, 1.5f, 0);

    private final TerrainQuad terrain;
    private final Ray ray;
    private final CollisionResults collisionResults;
    private Float realDistance;

    public TerrainChaseCamera(Camera camera, Spatial target, InputManager inputManager, TerrainQuad terrain) {
        super(camera, target, inputManager);

        setMinDistance(3);

        this.maxDistance = getMaxDistance();
        this.terrain = terrain;
        this.ray = new Ray();
        this.collisionResults = new CollisionResults();
    }

    @Override
    public void update(float tpf) {
        if (checkCollision()) {
            if (Objects.isNull(realDistance)) {
                realDistance = getDistanceToTarget();
            }

            Vector3f contactPoint = collisionResults.getClosestCollision().getContactPoint();

            // zoom keep trough "walls" yet, even multiplying by 1.1 or 0.9 to get 90% or 110%
            float distance = cam.getLocation().distance(contactPoint);

            zoomCamera(-distance);

            /*
             * also tried your suggestion of incrementDistance, but maybe i did not got the point
             *  
             * incrementDistance = lastDistance - distance
             * 
             * lastDistance = distance;
             * 
             * zoomCamera(incrementDistance);
             */
        } else if (Objects.nonNull(realDistance)) {
            // reset zoom to original before collision -- its working
            zoomCamera(realDistance - getDistanceToTarget());
            realDistance = null;
        }

        super.update(tpf);
    }

    private boolean checkCollision() {
        collisionResults.clear();

        ray.setOrigin(targetLocation.add(TARGET_HEAD_OFFSET));
        ray.setDirection(cam.getLocation().subtract(targetLocation).normalize());

        terrain.collideWith(ray, collisionResults);

        return collisionResults.size() != 0;
    }
}


I think that this will not work for the chase camera actually, because (at your current angle in the screenshot) the back facing ray is almost parallell to the ground… which means no matter how much you multiply that by to get an offset, it will always have a y value close to 0, and will never raise off the ground to prevent the clipping.

So I think the only way to fix this is to use the contactNormal as the offset value instead of using the zoomCamera method

However I do not know how this would be compatible with chase camera, because it looks like you can only change the camera’s location by calling the zoomCamera() method or by rotating the camera. Am I correct to make this assumption? Or is it possible to force-set the camera location without breaking the chase camera functionality?

I can test if chase camera breaks or not, I was trying changing zoom and rotation only but i can access camera location and change it.

I will give a try, what is the difference between contact normal vs contact point?

Is your sugestion to keep ray casting to camera direction and set camera location to contact normal?

Contact Point is the location of the point that has been collided with, and Contact Normal is the direction that point is facing in world space - so it is a directional vector, unlike the contact point that is a locational vector. This value is important for lighting calculation, but is also useful to use as an offset value in situations like this.

I checked my code, and I actually don’t do this type of offset yet - I left a comment in my code saying to do it eventually if it becomes a bigger issue, but for me it hasn’t seemed to be as noticeable. I do get a small amount of clipping through the terrain (and other models) but its only barely noticeable, and it occurs when the camera collides on the left/right side for me, rather than the bottom of the screen like your screenshot.

Two other things to note about your screenshot that stood out to me:

First is that the head location seems like it might still be too low to the ground, and the camera’s default position should also be above and behind the head
And second is that I notice you don’t aim the camera back up at the head when it is lowered to the ground; doing this seems to be a big reason as to why I don’t experience similar clipping when the player looks upwards, and the camera is consequentially lowered to the ground like in your screenshot.

I actually just set the camera location to the collision point in my code, and that’s the only extra thing I do if there’s a camera collision.

Thanks for the explanation, first of all.

I have intention to create height mountains with heightmap, not only a curve terrain. So eventually I will get this case of terrain height > head location, right?

You mean, maybe if fix the camera at backward head location, should decrease this problem, like using camera node example?
The reason I choose chase camera was that because the project was to do a mmorpg and some of requirements was the ability to see the front player.
The server is ok, but a 3d front-end is really new for me =S

Do you think there is a solution or another kind of camera, i can implement?
Maybe i need to lock the camera vertical rotation to be always up the head location?

Yes I have scenarios like this but don’t seem to get clipping as bad. I still do get some clipping on the left/right side (which is a lot less common and less noticeable), and I could benefit from adding the contactNormal as an offset to prevent it.

But having good camera positioning and making the camera always look at the player’s head seems to prevent me from getting the clipping at the bottom of the screen when the cameras on the ground.

In my code I have the camera set up so that moving the mouse up/down changes the vertical direction the camera is looking, but also alters the height of the camera’s position.
So looking up makes the camera position lower to a point below and behind their head (effectively making it look up at the player from the ground, avoiding clipping because its not looking parallel to the ground), or the camera gets raised up slightly above the height of the player’s head when they are looking down.

Left/right rotation would be completely unrelated to that, so you could still let the player spin around to look at the front of the model like you mentioned.

Edit:
Here’s a clip of how my camera works that can hopefully help explain what I mean.
https://i.imgur.com/N1cchOT.mp4

The clipping I get is very similar but on the left/right, although I had to put in a bit of effort to reproduce it since it doesn’t seem to happen to me when I’m playing/testing normally and not thinking about it.

I posted the edit with a video at literally the same second as your post :laughing:

2 Likes