Introduzione

Three.js ci offre diversi strumenti con cui possiamo controllare i movimenti della camera ascoltando gli input di mouse e tastiera che arrivano dall’utente.

Questi sono sicuramente il modo più veloce e comodo per gestire i movimenti della camera e vengono ampiamente utilizzati dato che, con pochissime righe di codice, ci forniscono numerose funzionalità, alcune di questi non banali da ricreare, soprattutto per chi è alle prime armi con javascript.

Ecco l'elenco che troviamo sulla documentazione di three.js:

  • ArcballControls
  • DragControls
  • FlyControls
  • FirstPersonControls
  • OrbitControls
  • MapControls
  • PointerLockControls
  • TrackballControls
  • TransformControls

DragControls e TransformControls non vengono utilizzati per muovere la camera, ma per gestire i movimenti degli altri oggetti nella scena, quindi non li tratteremo in questo articolo.

Sentiti libero di saltare direttamente alla sezione che più interessa.

Cominciamo

Innanzi tutto sistemiamo la scena in modo da avere un po' di elementi che ci aiutino a capire come ogni controls funzioni.

Modifichiamo queste righe

/**
* Cube
*/
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshNormalMaterial()

const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)

e, al posto di un semplice cubo, creiamo una griglia di parallelepipedi di altezza variabile.

/**
* Manhattan
*/
const material = new THREE.MeshNormalMaterial()

const size = 6

for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
const height = Math.random() * 4 + 1

const geometry = new THREE.BoxGeometry(1, height, 1)

const mesh = new THREE.Mesh(geometry, material)
mesh.position.set(-size + i * 2, height / 2, -size + j * 2)
scene.add(mesh)
}
}

E sposiamo la camera in modo da inquadrare bene la scena.

camera.position.set(8, 8, 8)
camera.lookAt(new THREE.Vector3())

Il risultato dovrebbe essere questo, ma potete generare una griglia diversa aumentando o diminuendo il valore della variabile size.

Ora possiamo vedere in azione i nostri controls.

ArcballControls

Ci permette di controllare la camera tramite una trackball virtuale, supportando anche gli input touch, fornendoci funzionalità avanzate di navigazione.

Importiamo la classe ArcballControls all'inizio del file e dopo aver creato la camera e il renderer aggiungiamo queste righe di codice.

import './style.css'
import * as THREE from 'three'
import { ArcballControls } from 'three/examples/jsm/controls/ArcballControls'

...

/**
* ArcballControls
*/
const controls = new ArcballControls(camera, renderer.domElement, scene)

Così facendo vedremo comparire 3 circonferenze che identificano la trackball virtuale.

A questo punto saremo in grado di effettuare diversi azioni, come ruotare intorno all'oggetto e traslarlo. Possiamo usare i tasti del mouse e la rotella in combinazione ai tasti CTRL e SHIFT.

È interessante notare che tenendo premuto SHIFT si può usare la rotella del mouse per modificare il FOV della camera con un effetto vertigine che può portare da una vista prospettica ad una ortogonale quasi perfetta circa equiparabile all'utilizzo di una OrthographicCamera.

È possibile disabilitare totalmente o anche solo alcuni funzionalità tramite le rispettive proprietà:

  • enabled (default true)
  • enablePan (default true)
  • enableRotate (default true)
  • enableZoom (default true)

O addirittura impostare dei limiti minimi e massimi per lo zoom e la distanza del dolly a seconda del tipo di camera utilizzata.

Inoltre è importante ricordare in invocare il metodo controls.update() se si applicano alla camera delle trasformazioni. Ecco un esempio:

/*
* controls.update() must be called after any manual changes
* to the camera's transform
*/
camera.position.set( 0, 20, 100 );
controls.update();

Tutti i dettagli li trovi sulla documentazione: https://threejs.org/docs/#examples/en/controls/ArcballControls

FlyControls

