[Solved] Fitting an object to cover the whole canvas a la CSS `object-fit: cover`

Hi. I have a plane geometry with large texture that I would like to fit in such a way as to cover the whole canvas. I’ve found various methods in some other threads, but either they fail to consider object size, or I am missing something (which is more likely).

Here’s an example of my current implementation: wild-wave-d4kf42 - CodeSandbox

I’ve managed to weave together a method to scale a plane based on image size (from R3F’s useAspect) + adjusting zoom (camera’s z coordinate) from this thread.

While canvas, plane and camera are certainly responsive, I’ve still failed to produce the desired result, which is:

  • if plane taller than it is wide (aspect < 1) and it’s wider than canvas, zoom out / decrease scale
  • if plane taller than it is wide (aspect < 1) and canvas is wider than plane, zoom in / increase scale
  • if plane wider than it is tall (aspect > 1) and it’s taller than canvas, zoom out / decrease scale
  • if plane taller than it is wide (aspect > 1) and canvas is taller than plane, zoom in / increase scale

In short, CSS object-fit: cover. I guess in my case FOV can also be used to achieve this (and keeping zoom constant), but so far this didn’t help.

If you don’t need this plane to be illuminated or to receive shadows, the simplest solution is probably to make a quad with custom shader material and do the math in NDC space.

2 Likes

A la CSS, rien que ça!

I made this a while ago but abandoned it while losing my hair over the rotation issue with the “contain” mode. The plane should stay inside the field of view while rotating, but it doesn’t. Apart from that, it emulates CSS contain, fit, and cover pretty well.

Et voilà!

ps: Switch the camera to see both helper and main view mode.

1 Like

There were a few issues. You were using the viewport size in your distance function, but you needed to use the object size (i.e. the image size). Also, the functions in my comment you linked use radians, not degrees!

I updated the comment to show a live codepen example.

Additionally, here’s your codesandbox updated and simplified with the corrections (try both “contains” and “cover” modes):

1 Like

Ahaha, sorry, I’ll work on my French :slight_smile:

Thanks for sharing your code!

1 Like

This is awesome, thank you. All the annotations help, as well!

1 Like

I was messing around with your code, trying to optimize, and I plugged the expanded expression for the tangent of half of vertical FoV (TAN_HALF_V_FOV) into Wolfram Alpha (I’m bad at math), and it gave a nice-looking optimization of the whole thing.
Instead of this:

const perspective = 800;
const V_FOV_DEG = (180 * (2 * atan(window.innerHeight / 2 / perspective))) / Math.PI;
const V_FOV_RAD = Math.PI * (V_FOV_DEG / 180);
const TAN_HALF_V_FOV = tan(V_FOV_RAD / 2);

One can do this:

const perspective = 800;
const TAN_HALF_V_FOV = window.innerHeight / (2 * perspective);
const V_FOV_DEG = (180 * (2 * atan(TAN_HALF_V_FOV))) / Math.PI;

Also, forgive my ignorance, but shouldn’t FoV (and tan of half of FoV, by extension) be recalculated on resize, since they are dependent on view height?

I’ve managed to cut down on a lot of calculations (including removing TAN_HALF_V_FOV altogether), as well as test my hypothesis regarding FoV recalculation. It all appears to work. Here’s the modified version, if anybody is interested: compassionate-fire-ys643m - CodeSandbox

For comparison, this is the custom shader solution:

https://alikim.com/qcode/index.html?glsl%20:%20css%20cover

the actual shader code being:

 // vertex
  varying vec2 vuv;

  void main() {
    gl_Position = vec4(position.xy, 1.0, 1.0);
    vuv = uv;
  }

 // fragment
  varying vec2 vuv;

  uniform sampler2D tex;
  uniform float aspect;

  void main() {

    vec2 uv = vuv;
  
    if(aspect > 1.0) uv.y = (aspect - 1.0 + vuv.y) / aspect;
    else if(aspect < 1.0) uv.x = vuv.x * aspect; 
    vec4 tex = texture2D(tex, uv);
    gl_FragColor = tex;   
  }
3 Likes

Ah, nice!

Actually no, the vertical fov of the camera is constant in Three.js (unless you explicitly change it), and the output of the scene is dependent on both the vertical fov and the view aspect ratio (and not the vertical resolution).

This is why, when you resize any three.js scene vertically, it seems to always vertically fit the same amount content (but you will see more or less content of the left and right sides of the scene depending on if you shrink or grow the view heigh, respectively).

Take any default three.js example (not this special one we made), and shrink the height of the window so that the width is a lot larger than the height, and you’ll see what I mean: the content will shrink so that the same vertical space appears in the scene, but you will notice a lot more content is visible on the left and right sides of the scene.

Essentially Three.js always re-calculates the horizontal fov based on both the vertical fov and the aspect ratio of the view.

This is why in our particular example we had kept vfov constant, but were recalculating the hfov.

1 Like

Ha, interesting, thanks for clarifying!
After some proper testing, it appears your approach, when optimized, involves less calculations:

// precalculate
const initialViewHeight = window.innerHeight;
const perspective = 800;
const tanVerFov = initialViewHeight / perspective;
const verFovDeg = (360 * atan(tanVerFov / 2)) / PI;

const camera = new PerspectiveCamera(verFovDeg, 1, 0.1, 10000);

// recalculate on resize
const shouldFitHeight = fitment === "cover"
  ? imgAspect > viewAspect
  : imgAspect <= viewAspect;

const distance = shouldFitHeight
  ? imgHeight / tanVerFov
  : imgWidth / (viewAspect * tanVerFov);

camera.aspect = viewAspect;
camera.position.z = distance;
1 Like