three.jselectrongltf

How to automatically center imported GLTF model and orient camera to show full model in Three.js


I'm working on some Three.js code to import a GLTF model and display it. The model is imported successfully through a different component, but no matter how I adjust the camera or reposition the model, I still can't have the camera display the front of a centered model with both the top and the bottom of the model visible.

I've tried an approach in which the camera is positioned at half the height of the model (calculated using a bounding box), but even that fails to work and the camera exists at the base of the model zoomed into one of its feet.

How do I automatically center and display the entire front of the model from head to toe (touching the edges of the window it is in so as to ensure maximal size) when it is imported into my app? The relevant part of my code is below:

    <script>
        const { ipcRenderer } = window.electron;

        var mixer, clock;
        let currentAnimationIndex = 0;

        function playNextAnimation() {
            if (currentAnimationIndex >= animations.length) return;

            const action = mixer.clipAction(animations[currentAnimationIndex]);
            action.reset().play();

            // Listen for the end of the current animation to play the next one
            action.clampWhenFinished = true;
            action.loop = THREE.LoopOnce;
            action.onFinished = () => {
                currentAnimationIndex++;
                playNextAnimation();
            };
        }

        ipcRenderer.on('load-gltf', (event, { filePath, characterName }) => {

            console.log(`Loading GLTF file: ${filePath}, Character Name: ${characterName}`);

            if (!filePath) {
                console.error('File path is undefined or invalid');
                return;
            }

            // Initialize Three.js scene
            const scene = new THREE.Scene();
            clock = new THREE.Clock();

            const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
            const renderer = new THREE.WebGLRenderer({  antialias: true, alpha: true, preserveDrawingBuffer: false });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(window.devicePixelRatio || 1); // Improve resolution on high-DPI screens
            renderer.setClearColor(0x000000, 0); // Transparent background

            // Enable Tone Mapping and adjust Exposure
            renderer.toneMapping = THREE.LinearToneMapping; // Choose a tone mapping algorithm
            renderer.toneMappingExposure = 2; // Increase exposure to brighten the scene

            renderer.physicallyCorrectLights = true;
            renderer.outputEncoding = THREE.sRGBEncoding;

            renderer.clear()
            document.body.appendChild(renderer.domElement);

            // Add lights
            const directionalLight = new THREE.DirectionalLight(0xffffff, 3);
            directionalLight.position.set(5, 10, 7.5);
            scene.add(directionalLight);

            const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
            scene.add(ambientLight);

            let modelRadius, modelSize;

            const loader = new THREE.GLTFLoader();
            loader.load(filePath, (gltf) => {
                const bbox = new THREE.Box3().setFromObject(gltf.scene);
                const center = bbox.getCenter(new THREE.Vector3());
                const size = bbox.getSize(new THREE.Vector3());
                gltf.scene.position.sub(center); 
                gltf.scene.position.y = size.y / 2;

                // Calculate camera position
                const verticalOffset = size.y * 0.5;
                const fovRad = camera.fov * Math.PI / 180;
                const distance = (Math.max(size.x, size.y, size.z) / Math.sin(fovRad / 2)) * 1.5;

                // Set camera position and target
                camera.position.set(0, verticalOffset, distance);
                camera.lookAt(0, verticalOffset, 0);
                controls.target.set(0, verticalOffset, 0);
                controls.update();

                // gltf.scene.fustrumCulled = false;

                scene.add(gltf.scene);b

                // Initialize AnimationMixer
                mixer = new THREE.AnimationMixer(gltf.scene);

                // Store animations
                if (gltf.animations && gltf.animations.length > 0) {
                    animations = gltf.animations;
                    console.log('Animations found in the model:');
                    animations.forEach((clip, index) => {
                        console.log(`${index + 1}: ${clip.name}`);
                    });

                    // Start playing animations sequentially
                    playNextAnimation();
                } else {
                    console.log('No animations found in the model.');
                }
                animate();
            });

            // Add OrbitControls
            const controls = new THREE.OrbitControls(camera, renderer.domElement);
            controls.target.set(0, 0, 0);
            controls.update();

            function animate() {
                renderer.clear(); // Clear canvas before rendering
                requestAnimationFrame(animate);
                controls.update();
                const delta = clock.getDelta();
                if ( mixer ) mixer.update( delta );
                renderer.render(scene, camera);
            }

            window.addEventListener('resize', () => {
                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(window.innerWidth, window.innerHeight);
                camera.position.set(0, verticalOffset, distance);
                camera.lookAt(0, verticalOffset, 0);
                controls.target.set(0, verticalOffset, 0);
                controls.update();
            });

        });
    </script>


Solution

  • I tried to recreate your code and I don't understand the overwriting of position.y after centering the model... Although removing this line and offset moves the model even more, and this [maybe] is due to the model itself, it's hard to tell. Also keep in mind that problems with centering the model do not necessarily have to come from the scene in general, they can also be caused by export parameters from Blender or other software, specifically with the pivot point or applied transformations, so inside model per se.

    In addition, hard-coded camera position can further decentralize the model.

    camera.position.set(0, verticalOffset, distance); // especially distance 
    

    As far as I know, in perspective projection, distance is related to the FOV and the half-width of the scene at the camera near plane.

    Try something like this

    <script type="importmap">
      {
        "imports": {
          "three": "https://unpkg.com/three@0.172.0/build/three.module.js",
          "three/addons/": "https://unpkg.com/three@0.172.0/examples/jsm/"
        }
      }
    </script>
    
    <script type="module">
    import * as THREE from "three";
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
    
    const s = new THREE.Scene();
    const c = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    const r = new THREE.WebGLRenderer();
    r.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(r.domElement);
    
    const aL = new THREE.AmbientLight(0xffffff);
    s.add(aL);
    
    const dL = new THREE.DirectionalLight(0xffffff, 1);
    dL.position.set(10, 10, 10).normalize();
    s.add(dL);
    
    const loader = new GLTFLoader();
    loader.load('https://threejs.org/examples/models/gltf/RobotExpressive/RobotExpressive.glb', function (gltf) {
      const model = gltf.scene;
    
      const bbox = new THREE.Box3().setFromObject(model);
      const center = bbox.getCenter(new THREE.Vector3());
      const size = bbox.getSize(new THREE.Vector3());
    
      model.position.sub(center);
    
      s.add(model);
    
      const maxDim = Math.max(size.x, size.y, size.z);
      const fovRad = (c.fov * Math.PI) / 180;
      const distance = maxDim / (2 * Math.tan(fovRad / 2));
    
      c.position.set(0, 0, distance * 1.2);
      c.lookAt(0, 0, 0);
    
      controls.target.set(0, 0, 0);
      controls.update();
    });
    
    const controls = new OrbitControls(c, r.domElement);
    controls.enableDamping = true;
    
    function a() {
      requestAnimationFrame(a);
      controls.update();
      r.render(s, c);
    }
    a();
    
    window.addEventListener('resize', () => {
      c.aspect = window.innerWidth / window.innerHeight;
      c.updateProjectionMatrix();
      r.setSize(window.innerWidth, window.innerHeight);
    });
    </script>