Using three.js Routines to Compute Aircraft Rotation

I previously created a helper file that contains subroutines that you can use to correctly rotate an aircraft object. Now that I am somewhat familiar with how three.js rotates objects, I wonder if I use three.js to perform these calculations?

I have created a demo using 2 objects, airobj (the aircraft object) and airaxe (an axis used to position airobj) and . Both use a rotation order of “YXZ”. The airobj is linked to airaxe.

I position airaxe using the following:

airaxe.rotation.z = Mod360(360 - ACBank) * DegRad;	// Bank
airaxe.rotation.x = ACPtch * DegRad;			// Pitch
airaxe.rotation.y = -ACHead * DegRad;			// Heading

where Mod360 is my subroutine that limits values to degrees and DegRad converts degrees to radians.

The airaxe object acts as a “base” from which to rotate airobj. Starting from this initial rotation of airaxe, I can Bank, Pitch or Yaw the airobj and it will behave perfectly. However, once I have done one of these actions, the others will no longer work correctly. So what I need is something that will update the rotation of airaxe.

Since you can get the world rotation of airobj, is there a simply way to transfer that rotation to airaxe?

I have looked online and it appears that getWorldRotation has been replaced by getWorldQuaternion. And it appears that you might have been able to use something as simple as airobj.getWorldQuaternion(airaxe) to transfer rotation from one object to another. (But this particular command generates an error - “e.setFromRtationMatrix is not a function”) After transferring the values to airaxe, I would set the airobj rotations to zero.

Does this approach make sense?
If so, what commands should I use?
Or is there an even simpler method that I am completely overlooking?

SOLVED?

Through trial and error, I think I have found a solution as shown in this demo.

I added the following variable to the beginning of the program:

var quaternion = new THREE.Quaternion();

Here is the subroutine for rotating the aircraft:

function rotePlane() {
	// Rotate airobj (child) by changes
	airobj.rotation.z = -ACBDif*DegRad;	// Change in Bank
	airobj.rotation.x = PPPDif*DegRad;	// Change in Pitch
	airobj.rotation.y = -YawDif*DegRad;	// Change in Yaw
	// Get world quaternion for child and save to parent 
	airobj.getWorldQuaternion(quaternion);
	airaxe.setRotationFromQuaternion(quaternion);
	// Zero out airobj rotations (so don't double up)
	airobj.rotation.z = 0;
	airobj.rotation.x = 0;
	airobj.rotation.y = 0;
	// Load new values (for display or use in program)
	ACBank = Mod360(-airaxe.rotation.z*RadDeg);
	ACPtch = airaxe.rotation.x*RadDeg;
	ACHead = Mod360(-airaxe.rotation.y*RadDeg);
}

Is this the best way to achieve this result?

This definitely simplifies my task. I can forget about all those Napier formulae and trying to make them work in 360 degree rotation error-free. (Computing yaw was giving me special problems.)

I worried that using the three.js routines might add to the computing time. However, the three.js routines would be called whenever I rotate an object. Even using two objects, I am probably saving time by eliminating the need to use my computations.

If this is okay, I will revise my rotation and flight simulation demos.

I have never looked closely at motor vehicle or aircraft rotation. But when I created the visualization Quaternion - Axis, Angle Visualization a few years ago, I read a little about it.

This year I wanted to move a model on an arbitrary 3D curve and took another look at quaternions.

The result was a function to derive the quaternion directly from a base e1, e2, e3 in the simplest form.

Possibly one can use this?

Quaternion - method .setFromBasis( e1, e2, e3 )

Example BasisToQuaternion uses three.module.129.Quaternion.js
Car Racing - For lovers of fast cars!



