Using Three.js to rotate a camera-followed sphere in the scene

Currently, I'm developing a Three.js scene with a toy concept where I aim to track a sphere using a camera [demo]. However, I am facing an issue wherein I can't seem to get the sphere to "roll" without causing the camera to also rotate along with it.

This is the code snippet I used to update the position of the sphere in each frame:

function moveSphere() {
  var delta = clock.getDelta(); // seconds
  var moveDistance = 200 * delta; // 200 pixels per second
  var rotateAngle = Math.PI / 2 * delta; // pi/2 radians (90 deg) per sec

  // handle movement in different directions
  if ( pressed['W'] ) {
    sphere.translateZ( -moveDistance );
  }
  if ( pressed['S'] ) 
    sphere.translateZ(  moveDistance );
  if ( pressed['Q'] )
    sphere.translateX( -moveDistance );
  if ( pressed['E'] )
    sphere.translateX(  moveDistance ); 

  // handle rotation
  var rotation_matrix = new THREE.Matrix4().identity();
  if ( pressed['A'] )
    sphere.rotateOnAxis(new THREE.Vector3(0,1,0), rotateAngle);
  if ( pressed['D'] )
    sphere.rotateOnAxis(new THREE.Vector3(0,1,0), -rotateAngle);
  if ( pressed['R'] )
    sphere.rotateOnAxis(new THREE.Vector3(1,0,0), rotateAngle);
  if ( pressed['F'] )
    sphere.rotateOnAxis(new THREE.Vector3(1,0,0), -rotateAngle);
}

Furthermore, here is the code fragment to track the sphere on each time tick:

function moveCamera() {
  var relativeCameraOffset = new THREE.Vector3(0,50,200);
  var cameraOffset = relativeCameraOffset.applyMatrix4(sphere.matrixWorld);
  camera.position.x = cameraOffset.x;
  camera.position.y = cameraOffset.y;
  camera.position.z = cameraOffset.z;
  camera.lookAt(sphere.position);
}

I'm looking for an easy way to allow the ball to roll without causing the camera to spin uncontrollably. While trying various combinations inside the if (pressed['W']) section like

sphere.rotateOnAxis(new THREE.Vector3(0,0,1), rotateAngle);
, I haven't found a natural solution to make the ball roll smoothly. Any insights or suggestions from others would be greatly appreciated!

Answer №1

The specific issue lies in the following line of code:

var cameraOffset = relativeCameraOffset.applyMatrix4(sphere.matrixWorld);

This line not only takes into account the position of the sphere but also its rotation. Therefore, if the sphere is rotated 180 degrees along the Y-axis, the resulting vector will be (0, 50, -200) added to the sphere's position.

To resolve this, you should extract the translation component from the sphere matrix and apply it to the offset. The revised code snippet below demonstrates how an intermediate matrix can store the sphere's position:

    /**
     * Adjusting the camera to follow the sphere
     **/
    var sphereTranslation = new THREE.Matrix4(); // Create once to minimize processing overhead

    function moveCamera() {
        var relativeCameraOffset = new THREE.Vector3(0, 50, 200);
        sphereTranslation.copyPosition(sphere.matrixWorld); // Retrieve only sphere's position
        var cameraOffset = relativeCameraOffset.applyMatrix4(sphereTranslation);
        camera.position.x = cameraOffset.x;
        camera.position.y = cameraOffset.y;
        camera.position.z = cameraOffset.z;
        camera.lookAt(sphere.position);
    }

Answer №2

