Three.js — Custom Camera Controls

Gianluca Lomarco
18 min readJun 13, 2023

--

Nel precedente articolo abbiamo visto tutti i controls presenti in three.js per controllare la camera.

Capita spesso che, per un motivo o per un altro, nessuno di questi faccia al caso nostro. Dobbiamo quindi essere in grado di implementare una soluzione custom. Vediamo alcuni esempi di come possiamo muovere la camera a seconda di diversi input che possono arrivare dall'utente.

Questo è il codice da cui partiremo.

import './style.css'
import * as THREE from 'three'

/**
* Scene
*/
const scene = new THREE.Scene()

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

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

/**
* render sizes
*/
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
}
/**
* Camera
*/
const fov = 60
const camera = new THREE.PerspectiveCamera(fov, sizes.width / sizes.height, 0.1)
camera.position.set(0, 0, 4)

/**
* renderer
*/
const renderer = new THREE.WebGLRenderer({
antialias: window.devicePixelRatio < 2,
logarithmicDepthBuffer: true,
})
renderer.setSize(sizes.width, sizes.height)

const pixelRatio = Math.min(window.devicePixelRatio, 2)
renderer.setPixelRatio(pixelRatio)
document.body.appendChild(renderer.domElement)

/**
* muovo indietro la camera
*/
// camera.position.z = 4

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

/**
* frame loop
*/
function tic() {
/**
* tempo trascorso dal frame precedente
*/
const deltaTime = clock.getDelta()
/**
* tempo totale trascorso dall'inizio
*/
// const time = clock.getElapsedTime()

renderer.render(scene, camera)

requestAnimationFrame(tic)
}

requestAnimationFrame(tic)

window.addEventListener('resize', onResize)

function onResize() {
console.log('resize')
sizes.width = window.innerWidth
sizes.height = window.innerHeight

camera.aspect = sizes.width / sizes.height
camera.updateProjectionMatrix()

renderer.setSize(sizes.width, sizes.height)

const pixelRatio = Math.min(window.devicePixelRatio, 2)
renderer.setPixelRatio(pixelRatio)
}

Input dell'utente

Se stiamo customizzando il controllo della camera per prima cosa dobbiamo fare un ragionamento su quali saranno gli input che ci arrivano dall'utente, e successivamente su come la camera, o la scena, deve reagire a tali input.

In questo articolo ci limiteremo agli input che possono arrivare dal mouse o dalla tastiera che non sono gli unici, ma sicuramente sono i più frequenti che ci troveremmo a dover gestire.

Mouse

Come abbiamo visto nei controls già presenti in three.js, il mouse, così come il touch, viene utilizzato per spostare la camera (il pan) o per la rotazione della camera attorno ad un punto di riferimento.

Questo avviene ascoltando il movimento del mouse e andando a recuperare le coordinate di quest'ultimo rispetto alla viewport del browser. Aggiungiamo il fondo al file queste righe.

window.addEventListener('mousemove', (event) => {
// print mouse coordinates inside window
console.log(event.clientX, event.clientY)
})

Aprendo l'inspector e muovendo il mouse all'interno della finestra del browser vedremo nella console i nostri log con le coordinate del cursore.

All'interno della variabile event trovate delle proprietà legate alla posizione del cursore e oltre a clientX e clientY ci sono anche: pageX, pageY, screenX e screenY. Non sono la stessa cosa e qui sotto potete vedere le differenze.

Sistema di riferimento

Le proprietà clientX e clientY sono le coordinate del cursore ed esprimono in pixel rispettivamente le distanze dal bordo sinistro e dal bordo superiore della viewport del browser, come riportato nell'immagine sottostante.

Quindi mentre il valore minimo di queste sarà sempre 0, il valore massimo invece dipende dalla dimensione della finestra del browser. La coordinata x del cursore potrebbe andare da 0 a 450 pixel su un dispositivo mobile, e da 0 a 1920 pixel o anche di più su desktop.

Risulta molto scomodo quindi utilizzare questi valori cosi come sono, dobbiamo in qualche modo renderli omogenei per tutti gli utenti a prescindere dal dispositivo che utilizzeranno.

Normalizziamo le coordinate

