Skip to main content
21-Topaz I
April 6, 2020

How can we make a 3D Widget on HoloLens visible in front of me everytime ?

  • April 6, 2020
  • 13 replies
  • 403889 views

How can we make a 3D Widget on HoloLens visible in front of me every time ? 

The goal here is to get a some UI containing 3D Widgets (3dButtons , 3dImages , 3d Labels and 3d Models) in front of current HoloLens view. For example after saying "show UI" the UI elements should appear in front view e.g. distance of 1,5 meters. Example:

 

2020-04-06_13-37-39.jpg

In the Tech Tip article "How to create a custom button pressable on HoloLens device?" is shown how to create a custom UI button. Now  based on the example of this project   the functionality will be extended by adding the possibility for dynamic display of the UI elements in front of the current view.

To be able to move the elements in front of us we need to calculated the current view matrix and the inverse matrix  of if.  For this goal we need to prepare some mathemtics function like , matrix inverse calculation, marix multiplication , matrix with vector multiplicaiton, vector product und dot vector product ... etc. When we sear in Internet there a lot of sources where we can find the informormation how to implement such mathematical tools.

  • The first steps is to calculate the view matrix - e.g. some code:

 

...
function get_mat4x4_camera(eyepos,eyedir,eyeup)
{
 // printVector(eyepos, "eyepos")
 // printVector(eyedir, "eyedir")
 // printVector(eyeup, "eyeup")
 var mat4x4 =[[1.0,0.0,0.0,0.0],[0.0,1.0,0.0,0.0],[0.0,0.0,1.0,0.0],[0.0,0.0,0.0,1.0]];
 
 var i=0;
 var eyeZaxis 	= this.vec_normilize(eyedir);
 var eyeYaxis 	= this.vec_normilize(eyeup); 
 var eyeXaxis 	= this.vec_normilize(this.vec_product(eyeYaxis,eyeZaxis))
 	 
 for (i=0;i<3;i++) mat4x4[i][0] = eyeXaxis[i]; 
 for (i=0;i<3;i++) mat4x4[i][1] = eyeYaxis[i]; 
 for (i=0;i<3;i++) mat4x4[i][2] = eyeZaxis[i]; 
 for (i=0;i<3;i++) mat4x4[i][3] = eyepos[i]; 
 //for (i=0;i<3;i++) mat4x4[i][3] = 0.0-eyepos[i]; 
 return mat4x4;
 //return this.transpondMat(mat4x4);
}
////////////////////// vector with length =1.0
function vec_normilize(ary1) {
ret=[];
 var len=this.vec_len(ary1);
 for (var i = 0; i < ary1.length; i++)
 ret[i]=ary1[i]/len;
 return ret;
 };
//vector product range =3
function vec_product(a, b) {
 
 var vecprod =[0.0,0.0,0.0];
 vecprod[0]=a[1]*b[2]- a[2]*b[1];
 vecprod[1]=a[2]*b[0]- a[0]*b[2];
 vecprod[2]=a[0]*b[1]- a[1]*b[0];
 
 return vecprod;
};

 

When we find programming code for mathematical operation we need to verify it by some examples where we know the results, because often there are some math sources which does not implement the correct solution.

I think a good resource are e.g.:

https://www.learnopencv.com/rotation-matrix-to-euler-angles/

 https://www.learnopencv.com/

https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles

https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Matrix_math_for_the_web

So that we do not need to re-invent the wheel, but we need to be careful to find the correct mathematical relations and to test and verify if they really work.

  • Next step is to calculate the invert matrix of the view matrix.  A good implementation I found is this one:

 