To successfully implement the desired functionality, the critical step was to create a sphere and then integrate that sphere into a group. This approach allowed for the manipulation of both the group (controlling the ball's position) and the sphere inside the group (enabling the ball to "roll"). By separating these entities, it became possible for the camera to track the sphere group as before while enabling independent rotation of the ball within the moving sphere group [view updated demo]:

  /**
  * Creating a scene object with a customized background color
  **/

  function getScene() {
    var scene = new THREE.Scene();
    scene.background = new THREE.Color(0x111111);
    return scene;
  }

  /**
  * Setting up the camera for the scene. Camera parameters:
  *   [0] field of view: denotes the visible portion of the scene in degrees
  *   [1] aspect ratio: defines the width-to-height aspect ratio of the scene
  *   [2] near clipping plane: objects closer than this are excluded from rendering
  *   [3] far clipping plane: objects farther than this are omitted from rendering
  **/

  function getCamera() {
    var aspectRatio = window.innerWidth / window.innerHeight;
    var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 10000);
    camera.position.set(0,150,400);
    camera.lookAt(scene.position);  
    return camera;
  }

  /**
  * Configuring the lighting setup for the scene. Light parameters:
  *   [0]: Hexadecimal color value of the light source
  *   [1]: Intensity or strength of the light
  *   [2]: Distance from the light where intensity becomes zero
  * @param {obj} scene: current scene object
  **/

  function getLight(scene) {
    var lights = [];
    lights[0] = new THREE.PointLight( 0xffffff, 0.6, 0 );
    lights[0].position.set( 100, 200, 100 );
    scene.add( lights[0] );

    var ambientLight = new THREE.AmbientLight(0x111111);
    scene.add(ambientLight);
    return light;
  }

  /**
  * Initializing the renderer for the scene
  **/

  function getRenderer() {
    // Create a WebGL renderer along with the canvas element
    var renderer = new THREE.WebGLRenderer({antialias: true});
    // Enable support for retina displays
    renderer.setPixelRatio(window.devicePixelRatio);
    // Define canvas size
    renderer.setSize(window.innerWidth, window.innerHeight);
    // Append canvas to the DOM
    document.body.appendChild(renderer.domElement);
    return renderer;
  }

  /**
  * Establishing controls for the scene
  * @param {obj} camera: three.js camera instance
  * @param {obj} renderer: three.js renderer instance
  **/

  function getControls(camera, renderer) {
    var controls = new THREE.TrackballControls(camera, renderer.domElement);
    controls.zoomSpeed = 0.4;
    controls.panSpeed = 0.4;
    return controls;
  }

  /**
  * Loading grass texture
  **/

  function getPlane(scene, loader) {
    var texture = loader.load('grass.jpg');
    texture.wrapS = texture.wrapT = THREE.RepeatWrapping; 
    texture.repeat.set( 10, 10 );
    var material = new THREE.MeshBasicMaterial({
      map: texture, side: THREE.DoubleSide
    });
    var geometry = new THREE.PlaneGeometry(1000, 1000, 10, 10);
    var plane = new THREE.Mesh(geometry, material);
    plane.position.y = -0.5;
    plane.rotation.x = Math.PI / 2;
    scene.add(plane);
    return plane;
  }

  /**
  * Adding a sky background
  **/

  function getBackground(scene, loader) {
    var imagePrefix = 'sky-parts/';
    var directions  = ['right', 'left', 'top', 'bottom', 'front', 'back'];
    var imageSuffix = '.bmp';
    var geometry = new THREE.BoxGeometry( 1000, 1000, 1000 ); 
    
    var materialArray = [];
    for (var i = 0; i < 6; i++)
      materialArray.push( new THREE.MeshBasicMaterial({
        map: loader.load(imagePrefix + directions[i] + imageSuffix),
        side: THREE.BackSide
      }));
    var sky = new THREE.Mesh( geometry, materialArray );
    scene.add(sky);
  }

  /**
  * Incorporating a spherical character
  **/

  function getSphere(scene) {
    var geometry = new THREE.SphereGeometry( 30, 12, 9 );
    var material = new THREE.MeshPhongMaterial({
      color: 0xd0901d,
      emissive: 0xaf752a,
      side: THREE.DoubleSide,
      flatShading: true
    });
    var sphere = new THREE.Mesh( geometry, material );

    // Creating a group for translations and rotations
    var sphereGroup = new THREE.Group();
    sphereGroup.add(sphere)

    sphereGroup.position.set(0, 24, 100);
    scene.add(sphereGroup);
    return [sphere, sphereGroup];
  }

  /**
  * Recording currently pressed keys
  **/

  function addListeners() {
    window.addEventListener('keydown', function(e) {
      pressed[e.key.toUpperCase()] = true;
    })
    window.addEventListener('keyup', function(e) {
      pressed[e.key.toUpperCase()] = false;
    })
  }

  /**
  * Updating sphere movements
  **/

  function moveSphere() {
    var delta = clock.getDelta(); // seconds
    var moveDistance = 200 * delta; // 200 pixels per second
    var rotateAngle = Math.PI / 2 * delta; // pi/2 radians (90 deg) per sec

    // Move forwards/backwards/left/right
    if ( pressed['W'] ) {
      sphere.rotateOnAxis(new THREE.Vector3(1,0,0), -rotateAngle)
      sphereGroup.translateZ( -moveDistance );
    }
    if ( pressed['S'] ) 
      sphereGroup.translateZ(  moveDistance );
    if ( pressed['Q'] )
      sphereGroup.translateX( -moveDistance );
    if ( pressed['E'] )
      sphereGroup.translateX(  moveDistance ); 

    // Rotate left/right/up/down
    var rotation_matrix = new THREE.Matrix4().identity();
    if ( pressed['A'] )
      sphereGroup.rotateOnAxis(new THREE.Vector3(0,1,0), rotateAngle);
    if ( pressed['D'] )
      sphereGroup.rotateOnAxis(new THREE.Vector3(0,1,0), -rotateAngle);
    if ( pressed['R'] )
      sphereGroup.rotateOnAxis(new THREE.Vector3(1,0,0), rotateAngle);
    if ( pressed['F'] )
      sphereGroup.rotateOnAxis(new THREE.Vector3(1,0,0), -rotateAngle);
  }

  /**
  * Track the sphere's movement
  **/

  function moveCamera() {
    var relativeCameraOffset = new THREE.Vector3(0,50,200);
    var cameraOffset = relativeCameraOffset.applyMatrix4(sphereGroup.matrixWorld);
    camera.position.x = cameraOffset.x;
    camera.position.y = cameraOffset.y;
    camera.position.z = cameraOffset.z;
    camera.lookAt(sphereGroup.position);
  }

  // Rendering loop
  function render() {
    requestAnimationFrame(render);
    renderer.render(scene, camera);
    moveSphere();
    moveCamera();
  };

  // State variables
  var pressed = {};
  var clock = new THREE.Clock();

  // Global instances
  var scene = getScene();
  var camera = getCamera();
  var light = getLight(scene);
  var renderer = getRenderer();

  // Include mesh elements
  var loader = new THREE.TextureLoader();
  var floor = getPlane(scene, loader);
  var background = getBackground(scene, loader);
  var sphereData = getSphere(scene);
  var sphere = sphereData[0];
  var sphereGroup = sphereData[1];

  addListeners();
  render();
