Functions to calculate the visible width / height at a given z-depth from a perspective camera

Thanks @looeee, this is helping align some things with DOM.

For some reason though, things aren’t perfectly centered. In this example, you see the teal square behind the DIV element is not exactly lined up:

I believe that by default Three.js centers everything perfectly, and I thought DOM would too (with the CSS I’ve given it). Trying to find why this happens…

Sorry, I updated the last example: I removed the -5 X position from the teal square.

But still, in the previous example you can see the teal sticks out beyond the pink, but I’m expecting (hoping) for it to be precisely hidden behind the pink. Maybe there’s floating-point error?

If I translate the pink square up with -52% instead of -50% then I can manage to cover up the teal square:

I wonder why.

Are you looking at the embedded codepen (i.e. here on the discourse site) ?
Embedded pens do some weird stuff with resizing. If I open it in a new window the squares match exactly.

EDIT: actually I also had to remove the

body {
    perspective: 800px;
}

So actually the CSS was making the pink square inaccurate, not the other way around.

Ah! Interesting! It does match perfectly without CSS perspective.

I thought that no matter what value of perspective is applied, positioning on DOM content on the Z=0 plane should be exactly the same whether there’s perspective or not.

Could this be an aliasing difference introduced when there is perspective? I need to research the browser implementations. I find that no matter which value of CSS perspective I apply, the offset is always the same as long as any value of perspective exists, so it makes me think there’s something about aliasing at play.


This is what I wanted to figure out next anyways: matching the CSS perspective with the Three.js perspective so that I can transform both the mesh and the <div> in unison. Maybe this will help point to what the issue is.


EDIT: Dang, setting antialias: true for the renderer and leaving the CSS perspective in place didn’t work, there’s no difference:

@looeee You won’t believe it: the problem of the div shifting is because the <canvas> element is an inline element, and somehow that affects the div when perspective is applied. Making it display: block solves the problem! See:

Awesome. now I can move on! Thanks for your visibleHeightAtZDepth and visibleWidthAtZDepth functions. :smile:

2 Likes

Aha! After taking a look at this again, I realized that instead of using these functions and a binary search, matching the Three.js perspective to CSS3d perspective boils down to a short equation:

const perspective = 800

// this equation
const fov = 180 * ( 2 * Math.atan( innerHeight / 2 / perspective ) ) / Math.PI

// then align the Three.js perspective with the CSS3D perspective:
camera = new THREE.PerspectiveCamera( fov, window.innerWidth / window.innerHeight, 1, 10000 );
camera.position.set( 0, 0, perspective );
document.body.style.perspective = `${perspective}px`

Here’s a simple example. Note that the blue/pink color is due to the teal WebGL plane rendering on top of the pink DOM plane (zoom the camera to displace the WebGL rendered behind the DOM element):

At this point, the WebGL object and the DOM element can be moved in unison in the same 3D space. For example, let’s animate the positions and rotation of both (again if you zoom, you can see which one is the WebGL and which one is the DOM):

As an follow-on feature would also control the DOM position with the OrbitControls.

2 Likes

I need these function,thank you

@looeee I don’t understand why you subtract (or add) the camera’s z position from the depth before calculating the visible height. Is it not assumed that depth is given in view space? And in which space will it always be correct to transform to view space by subtracting z when z>depth and adding z when z<depth? :confused:

@trusktr What you did with the binary/bisection search was basically to invert the function. Looking at the stripped-down function (with depth assumed positive and from view space origin and in view space units):

height = f(depth) = 2 * tan( vFOV / 2 ) * depth

this is invertible just by moving things around a bit:

depth = g(height) = height / ( 2 * tan( vFOV / 2 ) )
1 Like

Looks like some of the above codepens I posted broke. I need to update them. Nowadays I make sure to use versioned URLs to make sure demos don’t break.

I updated all the above pens so they work now. I used specific versions in the code to make sure they rely on the proper versions, like this:

import * as THREE from 'https://unpkg.com/three@0.116.1/build/three.module.js'
import {OrbitControls} from 'https://unpkg.com/three@0.116.1/examples/jsm/controls/OrbitControls.js'

@looeee Do you mind updating your two codepens above? These are your pens updated to use specific versions:

2 Likes

Hi
I am new to 3d graphics/rendering so apologies in advance
Do you think this is possible for orthographic camera

This fits good on Mobile device, both Android WebView and iOS webkit. You definitely knows the logic behind it. Good job, man.

1 Like