Ci permette muoverci nello spazio 3D come se stessimo volando. In base alla posizione del cursore la camera verra ruotata e possiamo muoverci avanti e indietro rispettivamente con i pulsanti sinistro e destro del mouse.

Modifichiamo il nostro codice in questo modo.

import './style.css'
import * as THREE from 'three'
import { FlyControls } from 'three/examples/jsm/controls/FlyControls'

...

/**
* FlyControls
*/
const controls = new FlyControls(camera, renderer.domElement)


/**
* Three js Clock
*/
const clock = new THREE.Clock()

/**
* frame loop
*/
function tic() {
/**
* tempo trascorso dal frame precedente
*/
const deltaTime = clock.getDelta()

// update controls
controls.update(deltaTime)

renderer.render(scene, camera)

requestAnimationFrame(tic)
}

Facciamo attenzione che questa volta dobbiamo invocare il metodo controls.update(…) all'interno del nostro frame loop, e a questo metodo dobbiamo passare il deltaTime, ovvero il tempo trascorso tra questo frame e quello precedente.

A questo punto possiamo pilotare la nostra camera:

  • rotazione della camera: muovendo il mouse oppure usando le frecce ←, ↑, ↓, →. ( rotazione sull’asse X, Y )
  • traslazione sinistra / destra: tasti A, D
  • traslazione avanti / indietro: tasti W, S, click sinistro / click destro mouse
  • traslazione su / giu: tasti R, F
  • rollio della camera: tasti Q, E ( rotazione sull’asse Z )
  • Con Q, ed E invece si fa rollare ( ruotare sull'asse Z )la camera.

Possiamo modificare sia la velocità di movimento che la velocità del rollio oltre a poter impostare il movimento automatico della camera con le proprietà:

  • movementSpeed
  • rollSpeed
  • autoForward
// velocità di movimento
controls.movementSpeed = 2

// velocità del rollio (rotazione asse Z)
controls.rollSpeed = 0.5

// movimento avanti automatico
controls.autoForward = true

Tenere sempre attivo l'autoForward è abbastanza scomodo ma potremmo decidere di attivarlo e disattivarlo usando la barra spaziatrice aggiungendo queste poche riche.


// attiva / disattiva autoForward con barra spaziatrice
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
controls.autoForward = !controls.autoForward
}
})

Tutti i dettagli li trovi sulla documentazione: https://threejs.org/docs/#examples/en/controls/FlyControls

FirstPersonControls

Come dice il nome ci permette di muoverci in prima persona ed è una implementazione alternative del FlyControls. Rispetto a quest'ultimo perdiamo la possibilità di rollare ma guadagniamo un po' più di controllo nel limitare o impedire la rotazione orizzontale o verticale.

L'implementazione è simile al FlyControls infatti, una volta importato e creato il controls, anche in questo caso dobbiamo invocare il metodo controls.update( deltaTime ) all'interno del frame loop.

import './style.css'
import * as THREE from 'three'
import { FirstPersonControls } from 'three/examples/jsm/controls/FirstPersonControls'

...

/**
* FirstPersonControls
*/
const controls = new FirstPersonControls(camera, renderer.domElement)

/**
* Three js Clock
*/
const clock = new THREE.Clock()

/**
* frame loop
*/
function tic() {
/**
* tempo trascorso dal frame precedente
*/
const deltaTime = clock.getDelta()

// update controls
controls.update(deltaTime)

renderer.render(scene, camera)

requestAnimationFrame(tic)
}

In aggiunta dobbiamo anche invocare il metodo controls.handleResize() nel caso la viewport del browser venisse ridimensionate, quindi lo faremo all'interno del nostro event listener.

...

window.addEventListener('resize', onResize)

function onResize() {
...

// handle resize
controls.handleResize()
}

Anche i controlli sono simili fatta eccezione per le frecce che hanno la stessa funzione dei tasti A, W, S, D.

I tasti Q ed E non ci fanno più rollare mentre R ed F continuano a muovere la camera su e giu.