body { margin: 0; overflow: hidden; }
canvas { width: 100%; height: 100%; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/88/three.min.js"></script>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

How can I trigger an audio element to play using onKeyPress and onClick in ReactJS?

While attempting to construct the Drum Machine project for freeCodeCamp, I encountered a perplexing issue involving the audio element. Despite my code being error-free, the audio fails to play when I click on the div with the class "drum-pad." Even though ...

Ways to automatically update ng-class based on changes in ng-model value

I am working on a code where I need to add classes to the 'label' based on whether the input box is empty or not. To achieve this, I am checking if the input box is null and adding classes accordingly. <div class="col-md-12"> <input ...

Merge two distinct JSON objects obtained through an API request using Javascript

Struggling with my currency conversion project, I'm trying to display JSON response results on my website but can't seem to make it work. The code snippet below, .then((response) => { return response.json(); }) .then((jsonRespo ...

Click event not triggering on a div component in React

I'm having an issue with an onclick event on a div that is not triggering. <div className={styles.scrollingDiv}> <div className={styles.rectangleDiv} /> <div className={styles.menuBarDiv}> ...

Change the text of a button by using an input field

How can I dynamically update button text based on input field value? <input class="paymentinput w-input" type="tel" placeholder="0" id="amount-field"> <button id="rzp-button1" class="paynowbutton w-button">Pay Now</button> I want the bu ...

Unable to process the post request

I've encountered an issue while attempting to redirect using the res.redirect() function. When I submit the form, it should insert the data into the database and then redirect me to the home root. However, instead of successfully redirecting, I'm ...

Exploring scroll functionality with Webdriver.io v4

My code is designed to log into the beta version of mediawiki, navigate to the Preferences page, and attempt to click on a button located at the bottom of the page. In order to achieve this, I am utilizing the scroll() function because using only .click() ...

What is the best way to maintain a div centered when the browser window shrinks smaller than the div's dimensions?

Is there a way to keep a div centered and create white space around it when the browser window is resized? I want this effect to happen whether the browser window gets larger or smaller. Currently, when the window size decreases, the left side of the div l ...

Parsing JSON in an Express application

I have developed a small application to test a larger one that I am currently working on. The small application reads data from a CSV file and then attempts to send this data to my API endpoint using POST requests. This is necessary as I need to send a lar ...

Creating dynamic headers in Axios within an ExpressJS application

I have a specific requirement that calls for setting a custom Request-Id header for each axios request. The value of this header is generated dynamically by a separate express middleware. While I could manually set the header like this: axios.get(url, he ...

Creating JavaScript objects through function calls

let getBoxWidth = function() { this.width=2; }; alert(getBoxWidth.width); Hello there! Can you explain why the output is undefined in this scenario? ...

Using React to map and filter nested arrays while also removing duplicates

Hello, I recently started working with react and I've encountered a challenge while trying to map an array. const fullMen = LocationMenuStore.menuItems['menu']['headings'].map((headings: any) => { <Typography>{ ...

Using the DRY principle in JavaScript to handle dynamic fields

Recently delving into Javascript, I encountered a dynamic field script that allows for adding and subtracting fields with the click of a button. The functionality is effective and serves its purpose well. $(document).ready(function() { var max_fields ...

Looking for assistance in implementing specific styling techniques with React

Trying to implement a slider on my React page sourced from a Github repository. The challenge lies in adding the CSS as it is in JS format. Even after incorporating webpack and an npm compiler, the styling seems unrecognized. Any suggestions or solutions ...

How can I create a dropdown menu that is dependent on another dropdown menu using Ajax in my Laravel application?

I have two dropdown fields that are dependent on each other - Class & Section. I am trying to Select * from sections where class_id=selected Class Id. Although I attempted to achieve this using java script, it doesn't seem to work for me. Here are ...

Using Java beans to present form data utilizing session

How can I utilize Java Beans and session beans to store user input data and display a preview of the entered details on the next page? I have already created a servlet using JSP, but now I want to incorporate Java Beans to showcase the form data. How shoul ...

Angular does not seem to support drop and drag events in fullCalendar

I am looking to enhance my fullCalendar by adding a drag and drop feature for the events. This feature will allow users to easily move events within the calendar to different days and times. Below is the HTML code I currently have: <p-fullCalendar deep ...

Problem with targeting the .prev() and closest() class of a text input box

Could you please review This Demo and advise why I am unable to select the first .fa class before the text box #name ? This is what I have been trying: $(this).prev().closest(".fa").addClass("err"); within this code block <div class="row"> & ...

Entering a new row and sending information through ajax

I'm looking for some help with a web page I have that includes a particular table structure: **Check out my Table*:* <table id="staff" class="table"> <thead> <tr> <th>First Name</th> <th>Last Nam ...

Is there a way to calculate the mean of radio buttons that each have their own distinct values?

My HTML page features a unique rating system created with radio buttons that resemble stars when filled in. The CSS styling gives them an appealing design. Additionally, I have included a submit button to display the selected star count when clicked. Here ...