Vytvoření interaktivního digitálního snímku se sledováním hlavy pomocí Three.js a TensorFlow.js

Článek byl původně zveřejněn na mém blogu

Během posledních několika týdnů jsem pracoval na novém vedlejším projektu replikace vizuálního efektu zvaného „head-coupled perspective“. Tato technika není nová, ale zajímalo mě, jak ji zprovoznit pomocí Three.js, abych mohl vytvořit nějaké interaktivní umění se sledováním hlavy.

Zde je konečný výsledek:

Jak se uživatel pohybuje, perspektiva se mění, aby navodila dojem, že se může dívat dovnitř rámu, i když se jedná o 2D zobrazení.

Grafika je vytvořena pomocí Three.js, závod je 3D model stažený ze Sketchfabu a sledování hlavy se provádí pomocí modelu MoveNet v TensorFlow.js.

Při nějakém průzkumu implementace perspektivního efektu jsem se dozvěděl, že to souvisí se změnou projekční matice kamery a narazil jsem na požadavek na stažení do repozitáře Three.js, který se zdál být blízko tomu, co jsem hledal.

PR bylo sloučeno a nový nástroj nazvaný frameCorners() byla přidána do knihovny. Podle dokumentů tento nástroj "nastaví projekční matici a čtveřici kamery PerspectiveCamera tak, aby přesně orámovala rohy libovolného obdélníku" .
Tohle znělo přesně jako to, co jsem potřeboval! Pokud se pozorně podíváte na ukázku výše, můžete si všimnout, že se změnou perspektivy se vnější rohy krabice nemění.

Aktualizace projekční matice kamery

Způsob použití tohoto nástroje je předat mu kameru a 3 vektory představující souřadnice bodů, které budou představovat váš libovolný obdélník.

CameraUtils.frameCorners(
  camera,
  bottomLeftCorner,
  bottomRightCorner,
  topLeftCorner,
  false // This boolean is for the argument `estimateViewFrustum` but to be honest I don't quite understand what it means.
);

Ve své scéně mám rovinnou geometrii použitou k vytvoření 5 ok, které tvoří moji "krabici". Tato geometrie je přibližně 100x100 a každá síť, která ji používá, má jinou polohu a rotaci podle toho, na kterou stranu krabice je použita.

Zde je ukázka kódu pro ilustraci toho, o čem mluvím

// Top part of the box
planeTop.position.y = 100;
planeTop.rotateX(Math.PI / 2);

// bottom part of the box
planeBottom.rotateX(-Math.PI / 2);

// Back of the box
planeBack.position.z = -50;
planeBack.position.y = 50;

// Right side of the box
planeRight.position.x = 50;
planeRight.position.y = 50;
planeRight.rotateY(-Math.PI / 2);

// Left side of the box
planeLeft.position.x = -50;
planeLeft.position.y = 50;
planeLeft.rotateY(Math.PI / 2);

S ohledem na tyto pozice můžeme vytvořit vektory reprezentující body, které chceme použít pro naši kameru:

let bottomLeftCorner = new THREE.Vector3();
let bottomRightCorner = new THREE.Vector3();
let topLeftCorner = new THREE.Vector3();

bottomLeftCorner.set(-50.0, 0.0, -20.0);
bottomRightCorner.set(50.0, 0.0, -20.0);
topLeftCorner.set(-50.0, 100.0, -20.0);

bottomLeftCorner vektor má x pozici -50, aby odpovídala x souřadnice planeLeft , y pozice je 0, aby odpovídala pozici y planeBottom výchozí hodnota je 0 a z pozici -20, abyste měli trochu hloubky, ale ne příliš.

Chvíli mi trvalo, než jsem pochopil, jak vybrat souřadnice svých vektorů, abych dosáhl požadovaného efektu, ale tento GIF mi hodně pomohl:

Jak měníte souřadnice vektorů, kamera mění polohu a orientaci, aby zarámovala tyto rohy.

To však byla jen jedna část řešení, druhá část se stala tak trochu náhodou. 😂

OrbitControls

Jednou se mi podařilo získat správné souřadnice pro mé vektory a použít frameCorners() util, poloha kamery odpovídala správnému obdélníku, ale při pokusu o změnu perspektivy pomocí sledování obličeje se stalo něco divného.

Přál bych si, abych to tehdy nahrál, abych vám mohl ukázat, co tím myslím, ale stejně se to pokusím vysvětlit.

V ukázce na začátku tohoto příspěvku můžete vidět, že bez ohledu na to, jak se mění perspektiva, zadní rovina je vždy rovnoběžná se mnou. Co se stalo, když jsem použil pouze frameCorners() je, že tato rovina se otáčela, takže se měnila poloha vektoru v, což vůbec nedávalo realistický efekt.

Trochu jako GIF níže, ale představte si, že se to děje pouze na jedné straně:

AŽ se tomu říká "Dolly zoom"!

Abych to zkusil odladit, myslel jsem si, že možná pomůže použití OrbitControls, které mi umožní otáčet se po scéně a možná použít pomocníka kamery, abych viděl, co se děje, ale místo toho to jen vyřešilo můj problém!

Pouze přidáním let cameraControls = new OrbitControls(camera, renderer.domElement); , nyní jsem mohl změnit perspektivu scény bez otáčení zadní roviny, díky čemuž vypadala mnohem realističtěji!

To, co se stalo potom, je čistá lenost... Mohl jsem se podívat hlouběji na to, jak funguje OrbitControls, abych přesně zjistil, kterou část potřebuji, ale místo toho, abych ušetřil trochu času (je to koneckonců jen vedlejší projekt), provedl jsem několik aktualizací přímo do OrbitControls.js soubor.

Nalezl jsem místo, kde je funkce handleMouseMoveRotate byl, duplikoval jej a nazval nový handleFaceMoveRotate zvládnout pohyby obličeje. Trochu jsem to upravil, abych místo souřadnic myši dostával souřadnice obličeje a TADAAA!! Fungovalo to! 🎉

Další kroky

Rád bych vytvořil několik dalších scén a mám nápad posunout tento projekt o něco dále, ale cítím, že si od něj právě teď potřebuji odpočinout.

Když trávím příliš mnoho času snahou o odladění vedlejšího projektu, někdy mi to ubírá legraci. Potřebuji to nějakou dobu nechat stranou a vrátit se k tomu, až budu mít chuť na tom znovu pracovat. 😊

Mezitím se můžete podívat na kód.