La rotazione della camera viene modificata esclusivamente dalla posizione del cursore del mouse all'interno della viewport.

Possiamo modificare la velocità di movimento e la velocità di rotazione della camera con le proprietà movementSpeed (default 1 ) e lookSpeed (default 0.005 ).

// aumenta velocità di movimento
constrols.movementSpeed = 2

// aumenta velocità rotazione camera
controls.lookSpeed = 0.03

La novità rispetto al FlyControls è che questa volta possiamo impedire o limitare l'inclinazione verticale.

// impedisce inclinazione verticale
controls.lookVertical = false
// limita gli angoli di inclinazione verticale
controls.verticalMin = Math.PI * 0.25
controls.verticalMax = Math.PI * 0.75

// ma occorre impostare a true la prop constrainVertical
controls.constrainVertical = true

Infine abbiamo la possibilità di alterare la velocità di movimento in base al valore della coordinata Y della camera. Questa modifica avviene solo quando la camera si muove in avanti e non per le altre direzioni.

Vediamo un esempio che possa chiarire il funzionamento.

// attiviamo la modalità heightSpeed
constrols.heightSpeed = true

// impostaimo i due valori di altezza minima e massima
// in cui la velocità verra modificata
constrols.heightMin = 0
constrols.heightMax = 10

// impostiamo il coefficiente
constrols.heightCoef = 2

In questo esempio quando la camera si trova alla coordinata y ≤ 0 la sua velocità di movimento sarà uguale a movementSpeed.

Quando invece la coordinata y ≥ 10, la sua velocità è uguale a movementSpeed + ( 10 × heightCoef ).

All'interno di questi due valori la velocità varia in maniera lineare al variare della coordinata y della camera.

Tutti i dettagli li trovi sulla documentazione: https://threejs.org/docs/#examples/en/controls/FirstPersonControls

OrbitControls

Come il nome vuole farci intuire questo controls ci permette di ruotare attorno ad un oggetto, o meglio attorno ad un target. È decisamente il pìu utilizzato, e la maggior parte degli esempi che trovi sulla documentazione di three.js adottano questo controls.

Aggiugiamolo al nostro codice e poi vediamo che possibilità di settaggio ci offre.

import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

...

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

/**
* Three js Clock
*/
const clock = new THREE.Clock()

/**
* frame loop
*/
function tic() {

...

// update controls only if auroRotare or enableDamping are set
controls.update()

renderer.render(scene, camera)

requestAnimationFrame(tic)
}

Rotazione automatica

Possiamo impostare la rotazione automatica della nostra camera modificandone anche la velocità.

Questo richiede di invocare il metodo controls.update() nel frame loop , ma a differenza degli altri controls che abbiamo visto precedentemente non richiede il deltaTime.

// attiva la rotazione automatica
controls.autoRotate = true
// imposta la velocità di rotazione
// valori negativi invertono la dirazione della rotazione
controls.autoRotateSpeed = 3.0

...

/**
* frame loop
*/
function tic() {
...

// update controls only if auroRotare or enableDamping are set
controls.update()

...
}

Damping (smorzamento)

Possiamo rendere la rotazione più piacevole aggiungendo un po di inerzia abilitando il damping.

// attiva il damping
controls.enableDamping = true
controls.dampingFactor = 0.07

E possiamo regolare questo effetto modificando il valore della proprietà dampingFactor (default 0.05). Valori che tendono allo 0 ammorbidiscono il movimento rendendolo più lento, mentre valori vicini a 1 fanno l'opposto.

Pan

Il pan è la traslazione della camera che possiamo effettuare tenendo premuto il testo destro del mouse e muovendo il cursore.

Possiamo modificare la velocita di movimento della camera quando usiamo il pan.

// default 1
controls.panSpeed = 2

Il pan può essere disabilitato.

// disattiva il pan
controls.enablePan = false

