Responsive renderer with limits

How can I resize the renderer (or the camera) in order not to see the unwanted parts of the scene?

I want to achieve exactly the same effect as this website.

I’m already using this:

const {width, height} = canvas;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);

Here’s the result I have:
Capture

But when I resize the window to a larger size:
Capture2

3 Likes

Where do you get “width” and “height”?
Notice that you should change those settings accordingly with the window size, I hope you have a window resize handler and you have to always read window.innerWidth and window.innerHeight on the fly. Example:

window.addEventListener( 'resize', function() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize( window.innerWidth, window.innerHeight );
}

EDIT I was thinking that I had a similar problem because I was rendering my scene by window inner sizes. If you do it this way, try to render it as screen.width and screen.height and then resize it as I shown above.

I just have to say, this website is really nice. It doesn’t seem to be using three.js, and i’m looking at the volcano and can’t quite figure out how they’ve done it. I’m looking at the frame breakdown but doesn’t make much sense. Anyone has any ideas?

@pailhead You’re right! I’ve not seen anywhere in the source code the word “three”. But I know the lead dev of the agency who created this website, and I know he’s a killer (this is the agency of the year on Awwwards). So they even may have developed their own 3d engine.

@ThoughtsRiff canvas is the reference to my canvas element, which is sized to 100vw and 100vh with CSS, so it does exactly the same thing with my code than with yours. I’ve made animated gifs to explain more precisely the result I want:

What I currently have:
test

What I want:
com-optimize

As you can see, there’s a special behavior in the second one, where scene bounds are never passed. I don’t know if this is something to do with the camera or the renderer, or both.

What I was saying is that you should create your image/texture/3dobject/whatever accordingly to “screen.width” and “screen.height”. Can you post your function where you create your scene?

Its using a 2D engine called PixiJS. It places a background image and a foreground image and then transforms the two as the mouse moves to create the 3D effect which is probably using similar code to this example.

To answer the question, you could try scaling the scene relative to the canvas width. This will require manual tweaking so I can only give an example but something like…

window.addEventListener('resize', function(){
    var width = window.innerWidth;
    var optimalWidth = 1000; //You define this based on your model.
    var scale = 1;

    if(width > optimalWidth){
         scale = (width-optimalWidth)/100 + 1; // change the 100 to something that suits your testing.
    }
    
    scene.scale.set(scale,scale,scale); // can change scene to your mesh here
});

In practice though you will probably need to take the height into account as well, so instead of checking against an optimal width it will be more like optimal ratio between width/height, but this should get an ok result for the moment.

1 Like

I understand but I wasn’t talking about resize handler, that you can still improve, check camera.aspectratio and renderer.setsize, you should find some useful post on stack overflow.

Anyway
I give you an example, I have a 3d scene in which I draw rectangles to cover the screen, from left to right and from bottom to top. In my drawing function, I draw rectangles in area that’s screen.width wide and screen.height tall.
what I mean? I mean that when you create your texture, you have to size it with screen properties instead of the window ones, regardless of how much the browser is big. This way, you can resize your window as you want but your first render will cover the whole screen. I hope I explained better. I never used pixi

Do you think that’s how they achieved the distortion? I can see it’s somewhat of a dense mesh but i couldn’t yet figure out what it was, I’m still trying to figure out how spector js works.

The other thing is both the foreground and background have displacement maps, which would also be used for the 3D. Soo mapping the textures to planes (like PlaneBufferGeometry) and using a displacement map (GPU side texture) to give them depth. If you are feeling brave you can unminify the source code to take a look, but that makes it pretty hard to read.

@ThoughtsRiff From what I understand of the question, nagman wants to load a single model once and have the 3D view scale responsively as the window is resized rather than redraw objects based on the initial viewport, which is what my method is based on. You can set up the defaults/optimal sizes on initialisation based on the viewport which would work for if someone was on desktop vs on mobile, and then add in responsive scaling based on those initial values.

1 Like

@calrk I’m suprised that the website is done with Pixi. I was convinced it was 3D.

I tried your solution and it does part of the job (I don’t see the left and right borders of the scene), but instead of scaling the whole view (like you’d do in photoshop), I does a camera-moving-like stuff:

tes2t

It seems to have something to do with renderer.setViewport, or maybe renderer.setScissor, but I’m not sure.

I’ve started something which is I thing the right approach:

