Quaternion - Axis, Angle Visualization

I’ve been using quaternions. Because rotation in space is not easy, I was looking for a simple visualization.

I found a lot of theory about quaternions and a workable converter.
http://www.energid.com/resources/orientation-calculator/

But no visualization for my own requirements. So I created it myself.

Quaternion and axis, angle are continuously converted into each other.

quaternion

If you want to try the little helper, you will find him there: http://threejs.hofk.de/
or directly there http://threejs.hofk.de/quaternion/quaternion_axisangle.html

Choose
change quaternion
or
change axis… angle
and an input method.

If the values in the input element are not normalized, you switch to the range element and it is normalized.

I have adapted the look to Firefox.

Here the code:

<!DOCTYPE html>
<!-- ........ QUATERNION - AXIX/ANGLE ........
/**
 * @author hofk / http://threejs.hofk.de/
*/
-->
<html lang="de">
<head>
	
	<meta charset="utf-8" />
	<title> quaternion </title>
	
		<!-- designed for Firefox -->
	<style>
		input[type="number"] {width: 180px}
		input[type="range"] {width: 160px; }
	</style>
	
	<script src="three.min.88.js"></script>
	<script src="OrbitControls.js"></script>
	
</head>

<body> 
	
	<div >  QUATERNION - AXIX/ANGLE <br />
	>>> <input type="radio" name="choose" id="quaternion" checked="checked">
	change quaternion:	4D vector (input also not normalized)
		<input type="button" id="reset"  value="reset" onclick="resetQuaternion()">
		 * if vector(0,0,0,w) - change first 0 <br />
		<input type="radio" name="inputQ" id="rangeQ" checked="checked"> 
	vector (
	xq	<input type="range" id="xqr" min="-1" max="1" value="0" step="0.001"> ,
	yq	<input type="range" id="yqr" min="-1" max="1" value="0" step="0.001"> ,
	zq	<input type="range" id="zqr" min="-1" max="1" value="0" step="0.001"> ,
	wq	<input type="range" id="wqr" min="-1" max="1" value="1" step="0.001">  )<br />
	<input type="radio" name="inputQ" id="numberQ">
	vector (
	xq	<input type="number" id="xqn" min="-1" max="1" value="0" step="0.001"> ,
	yq	<input type="number" id="yqn" min="-1" max="1" value="0" step="0.001"> ,
	zq	<input type="number" id="zqn" min="-1" max="1" value="0" step="0.001"> , 
	wq	<input type="number" id="wqn" min="-1" max="1" value="1" step="0.001"> ) <br />
	
	</div>
	
	<div id="outputQ">   </div> 
	
	<div id="webG">   </div> 
	
	<div id="outputA">   </div> 
	
	<div >
	<input type="radio" name="inputA" id="numberA">
	axis (
	xa	<input type="number" id="xan" min="-1" max="1" value="0" step="0.001"> ,
	ya	<input type="number" id="yan" min="-1" max="1" value="1" step="0.001"> ,
	za	<input type="number" id="zan" min="-1" max="1" value="0" step="0.001">  ) ... angle
		<input type="number" id="wan" min="-1" max="1" value="0" step="0.001"> * PI = 
		<span id="rad" >  </span> rad  = <span id="deg" >  </span> deg  <br />
	
	<input type="radio" name="inputA" id="rangeA" checked="checked"> 
	axis (
	xa	<input type="range" id="xar" min="-1" max="1" value="0" step="0.001"> ,
	ya	<input type="range" id="yar" min="-1" max="1" value="1" step="0.001"> ,
	za	<input type="range" id="zar" min="-1" max="1" value="0" step="0.001">  ) ... angle
		<input type="range" id="war" min="-1" max="1" value="0" step="0.001"> 
	<br />
	>>> <input type="radio" name="choose" id="axisangle" > change axis 3D vector (input also not normalized) and angle
	<input type="button" id="reset"  value="reset" onclick="resetAxisangle()">
	</div>
	
</body>

<script> 

'use strict' 

document.body.style.backgroundColor = '#cccccc';
var scene  = new THREE.Scene();
var width  = 0.75 * window.innerWidth;
var height = 0.75 * window.innerHeight;
var camera = new THREE.PerspectiveCamera( 75, width / height , 0.1, 2000 );
camera.position.set( 2, 2, 6 );
var renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( width, height );
renderer.setClearColor( 0x000000, 1 );
var container = document.getElementById( 'webG' );
//document.body.appendChild( container );
container.appendChild( renderer.domElement );
var controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.enableZoom = true;