Quando parliamo di normalizzazione riferito alle coordinate, e quindi ad un range generico che può andare da un valore A ad un valore B, ci riferiamo al fatto di trasformare i valori di quelle coordinate in modo va si trovino in un range che va da 0 a 1.

Facciamo in modo che quando il cursore è posizionato nell'angolo in alto a sinistra le sue coordinate siano x = 0 e y = 0. Mentre quando si trova nell'angolo in basso a destra le sue coordinate siano x = 1 e y = 1.

Per farlo è sufficiente dividere la proprietà clientX per la larghezza della viewport e la proprietà clientY per l'altezza della viewport.

x = event.clientX / window.innerWidth

y = event.clientY / window.innerHeight

Abbiamo risolto un primo problema ma adesso se sorge un secondo, ovvero che il sistema di riferimento utilizzato per le coordinate del cursore non è allineato con il sistema di riferimento del nostro spazio 3D.

Infatti per il cursore abbiamo gli assi X e Y che si incontrano nell'angolo in alto a sinistra della finestra mentre il sistema di riferimento dello spazio 3D si trova centrato nello schermo.

Inoltre l'orientamento dell'asse Y del cursore è invertito rispetto all'asse Y dello spazio.

Quindi ci serve centrare i due sistemi di riferimento e uniformare le direzioni degli assi. Le formule per trasformare le coordinate del cursore da un sistema di rifermento all'altro sono le seguenti:

x = 2 * clientX / window.innerWidth — 1

y = — 2 * clientY / window.innerHeight + 1

Ed il risultato sarà questo.

Quando il cursore si trove al centro avrà coordinate (0, 0), quando si trova in alto a sinistra avrà coordinate (-1, 1) e quando si troverà in basso a destra le coordinate saranno (1, -1).

Traduciamolo in codice. Creiamoci all'inizio del file una variabile mouse dove memorizzeremo le coordinate del mouse normalizzate, useremo un Vector2 con i valori x = 0 e y = 0.

import './style.css'
import * as THREE from 'three'

/**
* mouse
*/
const mouse = new THREE.Vector2(0,0)

...

All'interno dell'event listener leggiamo le coordinate del cursore a trasformiamo questi valori utilizzando le formule viste poco fa.

window.addEventListener('mousemove', (event) => {
...

mouse.x = (2 * event.clientX) / window.innerWidth - 1
mouse.y = (-2 * event.clientY) / window.innerHeight + 1
// print normalized mouse coordinates
console.log(mouse.x, mouse.y)
})

E nella console vediamo che i valori di x e y si trovano sempre nel range -1, 1.

Utilizzare questi valori adesso è molto semplice, basta decidere che cosa andare a manipolare e moltiplicare tali valori per l'intensità che si vuole ottenere.

Muovere la camera

Muoviamo la camera utilizzando le coordinate del cursore.

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

camera.position.x = mouse.x * 2
camera.position.y = mouse.y * 1.5

...
}

Queste due righe dentro alla funzione tic() assegnano le nuove coordinate della camera in base alle coordinate del cursore che cambiano man mano che lo muoviamo. Moltiplicando le coordinate x e y del cursore possiamo aumentare o diminuire l'intensità di questo spostamento.

Dato che stiamo spostando la camera, spostando il cursore sul lato destro, la camera si sposterà a destra, quindi vedremo il cubo muoversi nella direzione opposta.

Ma possiamo invertire la direzione moltiplicando le coordinare mouse.x e mouse.y per valori negativi.

camera.position.x = mouse.x * -2
camera.position.y = mouse.y * -1.5

Ruotare la camera

Un altro modo è quello di utilizzare le coordinate del mouse per ruotare la camera, un po' come avveniva nel caso del PointerLockControls.

Per esempio spostando il mouse verso destra vogliamo che la camera ruoti verso destra e viceversa se il mouse si trova a sinistra. Lo stesso per la verticale. Quando il mouse va verso l'altro vogliamo che la camera ruoti verso l'alto.

Quindi la coordinata x del cursore gestisce la rotazione attorno all'asse Y, mentre la coordinata y gestisce la rotazione attorno all'asse X. In questo esempio limitiamo la rotazione a +/- 45 gradi orizzontalmente e +/- 30 gradi verticalmente. Ecco come fare quindi.

