1

In Three.js, I have a scene that contains a plane and an orthographic camera.


Orthographic camera at -90deg:

If the camera is rotated to -90 deg on the x-axis (i.e. if it is looking straight down at the floor from above), then I can see the whole plane - and only the plane.

Scene setup:

enter image description here

What the camera sees:

enter image description here


Orthographic camera at -40deg:

But if the camera is rotated to e.g. -40 deg on the x-axis, then I can see the whole plane, plus other parts of the scene. Meaning, due to its rotation, the camera "sees more".

Scene setup:

enter image description here

What the camera sees:

enter image description here


My question is:

  • How can I calculate "how much more" the camera sees (due to its x rotation)?
  • And how can I adjust the camera's either (left, right, top, bottom) properties and/or y position to make it so that the camera sees only the plane - and nothing else? (What I mean is, vertically, the viewport should be filled with the plane, and it is ok if, horizontally, parts of the plane are outside of the viewport).

PS: To reiterate, this is an orthographic camera. The camera's left, right, top, and bottom are currently hardcoded to take into account the height of the plane and the aspect ratio of the screen. The camera's position is (0,0,0). So is the plane's position.

camera = new THREE.OrthographicCamera(
    -planeZ * aspectRatio / 2;
    planeZ * aspectRatio / 2;
    planeZ / 2;
    -planeZ / 2;
    -100,
    100
);
camera.position.set(0,0,0);

Update

I have created a code example based on @Trentium's code, but with significant changes, e.g. removed OrbitControls and used the angle calculation from @Marquizzo. See https://plnkr.co/edit/UxO37ShEAiJNAK8H?preview

1
  • 1
    Since the docs are quite sparse on information. I found this video that discusses orthographic cameras in Three.js, maybe give it a try. youtube.com/watch?v=FwcXultcBl4 Commented Oct 14, 2022 at 22:04

2 Answers 2

2

How does rotation influence an orthographic camera in Three.js?

What you're describing is a basic rotation of a square, Three.js doesn't do anything out of the ordinary. When the square is at right (90°) angles, the dissecting plane is the width of the square. When the square is at 45° angles, the dissecting plane is the diagonal of the square, which is √2 times bigger than the width, that's why it gives the impression that it covers a bigger area.

Solution

I think you should be able to achieve the scaling of your camera's top and bottom attributes with a simple sine function. When the camera is at 90°, it'll be at max height, but as it approaches 0, it'll become infinitely thin.

const maxSide = planeZ / 2;

// Set camera rotation
const angle = THREE.MathUtils.degToRad(90);
camera.rotation.x = angle;

// Scale frustum
camera.top = maxSide * sin(angle);
camera.bottom = -maxSide * sin(angle);
camera.updateProjecionMatrix();
Sign up to request clarification or add additional context in comments.

8 Comments

Thanks. This appears to work for the case where camera.position is (0,0,0). However, if I change the camera's position to e.g. (0,1,0) then it no longer works, i.e. I will be able to see some black space above the plane (with the camera rotated to -40).
I have created a code example based on @Trentium's code, but with significant changes, e.g. removed OrbitControls and used your angle calculation. See plnkr.co/edit/UxO37ShEAiJNAK8H?preview
Ultimately, I am trying to use this as part of a solution that allows panning, but clamped to certain left, right, top and bottom values (defined in world space). The problem I find is that if I simply multiply my top and bottom clamp values with sin(-cameraAngle) to work out by how much they have shortened due to the camera rotation, then the clamp limit at the top is too short. So I am thinking that either the above is incorrect, or rotating the camera on the x axis is not enough, but it may need to be moved on the y axis in addition?
Also, if I use cos(-cameraAngle) instead, then the space at the top looks ok, but I get some (although less) black space at the bottom. But cos only seems to work for the rotated version, not the normal one.
Actually, I think moved on the z axis, not y axis, to even out the top and bottom space?
|
1

Here's an implementation of @Marquizzo's answer (so please give him credit if this answers the mail), with some minor adjustments:

  • The example code is primarily lifted from the three.js orthographic example with some minor tweaks to the location of the main 300x300 plane, to ensure it passes through the XYZ origin.
  • The primary code adjustments to implement @Marquizzo's answer can be found near the bottom of the Snippet in the animate() function.
  • To calculate the camera angle relative to the main 300x300 XZ plane centered on the XYZ origin, this involves calculating the distance of the camera to the XYZ origin and then finding the angle between the camera's Y coordinate and the XZ plane. (From a trig perspective, this is taking the arcsin of the camera's Y position divided by the radius, in this case the distance to the origin.)
  • Note that since sin is being used to calculate the final frustums, there is no need to perform the arcsin and then reapply sin, but instead the camera.position.y / r can be used straight up in the frustum calculations. For clarity's sake(?!), I left the trig functions in...
  • Additionally, the bounds of the XZ plane (ie, 150) are hard coded for clearer understanding, and a minor adjustment of 10% (ie, * 1.10 ) is added to each frustum to provide a bit of border on the 300x300 plane to better see the results of the frustum adjustments while orbiting the scene.

