How to center the scene in camera so that it is fully in view?

Hi all,

I have a scene in Blender that gets exported to GLB with camera.
The camera in the scene in Blender shows a fully in-view scene

but when I load the glb in threejs and use the camera from that GLB I get a different view (not same as in Blender).

So when I move my the camera around to actually try to get a front-facing view the scene is “floating” halfway up the screen…

how can I:

  • make sure that the camera from blender and threejs are the same?
// Extract camera from GLB
    const extractedCamera = gltf.cameras[0]; // Assumes the first camera in the array

    // Update script's camera with the extracted camera
    if (extractedCamera) {
      camera.copy(extractedCamera);
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
    }
  • that the camera has the entire scene in view, instead of half in view.

Full script:

import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { RGBELoader } from "three/addons/loaders/RGBELoader.js";
import { roomConfig } from "./room_config_single_cabinet.js";

var pointer = new THREE.Vector2();
var scene = new THREE.Scene();
scene.background = new THREE.Color(0xdddddd);

const coordinates = {};

const size = 10;
const divisions = 10;

let selectedItem = null;

var camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);

const width = window.innerWidth;
const height = window.innerHeight;

var renderer = new THREE.WebGLRenderer({ antialias: true });

renderer.toneMapping = THREE.ReinhardToneMapping;
renderer.toneMappingExposure = 2.3;
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.gammaFactor = 2.2;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.powerPreference = "high-performance";
renderer.physicallyCorrectLights = true;
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const pmremGenerator = new THREE.PMREMGenerator(renderer);

const hdriLoader = new RGBELoader();
hdriLoader.load("paul_lobe_haus_2k.hdr", function (texture) {
  const envMap = pmremGenerator.fromEquirectangular(texture).texture;
  texture.dispose();
  scene.environment = envMap;
});

const controls = new OrbitControls(camera, renderer.domElement);
controls.update();

var hemiLight = new THREE.HemisphereLight(0xffeeb1, 0x000000, 1);
scene.add(hemiLight);

// GLTFLoader

var gltfLoader = new GLTFLoader();
gltfLoader.load(
  "glbs/GLB_testscene_3.glb",
  function (gltf) {
    var object = gltf.scene;

    object.traverse((n) => {
      if (n.isMesh) {
        n.castShadow = true;
        n.receiveShadow = true;
        console.log(n.name);
        if (n.material.map) n.material.map.anisotropy = 1;
      }
    });

    scene.add(object);

    // Extract camera from GLB
    const extractedCamera = gltf.cameras[0]; // Assumes the first camera in the array

    // Update script's camera with the extracted camera
    if (extractedCamera) {
      camera.copy(extractedCamera);
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
    }
  },
  undefined,
  function (error) {
    console.error("Error loading scene file", error);
  }
);

