Pan Arc Rotate Camera On Collision

Hey folks!

I have a question regarding ArcRotate camera panning when colliding with an object.

Ideally, I’d like my character to be able to aim up, when colliding with the ground object or if the camera’s beta is below a certain limit.

I also tried to use the built-in collision logic for the arc rotate camera, where the radius is reduced upon collision. But this did not really give me the desired affect.

private createCamera() {

  const focusPoint = this.createCameraFocusPoint(); // Create the camera focus point

   // Parent the focus point to the player mesh
    focusPoint.parent = this.mesh;

   // Check if the device is mobile and adjust the radius accordingly
   const isMobile = isMobileDevice();
   let radius: number;

  if (isMobile) {
    if (isLandscape()) {
      radius = 19; // Example: Smaller radius for mobile landscape (adjust as needed)
      console.log("Mobile Landscape: Setting camera radius to " + radius);
    } else {
      radius = 24; // Radius for mobile portrait
      console.log("Mobile Portrait: Setting camera radius to " + radius);
    }
  } else {
    radius = 20;   // Radius for desktop
    console.log("Desktop: Setting camera radius to " + radius);
  }


  // Parameters: name, alpha (rotation), beta (height), radius (distance), target position, scene
  this.camera = new ArcRotateCamera("playerCamera", 0, 1.5, radius, focusPoint.position, this.scene);
  this.camera.attachControl(this.scene.getEngine().getRenderingCanvas(), true);
  this.camera.inertia = 0.4; 


  this.camera.lockedTarget = focusPoint;


  this.camera.inputs.removeByType("ArcRotateCameraKeyboardMoveInput");
  this.camera.inputs.removeByType("ArcRotateCameraMouseWheelInput");

  this.camera.lowerBetaLimit = 0.02; // Prevent the camera from looking directly up
  this.camera.upperBetaLimit = Math.PI / 2 + 1; // Prevent the camera from looking directly down
    const pointersInput = this.camera.inputs.attached.pointers as ArcRotateCameraPointersInput;
      if (pointersInput) {
        pointersInput.angularSensibilityX = 650;
        pointersInput.angularSensibilityY = 650; 
      }
   
      this.camera._useCtrlForPanning = false;

      // Enable collisions for the scene
      this.scene.collisionsEnabled = true;

      // Enable collisions for the camera
      this.camera.checkCollisions = true;
      this.camera.collisionRadius = new Vector3(0.35, 0.35, 0.35); 
  
      // Enable collisions only for specific meshes (e.g., ground)
      const ground = this.scene.getMeshByName("ground");
      if (ground) {
        ground.checkCollisions = true;
      }
 
      
    // Handle camera collision response
    this.handleCameraCollisions();
}

private handleCameraCollisions() {
    // Save the initial radius of the camera
    const initialRadius = this.camera.radius;

    // Add an observable to check camera position before each render
    this.scene.registerBeforeRender(() => {
        if (this.camera.radius < initialRadius) {
            // Reset camera radius to initial value if it moved closer due to a collision
            this.camera.radius = initialRadius;
        }
    });
}



private createCameraFocusPoint() {
  // Create an invisible mesh that will act as the focus point for the camera
  const focusPoint = new Mesh("focusPoint", this.scene);
  focusPoint.position = (new Vector3(1.1, 2, 3.8)); // Set position relative to player mesh
  focusPoint.isVisible = false; // Make the mesh invisible
  return focusPoint;
}

I was then trying to implement something to allow the camera y panning while colliding with the ground or the camera beta reaches the lower limit. But couldn’t really make this work. I’ve created a playground I have set up this ground collisions logic here:

Any suggestions would be appreciated!

1 Like

I’m thinking about effectively shooting a sphere backwards from the camera

So basically updating the cameraFocusPoint y position in my example? I tried making this work, but failed also :upside_down_face:

effective camera placement and orientation is a lot of the feel of a game

it’s the boundaries of genres,… camera and control is the whole game really [imho]

