Carpetfizz
Carpetfizz

Reputation: 9149

THREE.js ArrowHelper not obeying intrinsic Euler angle rotation

I would like to set intrinsic rotations to a THREE.ArrowHelper. From my understanding, THREE.js natively uses intrinsic Tait-Bryan euler angles to represent 3D rotations.

In the code below, I create a unit vector representing the x-axis, THREE.Vector3(1, 0, 0).

I then rotate it about the Y and Z axis by an arbitrary amount.

Since there was some rotation about Y and Z, the X axis of the local coordinate system (which I assume points along the red vector) has also changed.

So, when I apply a rotation about X, I don't expect the arrow to move at all (except rotate in place...but that shouldn't be visible).

Instead, I see the arrow sweeping around, as if it's rotating about some arbitrary axis, and not its local x axis.

Thanks for any assistance!

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );

var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

var origin = new THREE.Vector3(0, 0, 0);
var xDir = new THREE.Vector3(1, 0, 0);
var length = 1;
var arrow = new THREE.ArrowHelper(xDir, origin, length, 0xff0000);
arrow.rotation.order = 'XYZ';
arrow.rotation.y = 0.5;
arrow.rotation.z = 0.5;
scene.add(arrow);

camera.position.z = 5;

var animate = function () {
  requestAnimationFrame( animate );

	arrow.rotation.x += 0.01;
  
  renderer.render( scene, camera );
};

animate();
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script>

Upvotes: 0

Views: 511

Answers (2)

user128511
user128511

Reputation:

The problem is that ArrowHelper is doing its own special rotation math. It creates a unit arrow facing up (+Y). It the uses custom math to setup the orientation to make that line point in the given direction in local space.

You can see this if you just create the arrow and then print the rotation

  var origin = new THREE.Vector3(0, 0, 0);
  var xDir = new THREE.Vector3(1, 0, 0);
  var length = 1;
  var arrow = new THREE.ArrowHelper(xDir, origin, length, color);
  console.log(arrow.rotation.x, arrow.rotation.y, arrow.rotation.z);

You'll see rotation.z is already set to rotate the +Y arrow to face +X so then you go and change those rotations and the arrow is no longer based facing +X.

That means manipulating the arrow via arrow.rotation won't work as expected.

If you parent the arrow to an Object3D and then rotate that object it will work as expected (or as I expect it 😅)

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );

var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

scene.add(new THREE.GridHelper(10, 10));

function addArrow(x, ry, rz, color) {
  var origin = new THREE.Vector3(0, 0, 0);
  var xDir = new THREE.Vector3(1, 0, 0);
  var length = 1;
  var arrow = new THREE.ArrowHelper(xDir, origin, length, color);

  var ob = new THREE.Object3D();
  ob.position.x = x;
  ob.rotation.order = 'XYZ';
  ob.rotation.y = ry;
  ob.rotation.z = rz;
  scene.add(ob);  
  ob.add(arrow);
  return ob;
}

addArrow(-4,   0,   0, 0xFF0000);
addArrow(-2,   0, 0.5, 0x00FF00);
addArrow( 0, 0.5, 0.5, 0xFFFF00);
const arrow = addArrow( 2, 0.5, 0.5, 0x00FFFF);

camera.position.z = 6;

const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.update();

var animate = function () {
  requestAnimationFrame( animate );
  	arrow.rotation.x += 0.01;

  renderer.render( scene, camera );
};
animate();
body { margin: 0; }
canvas { display: block; }
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>

What I expect.

Rotations are in the local coodinate system. Rotation order 'XYZ' means, assuming you only had the arrow the full matrix calculation would be

matrix = projection * 
         view *
         obTranslation *
         obXAxisRotation * 
         obYAxisRotation * 
         obZAxisRotation * 
         obScale * 
         arrowOrientation;

In any case

var origin = new THREE.Vector3(0, 0, 0);
var xDir = new THREE.Vector3(1, 0, 0);
var length = 1;
var arrow = new THREE.ArrowHelper(xDir, origin, length, 0xff0000);
var ob = new THREE.Object3D();
scene.add(ob);
ob.add(arrow);
ob.rotation.order = 'XYZ';

Viewed from 0, 0, 5 this gives us an arrow pointing right.

I like to look at matrices as applied from the right to the left so looking at the formula above first scale will be applied. It's 1,1,1 so no change

Next zAxisRotation is applied. 0.5 radians is about 30 degrees so the arrow is now either pointing 30 degrees up

Next yAxisRotation is applied. The 30 degree up arrow is now pointing 30 degrees back as into the distance.

Next xAxisRotation is applyed so this funkily pointed arrow will spin around x

run it and drag on the sample above to look from above. You'll see it matches the description.

So it's up to you then. You can make a +X facing ArrowHelper and parent it to an Object3D or you can just know that an ArrowHelper actually makes a +Y arrow and then set the rotations appropriately knowing that.

Upvotes: 1

Scaramouche
Scaramouche

Reputation: 3257

To be honest, I've never used THREE.js, but I'll try to visualize my point with this answer. The imaginary or world axis is represented by the gray arrow, notice how when you toggle Y and Z axes values, it affects the red arrow but not the other gray arrow, that's what I meant by the imaginary X axis is not moving.

Your red arrow is still rotating around the X axis but not its X axis, but the world's which is why when you change the arrow's Y and Z axes it looks like it's sweeping around when in actuality it just continues to rotate around the same fixed axis it was rotating around since the beginning.

Well, I really expect I didn't make more a mess than an explanation.

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );

var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

var origin = new THREE.Vector3(0, 0, 0);
var xDir = new THREE.Vector3(1, 0, 0);
var length = 1;
var arrow = new THREE.ArrowHelper(xDir, origin, length, 0xff0000);
var imaginaryXAxis = new THREE.ArrowHelper(xDir, origin, length+100, 0xffffff);
arrow.rotation.order = 'XYZ';
/*arrow.rotation.y = 0.5;
arrow.rotation.z = 0.5;*/
scene.add(arrow);
scene.add(imaginaryXAxis);

camera.position.z = 2;

var animate = function () {
  requestAnimationFrame( animate );
  	arrow.rotation.x += 0.01;

  renderer.render( scene, camera );
};
animate();

const yValue = arrow.rotation.y, zValue = arrow.rotation.z;

document.querySelector('button').addEventListener('click', (e) => {
    e.target.classList.toggle('affected')
    if(e.target.classList.contains('affected')){
      arrow.rotation.y=.5;
      arrow.rotation.z=.5;
      e.target.textContent = "Reset Y and Z to zero";
    } else {
      arrow.rotation.y=yValue;
      arrow.rotation.z=zValue;
      e.target.textContent = "Affect Y and Z";
    }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script>

<button>Affect Y and Z</button>

Upvotes: 1

Related Questions