THREE.Quaternion.prototype.setFromBasis = function( e1, e2, e3 ) {
	
	const	m11 = e1.x, m12 = e1.y, m13 = e1.z,
			m21 = e2.x, m22 = e2.y, m23 = e2.z,
			m31 = e3.x, m32 = e3.y, m33 = e3.z,
			trace = m11 + m22 + m33;

	if ( trace > 0 ) {

		const s = 0.5 / Math.sqrt( trace + 1.0 );

		this._w = 0.25 / s;
		this._x = -( m32 - m23 ) * s;
		this._y = -( m13 - m31 ) * s;
		this._z = -( m21 - m12 ) * s;

	} else if ( m11 > m22 && m11 > m33 ) {

		const s = 2.0 * Math.sqrt( 1.0 + m11 - m22 - m33 );

		this._w = ( m32 - m23 ) / s;
		this._x = -0.25 * s;
		this._y = -( m12 + m21 ) / s;
		this._z = -( m13 + m31 ) / s;

	} else if ( m22 > m33 ) {

		const s = 2.0 * Math.sqrt( 1.0 + m22 - m11 - m33 );

		this._w = ( m13 - m31 ) / s;
		this._x = -( m12 + m21 ) / s;
		this._y = -0.25 * s;
		this._z = -( m23 + m32 ) / s;

	} else {

		const s = 2.0 * Math.sqrt( 1.0 + m33 - m11 - m22 );

		this._w = ( m21 - m12 ) / s;
		this._x = -( m13 + m31 ) / s;
		this._y = -( m23 + m32 ) / s;
		this._z = -0.25 * s;

	}
	
	this._onChangeCallback();

	return this;
 
}
1 Like

Thanks for bringing your work to my attention.

I am transferring quaternions because that seems to be the preferred basis of measurement in three.js an it seems to work fine… Otherwise, I have been making do with Euler angles, which correspond directly to pitch, bank and yaw.

However, I appreciate that you have advanced to using quaternions and that you have laid out a path for others, like myself, to follow. I always enjoy expanding my knowledge because I am always trying to figure out how to do things better.

If you are interested, I tested my new simplified approach in my aircraft flight simulation and it seems to work fine.

On your car racing demo, did you use the terrain to define your rotation? Or did you map out the rotation and draw the terrain to fit?

Quaternions are a way to avoid gimbal lock and a long known and effective mathematical method. (Gimbal lock - Wikipedia , Quaternion - Wikipedia)

I had just wondered that unlike other methods like .setFromAxisAngle and .setFromEuler, the simple calculation from a base is not implemented in THREE. I couldn’t find it anywhere either, so I concocted it myself.