Inoltre possiamo cambiare il piano sul quale avviene la traslazione per opera del pan. Per farlo dobbiamo modificare il valore della proprietà screenSpacePanning (boolean: true). Di default viene utilizzato il piano dello schermo per traslare la camera.

controls.screenSpacePanning = false

Impostando il valore a false la traslazione effettuata dal pan avviene sul pipano orizzontale. Soluzione molto adottata quando vogliamo spostarci sopra ad un paesaggio o una mappa senza ruotare la camera ne cambiare altitudine.

Infine è possibile attivare il pan anche con i comandi da tastiera.

controls.listenToKeyEvents( window )

Di default le keys registrate per controllare la camera sono le seguenti

controls.keys = {
LEFT: 'ArrowLeft', //left arrow
UP: 'ArrowUp', // up arrow
RIGHT: 'ArrowRight', // right arrow
BOTTOM: 'ArrowDown' // down arrow
}

Ma possiamo modificarle a piacimento, per esempio impostanto i stasti A, W, S, D.

controls.keys = {
LEFT: 'KeyA',
UP: 'KeyW',
RIGHT: 'KeyD',
BOTTOM: 'KeyS',
}

Zoom

Possiamo disattivare lo zoom con la proprietà enableZoom.

// disattiva lo zoom
controls.enableZoom = false

Possiamo anche modificarne la velocità con zoomSpeed (default 1).

// dimezza la velocità del dolly
controls.zoomSpeed = 0.5

E a seconda del tipo di camera che stiamo utilizzando abbiamo diversi modi per limitarne i valori minimi e massimi.

Per la PerspectiveCamera dobbiamo utilizzare le proprietà minDistance (default 0 )e maxDistance (default infinito) che definiscono quanto vicino e quanto lontano può arrivare il nostro dolly.

controls.minDistance = 2
controls.maxDistance = 20

Nel caso stessimo usando una OrthographicCamera allora dovremmo usare le proprietà minZoom (default 0) e maxZoom( default infinito).

Rotation

Anche la rotazione può essere disattiva con la proprietà enableRotate.

controls.enableRotate = false

Possiamo per esempio impostare solo la rotazione automatica impedendo all'utente il controllo sulla rotazione.

Come per le altre funzionalità la proprietà rotateSpeed ( default 1 ) ci permette di modificare la velocità di rotazione controllata con il mouse.

controls.rotateSpeed = 2

In aggiunta possiamo anche limitare sia l'angolo polare (angolo verticale) e anche l'angolo azimutale (angolo orizzontale) con le proprietà minPolarAngle, maxPolarAngle, minAzimuthAngle, maxAzimuthAngle.

L'angolo polare più variare in un range da 0 a 180 gradi (Pi greco), mentre l'angolo azimutale non è limitato.

Limitiamo quindi l'inclinazione verticale tra -45 e 45 gradi.

controls.minPolarAngle = Math.PI * 0.25
controls.maxPolarAngle = Math.PI * 0.75

E la rotazione orizzontale tra -90 e + 90 gradi.

controls.minAzimuthAngle = -Math.PI * 0.5
controls.maxAzimuthAngle = Math.PI * 0.5

Tali limitazioni influenzano la rotazione anche in caso di rotazione automatica della camera.

Target

Infine possiamo impostare e aggiornare il target ovvero il punto focale attorno al quale ruota la nostra camera e verso il quale la camera guarda. Tale valore è un Vector3 con le coordinate del punto di riferimento da utilizzare, di default x = 0, y = 0, z = 0. Abbiamo visto come possiamo modificare un vettore posizione in questo articolo.

// modifcando le singole coordinare
controls.target.y = 2

// copiando i valori da un altro vettore
controls.target.copy( mesh.position )

Dopo aver modificato il target è necessario invocare il metodo controls.update() se non lo stiamo già facendo nel frame loop.

Tutti i dettagli li trovi sulla documentazione: https://threejs.org/docs/#examples/en/controls/OrbitControls

MapControls

