Model distortion with Orthographic Camera and Orbital Controls

I’m rendering a scene with a custom model in a .glb file type. When I move the camera to the model’s faces I get a consistent parallel projection view. However, if I view the model at an angle then I see top and bottom lines of the model are no longer parallel and moving away from each other. I tested with just a simple Cube Geometry and saw the same effect, which leads me to believe I’m using the controls and camera together incorrectly - I’m just not sure how.

Any help is appreciated! Screenshots and code below.

Versions

three: 0.168.0
node: 18.20.3

Screenshots


Code

export const TestOrthographicCamera = () => {
    let canvasRef;

    onMount(() => {
        // Sizes
        const sizes = {
            width: 800,
            height: 600,
        };

        // Scene setup
        const scene = new THREE.Scene();

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

        const directionalLight1 = new THREE.DirectionalLight(0xffffff, 0.5);
        directionalLight1.position.set(5, 5, 5);
        scene.add(directionalLight1);

        const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.5);
        directionalLight2.position.set(-5, -5, 5);
        scene.add(directionalLight2);

        // Model Container
        const modelContainer = new THREE.Group();
        scene.add(modelContainer);

        // Model Loader
        const loader = new GLTFLoader();
        loader.load(
            '/src/annotations/test2.glb',
            (gltf) => {
                modelContainer.add(gltf.scene);
                gltf.scene.scale.set(1, 1, 1);

                gltf.scene.traverse((child) => {
                    if (child.isMesh) {
                        child.material = new THREE.MeshStandardMaterial({
                            color: child.material.color,
                            map: child.material.map,
                            normalMap: child.material.normalMap,
                        });
                        child.castShadow = true;
                        child.receiveShadow = true;
                    }
                });

                const box = new THREE.Box3().setFromObject(modelContainer);
                const center = box.getCenter(new THREE.Vector3());
                modelContainer.position.sub(center);

                const boxHelper = new THREE.BoxHelper(modelContainer, 0xffff00);
                scene.add(boxHelper);
            },
            (progress) => {
                console.log('progress');
                console.log(progress);
            },
            (error) => {
                console.log('error');
                console.log(error);
            },
        );

        // Orthographic Camera
        const aspectRatio = sizes.width / sizes.height;
        const camera = new THREE.OrthographicCamera(
            -1 * aspectRatio,
            1 * aspectRatio,
            1,
            -1,
            0.1,
            100,
        );

        camera.position.set(0, 0, 50);
        camera.lookAt(0, 0, 0);
        scene.add(camera);

        // Controls
        const controls = new OrbitControls(camera, canvasRef);
        controls.enableDamping = true;
        controls.dampingFactor = 0.05;
        controls.mouseButtons = {
            MIDDLE: THREE.MOUSE.ROTATE,
            RIGHT: THREE.MOUSE.PAN,
        };
        controls.enableRotate = true;
        controls.enablePan = true;
        controls.enableZoom = true;

        // Renderer
        const renderer = new THREE.WebGLRenderer({
            alpha: true,
            canvas: canvasRef,
        });
        renderer.setSize(sizes.width, sizes.height);
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setClearColor(0xffffff, 1);
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.toneMappingExposure = 3.0;

        // Debugging
        const cameraHelper = new THREE.CameraHelper(camera);
        scene.add(cameraHelper);

        const axesHelper = new THREE.AxesHelper(5);
        scene.add(axesHelper);

        // Animate
        const tick = () => {
            renderer.render(scene, camera);
            window.requestAnimationFrame(tick);
        };
        tick();

        // Cleanup
        onCleanup(() => {
            renderer.dispose();
        });
    });

    return (
        <div>
            <h1>Scene</h1>
            <canvas ref={canvasRef} class="webgl" />
        </div>
    );
};

This is one famous optical illusion. Your brain expects perspective projection, so the parallel lines of the orthographic projections tricks the brain to assume they are diverging. They are parallel, indeed:

4 Likes

Brilliant!

1 Like