In car racing it is not a terrain. As you can see in the short source code, it is a self-defined BufferGeometry based on a CatmullRomCurve3 ( made of points const curvePoints = [ -6, 0, 10, .. ) For simplicity, the road is not lateral sloped even in curves, as it would make sense on a race track. The lanes are specified with const ls = ..; // length segments and const ws = .. ; // width segments and their width with const dw = [ .. ]; // width from the center line
are defined.

With

const t = []; // tangents
const n = []; // normals
const b = []; // binormals

arrays are used to store the vectors for each length segment. This saves the constant calculations at runtime of the race for a fixed curve. With a temporally changed path of an object one would have to accomplish this computation in each case up-to-date or win from other data. With the airplane surely yaw, pitch, roll ?

To prevent lateral slope of the road normal.y = 0; // to prevent lateral slope of the road is used.

The speed of the vehicles depends on the length of each segment. In the original version quite uniform. In the simplified demo very different.

CarRacingQuaternionSimple


<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/car-racing-for-lovers-of-fast-cars/27160 -->
<!-- https://discourse.threejs.org/t/using-three-js-routines-to-compute-aircraft-rotation/32707/3 -->
<head>
    <title> CarRacingQuaternionSimple </title>
    <meta charset="utf-8" />
    <style>    body { margin: 0;} </style>
</head>
<body> uses THREE.Quaternion.prototype.setFromBasis    </body>

<script type="module">

// @author hofk

import * as THREE from "../jsm/three.module.135.js";
import { OrbitControls } from "../jsm/OrbitControls.135.js";
import { GLTFLoader } from "../jsm/GLTFLoader.135.js";

THREE.Quaternion.prototype.setFromBasis = function( e1, e2, e3 ) {
    
    const    m11 = e1.x, m12 = e1.y, m13 = e1.z,
            m21 = e2.x, m22 = e2.y, m23 = e2.z,
            m31 = e3.x, m32 = e3.y, m33 = e3.z,
            trace = m11 + m22 + m33;

    if ( trace > 0 ) {

        const s = 0.5 / Math.sqrt( trace + 1.0 );

        this._w = 0.25 / s;
        this._x = -( m32 - m23 ) * s;
        this._y = -( m13 - m31 ) * s;
        this._z = -( m21 - m12 ) * s;

    } else if ( m11 > m22 && m11 > m33 ) {

        const s = 2.0 * Math.sqrt( 1.0 + m11 - m22 - m33 );

        this._w = ( m32 - m23 ) / s;
        this._x = -0.25 * s;
        this._y = -( m12 + m21 ) / s;
        this._z = -( m13 + m31 ) / s;

    } else if ( m22 > m33 ) {

        const s = 2.0 * Math.sqrt( 1.0 + m22 - m11 - m33 );

        this._w = ( m13 - m31 ) / s;
        this._x = -( m12 + m21 ) / s;
        this._y = -0.25 * s;
        this._z = -( m23 + m32 ) / s;

    } else {

        const s = 2.0 * Math.sqrt( 1.0 + m33 - m11 - m22 );

        this._w = ( m21 - m12 ) / s;
        this._x = -( m13 + m31 ) / s;
        this._y = -( m23 + m32 ) / s;
        this._z = -0.25 * s;

    }
    
    this._onChangeCallback();

    return this;
 
}

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 65, window.innerWidth / window.innerHeight, 0.01, 200 );
camera.position.set( 0, 8, 16 );
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setClearColor( 0x0fbd25, 1 );    
const container = document.createElement( 'div' );
document.body.appendChild( container );
container.appendChild( renderer.domElement );

new OrbitControls( camera, renderer.domElement );

const light = new THREE.AmbientLight( 0xffffff ); 
scene.add( light );

const gridHelper = new THREE.GridHelper( 30, 30 );
scene.add( gridHelper );

const curvePoints =  [
 -6, 0, 10,
 -1, 0, 10, 
 13, 0,  6,
  5, 2,  0,
  0, 2,  2, 
 -7, 2, -5,
-11, 0, 10,
 -6, 0, 10
];

const pts = [];
    
for ( let i = 0; i < curvePoints.length; i += 3 ) {
    
    pts.push( new THREE.Vector3( curvePoints[ i ], curvePoints[ i + 1 ], curvePoints[ i + 2 ] ) );
    
}

const ls = 300; // length segments
const ws = 1; // width segments 
const lss = ls + 1;
const wss = ws + 1;

const curve = new THREE.CatmullRomCurve3( pts );
const points = curve.getPoints( ls );
const len = curve.getLength( );
const lenList = curve.getLengths ( ls );

const faceCount = ls * ws * 2;
const vertexCount = lss * wss;

const indices = new Uint32Array( faceCount * 3 );
const vertices = new Float32Array( vertexCount * 3 );
const uvs = new Float32Array( vertexCount * 2 );

const g = new THREE.BufferGeometry( );
g.setIndex( new THREE.BufferAttribute( indices, 1 ) );    
g.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );

let idxCount = 0;
let a, b1, c1, c2;

// define indices

for ( let j = 0; j < ls; j ++ ) {
    
    for ( let i = 0; i < ws; i ++ ) {
        
        // 2 faces / segment,  3 vertex indices
        a =  wss * j + i;
        b1 = wss * ( j + 1 ) + i;        // right-bottom
        c1 = wss * ( j + 1 ) + 1 + i;
    //  b2 = c1                            // left-top
        c2 = wss * j + 1 + i;
        
        indices[ idxCount     ] = a; // right-bottom
        indices[ idxCount + 1 ] = b1;
        indices[ idxCount + 2 ] = c1; 
        
        indices[ idxCount + 3 ] = a; // left-top
        indices[ idxCount + 4 ] = c1 // = b2,
        indices[ idxCount + 5 ] = c2; 
       
        idxCount += 6;
        
    }
        
}