function onWindowResize() {
  const width = window.innerWidth;
  const height = window.innerHeight;

  const optimalRatio = 16/9;
  const actualRatio = width / height;

  if (actualRatio > optimalRatio) { // window too large
    // something to do maybe with renderer.setViewport, or setSize, or setScissor, and some math stuff using optimalRatio with width or height
  } else { // window too narrow
    // idem, but replacing the dimension (width or height) with the other
  }

  camera.aspect = width / height;
  camera.updateProjectionMatrix();

What I don’t understand is if @nagman 's texture/3d object has a fixed size that he can choose. Because if it is, when he creates the object he can sets its width and height as screen’s ones. This way, when he resizes window from shrinked to wide, the texture/image gets to its original size ( as screen ) and doesn’t stretch.
I’m gonna create a codepen, I’ll edit this reply.

EDIT 1: Notice also that if you use perspectiveCamera, FOV and distance are factors that you should count too.

@ThoughtsRiff I do use a perspectiveCamera, and this is why I cannot just set the size of the object.

It shouldn’t be that complicated. It is exactly the same effect as a simple object-fit: cover; in CSS, or a background-size: cover;.

Three.js should have something like “stop rendering everything that exceeds this frame”, without having to move the camera, its FOV, the meshes sizes, and so forth.

Are you able to post the full sample code you are working with, including the model?

Ouch, kinda hard because it is in a Next.js environment, but I can try.

The example you’ve posted seems to be working with an image, you seem to be working with a mesh. They have the width and height available to them and can apply it to logic. You don’t have that, and only have height basically by setting camera.fov.

What you need is something that relates to both that camera.fov and your subject, you need to frame it somehow. You could use a bounding box, geometric checks and whatnot, but the easiest thing would be to just pick some world size rectangle, and apply the same logic you would to an image/quad.

So what is happening in your example?

Let’s say the image has an aspect ratio of 2:1. When you set your window to be 1000px x 500px it exactly aligns with the image, no overflow no gaps on either side. Cool, lets make the window narrower first, 500px.

So now at 500px x 500px we have overflow in the horizontal axis. This is cool and it’s how THREE.PerspectiveCamera behaves “by default”. More on that in a second.

Let’s resize it to be wider than the image, 1500px x 500px. You’re doing that in your code and you experience what would be seen as a gap in the example demo. You’d see the entire image and 250px x 500px gaps on both sides.

Ok we don’t want that, so we need to do something to the image. We want the image to “fill the entire window”, so it needs to cover the 1500 x 500. The aspect ratio of that is 3:1, and the entire thing is 2:1. We want the 1000px of the image width to fit 1500px, so we will scale the image up by 1.5x. This gives us a height of 750px, and we can only fit 500px.

If the image was sitting on a mesh,

const image = new THREE.Mesh(new THREE.PlaneGeometry(1000,500))

And if you picked some constant fov:

const myFov = 45

There is only one ever position of the camera that would render this mesh to fit a 1000px x 500px screen:

cam.position.z = heightHalf / Math.tan( THREE.Math.degToRad(myFov/2)) 
cam.position.x = cam.position.y = 0

Sweet. Now when we apply the usual resize logic:

cam.aspect = aspect
cam.updateProjectionMatrix()

And we resize the screen to 500px x 500px we get your result. But when we go to 1500px x 500px we see gaps.

We have:

500 x 500
1000 x 500
1500 x 500

Let’s divide all this with 500:

1x1 
2x1
3x1

So the aspect of 1/2 is ideal and aligns with the image with no overflow or gaps. 1x1 has overflow, 3x1 has gaps. We like the behavior of < 1/2 but we don’t like the behavior of > 1/2.

How about this?

if(aspect < myAspect)
  defaultCameraResize()
else 
  differentCameraResize()

The default behavior has already been mentioned above, we just compute the aspect and apply it to the camera. Since we don’t want to change the position of the camera, we need to change something else. The aspect always has to correspond to the viewport, so we still have to just compute that and pass it, otherwise our geometry would deform. How about we change the fov?

renderer.setSize( window.innerWidth, window.innerHeight );
camera.aspect = window.innerWidth / window.innerHeight
const viewAspect = viewWidth / viewHeight 
const special = camera.aspect > viewAspect
uniform.value = special ? 1 : 0

if(special){
  const camH = Math.tan(THREE.Math.degToRad(myFov/2))
  const ratio = camera.aspect / viewAspect
  const newH = camH / ratio
  const newFov = THREE.Math.radToDeg(Math.atan(newH)) * 2
  camera.fov = newFov
 } else {
  camera.fov = myFov
 }
camera.updateProjectionMatrix()

Fiddle:
https://jsfiddle.net/pailhead/qt5j2fbz/

So this is a generic implementation of that example. You can just find these numbers by trial and error based on your scene scale, or use some heuristic to compute it.

3 Likes

@pailhead THANKS!!! You’re my savior :grin:
I’ve not understood everything, but my first test is a succeed!

I was thinking that playing with the fov would influence the perspective, but no.

I’ll try to write an article for this with some illustrations. I think this kind of a question pops up often. I don’t like just giving the solution hence the step by step explanation.

Can you tell me which parts you did not understand?

1 Like

I think you lost me at this point.

I was the best student at Maths in my school class, but as no one has ever told me what was the use of that, I’ve forgotten absolutely everything :confused:

Also you said that the fov was related to the height?