Indicata soprattutto per muovere la camera sopra ad una mappa dove si vuole avere una vista a volo d'uccello.

import './style.css'
import * as THREE from 'three'
import { MapControls } from 'three/examples/jsm/controls/MapControls'

...

/**
* MapControls
*/
const controls = new MapControls(camera, renderer.domElement)

/**
* Three js Clock
*/
const clock = new THREE.Clock()

/**
* frame loop
*/
function tic() {

...

// update controls only if auroRotare or enableDamping are set
controls.update()

renderer.render(scene, camera)

requestAnimationFrame(tic)
}

La classe MapControls estende la classe OrbitControls, quindi ne eredita metodi e proprietà, ma oltre a disabilitare lo screen space panning, imposta un preset diverso per gli eventi con mouse e touch screen.

Infatti a differenza dell'OrbitControls, il pan viene effettuato con il click sinistro del mouse (e con il singolo touch), mentre con il tasto destro viene effettuata la rotazione (o con il doppio touch) come possiamo vedere in seguito.

controls.mouseButtons = {
LEFT: THREE.MOUSE.PAN,
MIDDLE: THREE.MOUSE.DOLLY,
RIGHT: THREE.MOUSE.ROTATE
}

controls.touches = {
ONE: THREE.TOUCH.PAN,
TWO: THREE.TOUCH.DOLLY_ROTATE
}

Tutti i dettagli li trovi sulla documentazione: https://threejs.org/docs/#examples/en/controls/MapControls

PointerLookControls

Basato sulla Pointer Lock API è la scelta perfetta per i giochi 3D in prima persona. A differenza degli altri controls che una volta creati erano già funzionanti e al massimo ci limitavamo a cambiare qualche parametro, in questo caso l'implementazione è lasciamo in mano a noi e dipende molto caso per caso.

Per prima cosa diciamo che questa classe estende una classe chiamata EventDispatcher la quale gli permette di poter ascoltare degli eventi ed invocare delle funzioni (listener) quando tali eventi vengono emessi.

Per poter attivare questo controls occorre che l'utente abbia interagito con la pagina facendo click da qualche parte, magari su un pulsante play per esempio, e successivamente possiamo invocare il metodo controls.lock().

Mentre è possibile disattivarlo utilizzando il metodo controls.unlock().

import './style.css'
import * as THREE from 'three'
import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls'

...

/**
* PointerLockControls
*/
const controls = new PointerLockControls(camera, document.body)

...

/*
* lock controls
*/

window.addEventListener('click',function() {
// cliccando sulla window attiviamo il controls se non è attivo
if(!controls.isLocked) {
constrols.lock()
}
})

È possibile disattivarlo anche premendo il tasto Esc della tastiera.

Una volta attivato scomparirà il cursore e potremo guardarci intorno muovendo il mouse anche oltre i limiti della finestra del browser senza perdere il focus dalla scena.

Per poterci muovere possiamo utilizzare due metodi: moveForward(…) per muoverci avanti e indietro e moveRight(…) con possiamo muoverci lateralmente.

A questi metodi dobbiamo passare come parametro la distanza di quanto vogliamo muoverci.

// muove avanti di 1
controls.moveForward( 1 )
// muove indietro di 1
controls.moveForward( -1 )

// muove verso destra di 1
controls.moveRight( 1 )
// muove vverso sinistra di 1
controls.moveRight( -1 )

Questa volta il controls non ci permette di muoverci verticalmente e mantiene ancorati a terra. L’implementazione del salto richiede la conoscenza della fisica e di come le forze interagiscono sul moto di un corpo avente un certa massa, per non parlare del fatto che dovremmo anche gestire le collisioni con gli oggetti sui quali atterriamo. Quindi ne parleremo in un articolo dedicato.

Vediamo un modo rapido e veloce per implementare i movimenti sul piano orizzontale usando i tasti AWSD.

/**
* velocità di movimento
*/
const vel = new THREE.Vector3(0, 0, 0)