let x, y, z;
let posIdx = 0; // position index

let tangent;
const normal = new THREE.Vector3( );
const binormal = new THREE.Vector3( 0, 1, 0 );

              // calculate  ...
const t = []; // tangents
const n = []; // normals
const b = []; // binormals
                // ... for every segment

for ( let j = 0; j < lss; j ++ ) {

    // to the points
    
    tangent = curve.getTangent(  j / ls );
    t.push( tangent.clone( ) );
    
    normal.crossVectors( tangent, binormal );
    
    normal.y = 0; // to prevent lateral slope of the road
    
    normal.normalize( );
    n.push( normal.clone( ) );
    
    binormal.crossVectors( normal, tangent ); // new binormal
    b.push( binormal.clone( ) );    
    
}

const dw = [  -0.12, 0.12  ]; // width from the center line

// create lanes

for ( let j = 0; j < lss; j ++ ) {  // length
        
    for ( let i = 0; i < wss; i ++ ) { // width
     
        x = points[ j ].x + dw[ i ] * n[ j ].x;
        y = points[ j ].y;
        z = points[ j ].z + dw[ i ] * n[ j ].z;         
        
        vertices[ posIdx ] = x;
        vertices[ posIdx + 1 ] = y;
        vertices[ posIdx + 2 ] = z;
        
        posIdx += 3;
        
    }
    
}

const material = new THREE.MeshBasicMaterial( { color: 0xffffff, side: THREE.DoubleSide, wireframe: true } ) ;

const roadMesh = new THREE.Mesh( g, material );
scene.add( roadMesh );

const loader = new GLTFLoader( );

const blueCar = new THREE.Object3D( );
loader.load( 'car/car_02.gltf', processBlueCar ); // (CC-BY) Poly by Googl

let iBlue = 0;

animate( );

//............................

function animate( ) {
    
    requestAnimationFrame( animate );
    
    driving( );
    
    renderer.render( scene, camera );
        
}

function driving( ) {
 
    if ( iBlue === lss ) {
        
        iBlue = 0; // loop
 
    }
    
    blueCar.quaternion.setFromBasis( t[ iBlue ], b[ iBlue ], n[ iBlue ] ); 
    blueCar.position.set( points[ iBlue ].x , points[ iBlue ].y, points[ iBlue ].z );
    iBlue ++;
  
}

function processBlueCar( gltf ) {
    
    gltf.scene.rotation.y = Math.PI;  // gltf.scene is centered, rotation needed
    blueCar.add( gltf.scene );
    blueCar.scale.set( 0.0015, 0.0015, 0.0015 ); // because gltf.scene is very big
    scene.add( blueCar );
    
}

</script>
</html>

This variant is more suitable for an aircraft.
2021-12-17 18.29.46

FlightRouteQuaternion




<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/car-racing-for-lovers-of-fast-cars/27160 -->
<!-- https://discourse.threejs.org/t/using-three-js-routines-to-compute-aircraft-rotation/32707/5 -->
<head>
    <title> FlightRouteQuaternion </title>
    <meta charset="utf-8" />
    <style>    body { margin: 0;} </style>
</head>
<body> uses THREE.Quaternion.prototype.setFromBasis    </body>

<script type="module">

// @author hofk

import * as THREE from "../jsm/three.module.135.js";
import { OrbitControls } from "../jsm/OrbitControls.135.js";
import { GLTFLoader } from "../jsm/GLTFLoader.135.js";