var light1 = new THREE.PointLight( 0xffffff, 1, 0 );
light1.position.set( -1, 5, -7 );
scene.add( light1 ); 
var light2 = new THREE.PointLight( 0xffffff, 1, 0 );
light2.position.set( 1, -1, 4 );
scene.add( light2 ); 

//var clock  = new THREE.Clock( true );
//var time;

var sphereBodyGeometry, sphereHeadGeometry, sphereNoseGeometry, sphereEyeGeometry;
var sphereBody, sphereHead, sphereNose, sphereEyeP, sphereEyeN;

var cylinderLimbGeometry;
var cylinderArmP, cylinderArmN;

var xv, yv, zv, wv;

var amount3, amount4, s;

var side =  THREE.DoubleSide;

var materials = [
																						// material index:
	new THREE.MeshBasicMaterial( { transparent: true, opacity: 0.1,	side: side } ),	//  0 transparent	
	new THREE.MeshPhongMaterial( { color: 0xff0000, emissive: 0xff0000, transparent: true, opacity: 0.8, side: side } ),	//  1 red
	new THREE.MeshPhongMaterial( { color: 0x00ff00, emissive: 0x00ff00, transparent: true, opacity: 0.8, side: side } ),	//  2 green
	new THREE.MeshPhongMaterial( { color: 0x0000ff, emissive: 0x0000ff, transparent: true, opacity: 0.8, side: side } ),	//  3 blue
	new THREE.MeshPhongMaterial( { color: 0xffff00, emissive: 0xffff00, transparent: true, opacity: 0.8, side: side } ),	//  4 yellow
	new THREE.MeshPhongMaterial( { color: 0xff00ff, emissive: 0xff00ff, transparent: true, opacity: 0.8, side: side } ),	//  5 mgenta
	new THREE.MeshPhongMaterial( { color: 0x00ffff, emissive: 0x00ffff, transparent: true, opacity: 0.8, side: side } ),	//  6 cyan	

];

var segments = 16;

sphereBodyGeometry = new THREE.SphereGeometry( 0.8, segments, segments );

for ( var k = 0; k <  segments * segments - segments; k ++ ) {
	
	sphereBodyGeometry.faces[ 2 * k ].materialIndex = k % 6 + 1;
	sphereBodyGeometry.faces[ 2 * k + 1 ].materialIndex = k % 6 + 1;
	
}

sphereBody = new THREE.Mesh( sphereBodyGeometry, materials );
scene.add( sphereBody );

sphereHeadGeometry = sphereBodyGeometry.clone();
sphereHeadGeometry.scale( 0.5, 0.5, 0.5 );
sphereHead = new THREE.Mesh( sphereHeadGeometry, materials );
sphereHead.position.y = 1.2;
sphereBody.add( sphereHead );

sphereNoseGeometry = sphereBodyGeometry.clone();
sphereNoseGeometry.scale( 0.15, 0.15, 0.15 );
sphereNose = new THREE.Mesh( sphereNoseGeometry, materials[ 3 ] );
sphereNose.position.z = 0.45;
sphereHead.add( sphereNose );

sphereEyeGeometry = sphereBodyGeometry.clone();
sphereEyeGeometry.scale( 0.1, 0.1, 0.1 );
sphereEyeP = new THREE.Mesh( sphereEyeGeometry, materials[ 4 ] );
sphereEyeP.position.set ( 0.15, 0.15, 0.3);
sphereHead.add( sphereEyeP );
sphereEyeN = new THREE.Mesh( sphereEyeGeometry, materials[ 4 ] );
sphereEyeN.position.set ( -0.15, 0.15, 0.3);
sphereHead.add( sphereEyeN );

cylinderLimbGeometry = new THREE.CylinderGeometry(0.1, 0.1, 0.7, segments, 1, false);

cylinderArmP = new THREE.Mesh( cylinderLimbGeometry , materials[ 1 ] );
cylinderArmP.rotation.z = 1.57;
cylinderArmP.position.x = 0.65;
cylinderArmP.position.y = 0.65;
sphereBody.add( cylinderArmP );