1 Like

So true, now that I’ve added shooting to the game I really need to re-work the camera logic so that the aiming works better…

Right now I have allowed the camera to go below the ground and set backFaceCulling to false, but this just doesn’t feel right.

And when I turn the collision on with the ground I can’t aim upwards enough to hit targets in the air.

But when I turn the handleCameraCollisions Logic off to let the camera go towards the player, it still doesn’t feel quite right…

1 Like

in the last example if the camera kept it’s horizon it might be very nice
it pans up to the sky to nobody’s benefit

I’ll try to play around with this approach. I know eventually, I’ll get it right :sweat_smile:

1 Like

I wonder if the forum could find a tide whereupon to collectively test multiplayer projects.

I’m pretty sure yes!

I know it’s not quite the playground, but I’m actually working on a new Babylon + Colyseus template for Multiplayer I want to opensource once it’s ready!

camera collisions didn’t work in my case as well. I ended up using a few raycast to check if there’s anything between the camera and the player, if there’s I switch from 3rd person view to first person until it’s clear again

1 Like

Thanks for the suggestion! Since I’m super conscious of performance (want to achieve 40-50 fps even on low-tier mobiles) I think using raycast might be an overkill, especially that my camera can only collide with the ground.

Using the built-in collision logic works well for moving the camera towards the player, but resetting logic was a but glitchy and I couldn’t really get it to work properly…

I’m playing round with reducing the radius if there the beta value goes below a certain amount and see how that goes.

If nothing gives, I’ll eventually simply set the camera’s positions high enough and the radius low enough so that camera would never clip through the ground.

If I’m not mistaken, rays are quite cheap, cheaper than I thought.
They snag when tested against a bazillion meshes iirc.

1 Like

Ohhh okay that’s pretty good to know!

After a bit of thinking I went with reducing radius if the beta angle goes below a certain amount. The main reason for this is I wanted to have the same behaviour while airborne, not just when the camera was colliding with the ground.

I really like how the camera works now, need to fine tune the parameters but it’s already pretty close to what I was aiming for (no pun intended :joy:)

Maybe it’s overcomplicated, but this is how I implemented this:

private adjustCameraRadiusBasedOnBeta(): void {
    if (!this.camera || this.initialCameraRadius === undefined) {
        // Ensure camera and initial radius are set
        return;
    }

    const currentBeta = this.camera.beta;

    // Check if beta is above the threshold where radius reduction starts
    if (currentBeta > this.betaThresholdNormalToReduced) {
        // Calculate the range of beta angles over which the reduction occurs
        const betaRangeForReduction = this.camera.upperBetaLimit - this.betaThresholdNormalToReduced;

        if (betaRangeForReduction <= 0.001) { // Using a small epsilon
            // If range is too small or invalid, just set to the minimum radius factor
            this.camera.radius = this.initialCameraRadius * this.minRadiusAtUpperBetaFactor;
            return;
        }

        // Calculate 't' - a normalized value (0 to 1) representing progress through the reduction range.
        // t = 0 when currentBeta is at betaThresholdNormalToReduced.
        // t = 1 when currentBeta is at camera.upperBetaLimit.
        let t = (currentBeta - this.betaThresholdNormalToReduced) / betaRangeForReduction;
        t = Math.max(0, Math.min(t, 1)); // Clamp t between 0 and 1 for robustness

        // Interpolate the radius.
        // When t=0, newRadius = initialCameraRadius.
        // When t=1, newRadius = initialCameraRadius * minRadiusAtUpperBetaFactor.
        const newRadius = this.initialCameraRadius * (1 - t * (1 - this.minRadiusAtUpperBetaFactor));
        this.camera.radius = newRadius;

   ${newRadius.toFixed(2)}`);

    } else {
        if (this.camera.radius !== this.initialCameraRadius) {
            this.camera.radius = this.initialCameraRadius;
          ${this.initialCameraRadius.toFixed(2)}`);
        }
    }
}
1 Like