function tic() {
...

camera.rotation.y = -mouse.x * Math.PI * 0.25
camera.rotation.x = (mouse.y * Math.PI) / 6

...
}

Facciamo attenzione ai segni ricordandoci che le rotazioni positive seguono la regola della mano destra, quindi posiziona il pollice della mano destra orientato lungo l'asse di rotazione e curva le altre dite della mano. Queste indicano la direzione della rotazione positiva.

Orbita della camera

Un po' più complicato è fare orbitare la camera attorno ad un punto. Vediamo un paio di modi per farlo.

Iniziamo con un orbita piana, ovvero muoviamo la camera facendogli compiere un movimento circolare, ad una distanza R = 4 dal centro, sul piano orizzontale identificato dagli assi X e Z.

La rotazione quindi avverrà attorno all'asse Y e useremo la coordinata x del cursore per incrementare o decrementare l'ampiezza dell'angolo.

Quindi per ogni angolo ɑ dobbiamo trovare le nuove coordinate della camera e successivamente occorre ruotare la camera nel suo sistema di riferimento locale in modo che guardi il cubo al centro della scena.

Per trovare le coordinate sfrutteremo le nostre conoscenze di trigonometria, quindi stando allo schema riportato sotto, le nuove coordinate della camera saranno:

Px = R * sin( ɑ )

Pz = R * cos( ɑ )

Per orientare la camera in direzione del cubo possiamo utilizzare il metodo lookAt( … ) passandogli le coordinate del cubo, o del centro dello spazio 3D.

Traduciamolo in codice e dentro alla funzione tic() scriviamo:

function tic() {
...

// angle change between -180 e +180 degrees
const angle = mouse.x * Math.PI

camera.position.x = 4 * Math.sin(angle)
camera.position.z = 4 * Math.cos(angle)

camera.lookAt(mesh.position)

...
}

Muovendo il cursore del mouse a destra e sinistra dello schermo vedrai il cubo ruotare, in realtà è la camera che gli sta girando intorno.

Allo stesso modo possiamo far ruotare la camera sul piano verticale ci basterà utilizzare la coordinata y del cursore per cambiare l'angolo di rotazione, e modificare le coordinate y e z della camera. Ma dovremmo fare attenzione a due cose:

  • limitare l'angolo in modo che non superi i +/- 90 gradi.
  • usare l'angolo invertito di segno altrimenti la rotazione sarà opposta al movimento del cursore
// angolo invertito e limitato 
const angle = -mouse.y * Math.PI * 0.5

camera.position.y = 4 * Math.sin(angle)
camera.position.z = 4 * Math.cos(angle)

camera.lookAt(mesh.position)

Coordinate polari

Gestire due angoli di rotazione della camera non è un problema banale e non possiamo utilizzare il metodo usato nei precedenti esempi. Ma possiamo utilizzare il metodo delle coordinate polari. Tale metodo ci permette tramite due angoli di rotazione, orizzontale e verticale di trovare le coordinate di un punto su una sfera di raggio noto.

Se pensiamo al sistema di geolocalizzazione terreste, il procedimento è simile, tramite longitudine e latitudine possiamo trovare una località sul pianeta terra. Latitudine e longitudine sono due coordinate polari. Però mentre la latitudine è calcolata a partire dall'equatore terreste e i suo valori vanno da -90 a +90 gradi, nel nostro caso l'angolo β parte del polo assumerà valori da 0 a 180 gradi.

Fortunatamente non dobbiamo preoccuparti troppo dei calcoli matematici dietro a questo modello. Infatti three.js ci mette a disposizione un modo molto semplice per creare un vettore con le coordinate x, y e z di un punto su una sfera di raggio R conoscendo le sue coordinate polari ɑ, β.

position.setFromSphericalCoords(R, β, ɑ)

// create a new Vector3
const position = new THREE.Vector3()

// set the vector from sperical coordinates
position.setFromSphericalCoords(radius, angleB, angleA)

Quindi spostiamoci nella nostra funzione tic() e calcoliamoci gli angoli ɑ e β.