function matrix_invert(M){
 // I use Guassian Elimination to calculate the inverse:
 // (1) 'augment' the matrix (left) by the identity (on the right)
 // (2) Turn the matrix on the left into the identity by elemetry row ops
 // (3) The matrix on the right is the inverse (was the identity matrix)
 // There are 3 elemtary row ops: (I combine b and c in my code)
 // (a) Swap 2 rows
 // (b) Multiply a row by a scalar
 // (c) Add 2 rows
 
 //if the matrix isn't square: exit (error)
 if(M.length !== M[0].length){return;}
 
 //create the identity matrix (I), and a copy (C) of the original
 var i=0, ii=0, j=0, dim=M.length, e=0, t=0;
 var I = [], C = [];
 for(i=0; i<dim; i+=1){
 // Create the row
 I[I.length]=[];
 C[C.length]=[];
 for(j=0; j<dim; j+=1){
 
 //if we're on the diagonal, put a 1 (for identity)
 if(i==j){ I[i][j] = 1; }
 else{ I[i][j] = 0; }
 
 // Also, make the copy of the original
 C[i][j] = M[i][j];
 }
 }
 
 // Perform elementary row operations
 for(i=0; i<dim; i+=1){
 // get the element e on the diagonal
 e = C[i][i];
 
 // if we have a 0 on the diagonal (we'll need to swap with a lower row)
 if(e==0){
 //look through every row below the i'th row
 for(ii=i+1; ii<dim; ii+=1){
 //if the ii'th row has a non-0 in the i'th col
 if(C[ii][i] != 0){
 //it would make the diagonal have a non-0 so swap it
 for(j=0; j<dim; j++){
 e = C[i][j]; //temp store i'th row
 C[i][j] = C[ii][j];//replace i'th row by ii'th
 C[ii][j] = e; //repace ii'th by temp
 e = I[i][j]; //temp store i'th row
 I[i][j] = I[ii][j];//replace i'th row by ii'th
 I[ii][j] = e; //repace ii'th by temp
 }
 //don't bother checking other rows since we've swapped
 break;
 }
 }
 //get the new diagonal
 e = C[i][i];
 //if it's still 0, not invertable (error)
 if(e==0){return}
 }
 
 // Scale this row down by e (so we have a 1 on the diagonal)
 for(j=0; j<dim; j++){
 C[i][j] = C[i][j]/e; //apply to original matrix
 I[i][j] = I[i][j]/e; //apply to identity
 }
 
 // Subtract this row (scaled appropriately for each row) from ALL of
 // the other rows so that there will be 0's in this column in the
 // rows above and below this one
 for(ii=0; ii<dim; ii++){
 // Only apply to other rows (we want a 1 on the diagonal)
 if(ii==i){continue;}
 
 // We want to change this element to 0
 e = C[ii][i];
 
 // Subtract (the row above(or below) scaled by e) from (the
 // current row) but start at the i'th column and assume all the
 // stuff left of diagonal is 0 (which it should be if we made this
 // algorithm correctly)
 for(j=0; j<dim; j++){
 C[ii][j] -= e*C[i][j]; //apply to original matrix
 I[ii][j] -= e*I[i][j]; //apply to identity
 }
 }
 }
 
 //we've done all operations, C should be the identity
 //matrix I should be the inverse:
 return I;
}

 

  • Further we need to extract from the inverse matrix the Euler angels for rx , ry, and rz:
//==============================
function lcs2Euler(	X1x, X1y, X1z,
				 Y1x, Y1y, Y1z,
				 Z1x, Z1y, Z1z) {
				
	var x=0;
	var y=0;
	var z=0;
 
 var sy = Math.sqrt(X1x * X1x + Y1x * Y1x);
 if (!sy < DBL_EPSILON) {
 x = Math.atan2( Z1y, Z1z);
 y = Math.atan2(-Z1x, sy);
 z = Math.atan2( Y1x, X1x);
 }
 else {
 
 x = Math.atan2(-Y1z, Y1y);
 y = Math.atan2(-Z1x, sy);
		z = 0;
		}
	//printVector	([pre,nut,rot],"lcs2Euler");
	return [x,y,z];
}
  • We already calucalted the rotation and now we need to calculate the x,y, z postion. This will be a point which is in front of use - along the gaze vector with particular distance and then x, and z corrinates of the view X and View Y vector:
$scope.setWidgetProp( wdgName, 'rx', rad2deg(EulerAnglesInv[0]) )
$scope.setWidgetProp( wdgName, 'ry', rad2deg(EulerAnglesInv[1]) )
$scope.setWidgetProp( wdgName, 'rz', rad2deg(EulerAnglesInv[2]) )
 
var eye_dir_norm = vec_normilize($scope.eyedir) 
//projected to normal
 var eyeZaxis 	= eye_dir_norm;
 var eyeYaxis 	= vec_normilize($scope.eyeup);
 var eyeXaxis 	= neg_dir(vec_normilize(vec_product(eyeYaxis,eyeZaxis)))
 var newCalc = [];
 // here it will translate along the eye_dir and move in x and y axis
 for (var i=0; i<3; i++) 
 		newCalc[i]= $scope.eyepos[i] + disScale*eye_dir_norm[i]
 + Xcomp*eyeXaxis[i] + Ycomp*eyeYaxis[i];
 
 //correction of the translation to the view plane in distnace of disScale
 $scope.setWidgetProp( wdgName, 'x', newCalc[0] )
 $scope.setWidgetProp( wdgName, 'y', newCalc[1] )
 $scope.setWidgetProp( wdgName, 'z', newCalc[2] )

 