cylinderArmN = new THREE.Mesh( cylinderLimbGeometry , materials[ 5 ] );
cylinderArmN.rotation.z = 1.57;
cylinderArmN.position.x = -0.65;
cylinderArmN.position.y = 0.65;
sphereBody.add( cylinderArmN );

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

var axesHelper = new THREE.AxesHelper( 100 );
scene.add( axesHelper );

arrow( "x" );
arrow( "y" );
arrow( "z" );

var quaternion = document.getElementById( "quaternion" );
var rangeQ = document.getElementById( "rangeQ" );
var numberQ = document.getElementById( "numberQ" );

var axisangle = document.getElementById( "axisangle" );
var rangeA = document.getElementById( "rangeA" );
var numberA = document.getElementById( "numberA" );

var xqr = document.getElementById( "xqr" );
var yqr = document.getElementById( "yqr" );
var zqr = document.getElementById( "zqr" );
var wqr = document.getElementById( "wqr" );

var xqn = document.getElementById( "xqn" );
var yqn = document.getElementById( "yqn" );
var zqn = document.getElementById( "zqn" );
var wqn = document.getElementById( "wqn" );

var xar = document.getElementById( "xar" );
var yar = document.getElementById( "yar" );
var zar = document.getElementById( "zar" );
var war = document.getElementById( "war" );

var xan = document.getElementById( "xan" );
var yan = document.getElementById( "yan" );
var zan = document.getElementById( "zan" );
var wan = document.getElementById( "wan" );

var rad = document.getElementById( "rad" );
var deg = document.getElementById( "deg" );

var lineGeometry = new THREE.Geometry();
lineGeometry.vertices.push(	new THREE.Vector3( 0,0,0), new THREE.Vector3( 0,0,0) );
var direction= new THREE.Line( lineGeometry, materials[ 4 ] );
scene.add( direction );

sphereBody.applyQuaternion( new THREE.Quaternion( 0, 0, 0, 1 ) );

animate();

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

function arrow( axes ) {
	
	var dir;
	var origin = new THREE.Vector3( 0, 0, 0 );
	var len = 3.5;
	var col = axes === "x" ? 0xff0000 : ( axes === "y" ? 0x00ff00 : 0x0000ff );
	if ( axes === "x" ) dir = new THREE.Vector3( 1, 0, 0 );
	if ( axes === "y" ) dir = new THREE.Vector3( 0, 1, 0 ); 
	if ( axes === "z" ) dir = new THREE.Vector3( 0, 0, 1 );
	var arrowHelper = new THREE.ArrowHelper( dir, origin, len, col);
	sphereBody.add( arrowHelper );
	
}

function resetQuaternion( ) {
	
	if ( quaternion.checked ) {
		
		xqr.value = xqn.value = 0;
		yqr.value = yqn.value = 0;
		zqr.value = zqn.value = 0;
		wqr.value = wqn.value = 1;
		
	}
	
}

function resetAxisangle( ) {
	
	if ( axisangle.checked ) {
		
		xar.value = xan.value = 0;
		yar.value = yan.value = 1;
		zar.value = zan.value = 0;
		war.value = wan.value = 0;
		
	}
	
}

function setAxis( ) {
	
	lineGeometry.vertices[ 0 ].set( -xv * 3, -yv * 3, -zv * 3 );
	lineGeometry.vertices[ 1 ].set( xv * 3, yv * 3, zv * 3 );
	
}

function normalize3D( ) {
	
	amount3 = Math.sqrt( xv * xv + yv * yv + zv * zv );
	
	xv = xv / amount3;
	yv = yv / amount3;
	zv = zv / amount3;
	
}

function normalize4D( ) {

	amount4 = Math.sqrt( xv * xv + yv * yv + zv * zv + wv * wv );
	
	xv = xv / amount4;
	yv = yv / amount4;
	zv = zv / amount4;
	wv = wv / amount4;
		
}

function outputQuaternion( ) {
	
	xqr.value = xqn.value = Math.floor( xv * 1000 ) / 1000;
	yqr.value = yqn.value = Math.floor( yv * 1000 ) / 1000;
	zqr.value = zqn.value = Math.floor( zv * 1000 ) / 1000;
	wqr.value = wqn.value = Math.floor( wv * 1000 ) / 1000;
	
}

