0

I have implemented a simple 3D model viewer with three.js. The project contains a database of 3D models, divided into different categories. In one category, the models are tall (with a relatively small width, the height is much higher than the height of the other models), in another category, the models are small (relative to all products, their height and width are smaller than in others, these are small models), in another category, the models are large (their height and wider than many models from other categories.

The viewer has a fixed canvas width and height. For this reason, when loading a model in canvas, many models are immediately loaded at a small scale, which requires subsequent multiple zooming. There are also models, the upper part of which does not fit into the canvas, and the lower part does, at boot time. This also requires subsequent scaling.

It is necessary that the viewer first estimates the dimensions of the model, after that it automatically selects the scale for the model individually, after that it centers the model vertically and horizontally.

It is also necessary that the plane in which the model lies (the model has tangible width and height, the thickness is very small, all these models are close to flat) coincide with the plane of the screen. At boot time, many models have an offset for these planes. How do I implement this so that the viewer automatically expands the model?

Below I am attaching screenshots for models from different categories - where they are clearly recorded: tall model, the upper part does not fit into the canvas model with small width and height short model, width is much less than canvas width model whose plane is deviated from the plane of the screen

Below is the code of the viewer that is responsible for initializing the scene:

var objectUrl = $('#modelViewerModal').data('object-url');//'/storage/3d/qqq.obj';

var mesh, renderer, scene, camera, controls;

init();
animate();

function init() {
    const screenshotPageWidth = $(document).width();
    const screenshotPageHeight = $(document).height();

    let modalBody = document.querySelector('#scene-container');

    // renderer
    renderer = new THREE.WebGLRenderer({modalBody});

    var height = screenshotPageHeight / 2;
    var width = screenshotPageWidth / 2;
    if (screenshotPageHeight < screenshotPageWidth) { // landscape orientation
        if (width > 3 * height / 2) {
            width = 3 * height / 2;
        } else if (width < 3 * height / 2) {
            height = 2 * width / 3;
        }
    } else if (screenshotPageHeight > screenshotPageWidth) { // portrait orientation
        if (height > 2 * width / 3) {
            height = 2 * width / 3;
        } else if (height < 2 * width / 3) {
            width = 3 * height / 2;
        }
    }

    // let limitHeight = screen.height - 137;

    renderer.setSize(width, height);

    modalBody.appendChild( renderer.domElement );

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

    // camera
    camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 10000 );
    camera.position.set( 20, 20, 20 );

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

    // ambient
    scene.add( new THREE.AmbientLight( 0x222222 ) );

    // light
    var light = new THREE.DirectionalLight( 0xffffff, 1 );
    light.position.set( 2000, 2000, 2000 );
    scene.add( light );

    var spotLight_01 = getSpotlight('rgb(145, 200, 255)', 1);
    spotLight_01.name = 'spotLight_01';
    var spotLight_02 = getSpotlight('rgb(255, 220, 180)', 1);
    spotLight_02.name = 'spotLight_02';
    scene.add(spotLight_01);
    scene.add(spotLight_02);

    // geometry
    var geometry = new THREE.SphereGeometry( 5, 12, 8 );

    // material
    const material = new THREE.MeshPhongMaterial({
        color: 0xeae8dc,
        side: THREE.DoubleSide,
        transparent: false,
        flatShading: false,
        opacity: 0
    });

    // mesh
    var objLoader = new THREE.OBJLoader();
    objLoader.load(objectUrl,
        function ( obj ) {
            mesh = obj;
            mesh.scale.setScalar( 0.01 );

            obj.traverse( function( child ) {
                if ( child.isMesh ) child.material = material;
            } );

            // center the object
            var aabb = new THREE.Box3().setFromObject( mesh );
            var center = aabb.getCenter( new THREE.Vector3() );

            mesh.position.x += (mesh.position.x - center.x);
            mesh.position.y += (mesh.position.y - center.y);
            mesh.position.z += (mesh.position.z - center.z);

            scene.add( mesh );
            animate();
        } );
}

function animate() {
    requestAnimationFrame( animate );

    controls.update();

    renderer.render( scene, camera );
}

function getSpotlight(color, intensity) {
    var light = new THREE.SpotLight(color, intensity);
    light.castShadow = true;

    light.shadow.mapSize.x = 4096;
    light.shadow.mapSize.y = 4096;

    return light;
}
Vitaly Vesyolko
  • 558
  • 5
  • 22

1 Answers1

1

This can be done with just two things - the camera object, and the mesh object. What you need to do use use the camera frustum (the representation of the viewable area of the camera https://en.wikipedia.org/wiki/Viewing_frustum) to calculate the distance at which it is the same height (or width) as your model. The height of the model can easily be acquired from the geometry.

// Get the size of the model
mesh.geometry.computeBoundingBox()
const modelSize = mesh.geometry.boundingBox.getSize()
const width = modelSize.x // the exact axis will depend on which angle you are viewing it from, using x for demonstration here

// Compute necessary camera parameters
const fov = camera.fov
const aspect = camera.aspect // camera aspect ratio (width / height)
// three.js stores camera fov as vertical fov. We need to calculate horizontal fov
const hfov = (2 * Math.atan(Math.tan(MathUtils.degToRad(fov) / 2) * aspect) * 180) / Math.PI;
// calculate the distance from the camera at which the frustum is the same width as your model
const dist = (width * 0.5) / Math.tan(MathUtils.degToRad(hfov * 0.5));
// Position camera exactly based on model. There are more elegant ways to do this, but this is a quick and dirty solution
// Camera looks down its own negative Z axis, adding Z effectively "zooms out"
camera.position.copy(mesh.position).translateZ(dist)

These fov and frustum calculations may appear daunting, but these are well-solved problems in 3D engines, and the exact algorithms can be looked up pretty quickly with a few searches.

This solution obviously only works with width, and uses a rigid method for moving the camera, but hopefully it provides you with the tools to apply the solution in a way that works for your application.

Hobo Joe
  • 737
  • 1
  • 9
  • 11