function tic() {
...

const angleA = -mouse.x * Math.PI
const angleB = (mouse.y * 0.5 + 0.5) * Math.PI
const radius = 4

const position = new THREE.Vector3()
position.setFromSphericalCoords(radius, angleB, 0)

camera.position.copy(position)

...
}

Per l'angolo ɑ ci basta invertire il valore della coordinate x del cursore e moltiplicare per Math.PI, quindi spostando il cursore verso destra (coordinata x positiva ) la camera ruoterà in senso orario (rotazione negativa).

Per l'angolo β dobbiamo fare attenzione che quest'ultimo deve partire da 0 arrivare a 180 gradi, ma la coordinata y del cursore varia da -1 a 1.

Quindi prima scaliamo e trasliamo il valore della coordinata y del cursore in questo modo:

(mouse.y * 0.5 + 0.5)

cosi da riportarla in un range che va da 0 a 1 e successivamente la moltiplichiamo per Math.PI.

const angleB = (mouse.y * 0.5 + 0.5) * Math.PI

Quindi se il cursore è al centro dello schermo (mouse.y = 0), β sarà uguale a 90 gradi, quindi ci troveremo sull'equatore della nostra spera. Se il cursore va in basso ci avvicineremo al polo nord (angolo che tende allo 0), se il cursore si trova in alto la camera si avvicina al polo sud (angolo che tende a 180 gradi).

// se mouse.y = 0 => angleB = 0.5 * Math.PI  => 90 gradi
// se mouse.y = -1 => angleB = 0
// se mouse.y = 1 => angleB = (0.5 + 0.5) * Math.PI. => 180 gradi

Ecco che abbiamo implementato la nostra camera orbitale. Possiamo modificare a piacimento ogni parametro per personalizzare il suo comportamento. Ma adesso parliamo di zoom.

Zoom

Dato che non abbiamo ancora visto in azione la OrthographicCamera, limitiamo a vedere come possiamo gestire lo zoom per la PerspectiveCamera.

Con la PerspectiveCamera possiamo gestire lo zoom in 3 modi diversi, ovvero modificando:

  • la distanza tra la camera e l'oggetto (tecnicamente non è uno zoom)
  • la proprietà zoom
  • la proprietà FOV (field of view)

Qualunque sia il metodo utilizzato, modificheremo questi valori utilizzando la rotella del mouse. Aggiungiamo un eventListener che ascolti. l'event wheel.

window.addEventListener('wheel', (event) => {
// print the delta Y scroll amount
console.log(event.deltaY)
})

La proprietà event.deltaY ci fornisce sia la direzione che l'entità dello scroll e dovremmo vedere nella console valori simili a questi.

Anche in questo caso conviene trovare un modo per normalizzare il valore con cui controlleremo lo zoom.

Io lo farò in questo modo. Creiamo una variabile zoomFactor = 0 che dovrà poter variare in un range da -1 a 1, in maniera simile alle coordinate del cursore.

...

/**
* mouse
*/
const mouse = new THREE.Vector2()

/**
* Zoom
*/
let zoomFactor = 0

...

All'interno dell'eventListener usiamo deltaY per incrementare o decrementare la variabile appena creata. Ma la proprietà deltaY, come si vede dall'immagine in alto può assumere valori anche moto alti, quindi non possiamo sommarla direttamente a zoomFactor, dobbiamo prima trasformarla in un numero più piccolo di 1. Potremmo per esempio dividerla per 1000.

window.addEventListener('wheel', (event) => {
// print the delta Y scroll amount
console.log(event.deltaY)

zoomFactor += event.deltaY / 1000
})

Tra poco vedremo come gestire la velocità dello zoom, per adesso preoccupiamoci di fare in modo zoomFactor non possa diventare più piccolo di -1 o più grande 1. Infatti ora come ora, scrollando più volte nella stessa direzione la variabile zoomFactor uscirebbe dal nostro range di riferimento.

Per fare questo useremo la funzione clamp che troviamo in three.js.

window.addEventListener('wheel', (event) => {
// print the delta Y scroll amount
console.log(event.deltaY)

zoomFactor += event.deltaY / 1000

zoomFactor = THREE.MathUtils.clamp(zoomFactor, -1, 1)
})

Questa funzione riceve come primo parametro il valore di zoomFactor incrementato del deltaY/1000, e due valori degli estremi, nel nostro caso -1 e 1.

