`Eye tracking scenario requires a specific view matrix to function properly`

In the midst of a small project, I am currently exploring the use of a webcam to track a user's 'eye position' in order to manipulate a threeJS scene or WebGL environment. The goal is to create an illusion that the screen is a window into a 3D world by adjusting the camera angle based on the eye position relative to the center of the screen. To achieve this effect, it seems that manipulating the view matrix is the key, assuming the 'eye camera angle' is known. In the image provided, the left shows a typical view frustum for a standard view matrix with a straight camera angle, while the right demonstrates the desired matrix and resulting view frustum with an upward movement of the 'eye camera position'.

https://i.sstatic.net/EyYY6.jpg

Would simply swapping out the standard view matrix suffice? What would the new matrix look like?

Please consider my limited knowledge of linear algebra when responding, as I have a basic understanding. Additionally, note that this browser-based project will utilize WebGL and JavaScript for implementation.

By the way, I recently discovered that what I am attempting is identified as 'Head-coupled perspective.'

Answer №1

While this answer may not cover everything, the basic idea is there for a function similar to glFrustum. The math is solid, but the presentation could use some improvement. Fortunately, Three.js supports this concept through Camera.setViewOffset.

To utilize Camera.setViewOffset, you define a larger view and specify which portion of the canvas represents that view. In this scenario, we're focusing on adjusting the smaller view without concerning ourselves with a larger perspective.

In order to align with the user's eye position, you'll need to select a central area within the broader view and adjust the camera accordingly.

The calculation detailing how much to move each element might seem daunting at first glance, but it seems like they should be equal — the only difference being that the camera moves in world units while setViewOffset operates in pixels. Given that CSS pixels equate to 96 per inch, moving the eye inches results in an opposing shift offsetting by 96 * distance moved.

Furthermore, understanding the user's eye-screen distance along with the screen size enables us to compute an appropriate field of view based on the setup (e.g., a 60-inch TV with the viewer standing 3 feet away versus someone facing an 11-inch notebook from a foot apart).

let mouse = new THREE.Vector2(), INTERSECTED;
let radius = 100, theta = 0;

const camera = new THREE.PerspectiveCamera(70, 1, 1, 10000);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);

const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set( 1, 1, 1 ).normalize();
scene.add( light );

const geometry = new THREE.BoxBufferGeometry(1, 1, 1);

for (let z = 0; z < 50; ++z) {
  for (let y = -1; y <= 1; ++y) {
    for (let x = -1; x <= 1; ++x) {
      if (x === 0 && y === 0) {
         continue;
      }
      const object = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ color: Math.random() * 0xffffff}));
  
      object.position.x =  x * 1.5;
      object position.y =  y * 1.5;
      object.position.z = -z * 2.5;

      scene.add(object);
    }
  }
}

renderer = new THREE.WebGLRenderer({canvas: document.querySelector("canvas")});
resize(true);
document.addEventListener( 'mousemove', onDocumentMouseMove, false );

requestAnimationFrame(render);

function resize(force) {
  const canvas = renderer.domElement;
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  if (force || canvas.width !== width || canvas.height !== height) {
    renderer.setSize(width, height, true);
    camera.aspect = width / height;
    camera.updateProjectionMatrix();
  }
}

function onDocumentMouseMove(event) {
  event.preventDefault();
  const canvas = renderer.domElement;
  // compute mouse movement in inches
  mouse.x = (event.clientX - canvas.clientWidth  / 2) / 96;
  mouse.y = (event.clientY - canvas.clientHeight / 2) / 96;
}

function render(time) {
  const worldUnits = 1;
  const pixelUnits = worldUnits * 96;  

  const canvas = renderer.domElement;
  const fullWidth  = canvas.clientWidth;
  const fullHeight = canvas.clientHeight;
  const aspect = fullWidth / fullHeight;
  
  const eyeOffsetX = mouse.x;
  const eyeOffsetY = mouse.y;
  
  const eyeDistFromScreenInches = 12;
  const halfHeightOfScreenInches = fullHeight / 96 * .5;
  const halfFov = Math.atan2(halfHeightOfScreenInches, eyeDistFromScreenInches) * 180 / Math.PI;
  const width   = fullWidth;
  const height  = fullHeight;
  const centerX = fullWidth / 2;
  const centerY = fullHeight / 2;
  const left    = centerX - width / 2 - eyeOffsetX * pixelUnits;
  const top     = centerY - height / 2 - eyeOffsetY * pixelUnits;
  
  camera.fov = halfFov * 2;  
  camera.position.x = eyeOffsetX * worldUnits;
  camera.position.y = -eyeOffsetY * worldUnits;
  
  camera.setViewOffset(fullWidth, fullHeight, left, top, width, height);
  camera.updateProjectionMatrix();
  
  renderer.render(scene, camera);
  
  requestAnimationFrame(render);
}
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/87/three.min.js"></script>
<canvas></canvas>

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

Is there a way to efficiently import multiple Vue plugins in a loop without the need to manually type out each file individually?