THREE.Quaternion.prototype.setFromBasis = function( e1, e2, e3 ) {
    
    const    m11 = e1.x, m12 = e1.y, m13 = e1.z,
            m21 = e2.x, m22 = e2.y, m23 = e2.z,
            m31 = e3.x, m32 = e3.y, m33 = e3.z,
            trace = m11 + m22 + m33;

    if ( trace > 0 ) {

        const s = 0.5 / Math.sqrt( trace + 1.0 );

        this._w = 0.25 / s;
        this._x = -( m32 - m23 ) * s;
        this._y = -( m13 - m31 ) * s;
        this._z = -( m21 - m12 ) * s;

    } else if ( m11 > m22 && m11 > m33 ) {

        const s = 2.0 * Math.sqrt( 1.0 + m11 - m22 - m33 );

        this._w = ( m32 - m23 ) / s;
        this._x = -0.25 * s;
        this._y = -( m12 + m21 ) / s;
        this._z = -( m13 + m31 ) / s;

    } else if ( m22 > m33 ) {

        const s = 2.0 * Math.sqrt( 1.0 + m22 - m11 - m33 );

        this._w = ( m13 - m31 ) / s;
        this._x = -( m12 + m21 ) / s;
        this._y = -0.25 * s;
        this._z = -( m23 + m32 ) / s;

    } else {

        const s = 2.0 * Math.sqrt( 1.0 + m33 - m11 - m22 );

        this._w = ( m21 - m12 ) / s;
        this._x = -( m13 + m31 ) / s;
        this._y = -( m23 + m32 ) / s;
        this._z = -0.25 * s;

    }
    
    this._onChangeCallback();

    return this;
 
}

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 65, window.innerWidth / window.innerHeight, 0.01, 200 );
camera.position.set( 0, 10, 34 );
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setClearColor( 0x1144dd, 1 );    
const container = document.createElement( 'div' );
document.body.appendChild( container );
container.appendChild( renderer.domElement );

new OrbitControls( camera, renderer.domElement );

const light = new THREE.AmbientLight( 0xffffff, 1.5 ); 
scene.add( light );

const gridHelper = new THREE.GridHelper( 30, 10 );
scene.add( gridHelper );

const curvePoints =  [
 -6,  1, 10,
 -1,  1, 10,
  3,  2,  4,
  6, 15,  4,
  6, 62,  4,
 15, 15,-15,
 15,  9,-16,
 17,  6,-16,
 10,  9,  7,
  2,  9,  8,
 -4,  8,  7,
 -8,  7,  1,
 -9,  7, -4,
 -6,  6, -9,
  0,  5,-10,
  7,  5, -7,
  7,  5,  0,
  0,  5,  2,
 -5,  4,  2,
 -7,  4, -5,
 -8,  3, -9,
-12,  3, -10,
-15,  2, -7,
-15,  2, -2,
-14,  1,  3,
-11,  1, 10,
 -6,  1, 10
];

const pts = [];
    
for ( let i = 0; i < curvePoints.length; i += 3 ) {
    
    pts.push( new THREE.Vector3( curvePoints[ i ], curvePoints[ i + 1 ], curvePoints[ i + 2 ] ) );
    
}

const ls = 1500; // length segments
const lss = ls + 1;

const curve = new THREE.CatmullRomCurve3( pts );
const points = curve.getPoints( ls );
const len = curve.getLength( );
const lenList = curve.getLengths ( ls );

const line = new THREE.LineLoop( new THREE.BufferGeometry( ).setFromPoints( points ), new THREE.LineBasicMaterial( { color: 0x3366ff } ) );
scene.add( line );

let tangent;
const normal = new THREE.Vector3( );
const binormal = new THREE.Vector3( 0, 1, 0 );

              // calculate  ...
const t = []; // tangents
const n = []; // normals
const b = []; // binormals
                // ... for every segment