Se zoomFactor è minore di -1, la funzione ritorna -1. Se invece fosse maggiore di 1 ritornerebbe il valore 1. Ritorna il valore di zoomFactor se questo è compreso tra -1 e 1.

Abbiamo quindi il nostro fattore di zoom normalizzato vediamo utilizzarlo.

Distanza

Abbiamo detto quindi che il primo modo in cui possiamo gestire lo zoom è banalmente avvicinando o allontanando al camera dal soggetto inquadrato.

Nell'esempio della camera orbitante la distanza non era altro che il raggio della sfera sulla cui superficie andavamo a muovere la camera. In questo esempio quindi ci basta usare la variabile zoomFactor per modificare il valore del raggio.

function tic() {

...

const angleA = -mouse.x * Math.PI
const angleB = (mouse.y * 0.5 + 0.5) * Math.PI
// radius change between 2 and 6
const radius = 4 + zoomFactor * 2

const pos = new THREE.Vector3().setFromSphericalCoords(radius, angleB, angleA)

...
}

FOV (field of view)

Un altro modo per zoommare è quello di aumentare o diminuire il field of view (FOV) della camera.

Riducendo l'angolo di apertura della camera si riduce anche la porzione di scena visibile che risulterà quindi ingrandita, senza modificare la distanza tra la camera e l'oggetto.

Ma cambiando il FOV della camera dobbiamo ricordarci anche di aggiornare la matrice di proiezione.

function tic() {

...

const angleA = -mouse.x * Math.PI
const angleB = (mouse.y * 0.5 + 0.5) * Math.PI
const radius = 4

const pos = new THREE.Vector3().setFromSphericalCoords(radius, angleB, angleA)

// change camera fov = 60 +/- 30 degrees
camera.fov = 60 + zoomFactor * 30
camera.updateProjectionMatrix()

...
}

Proprietà zoom

In realtà la camera ha una proprietà che si chiama appunto zoom con valore di default = 1 che viene utilizzato internamente dalla PerspectiveCamera per modificare il Field of View effettivo utilizzato per generare la matrice di proiezione. Anche modificando la proprietà zoom dobbiamo poi aggiornare la matrice di proiezione. Con valori minori di 1 aumenta il FOV della camera, mentre con valori maggiori di 1, diminuisce il FOV della camera. Quindi il risultato è simile a quando fatto con l'esempio precedente e possiamo implementarlo in questo modo.

function tic() {

...

const angleA = -mouse.x * Math.PI
const angleB = (mouse.y * 0.5 + 0.5) * Math.PI
const radius = 4

const pos = new THREE.Vector3().setFromSphericalCoords(radius, angleB, angleA)

// change zoom between 0.5 and 1.5
camera.zoom = 1 + zoomFactor * 0.5
camera.updateProjectionMatrix()

...
}

Velocità dello zoom

Per gestire comodamente la velocità dello zoom possiamo creare una variabile zoomSpeed = 0.25 e usare questo parametro per moltiplicare il event.deltaY/1000 prima di incrementarlo allo zoomFactor. Aumentando il valore aumenta la velocità dello zoom, mentre diminuendolo la velocità diminuisce.

...

/**
* Zoom
*/
let zoomFactor = 0
let zoomSpeed = 0.25
...


window.addEventListener('wheel', (event) => {
// print the delta Y scroll amount
console.log(event.deltaY)

zoomFactor += ( zoomSpeed * event.deltaY ) / 1000

zoomFactor = THREE.MathUtils.clamp(zoomFactor, -1, 1)
})

Drag & drop

Fino ad ora abbiamo utilizzato le coordinate del cursore senza la necessità di usare i tasti del mouse, infatti ascoltavamo l'evento mousemove. Ma ci potrebbe capitare spesso di dover controllare la camera usando il meccanismo del drag and drop. A parte il PointerLockControls infatti tutti gli altri controls di three.js sfruttano questo meccanismo, vediamo come implementarlo in maniera custom.

Il drag and drop funziona in questo modo.

L'utente clicca con il mouse in un punto generico A dello schermo e successivamente trascina il cursore in un altro punto B dello schermo per poi rilasciare il click.