So that now we can display the UI widgets with some calls like this:

 $scope.setWdgetPos('ShopingCard3DImage', 1.8, -0.25, 0.35)
 $scope.setWdgetPos('3DImage-1' , 1.6, 0.28, -0.1)
 $scope.setWdgetPos('3DImage-2' , 1.6, 0.28, 0.05)
 $scope.setWdgetPos('3DImage-3' , 1.6, 0.28, 0.3)
 $scope.setWdgetPos('3DButton-1' , 1.6, -0.25, -0.1)
 $scope.setWdgetPos('3DButton-2' , 1.6, -0.25, 0.0)
 $scope.setWdgetPos('3DButton-3' , 1.6, -0.25, 0.1)
 $scope.setWdgetPos('model-2' , 1.7, 0, 0.1 , 1)
 $scope.setWdgetPos('3DLabel-1' , 1.6, 	0.25, 0.14)

 

In this article I provided a demo project which is attached to this article as zip file. I attached also the javascript math function which I used.

 

 Where we have the widget name, distance along the gaze vector from the device and the X and Y position /X and Y view vectors /screen 

  • To be able to get the device  gaze , up(y) and the x vector we can use the 'tracking' event:

 

//==================== 
$scope.eyeMat=[];
$scope.eyeInvMat=[];
//====================
$rootScope.$on('tracking', function( tracker,fargs ) {

 for(var i=0; i<fargs.position.length; i++)
 {$scope.eyepos[i]	=fargs.position[i];
 $scope.eyedir[i]	=fargs.gaze[i];
	 $scope.eyeup [i]	=fargs.up [i];	 
 }

 $scope.eyeMat	=get_mat4x4_camera(fargs.position,neg_dir(fargs.gaze),[0,1,0]); 
 $scope.eyeInvMat	= matrix_invert($scope.eyeMat)
 
 var EulerAngles		=transfMat2Euler($scope.eyeMat);
 var EulerAnglesInv	 =transfMat2Euler($scope.eyeInvMat);

 $scope.$applyAsync();
//////////////////////// finished tml3dRenderer.setupTrackingEventsCommand 
})

 

The tracking callback function will save the current view vectors (gaze, up, postion) to a global variables which could be used later from the other functions.

I created a demo project which is one possible example showing how we can implement such functionality. The sample project is for the HoloLens - I tested with on preview mode and on the  HoloLens 1 device and it was working fine. (- zipped project with password "PTC" - please, unzip it and import it to Studio)

A note about the attached file:

- the file project-"HoloLens3dFlexibleUI-article_ExampleProject.zip" should be unzipped to HoloLens3dFlexibleUI-article.zip - which is a project file for Vuforia Studio - demo project and could be imported in the Vuforia Studio UI. To unzip the project-example.zip you need a password simple: 'PTC ' - in capital characters

-the file: myMathFunc.zip contains the used mathematical function definitions (zipped js file) . The javascript file is also contained by  the Studio project/ upload folder.

When we test the project:

 

2020-04-06_14-42-25.jpg

 

Where we see in front in the middle a cursor where we can tab or we can say show UI to display the UI:

 

2020-04-06_14-48-59.jpg

13 replies

21-Topaz I
March 12, 2024

Hi @aletenti , did you set the 3D container property enable Tracking event? 

2024-03-12_16-59-15.jpg

if yes then need to check it furhter....

12-Amethyst
March 13, 2024

Hi Roland, you are right... I forgot to set that property.... 🙂

 

Now it works, thank you very much!

12-Amethyst
March 13, 2024

Hello @RolandRaytchev ,

I have edited your code in order to develop a 3D-Image with also other 3D elements inside it (like buttons, toggles, etc.) and they all follow my view. Here's the code of a main 3D-Image with 5 toggle buttons inside it and they all follow the main 3D-image (set as "father"). In addition, I control the auto-follow by an interval triggered/untriggered from application events.

 

/* ***************** */
// use from :
//it is not necessary to re-invent the wheel
//math https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Matrix_math_for_the_web
//it is not necessary to re-invent the wheel
//=======================================================================================

 //---------------Loading the API----------------------- 