for ( let j = 0; j < lss; j ++ ) {

    // to the points
    
    tangent = curve.getTangent(  j / ls );
    t.push( tangent.clone( ) );
    
    normal.crossVectors( tangent, binormal );    
	normal.y = 0; // to prevent lateral slope 	
	normal.normalize( );
    n.push( normal.clone( ) );
    
    binormal.crossVectors( normal, tangent ); // new binormal
    b.push( binormal.clone( ) );    
    
}

const loader = new GLTFLoader( );

const shuttle = new THREE.Object3D( );
loader.load( 'Space Shuttle/SpaceShuttle(1).gltf', processShuttle ); // (CC-BY) Poly by Googl

let iShuttle = 0;

animate( );

//............................

function animate( ) {
    
    requestAnimationFrame( animate );
    
    driving( );
    
    renderer.render( scene, camera );
        
}

function driving( ) {
 
    if ( iShuttle === lss ) {
        
        iShuttle = 0; // loop
 
    }
    
    shuttle.quaternion.setFromBasis( t[ iShuttle ], b[ iShuttle ], n[ iShuttle ] ); 
    shuttle.position.set( points[ iShuttle ].x , points[ iShuttle ].y, points[ iShuttle ].z );
    iShuttle ++;
  
}

function processShuttle( gltf ) {
    
    gltf.scene.rotation.x = -Math.PI / 2;  // gltf.scene is centered, rotation needed
    gltf.scene.rotation.z = -Math.PI / 2; 
    shuttle.add( gltf.scene );
    shuttle.scale.set( 0.05, 0.05, 0.05 ); // because gltf.scene is big
    scene.add( shuttle );
    
}

</script>
</html>

Thanks.

I have read about gimbal lock, but have never run into a situation where it applied to my simulation. The main reason is probably that I have been using 2 different methods of orienting the aircraft. With respect to controlling aircraft pitch, I used what I called “pitch plane pitch” (PPP), where the pitch acted along a 360 degree plane that is vertical to the aircraft and banks with the aircraft. If I had used aircraft pitch (ACP) (the angle the aircraft is pitched up relative to the horizon), I might have run into problems because - as the aircraft approaches vertical - the effect of using the controls to change PPP has less and less of an effect on ACP. But because I used PPP as my primary, my vertical control over the aircraft never diminished. It appears that the new method involving linked objects allows aircraft pitch to act in the same manner.

Now that I better understand what you are doing, I see that it might be helpful in creating paths for other aircraft which I plan to add to the scene - including both friend and foe. So thank you very much for that.

Since you are using a different control of the aircraft than the tangent of the trajectory, it is certainly not relevant to you, but may be to other readers of these posts.

I had deleted the lines


normal.y = 0; // to prevent lateral slope 	
	normal.normalize( );

since a lateral tilt would be normal for a flying machine.

That looked good, too.

However, when I added the steep flight and the takeoff abort by changing the points, there was a problem.

Due to the strong turning and tight arcs, there are problems. The shuttle lays down on its back. At the target takeoff point, there is a jerky 180° reversal.
If you want to avoid this, you have to include an additional position control here as well.

Actually, in my flight simulation, I use the coordinate system to show direction of flight, rather than the direction of the aircraft. The reasons are somewhat complex, but explained on my webpage.

So I am perfectly happy with a system, such as yours, that generates a flight path. With regard to the aircraft orientation, my flight simulation determines the orientation of the aircraft by taking into account the flight path.

For example, with regard to level turning flight, a certain bank angle will generate a certain turn rate. The formula is G * Tan(Bank Angle)) / V. As this indicates, the best turn rate can be obtained at the lowest speed. However, not every aircraft - such as the shuttle - is made for slow speed flight. Your maximum rate of turn is at 90 degrees bank. However, at that bank, the aircraft is not generating vertical lift and will descend at an acceleration of G. If you increase power while in a level turn, the aircraft will begin to climb. So these are the kind of factors you could include in a flight model.

And I can see that a flight path that you might want the aircraft to fly could be impossible. For example, an aircraft cannot turn beyond a certain rate at a certain speed. Not can an aircraft make a 90 degree banked turn without losing altitude.