I have no idea what I’m doing, but a pressing need to do it. This is for an interactive aimed at people with additional needs - and so touchscreen was removed and on-screen html buttons used instead. These work perfectly to pan, rotate and zoom - with a reset button that returned to the original view. Now I’ve been asked to allow touchscreen. I have it all working, except the reset view is weird - and the model drifts off-screen first before snapping back to the initial view.
Edit It just one model - with pan, rotate, zoom. no other elements, nothing else in the scene etc. **
I suspect an issue with updating orbitcontrols - but I’ve tried many things to no avail. Using buttons still works perfectly, including reset - but if I use the touchscreen/mouse for anything, the reset does the ‘model drifts off (as if swiped - if I rotate left then reset it drifts right, rotate right it drifts left, rot up drift down etc.) and then snaps back’ thing. I’ve never used three.js and am not a coder - this is all held together with hope and sellotape.
Models come in various sizes, so the posIncrement and odd calculations are to adjust pan and position to account for this. chatGPT helped a lot, but I also find it makes as many mistakes as solutions. Any assistance much appreciated.
import * as THREE from './three.module.js';
import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';
import { RoomEnvironment } from './jsm/environments/RoomEnvironment.js';
import { OrbitControls } from './jsm/controls/OrbitControls.js';
const canvas = document.getElementById('canvas-container');
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
// Create and customize the controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0); // Set the target to the center of the scene
controls.enableDamping = true; // Enable smooth camera movement
controls.dampingFactor = 0.05; // Adjust damping factor for smoothness
controls.enableZoom = true; // Enable zooming
controls.enableRotate = true; // Enable rotating
controls.enablePan = true; // Enable panning
// set initial 3d World size and params
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio( window.devicePixelRatio );
// renderer.setClearColor(0x80a8ab); // if you want single color backgrounds
renderer.outputEncoding = THREE.sRGBEncoding;
canvas.appendChild(renderer.domElement);
const pmremGenerator = new THREE.PMREMGenerator( renderer );
scene.environment = pmremGenerator.fromScene( new RoomEnvironment()).texture;
// define vars of global scope
let storedLookAtPosition = new THREE.Vector3();
let isMoving = false
let boundingBox, modelSize, posIncrement, cameraDistance
// Use GLTFLoader for .glb or .gltf files
// Function to load the 3D model asynchronously
const loadModel = async () => {
return new Promise((resolve, reject) => {
const modelPath = sessionStorage.getItem('modelName') || "./Galley_Cannons.glb";
const loader = new GLTFLoader();
loader.load(modelPath, (gltf) => {
const model = gltf.scene;
// Determine the size of the loaded model
boundingBox = new THREE.Box3().setFromObject(model);
modelSize = boundingBox.getSize(new THREE.Vector3()).length();
// Calculate the camera distance based on the model size and FOV
cameraDistance = (modelSize * 0.4) / Math.tan((camera.fov / 2) * (Math.PI / 180));
//calc a rough increment for position/pan based on model size
posIncrement = ( 0.1 + (modelSize - 6) * (2 - 0.1) / (100 - 6) );
scene.add(model); // no longer needed if we use box/pivot to recentre
resolve(model); // Resolve the Promise when the model is loaded
}, undefined, (error) => {
reject(error); // Reject the Promise if there is an error loading the model
});
});
};
// Animation loop function
function animate() {
requestAnimationFrame(animate);
TWEEN.update(); // Update the Tween.js animations
renderer.render(scene, camera);
}
// Function to be executed after the model is loaded
const loadingClear = (model) => {
// Event listeners for arrow controls ROTATE
const rarrows = document.querySelectorAll('.rarrow');
rarrows.forEach((rarrow) => {
rarrow.addEventListener('click', handleRotate);
});
// Event listeners for arrow controls PAN
const parrows = document.querySelectorAll('.parrow');
parrows.forEach((parrow) => {
parrow.addEventListener('click', handlePan);
});
// Event listeners for zoom in and out buttons
const zoomInButton = document.getElementById('zoom-in');
const zoomOutButton = document.getElementById('zoom-out');
zoomInButton.addEventListener('click', handleZoom);
zoomOutButton.addEventListener('click', handleZoom);
// reset view button
const resetter = document.getElementById('resetView');
resetter.addEventListener('click', resetView);
// Initialize the camera position and model rotation
camera.position.set(0, 0, cameraDistance);
model.position.set(0, posIncrement * -10, 0);
model.rotation.set(0, 1.5, 0);
console.log(camera.position);
// Render the scene once after controls are set up
renderer.render(scene, camera);
// Function to handle rotation using arrows
function handleRotate(event) {
if (isMoving) { return; }
isMoving = true;
model.rotation.order = 'XYZ';
const rotateSpeed = 0.55; // Adjust the rotation speed as needed
// Create an object to hold the rotation values
const targetRotation = {
x: model.rotation.x,
y: model.rotation.y,
z: model.rotation.z,
};
switch (event.target.id) {
case 'r-arrow-up':
targetRotation.x -= rotateSpeed;
break;
case 'r-arrow-left':
targetRotation.y -= rotateSpeed;
break;
case 'r-arrow-right':
targetRotation.y += rotateSpeed;
break;
case 'r-arrow-down':
targetRotation.x += rotateSpeed;
break;
default:
break;
}
new TWEEN.Tween(model.rotation)
.to(targetRotation, 600)
.easing(TWEEN.Easing.Quadratic.InOut)
.onComplete(() => {
isMoving = false; // Reset the flag when panning is complete
})
.start();
controls.update();
renderer.render(scene, camera);
}
// Function to handle pan using arrows
function handlePan(event) {
const panSpeed = posIncrement * 3; // Adjust the panning speed as needed
const targetPosition = model.position.clone();
switch (event.target.id) {
case 'p-arrow-up':
targetPosition.y += panSpeed;
// Limit panning up to modelSize / 2
targetPosition.y = Math.min(targetPosition.y, (modelSize / 3));
break;
case 'p-arrow-left':
targetPosition.x -= panSpeed;
// Limit panning left to -modelSize
targetPosition.x = Math.max(targetPosition.x, (modelSize * -1));
break;
case 'p-arrow-right':
targetPosition.x += panSpeed;
// Limit panning right to modelSize
targetPosition.x = Math.min(targetPosition.x, modelSize);
break;
case 'p-arrow-down':
targetPosition.y -= panSpeed;
// Limit panning down to -modelSize
targetPosition.y = Math.max(targetPosition.y, (modelSize * -.9));
break;
default:
break;
}
// Create a Tween for smooth panning
new TWEEN.Tween(model.position)
.to(targetPosition, 300) // Adjust the duration (in milliseconds) as needed
.easing(TWEEN.Easing.Quadratic.InOut)
.onComplete(() => {
isMoving = false; // Reset the flag when panning is complete
})
.start();
controls.update();
renderer.render(scene, camera);
}
// Function to handle zoom in and out
function handleZoom(event) {
if (isMoving) { return; }
isMoving = true;
const zoomSpeed = 0.3; // Adjust the zoom speed as needed
const zoomFactor = event.target.id === 'zoom-in' ? 1 - zoomSpeed : 1 + zoomSpeed;
// Calculate the zoom direction vector
const zoomDirection = camera.position.clone().sub(storedLookAtPosition).normalize();
// Calculate the zoom amount based on the zoom factor
const zoomAmount = camera.position.distanceTo(storedLookAtPosition) * (zoomFactor - 1);
// Calculate the new camera position
const newCameraPosition = camera.position.clone().add(zoomDirection.multiplyScalar(zoomAmount));
// Animate the camera to the new position
new TWEEN.Tween(camera.position)
.to(newCameraPosition, 600)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(() => {
// Use camera.lookAt to set the look-at target
camera.lookAt(storedLookAtPosition);
})
.onComplete(() => {
isMoving = false; // Reset the flag when panning is complete
})
.start();
controls.update();
renderer.render(scene, camera);
}
function resetView() {
if (isMoving) { return; }
isMoving = true;
const cameraTween = new TWEEN.Tween(camera.position)
.to({ x: 0, y: 0, z: cameraDistance }, 600)
.easing(TWEEN.Easing.Quadratic.InOut);
const rotationTween = new TWEEN.Tween(model.rotation)
.to({ x: 0, y: 1.5, z: 0 }, 600)
.easing(TWEEN.Easing.Quadratic.InOut);
const positionTween = new TWEEN.Tween(model.position)
.to({ x: 0, y: posIncrement * -10, z: 0 }, 650)
.easing(TWEEN.Easing.Quadratic.InOut)
.onComplete(() => {
isMoving = false;
controls.update();
renderer.render(scene, camera);
controls.target.set(0, 0, 0);
});
cameraTween.start();
rotationTween.start();
positionTween.start();
}
animate();
document.getElementById('progressWrapper').style.display = 'none';
};
// Load the model and execute loadingClear after loading is complete
async function init() {
try {
const loadedModel = await loadModel();
loadingClear(loadedModel);
} catch (error) {
console.error('Error loading the model:', error);
}
}
init(); // Call the initialization function to load the model and perform actions after loading
window.onload = function() {
// Render onload
TWEEN.update(); // Update the Tween.js animations
renderer.render(scene, camera);
// everything is done - remove progress bar
}