$scope.asyncLoadScript = function (fname) {
 return new Promise((resolve, reject) => {
 if(fname) { var head = document.head || document.getElementsByTagName('head')[0],
 script = document.createElement('script');
 script.async = true; script.onload = resolve;
 script.onerror = reject; script.type = 'text/javascript';
 script.src=fname; head.appendChild(script); } });}

//var mathe =undefined;
//////////////////////////////////////////
$scope.mathModuleLoad = function() {
console.log("mathModuleLoad started"); 
//var mathe =undefined;
$scope.asyncLoadScript("app/resources/Uploaded/myMathFunc.js").then(
 function() { console.log("myMathFunc was loaded successfully") ;TESTLOAD();}, 
 function() { console.log("error for when loading myMathFunc.js module");} );
}
//
$scope.mathModuleLoad(); //LOAD dhere the myMathFunc.js for 

//=======================================================================================
$scope.foo= function (){console.info ("somewhere $scope.foo() was called");};
//////////////

$rootScope.$on("modelLoaded", function() {
//==========================
 // $scope.setEYEtrack();//set track
 $scope.setWidgetProp('3DContainer-1','enabletrackingevents',true);
 console.log("now check again the setting of the envronment")
 console.warn($scope.app.view.Home.wdg['3DContainer-1'].enabletrackingevents)
 //======================================= 
 $scope.FirstTimeCalled=false;
 $scope.setEYEtrack();
 
 TESTLOAD()
 //setAllWdgSProp($scope,'Home','visible',false)
 //printWdgProp($scope,'Home')
 
 $scope.initializePos();

});

//============================================================


$scope.setEYEtrack= function() {
 //==============
 $scope.eyepos=[];
 $scope.eyedir=[];
 $scope.eyeup =[];
 $scope.eyeMat=[];
 $scope.eyeInvMat=[];
 
 $scope.postionShopingCardImag=[];
 $scope.trackCount=0;
 

 //==============
 $rootScope.$on('tracking', function( tracker,fargs ) {
 for(var i=0; i<fargs.position.length; i++)
 {$scope.eyepos[i]	=fargs.position[i];
 $scope.eyedir[i]	=fargs.gaze[i];
	 $scope.eyeup [i]	=fargs.up [i];	 
 }

 $scope.eyeMat	=get_mat4x4_camera(fargs.position,neg_dir(fargs.gaze),[0,1,0]); 
 $scope.eyeInvMat	= matrix_invert($scope.eyeMat)
 
 var EulerAngles		 =transfMat2Euler($scope.eyeMat);
 var EulerAnglesInv	 =transfMat2Euler($scope.eyeInvMat);

 
 //================================== 
 if( !$scope.FirstTimeCalled)
 {$scope.FirstTimeCalled=true;
 
 }
 $scope.$applyAsync();
//////////////////////// finished tml3dRenderer.setupTrackingEventsCommand 
})
 //====setEYEtrack
}
//=================================================================================

var interval;
var timeout;
var newPos = {};
$scope.timeoutIsRunning = false;

$scope.initializePos = function() {
 newPos["3DImage-1"] = { disScale: 1, x: 0, y: 0, z: 0 }
 calcPosFromFather("3DImage-1", "3DToggleButton-1");
 calcPosFromFather("3DImage-1", "3DToggleButton-2");
 calcPosFromFather("3DImage-1", "3DToggleButton-3");
 calcPosFromFather("3DImage-1", "3DToggleButton-4");
 calcPosFromFather("3DImage-1", "3DToggleButton-5");
}

function calcPosFromFather(father, target) {
 	var currentPosFather = {
 	x: $scope.getWidgetProp(father, 'x'),
 	y: $scope.getWidgetProp(father, 'y'),
 	z: $scope.getWidgetProp(father, 'z')
 }
	var newPosFather = newPos[father];
 
 	var currentPosTarget = {
 	x: $scope.getWidgetProp(target, 'x'),
 	y: $scope.getWidgetProp(target, 'y'), 
 	z: $scope.getWidgetProp(target, 'z') 
 }
 
 var deltaFather = { 
 	x: currentPosFather.x - newPosFather.x,
 	y: currentPosFather.y - newPosFather.y,
 	z: currentPosFather.z - newPosFather.z	 
 };
 
 	newPos[target] = {
 	disScale: 1,
 	x: currentPosTarget.x - deltaFather.x,
 	y: currentPosTarget.y - deltaFather.y,
 	z: currentPosTarget.z - deltaFather.z
 }
}