window.addEventListener("resize", () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

function animate() {
  requestAnimationFrame(animate);
  controls.update();
  renderer.render(scene, camera);
}
animate();

thanks already for any tip or trick :wink:

  • […] the camera has the entire scene in view […]

Consider dropping OrbitControls in favor of CameraControls - as this method is most likely what you’re looking for.

CameraControls are a more advanced cousin of OrbitControls - but the API is pretty much the same, so you should be able to swap the code without pretty much any issues.

  • make sure that the camera from blender and threejs are the same?

As for this - you can’t. Camera size is blender is static - and browser window can be resized. Unless you fix your three.js canvas size to a specific size & aspect ratio, there’s no reasonable way of making the browser look precisely as the viewport in Blender.

well, I can in fact, set the width and height, making it a fixed aspect ratio.
I will update the code to camera controls and see where that takes me :slight_smile: for now.

No, the Camera Controls just show nothing: if I zoom or rotate or whatever, I just don’t get anything in view when keeping the current code. Overriding the initial camera when glb is loaded might the the issue?

when doing
controls.fitToBox(object, true, { cover: true });
or
controls.setFocalOffset(0.001, 0.001, 0, true);
The scene just goes blank. Nothing comes in view when rotating or zooming. And no console errors.

i believe cameras in the gltf are nested in groups. if you fish out a camera like that it’s probably zeroed/missing relative group positioning. i have been using blender cameras in threejs with no issues.

here’s one example, using a tool i’ve made to extract the scene graph (gltfjsx)

as you can see in the video the camera was part of a transformed group, this isn’t the tool, it came like that in the glb.

Updated code with camera-controls

import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { RGBELoader } from "three/addons/loaders/RGBELoader.js";
import { roomConfig } from "./room_config_single_cabinet.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import CameraControls from "camera-controls";
CameraControls.install({ THREE: THREE });

var pointer = new THREE.Vector2();
var scene = new THREE.Scene();
scene.background = new THREE.Color(0xdddddd);

const coordinates = {};

const size = 10;
const divisions = 10;
const gridHelper = new THREE.GridHelper(size, divisions);
scene.add(gridHelper);

let selectedItem = null;

const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(60, width / height, 0.01, 100);
camera.position.set(0, 0, 5);
const helper = new THREE.CameraHelper(camera);
scene.add(helper);
var renderer = new THREE.WebGLRenderer({ antialias: true });

renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
renderer.shadowMap.enabled = true;
renderer.gammaFactor = 2.2;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.powerPreference = "high-performance";
renderer.physicallyCorrectLights = true;
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const pmremGenerator = new THREE.PMREMGenerator(renderer);

const hdriLoader = new RGBELoader();
hdriLoader.load("paul_lobe_haus_2k.hdr", function (texture) {
  const envMap = pmremGenerator.fromEquirectangular(texture).texture;
  texture.dispose();
  scene.environment = envMap;
});

const controls = new CameraControls(camera, renderer.domElement);
controls.update();

var hemiLight = new THREE.HemisphereLight(0xffeeb1, 0x000000, 1);
scene.add(hemiLight);

// GLTFLoader

var gltfLoader = new GLTFLoader();
gltfLoader.load(
  "glbs/GLB_testscene_3.glb",
  function (gltf) {
    var object = gltf.scene;

    object.traverse((n) => {
      if (n.isMesh) {
        n.castShadow = true;
        n.receiveShadow = true;
        console.log(n.name);
        if (n.material.map) n.material.map.anisotropy = 1;
      }
    });
    object.updateMatrixWorld(true);

    scene.add(object);

    // Extract camera from GLB
    const extractedCamera = gltf.cameras[0]; // Assumes the first camera in the array

    // Update script's camera with the extracted camera
    if (extractedCamera) {
      camera.copy(extractedCamera);
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
    }
    const box = computeBoundingBox(object);
    positionCameraWithBoundingBox(box);
  },
  undefined,
  function (error) {
    console.error("Error loading scene file", error);
  }
);

window.addEventListener("resize", () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

function computeBoundingBox(object) {
  const boundingBox = new THREE.Box3();
  boundingBox.setFromObject(object);

  return boundingBox;
}

function positionCameraWithBoundingBox(boundingBox) {
  const center = new THREE.Vector3();
  boundingBox.getCenter(center);

  // Set the camera position based on the bounding box center
  camera.position.copy(center);

  // You might also want to adjust the camera distance or controls based on the bounding box size
  const size = new THREE.Vector3();
  boundingBox.getSize(size);
  const maxDim = Math.max(size.x, size.y, size.z);

  const fov = camera.fov * (Math.PI / 180);
  const cameraDistance = maxDim / (2 * Math.tan(fov / 2));

  camera.position.z += cameraDistance;

  // Update controls if you are using camera-controls
  //controls.camera.copy(center);
  controls.update();
}

function animate() {
  requestAnimationFrame(animate);
  controls.update();
  renderer.render(scene, camera);
}
animate();

output:

Not sure what you mean by that @drcmda
this is what is in blender


I think the problem was just that you weren’t setting the orbitControls.target after you load… the camera will always try to point at controls.target so after you were positioning, the orbitcontrols was immediately pointing it back at the original controls.target.

Try switching back to OrbitControls, but after you set up the camera, do:

   if (extractedCamera) {
      camera.copy(extractedCamera);
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
       controls.target.set(0,0,-1).applyQuaternion(camera.quaternion).add(camera.position);
    }

You may have to flip the -1 to a 1… in controls.target.set(0,0,-1)… I can’t remember…

Hi @manthrax thanks for the update
I will try to update this tonight.
Quick question: could you explain what this line does exactly?

controls.target.set(0,0,-1).applyQuaternion(camera.quaternion).add(camera.position)

controls.target.set(0,0,-1) – set the controls.target position to -1 on the z axis…

.applyQuaternion(camera.quaternion) – Apply the rotation of the camera, to that 0,0,-1 point…
That rotates the point by the cameras rotation

.add(camera.position) — adds the camera position to that point…

so now… controls.target should be a point, -1 units in front of the camera… thus, the camera will be looking in that direction already, and won’t change direction when orbitcontrols updates.

If I have the - / + confused, you might see the camera flip to facing the opposite direction that it should… if that happens, change the 0,0,-1 to 0,0,1

You don’t have to use 1 as the unit, it can be any distance… it will just control how far away from orbitcontrols.target the camera will orbit around.