Con l'evento mousemove prima noi andavamo ad assegnare le coordinate del cursore alle proprietà mouse.x e mouse.y.

In questo caso quali coordinate dobbiamo utilizzare? Le coordinate del punto A o del punto B?

Beh la risposta è nessuna delle due. Infatti per il drag end drop a noi non interessano ne le coordinate di A ne le coordinate di B, bensì ci interessa lo spostamento del cursore da A verso B. In parole povere ci serve sapere quali sono i valori di x e y necessari per passare dal punto A al punto B. Questi saranno i valori con cui andremo ad incrementare le proprietà mouse.x e mouse.y.

Trovare questi valori equivale a fare la differenza tra i due vettori posizione B — A. Le immagini sottostanti mostrano 3 casi di drag and drop consecutivi, il punto rosso centrale identifica come cambiano mouse.x e mouse.y in seguito and drag and drop da A a B.

utente trascina il mouse da A a B per la prima volta
poi trascina il cursore una seconda volta
Infine trascina un ultima volta il cursore

Forse è più semplice a farsi che a dirsi. Vediamo come implementare il codice.

Abbiamo bisogno di un altra variabile per memorizzare la posizione del cursore nel punto A, e di una variabile per sapere se il drag è iniziato oppure no.

/**
* mouse
*/
const mouse = new THREE.Vector2()
// store cursor position A
const prevMouse = new THREE.Vector2()
// store drag status
let drag = false

A questo punto dobbiamo ascoltare due eventi mousedown e mouseup che ci serviranno per modificare la variabile drag e attivare/disattivare il drag and drop.

...

window.addEventListener('mousemove', (event) => {
...
})

window.addEventListener('mousedown',(event) => {
// set mouse starting position (A)
prevMouse.x = (2 * event.clientX) / window.innerWidth - 1
prevMouse.y = (-2 * event.clientY) / window.innerHeight + 1
// start drag
drag = true
})

window.addEventListener('mouseup',() => {
// end drag
drag = true
})

Nel momento in cui l'utente clicca con il mouse memoriziamo le coordinate del cursore (posizione A) nella variabile prevMouse e impostiamo la variabile drag = true. Quando il pulsante verrà rilasciato la variabile drag tornerà false per disattivare il drag and drop.

Spostiamoci nel mousemove per completare il lavoro. Aggiungiamo un controllo e solo se la variabile drag è true allora eseguiremo la nostra logica.

window.addEventListener('mousemove', (event) => {

if (drag) {
...
}
})

A questo punto la posizione del cursore sarà cambiata, esso infatti si sarà portato nella posizione B. Quindi prendiamo le nuove coordinate del cursore e calcoliamoci la differenza tra la posizione B e A, come schematizzato poco fa.

window.addEventListener('mousemove', (event) => {
// print mouse coordinates inside window
// console.log(event.clientX, event.clientY)

if (drag) {
// store the new cursor position (B)
const currentMouse = new THREE.Vector2(
(2 * event.clientX) / window.innerWidth - 1,
(-2 * event.clientY) / window.innerHeight + 1
)
// calc the difference between position B and A
const diff = currentMouse.clone().sub(prevMouse)

...

}
})

Fate attenzione che dobbiamo usare il metodo clone() per mantenere invariate le coordinate della variabile currentMouse, altrimenti verrebbe modificato il metodo sub(…).

Ora dobbiamo sommare al vettore mouse il vettore diff e assegnare a prevMouse le coordinate di currentMouse (ecco perché prima abbiamo usato il metodo clone() ). Infatti il currentMouse sarà il prevMouse al prossimo spostamento del cursore.


window.addEventListener('mousemove', (event) => {
// print mouse coordinates inside window
// console.log(event.clientX, event.clientY)

if (drag) {
// store the new cursor position (B)
const currentMouse = new THREE.Vector2(
(2 * event.clientX) / window.innerWidth - 1,
(-2 * event.clientY) / window.innerHeight + 1
)
// calc the difference between position B and A
const diff = currentMouse.clone().sub(prevMouse)

mouse.add(diff)

prevMouse.copy(currentMouse)

}
})