Best to enter "Full page" mode after starting the code snippet...

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Three.js WebGL + CSS3D (Stack Snippet)</title>
    <style>
      html, body { margin:0; height:100%; overflow:hidden; background:#000; }
      /* Stack the two renderers exactly on top of each other */
      canvas, #css3d {
        position:absolute; top:0; left:0; width:100%; height:100%;
      }
    </style>

    <!-- Import map to resolve bare specifiers used by addons -->
    <script type="importmap">
    {
      "imports": {
        "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
        "three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
      }
    }
    </script>
  </head>
  <body>
    <div id="css3d"></div>

    <script type="module">
      import * as THREE from 'three';
      import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
      import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';

      let camera, scene, renderer, controls;
      let scene2, renderer2;

      const frustumSize = 500;
      const material = new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.DoubleSide });

      init();
      animate();

      function init() {
        // Camera (orthographic)
        const aspect = window.innerWidth / window.innerHeight;
        camera = new THREE.OrthographicCamera(
          (frustumSize * aspect) / -2,
          (frustumSize * aspect) /  2,
          frustumSize / 2,
          -frustumSize / 2,
          1,
          1000
        );
        camera.position.set(-200, 200, 200);
        camera.lookAt(0, 0, 0);

        // Scenes
        scene = new THREE.Scene();
        scene.background = new THREE.Color(0x000000);
        scene2 = new THREE.Scene();

        // WebGL renderer (for the planes)
        renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.outputColorSpace = THREE.SRGBColorSpace;
        document.body.appendChild(renderer.domElement);

        // CSS3D renderer (for HTML elements)
        renderer2 = new CSS3DRenderer();
        renderer2.setSize(window.innerWidth, window.innerHeight);
        renderer2.domElement.id = 'css3d';
        document.body.appendChild(renderer2.domElement);

        // Controls (use CSS3D domElement to capture pointer events)
        controls = new OrbitControls(camera, renderer2.domElement);
        controls.minZoom = 0.5;
        controls.maxZoom = 2;
        controls.enableDamping = true;

        // Create planes + matching CSS3D elements
        createPlane(
          100, 100, 'chocolate',
          new THREE.Vector3(-50, 0, 0),
          new THREE.Euler(0, -90 * THREE.MathUtils.DEG2RAD, 0)
        ); // left

        createPlane(
          100, 100, 'saddlebrown',
          new THREE.Vector3(0, 0, 50),
          new THREE.Euler(0, 0, 0)
        ); // right

        createPlane(
          100, 100, 'yellowgreen',
          new THREE.Vector3(0, 50, 0),
          new THREE.Euler(-90 * THREE.MathUtils.DEG2RAD, 0, 0)
        ); // top

        createPlane(
          300, 300, 'seagreen',
          new THREE.Vector3(0, 0, 0),
          new THREE.Euler(-90 * THREE.MathUtils.DEG2RAD, 0, 0)
        ); // bottom

        window.addEventListener('resize', onWindowResize);
      }

      function createPlane(width, height, cssColor, pos, rot) {
        // CSS3D element
        const el = document.createElement('div');
        el.style.width = width + 'px';
        el.style.height = height + 'px';
        el.style.opacity = '0.75';
        el.style.background = cssColor;

        const cssObj = new CSS3DObject(el);
        cssObj.position.copy(pos);
        cssObj.rotation.copy(rot);
        scene2.add(cssObj);

        // Matching WebGL mesh (so you can see it in WebGL renderer too)
        const geom = new THREE.PlaneGeometry(width, height);
        const mesh = new THREE.Mesh(geom, material);
        mesh.position.copy(pos);
        mesh.rotation.copy(rot);
        scene.add(mesh);
      }

      function onWindowResize() {
        const aspect = window.innerWidth / window.innerHeight;

        camera.left   = - (frustumSize * aspect) / 2;
        camera.right  =   (frustumSize * aspect) / 2;
        camera.top    =   frustumSize / 2;
        camera.bottom = - frustumSize / 2;
        camera.updateProjectionMatrix();

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

      function animate() {
        requestAnimationFrame(animate);

        // --- Your frustum tweak block (kept as-is, just safer math) ---
        function distanceToOrigin(p) { return Math.hypot(p.x, p.y, p.z); }
        camera.position.z = 0; // as in your original code
        const r = distanceToOrigin(camera.position) || 1e-6; // avoid divide-by-zero
        const angle = Math.asin(THREE.MathUtils.clamp(camera.position.y / r, -1, 1));

        const s = 150 * Math.sin(angle) * 1.10;
        camera.left = -s;
        camera.right = s;
        camera.top = s;
        camera.bottom = -s;
        camera.updateProjectionMatrix();
        // -------------------------------------------------------------

        controls.update();
        renderer.render(scene, camera);
        renderer2.render(scene2, camera);
      }
    </script>
  </body>
</html>

10 Comments

@Marquizzo here's an implementation of your answer...
Hmm… this is a great execution! But I feel like this unnecessarily adds an extra step of complexity because the original question explicitly had the camera placed at position 0, 0, 0. It only needed to deal with its own x-rotation, no need to deal with distanceToOrigin().
@Marquizzo the OP's question indicates The camera's position is (0,0,0). So is the plane's position. I tried this configuration, and right out of the gate the view is inoperative, at least in conjunction with orbit controls, because there's nothing to orbit!... Hence, I generalized the solution in order to leverage prebuilt mouse controls (orbit controls in this case), allowing your concept to function in both the XZ, with of course the limitations of the dissecting plane of a rectangle as noted in your answer...
Thanks, I'm trying to work through this, but right out of the gate, it doesn't seem to be close enough to my setup to be easily understandable. I do not use the Orbit Controls, and my camera is positioned at (0,0,0) and the plane is meant to take up the full viewport height (so *1.10 is not needed). With those changes, your solution appears to just show a tiny orange square in the top-left corner.
If I change the camera position to (0,1,0) and set the rotation to -90 it will show me the plane as seen from the top, and the plane will occupy the whole height of the viewport as desired. However, if I change the rotation to -40 instead, then the plane only occupies a fraction of the viewport's height. The question was how to make the plane occupy the whole height of the viewport.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.