...

/**
* frame loop
*/
function tic() {
/**
* tempo trascorso dal frame precedente
*/
const deltaTime = clock.getDelta()

if (controls.isLocked) {
// muovo il controls in base alle velocità lungo gli assi X e Z
controls.moveForward(vel.z * deltaTime)
controls.moveRight(vel.x * deltaTime)
}

renderer.render(scene, camera)

requestAnimationFrame(tic)
}

Aggiungiamo i listener per manipolare la velocità di movimento.

window.addEventListener('keydown', function (e) {
// in base al tasto premuto imposto la velocità
switch (e.code) {
case 'KeyA':
case 'ArrowLeft':
vel.x = -1
break
case 'KeyD':
case 'ArrowRight':
vel.x = 1
break
case 'KeyW':
case 'ArrowUp':
vel.z = 1
break
case 'KeyS':
case 'ArrowDown':
vel.z = -1
break
}
})


window.addEventListener('keyup', function (e) {
// quando uno dei tasti viene rilasciato
// riporto la velocità corrispondente a 0
switch (e.code) {
case 'KeyA':
case 'ArrowLeft':
case 'KeyD':
case 'ArrowRight':
vel.x = 0
break
case 'KeyW':
case 'ArrowUp':
case 'KeyS':
case 'ArrowDown':
vel.z = 0
break
}
})

Infine potremmo limitare l'angolo di rotazione polare come per l'OrbitControls utilizzando le proprietà minPolarAngle (default 0), maxPolarAngle (default Math.PI, 180 gradi).

Tutti i dettagli li trovi sulla documentazione: https://threejs.org/docs/#examples/en/controls/PointerLockControls

TrackballControls

Simile all'OrbitControls con la differenza che l'angolo polare non è limitato, permettendoci di ruotare attorno agli oggetti anche oltre i poli, e che è diversa la nomenclatura di alcuni metodi e proprietà.

import './style.css'
import * as THREE from 'three'
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls'

...

/**
* TrackballControls
*/
const controls = new TrackballControls(camera, renderer.domElement)

/**
* frame loop
*/
function tic() {

...

// update controls only if auroRotare or enableDamping are set
controls.update()

renderer.render(scene, camera)

requestAnimationFrame(tic)
}

Per esempio invece di enableZoom, enablePan ed enableRotate con valore di default 1 abbiamo le proprietà:

  • noPan (default false)
  • noZoom (default false)
  • noRotate (default false)

Il damping è impostato di default con la proprietà staticMoving uguale a false. Mentre per modificare il fattore del damping occorre modificare la proprietà dynamicDampingFactor (default 0.2).

controls.dynamicDampingFactor = 0.05

Possiamo infine modificare le velocità di rotazione, del pan e dello zoom e di quest'ultimo impostarne il limite minimo e massimo in base al tipo di camera utilizzato (vedi OrbitControl o documentazione).

Tutti i dettagli li trovi sulla documentazione: https://threejs.org/docs/#examples/en/controls/TrackballControls

Conclusioni

Abbiamo sviscerato tutti gli strumenti che three.js ci mette a disposizione per controllare la nostra camera e ci manca da vedere come gestire i movimenti della camera in maniera custom. Infatti ci sono tantissimi altri modi per muovere la camera, per esempio usando lo scroll della pagina per muoverla lungo un percorso. O addirittura muoverla lungo la superficie curvilinea di qualche nostro oggetto, … insomma anche in questo caso, l'unico limite è la creatività.

Ti invito a provare i vari controls per capirne realmente le potenzialità e i limiti di questi strumenti.

Nel prossimo articolo approfondiremo vediamo appunto come gestire in maniera custom il controllo della camera. Vedremo diversi input che possiamo usare e molteplici strategie per muovere la camera.

--

--

Gianluca Lomarco

I am a creative developer working with webGL and fullstack Main teacher at Boolean Academy. Now I’am starting a new experience as content creator in YouTube