Three.js experiencing issues with quaternion rotation malfunctioning after exceeding a rotation angle of around 90 degrees

Trying out two-finger touch events to pinch, rotate, and zoom a THREE.Mesh object using quaternions has been quite an interesting experience. As I delve into this new rotation method, I've noticed an intriguing behavior that puzzles me. When I rotate the object by about 90° through total touch drag, the rotation starts to jitter erratically. However, upon dragging it back under 90°, the rotation gradually smoothens out, albeit in a noticeably nonlinear manner. It's almost as if there's a hidden pattern behind this jittery rotation (or maybe it’s just a guess).

As I scratch my head over this, here's the code snippet I'm currently using to handle the rotation of the obj:

  // global variables
var 
      // stores the original object scale
    pinchScale = new THREE.Vector3(),

      // tracks the first touch
    touch1 = new THREE.Vector2(),

      // tracks the second touch
    touch2 = new THREE.Vector2(),

      // records the initial position of the first touch
    touch1OnHold = new THREE.Vector2(),

      // records the initial position of the second touch
    touch2OnHold = new THREE.Vector2(),

      // keeps track of the total rotation during the last touchmove event
    angularHoldPrev = new THREE.Quaternion();

  ⋮

  // key-value pairs within an addEventListener function
touchstart: function (event) {
  event.preventDefault();
  if ( event.touches.length === 1 ) {
    …
  } else if ( event.touches.length === 2 ) {
    touch1OnHold.set(event.touches[0].pageX, event.touches[0].pageY);
    touch2OnHold.set(event.touches[1].pageX, event.touches[1].pageY);
    angularHoldPrev.set(0, 0, 0, 1)
  }
},
touchmove: function (event) {
  event.preventDefault();
  if ( event.touches.length === 1 ) {
    …
  } else if ( event.touches.length === 2 ) {
    touch1.set(event.touches[0].pageX, event.touches[0].pageY);
    touch2.set(event.touches[1].pageX, event.touches[1].pageY);
    var 
          // calculate the spread between the two touches at the current event and at the start of the hold
        touchDiff = touch2.clone().sub(touch1),
        touchDiffOnHold = touch2OnHold.clone().sub(touch1OnHold),

          // determine the axis of the camera regardless of the object's orientation
        axis1 = new THREE.Vector3(0, 0, 1).applyQuaternion(obj.quaternion.clone().inverse()),

          // calculate the rotation around this axis based on the touch movement
        rot1 = new THREE.Quaternion().setFromAxisAngle(axis1, (Math.atan2(touchDiffOnHold.y, touchDiffOnHold.x) - Math.atan2(touchDiff.y, touchDiff.x))).normalize(),

          // calculate the barycenter of the touch at the present event and the start of the hold
        touchCentre = touch1.clone().add(touch2).multiplyScalar(.5),
        touchCentreOnHold = touch1OnHold.clone().add(touch2OnHold).multiplyScalar(.5),

          // determine the axis of touch barycenter movement on the xy plane regardless of the object orientation
        axis2 = new THREE.Vector3(touchCentre.y - touchCentreOnHold.y, touchCentre.x - touchCentreOnHold.x, 0).applyQuaternion(obj.quaternion.clone().inverse()),

          // calculate a rotation proportionate to the magnitude of touch movement
        rot2 = new THREE.Quaternion().setFromAxisAngle(axis2, axis2.length() * rotationSensitivity).normalize(),

          // combine the two rotations
        rot = rot1.multiply(rot2);

      // reverse the last rotation if it's not an empty quaternion
    if (!angularHoldPrev.equals(new THREE.Quaternion())) obj.quaternion.multiply(angularHoldPrev.inverse());

      // apply the calculated rotation
    obj.quaternion.multiply(rot);

      // store this rotation for the next event
    angularHoldPrev.copy(rot);

      // adjust the object's scale based on the change in touch spread
    obj.scale.copy(pinchScale.clone().multiplyScalar(touchDiff.length() / touchDiffOnHold.length()))
  }
},
touchend: function (event) {
  event.preventDefault();

    // restore the original object scale
  pinchScale.copy(obj.scale)
}

If anyone has insights on how I can achieve consistent and proportional rotation with all two-touch inputs, I'd be grateful for your help. Thanks!