Il nostro drag and drop adesso funziona e possiamo trascinare la camera facendola orbitare attorno al cubo. Con questo meccanismo le coordinare mouse.x e mouse.y possono superare i limiti -1 e 1. Benché non sia un problema per la rotazione orizzontale, per la rotazione verticale invece lo è, quindi conviene limitare nuovamente la coordinata mouse.y in modo che resti sempre compresa tra -1 e 1. Possiamo farlo con la funzione clamp ed ecco il risultato finale.

window.addEventListener('mousemove', (event) => {
// print mouse coordinates inside window
// console.log(event.clientX, event.clientY)

if (drag) {
// store the new cursor position (B)
const currentMouse = new THREE.Vector2(
(2 * event.clientX) / window.innerWidth - 1,
(-2 * event.clientY) / window.innerHeight + 1
)
// calc the difference between position B and A
const diff = currentMouse.clone().sub(prevMouse)

// increase mouse coordinates
mouse.add(diff)
// clamp mouse.y value bewtween -1 and 1
mouse.y = THREE.MathUtils.clamp(mouse.y, -1, 1)

// set new starting position (A)
prevMouse.copy(currentMouse)
}
})

Damping Factor

Per concludere parliamo di damping, ovvero dello smorzamento. Alcuni controls di three.js infatti ci permettevano di attivare questa modalità aggiungendo un po' di inerzia ai movimenti per renderli più piacevoli.

Per farlo viene utilizzata l'interpolazione lineare. Quello che noi abbiamo fatto fino ad adesso è stato muovere la camera dal punto di partenza (a) al nuovo punto finale (b) in base alle coordinate del cursore. Smorzare questo movimento vuol dire che ad ogni frame la camera non raggiungerà il punto finale ma, si avvicinerà solamente (c) in base ad un fattore di smorzamento (pct) che quindi determina la velocità con cui la destinazione verrà raggiunta. Qui sotto trovi uno schema di come funziona.

ac = ab * pct

cb = (1 — pct) * ab

ac + cb = ab * pct + 1 * ab — pct * ab = ab

esempio di interpolazione tra i punti (a) e (b) = punto (c) dove ac = ab * pct e cb = ab * (1 — pct)

Nel caso della camera orbitale la variabile sulla quale dovremmo aggiungere questa interpolazione lineare non è la posizione della camera bensì le coordinate mouse.x e mouse.y che determinano le coordinate polari della camera sulla sfera.

Vediamo come implementare il damping sulla camera orbitale. Aggiungiamo due variabili dampingMouse e dampingFactor.

/**
* mouse
*/
const mouse = new THREE.Vector2()
const prevMouse = new THREE.Vector2()
let drag = false

// store the interpolated mouse coordinates
const dampingMouse = new THREE.Vector2()
// store damping intensity
let dampingFactor = 0.05

E a questo punto nella funzione tic() usiamo la funzione lerp(…) dei vettori in questo modo.

function tic() {
...

dampingMouse.lerp(mouse, dampingFactor)

const angleA = -dampingMouse.x * Math.PI
const angleB = (dampingMouse.y * 0.5 + 0.5) * Math.PI
const radius = 4 //+ zoomFactor * 2

...

const pos = new THREE.Vector3().setFromSphericalCoords(radius, angleB, angleA)

// camera.position.y = 4 * Math.sin(angle)
// camera.position.z = 4 * Math.cos(angle)
camera.position.copy(pos)

camera.lookAt(mesh.position)

...
}

Abbiamo quindi ottenuto un movimento smorzato che possiamo modificare utilizzando la variabile dampingFactor per renderlo più o meno veloce.

Qui trovi il codice completo:

Conclusioni

Come hai potuto vedere, gestire in modo custom il controllo della camera non è sicuramente un passeggiata ma è senz'altro molto stimolante. Non abbiamo visto tutti i modi per farlo. Potremmo infatti avere la necessità di muovere la camera lungo un percorso stabilito utilizzando lo scroll della pagina.

Prova a tu adesso a pensare un modo per controllare la camera e ad implementarlo. Questo ti premetterà prendere familiarità con le trasformazioni della camera e con il frame loop.

Nel prossimo articolo parleremo parleremo di OrthographicCamera. vedremo in cosa differisce dalla PerspectiveCamera e come possiamo utilizzarla e controllarla.

--

--

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