function setNewPos(name, father) {
 	var obj = newPos[name];
 	if (father) {
 	newPos[name].father = father
 	}
	$scope.setWdgetPos(name, obj['disScale'], obj['x'], obj['y'], obj['z'], father?false:true); 
}

function startTimeout() {
 	$scope.timeoutIsRunning = true;
	timeout = $timeout(function() {
 	setNewPos('3DImage-1');
 	setNewPos('3DToggleButton-1', '3DImage-1');
 	setNewPos('3DToggleButton-2', '3DImage-1');
 	setNewPos('3DToggleButton-3', '3DImage-1');
 	setNewPos('3DToggleButton-4', '3DImage-1');
 	setNewPos('3DToggleButton-5', '3DImage-1');
 	$scope.timeoutIsRunning = false; 
 }, 1500);
}

$scope.startInterval = function() {
	interval = $interval(function() {
 if (!$scope.timeoutIsRunning) {
 startTimeout();
 }
 }, 100);
}

$scope.toggleButton = function(){
 	$scope.startInterval();
};

$scope.untoggleButton = function() {
	$interval.cancel(interval);
 $scope.timeoutIsRunning = false;
 	$timeout.cancel(timeout);
}
////// widget
$scope.setWdgetPos=function(wdgName,disScale,Xcomp,Ycomp,Zcomp,isFather){
 
 const args = Array.from(arguments);
 //console.log(args) // [1, 2, 3]
 if(args.length <=6) 
 $scope.eyeMat	=get_mat4x4_camera($scope.eyepos,neg_dir($scope.eyedir),[0,1,0]); 
 else
 { //rotate about x and y 180 degree - axis flip direction of y and z axes
 $scope.eyeMat	=get_mat4x4_camera($scope.eyepos,$scope.eyedir,[0,-1,0]); 
 } 
 $scope.eyeInvMat	= matrix_invert($scope.eyeMat)

 var EulerAngles		=transfMat2Euler($scope.eyeMat);
 var EulerAnglesInv	 =transfMat2Euler($scope.eyeInvMat);
 printVector(convEuler2Dec(EulerAnglesInv),"EulerAnglesInv");
 //rotation via Euler correction to a front of the view in distnace of disScale
 //coordinates of the view plane Xcomp and Ycomp
 $scope.setWidgetProp( wdgName, 'rx', rad2deg(EulerAnglesInv[0]) )
 $scope.setWidgetProp( wdgName, 'ry', rad2deg(EulerAnglesInv[1]) )
 $scope.setWidgetProp( wdgName, 'rz', rad2deg(EulerAnglesInv[2]) )

 var eye_dir_norm = vec_normilize($scope.eyedir) 
 //var eye_dir_norm = vec_normilize( vec_product($scope.eyedir,[0,0,1])) //projected to normal
 var eyeZaxis 	= eye_dir_norm;
 var eyeYaxis 	= vec_normilize($scope.eyeup);
 var eyeXaxis 	= neg_dir(vec_normilize(vec_product(eyeYaxis,eyeZaxis)))
 var newCalc = [];
 // here it will translate along the eye_dir and move in x and y axis
 for (var i=0; i<3; i++) 
 newCalc[i]= $scope.eyepos[i] + disScale*eye_dir_norm[i] + Xcomp*eyeXaxis[i] + Ycomp*eyeYaxis[i] /*+ Zcomp*eyeZaxis[i];*/

 //correction of the translation to the view plane in distance of disScale
 $scope.setWidgetProp( wdgName, 'x', newCalc[0] )
 $scope.setWidgetProp( wdgName, 'y', newCalc[1] )
 if (!isFather) { 
 if (newCalc[2] < 0) {
 newCalc[2] -= parseFloat($scope.app.params.zOffset) 
 } else {
 newCalc[2] += parseFloat($scope.app.params.zOffset) 
 }
 }
 $scope.setWidgetProp( wdgName, 'z', newCalc[2] )
};
//=================================================================================
/* ***************** */

 

Here's some screenshots:

 

1- initial view

aletenti_0-1710348830603.png

 

2- 2nd view

aletenti_1-1710348887938.png

 

3- activated auto-follow

aletenti_2-1710348910886.png

 

As you can see, all the elements stay linked with the "father".

 

I'm available to share a test project.