function outputAxisAngle( ) {
	
	xar.value = xan.value = Math.floor( xv * 1000 ) / 1000; 
	yar.value = yan.value = Math.floor( yv * 1000 ) / 1000; 
	zar.value = zan.value = Math.floor( zv * 1000 ) / 1000;
	war.value = wan.value = Math.floor( wv * 1000 ) / 1000;
	
	rad.innerHTML =  Math.floor( wv * Math.PI * 1000 ) / 1000;
	deg.innerHTML =  Math.floor( wv * 180 * 1000 ) / 1000;
	
}

function animate( ) {

	requestAnimationFrame( animate ); 
	// time = clock.getElapsedTime( );
	
	lineGeometry.verticesNeedUpdate  = true;
	
	if ( quaternion.checked ) {
		
		if ( rangeQ.checked ) {
			
			xv = xqr.value;
			yv = yqr.value;
			zv = zqr.value;
			wv = wqr.value;
			
			normalize4D( );
			
			xqn.value = Math.floor( xv * 1000 ) / 1000;
			yqn.value = Math.floor( yv * 1000 ) / 1000;
			zqn.value = Math.floor( zv * 1000 ) / 1000;
			wqn.value = Math.floor( wv * 1000 ) / 1000;
			
		}
		
		if ( numberQ.checked ) {
			
			xv = xqn.value;
			yv = yqn.value;
			zv = zqn.value;
			wv = wqn.value;
			
			normalize4D( );
			
			xqr.value = Math.floor( xv * 1000 ) / 1000; 
			yqr.value = Math.floor( yv * 1000 ) / 1000; 
			zqr.value = Math.floor( zv * 1000 ) / 1000; 
			wqr.value = Math.floor( wv * 1000 ) / 1000;
			
		}
		
		sphereBody.quaternion.set( xv, yv, zv, wv );
		
		s =  Math.sqrt( 1 - wv * wv );
		
		if ( s > 0.0001 ) { // prevent divison by zero
			
			xv = xv / s;
			yv = yv / s;
			zv = zv / s;
			
		}
		
		if ( wv > 0 ) {
			
			wv =  2 * Math.acos( wv ) / Math.PI;
			
		} else {
			
			wv = - 2 * ( 1 -  Math.acos( wv ) / Math.PI );
			
		}
		
		setAxis();
		
		outputAxisAngle( );
		
	}
	
	if ( axisangle.checked ) {
		
		if ( rangeA.checked ) {
			
			xv = xar.value;
			yv = yar.value;
			zv = zar.value;
			wv = war.value;
			
			normalize3D( );
			
			xan.value = Math.floor( xv * 1000 ) / 1000;
			yan.value = Math.floor( yv * 1000 ) / 1000;
			zan.value = Math.floor( zv * 1000 ) / 1000;
			wan.value = Math.floor( wv * 1000 ) / 1000;
			
			rad.innerHTML =  Math.floor( wv * Math.PI * 1000 ) / 1000;
			deg.innerHTML =  Math.floor( wv * 180 * 1000 ) / 1000;
			
		}
		
		if ( numberA.checked ) {
			
			xv = xan.value;
			yv = yan.value;
			zv = zan.value;
			wv = wan.value;
			
			normalize3D( );
			
			xar.value = Math.floor( xv * 1000 ) / 1000;
			yar.value = Math.floor( yv * 1000 ) / 1000;
			zar.value = Math.floor( zv * 1000 ) / 1000;
			war.value = Math.floor( wv * 1000 ) / 1000;
			
			rad.innerHTML =  Math.floor( wv * Math.PI * 1000 ) / 1000;
			deg.innerHTML =  Math.floor( wv * 180 * 1000 ) / 1000;
			
		}
		
		wv = Math.PI * wv;
		
		s = Math.sin( wv / 2 );
		
		setAxis();
		
		xv = s * xv / amount3;
		yv = s * yv / amount3;
		zv = s * zv / amount3;
		wv = Math.cos( wv / 2 );
		
		amount4 = Math.sqrt( xv * xv + yv * yv + zv * zv + wv * wv );
		
		xv = xv / amount4;
		yv = yv / amount4;
		zv = zv / amount4;
		wv = wv / amount4;
		
		outputQuaternion( );
		
		sphereBody.quaternion.set( xv, yv, zv, wv );
		
	}
	
	renderer.render( scene, camera );
	controls.update();
	
}

</script>  
</html>
5 Likes