I am looking for a way to streamline this code by using a loop to dynamically import all .js files from a specified directory (in this case, the 'plugins' directory). const plugins = ['AlertPlugin', 'AxiosPlugin', 'Confi ...

Is there a method for configuring NextAuth to utilize the /src/app directory instead?

Is there a specific method to instruct NextAuth to redirect to this particular directory, but within the src/app directory? All of my routes are now located there due to the changes in Next.js 13. Below is an example of the code I am using: export const a ...

JavaScript counter unexpectedly resetting to zero instead of the specified value

After gathering feedback from voters: My current issue revolves around a Java counter I created. Ideally, the numbers should start at 0 and increment to the specified number upon page load. You can see an example of this functionality here: https://codepe ...

Filter an array of objects in Javascript and determine the number of elements that meet the filtering criteria

I have a collection of various objects structured like the following: [{ color:'red', 'type':'2', 'status':'true' }, { color:'red', 'type':'2', 'statu ...

Tips for modifying the width of the mat-header-cell in Angular

Is there a way to customize the mat-header-cell in Angular? I've been trying to change its width without success. Any suggestions would be greatly appreciated. <ng-container cdkColumnDef="name"> <mat-header-cell *cdkHeaderCellDe ...

Using slashes in the import statement versus using require statement

Note: I am currently running on node version 14.1 Here is the line of code I'm experimenting with: import "module-alias/register"; This results in the following error: Error [ERR_MODULE_NOT_FOUND]: Cannot find module The error seems to point to t ...

Preserve the state of Bootstrap 5 tabs upon refreshing the page

<ul class="nav nav-pills" id="my_tabs"> <li class="nav-item" role="presentation"> <a class="nav-link active" data-bs-toggle="pill" href="#tab1" aria-selected=&quo ...

Encountering a "Raphael is undefined" error message when working with Treant.js

I need help creating an organizational flow chart using treant.js. Below is my code snippet, but I'm encountering a 'Raphael is not defined' error that I can't seem to solve. Can someone please assist me with identifying the root cause ...

Substitute the value in the object associated with a mystery key with a different value from the second object

I am dealing with the following objects: http ={" xxx": "#phone#","yyy": "1234", "zzz":5678 } input= {"phone": "2", "id": "258 }, Can someone help me find the #phone# val ...

Error: Unable to find the view "subscribers/index" in the views directory while running the mocha/chai test code

I am encountering the following error: Error: Failed to lookup view "subscribers/index" in views directory Here is the structure of my directories: +---controllers | coursesController.js | errorController.js | homeController.js | ...

Displaying Spinner and Component Simultaneously in React when State Changes with useEffect

Incorporating conditional rendering to display the spinner during the initial API call is crucial. Additionally, when the date changes in the dependency array of the useEffect, it triggers another API call. Question: If the data retrieval process is inco ...

Saving information from JSON data obtained through the Google People API

My server is connected to the Google People API to receive contact information, and the data object I receive has a specific structure: { connections: [ { resourceName: 'people/c3904925882068251400', etag: '%EgYBAgkLNy4aDQECAwQFBgcICQoLD ...

Determine the coordinates of the mouse cursor within a specific div element

Similar Questions: Determining Mouse Position Relative to a Div Getting the Mouse Position Using JavaScript in Canvas Is there a way to retrieve the mouse position within a canvas element with fixed dimensions and automatic margin? I am unable to ...

The process of retrieving an item from an HTML node using Vue.js

My scenario involves a Vue app with the following data: data: function() { return { items: [ {id: 1, text: 'one', other_data: {}}, {id: 2, text: 'two', other_data: {}}, {id: 3, text: &apo ...

Enjoy the convenience of accessing your QR code result with just a click on the link provided

Stumbled upon this code snippet on . It allows scanning QR codes and displaying the results as plain text. However, I wanted the result to be a clickable link, like www.google.com/(qrcodescanresult). <script src="html5-qrcode.min.js">< ...

React Native backhandler malfunctioning - seeking solution

Version react-native-router-flux v4.0.0-beta.31, react-native v0.55.2 Expected outcome The backhandler should respond according to the conditions specified in the backhandler function provided to the Router props. Current behavior Every time the har ...

Ensuring proper integration of this regular expression for password validation

Trying to create a jQuery function that can efficiently check if passwords meet specific requirements. The password must contain at least 3 characters that are uppercase, numbers, or special characters, in any combination. I currently have a function usi ...

Using useRef to update the input value

I utilized the useRef hook from React to capture an element. When I use console.log(this.inputRef), I receive this output: <input aria-invalid="false" autocomplete="off" class="MuiInputBase-input-409 MuiInput-input-394" placeholder="Type ItemCode or ...

Using JavaScript, you can move through an array of divs by detecting keypress events

<div id="wrapper"> <img class="seperator selected" src="imgs/character_1.png" tabindex="0" /> <img class="seperator" src="imgs/character_2.png" tabindex="0" /> <br /> <img class="seperator" src="imgs/character_3.p ...

When navigating to a new route in VueJS with parameters, the browser displays the previously used

Scenario In my app, I am using vue.js 2.6 and vue-router 3.0. Within the app, there are links to detail pages set up like this: <div v-for="item in items"> <router-link :to="{name: 'details', params: {key: item.shortKey}}"> &l ...