With an OrthographicCamera, objects don’t get smaller when they go further away from the camera so this trick doesn’t work.

What you would need to do is make sure the ortho camera box dimensions are the size of the object, or scale the object to fit the size of the camera box. This is easier than the above with perspective camera, you just need a multiplier.

For example, if your ortho cam width and height are both 10, and the object width/height is 5, then you need to scale the object by 2 so it fits on the screen (or reduce the camera width/height to 5). Simple as that.

1 Like

In case it helps anyone, here’s how to fit an object (most likely a flat plane) on the screen so that it “fits” depending on aspect ratio. For example, if we have an object taller than it is wide we want the Y dimension (the tall dimension) to fit within the view, but if the object is wider than it is tall we want the X dimension (the width dimension) to fit within view.

The following distanceToFitObjectToView function will tell you at what distance to place the object so that it fits within view regardless which dimension is bigger:

(The code has TypeScript type annotations, but you can remove them, f.e. the : number stuff)

(Note, angles are in radians!)

/**
 * Convert vertical field of view to horizontal field of view, given an aspect
 * ratio. See https://arstechnica.com/civis/viewtopic.php?f=6&t=37447
 *
 * @param vfov - The vertical field of view.
 * @param aspect - The camera aspect ratio, which is generally width/height of the viewport.
 * @returns - The horizontal field of view.
 */
function vfovToHfov(vfov: number, aspect: number): number {
  const {tan, atan} = Math
  return atan(aspect * tan(vfov / 2)) * 2
}

/**
 * Get the distance from the camera to fit an object in view by either its
 * horizontal or its vertical dimension.
 *
 * @param size - This should be the width or height of the object to fit.
 * @param fov - If `size` is the object's width, `fov` should be the horizontal
 * field of view of the view camera. If `size` is the object's height, then
 * `fov` should be the view camera's vertical field of view.
 * @returns - The distance from the camera so that the object will fit from
 * edge to edge of the viewport.
 */
function _distanceToFitObjectInView(size: number, fov: number): number {
  const {tan} = Math
  return size / (2 * tan(fov / 2))
}

function distanceToFitObjectToView(
  cameraAspect: number,
  cameraVFov: number,
  objWidth: number,
  objHeight: number
): number {
  const objAspect = objWidth / objHeight
  const cameraHFov = vfovToHfov(cameraVFov, cameraAspect)

  let distance: number = 0

  if (objAspect > cameraAspect) {
    distance = _distanceToFitObjectInView(objHeight, cameraVFov)
  } else if (objAspect <= cameraAspect) {
    distance = _distanceToFitObjectInView(objWidth, cameraHFov)
  }

  return distance
}

Demo

Here’s a live example showing the concepts put together. Open it in new tab, try resizing vertically and horizontally, and try both “contain” and “cover” modes in the code (like CSS object-fit):

5 Likes

Oops! I forgot to include the vfovToHfov function. Updated the above post.

This all looks good mathematically but the aspects are reversed I think. This works for me:

   if (objAspect < cameraAspect) {
      // we are height constrained
      distance = _distanceToFitObjectInView(objHeight, cameraVFov)
   } else {
      // we are width constrained
      distance = _distanceToFitObjectInView(objWidth, cameraHFov)
   }

It depends on what behavior you want. If you want the cover equivalent of CSS object-fit, then your conditional is the one you want. If you want the contain behavior, then you’d want to use the conditional in my example.

An improved version of distanceToFitObjectToView would have an additional fitment parameter that accepts values of “cover” or “contain”, and the conditional would be:

// ...same as before...

  if (fitment === 'contain' ? objAspect <= cameraAspect : objAspect > cameraAspect) { // THIS CHANGED
    distance = _distanceToFitObjectInView(objHeight, cameraVFov)
  } else if (objAspect <= cameraAspect) {
    distance = _distanceToFitObjectInView(objWidth, cameraHFov)
  }

// ...same as before...
1 Like

I added an codepen example to the above comment.

Thanks @trusktr, I understand where you are coming from now.

Yes, it was the contain behaviour I wanted.

1 Like

I was a student, i wrote my graduated project about three.js. I wrote 3d website with three.js for our university website. My lecturer was banned unity webgl. So this was very difficult for me. Now, my carrier is with webgl. I am developing still webgl games. You can see my games on https://gameportalhub.com. But i’m curious, why still you use three.js. You can also coding on unity webgl. Everything you can do with three.js, you can do with unity webgl more easily. still seeing the three.js codes made me emotional