Answer №1

After some tinkering, I managed to get it to work by changing the approach to touchmove rotations. Instead of being relative to touchstart, I made them incremental, resulting in a cleaner solution. The rotV variable adds angular velocity to the animation loop, giving the object a smooth inertial movement. (Shoutout to @WestLangley for the optimization tips)

var touch1 = new THREE.Vector2(),
    touch2 = new THREE.Vector2(),
    touch1Prev = new THREE.Vector2(),
    touch2Prev = new THREE.Vector2(),
    rotV = new THREE.Quaternion(),

    touchDiff = new THREE.Vector2(),
    touchDiffPrev = new THREE.Vector2(),
    touchCentre = new THREE.Vector2(),
    touchCentrePrev = new THREE.Vector2(),
    axis1 = new THREE.Vector3(),
    axis2 = new THREE.Vector3(),
    rot1 = new THREE.Quaternion(),
    rot2 = new THREE.Quaternion(),
    adjq = new THREE.Quaternion();

⋮
    
touchstart: function (event) {
  event.preventDefault();
  if ( event.touches.length === 1 ) {
    …
  } else if ( event.touches.length === 2 ) {
    touch1Prev.set(event.touches[0].pageX, event.touches[0].pageY);
    touch2Prev.set(event.touches[1].pageX, event.touches[1].pageY)
  }
},
touchmove: function (event) {
  event.preventDefault();
  adjq.copy(obj.quaternion).inverse();
  if ( event.touches.length === 1 ) {
    …
  } else if ( event.touches.length === 2 ) {
    touch1.set(event.touches[0].pageX, event.touches[0].pageY);
    touch2.set(event.touches[1].pageX, event.touches[1].pageY);

    touchDiff.copy(touch2).sub(touch1);
    touchDiffPrev.copy(touch2Prev).sub(touch1Prev);
    axis1.set(0, 0, 1).applyQuaternion(adjq);
    rot1.setFromAxisAngle(axis1, (Math.atan2(touchDiffPrev.y, touchDiffPrev.x) - Math.atan2(touchDiff.y, touchDiff.x))).normalize();

    touchCentre.copy(touch1).add(touch2).multiplyScalar(.5);
    touchCentrePrev.copy(touch1Prev).add(touch2Prev).multiplyScalar(.5);
    axis2.set(touchCentre.y - touchCentrePrev.y, touchCentre.x - touchCentrePrev.x, 0).applyQuaternion(adjq);
    rot2.setFromAxisAngle(axis2, axis2.length() * rotationSensitivity * 10).normalize();

    obj.quaternion.multiply(rot1.multiply(rot2));
    rotV.multiply(rot1.slerp(adjq.set(0, 0, 0, 1), .9));
    obj.scale.multiplyScalar(touchDiff.length() / touchDiffPrev.length());

    touch1Prev.copy(touch1);
    touch2Prev.copy(touch2)
  }
},
touchend: function (event) {
  event.preventDefault()
}

Despite the success, I'm still curious about the root cause of the initial issue. Oh, the mysteries of coding! 😄

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

Add a directive on the fly, establish a connection, and display it dynamically

Within my markup, I have a tag element called popup-window which is handled by a specific directive. If I wish to incorporate multiple similar widgets that can be displayed or hidden in various locations, I currently need to include all these elements dire ...

What is the most effective way to enlarge an HTML table in the center?

Currently, I am dynamically generating an HTML table using a repeater. The table consists of four columns that I populate with company data. My goal is to enable users to click on a row and have another row appear below it, containing a Google map and addi ...

I am interested in developing a JavaScript program that can calculate multiples of 0.10

I am looking to let users input values that are multiples of 0.10, such as - 0.10, 0.20, 0.30....1.00, 1.10, 1.20...1.90, and so on. When a user enters a value in the text box, I have been checking the following validation: amount % 0.10 == 0 Is this a ...

PHP code in Wordpress incorporating an Ajax request

Does anyone know how to fetch user data as a string using ajax in WordPress? I think I understand the basic concept PHP This code goes in my functions.php file add_action('template_redirect', 'edit_user_concept'); function edit ...

Exploring the capabilities of Redux Toolkit's createEntityAdapter in designing versatile data grids

