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>
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>