Seeking guidance on utilizing createEntityAdapter from Redux Toolkit. In my application, I display package information and details using the master/detail feature of the AG Grid library. Packages are loaded initially, followed by fetching detailed data as ...

Creating a drop-down menu within an HTML table along with a D3 bar chart

How can I implement a drop-down menu allowing the user to choose a time interval for this HTML table and d3 bar chart? The time intervals needed are: Now, 24 hours, 48 hours, 72 hours, 1 week, and 1 month. I am relatively new to creating dynamic tables and ...

Efficiently organizing reducers into separate files in ReactJS and merging them together

My App is a simple counter app where buttons are images with their own counters. https://i.stack.imgur.com/qkjoi.png In my App.js file, I imported the reducer for the counters using the following code: import reducer from './reducers/reducerCounter&a ...

What is the best way to notify administrator users when their accounts have exceeded the timeout period?

Working on the website for our high school newspaper, I've encountered a recurring issue on the admin page. Users are getting logged out after creating an article due to a time limit constraint. To address this problem, my goal is to implement an aler ...

Converting a curl command to a $.ajax() call in JavaScript: A step-by-step guide

I'm attempting to retrieve data from the Zomato API by using jquery ajax, however, they have provided a curl command instead. curl -X GET --header "Accept: application/json" --header "user-key: key" "https://developers.zomato.com/api/v2.1/cities" Is ...

Using numerous WebGL models within a single webpage

In the research lab where I work, we are currently developing a webpage that will showcase a long list of 3D models that can be scrolled through, totaling around 50 models. Originally, we planned to achieve this by using separate THREE.js WebGL contexts. H ...

Clear the cache following the service call

I am currently working on a service that has two methods. Utilizing the built-in cache support for $resource, I am trying to implement a way to refresh the cache when a service call is made from the controller. I attempted to use $cacheResource without suc ...

Switch effortlessly between various THREE.EffectComposer scenes with a single renderer in three.js

Currently, I'm experimenting with creating intricate scenes using Composer in three.js. I'm curious to know if it's achievable to switch between two scenes with distinct composer effects applied to them. To better understand this concept, I& ...

Getting started with a project using meteor.js and vue.js

As a beginner in meteor.js, I am eager to create a project using both meteor.js and vue.js. However, I am struggling to find the right method for managing files in meteor.js. Could someone please assist me by providing a demo project or video link that c ...

Having trouble loading CSS and JavaScript files in CodeIgniter version 3.0.4?

I am facing an issue with loading my CSS and JS files in the view file. I have already added them to the folder and set the base URL. These codes were working fine with a project I previously did on an older version of CodeIgniter. What could be causing ...

Utilizing Conditional Styling for an Array of Objects within a Textarea in Angular 6

My array contains objects structured as follows: list =[ { name:"name1", value:true } { name:"name2", value:false } { name:"name3", value:true } { name:"name4", value:false ...

Tips for changing the state of a toggle button with JavaScript

I need help creating a toggle button. To see the code I'm working on, click here. In my JavaScript code, I am reading the value of a 'checkbox'. If the value is true, I add another div with a close button. When the close button is clicked, ...

Panoramic viewer for Three.js with multi-resolution image support

Is there a way to incorporate a three.js panorama viewer with multi-resolution images similar to pannellum? Check out pannellum's example here: . You can find the current code using three.js here:(//codepen.io/w3core/pen/vEVWML). Starting from an e ...

Looking to receive child events within a Vue 3 setup()?

Looking to convert the code below to use Vue 3 composition API. I am trying to listen for an event from a child component in a parent component that utilizes the render function. In Vue 3, $on has been removed and I'm unsure of how to replace it in t ...

Problem related to permissions within a node.js package

Introducing my newly built npm module called emeraldfw, now available for public use. Here is a glimpse of the contents in my package.json file: { "name": "emeraldfw", "version": "0.6.0", "bin": "./emeraldfw.js", "description": "Emerald Framework ...

Unlocking the power of setting global variables and functions in JavaScript

Within my language.js file, the following functions are defined: function setCookie(cookie) { var Days = 30; //this cookie will expire in 30 days var exp = new Date(); exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000); document.cookie = coo ...