Lær å gi et bilde 3D-effekt med WebGL

Med bruk av WebGL og grafikkbiblioteket Pixi.js.

Robert Bue, Kalle Pohjapelto og Trym Søvik Lyssand i Good Morning Naug forklarer deg hvordan du kan gjenskape 3d-effekten som blant annet Facebook bruker. 📸: Privat
Robert Bue, Kalle Pohjapelto og Trym Søvik Lyssand i Good Morning Naug forklarer deg hvordan du kan gjenskape 3d-effekten som blant annet Facebook bruker. 📸: Privat Vis mer

På nyere mobiltelefoner med to eller flere kameralinser så har man det som ofte kalles portrettmodus. I praksis så betyr det at mobilen tar ulike bilder med hver linse, lager et dybdekart og setter sammen disse bildene slik at bakgrunnen blir uklar, men det som i forgrunnen er skarpt - noe som gir en fin effekt.

Bilde av utvikler

I 2019 kom også muligheten for å få denne effekten med bevegelse på Facebook. Du kan enten lage dybden selv eller så kan man laste opp et portrettbilde og så oppdager Facebook det automatisk slik at man får muligheten for publisere bilde med 3D-effekt.

Bilde av utvikler

Depth map og WebGL

Vi i Good Morning Naug tenkte å lage denne effekten på bildene av våre ansatte, og jeg skal nå vise dere hvordan det kan gjøres.

Vi skal bruke WebGL og nærmere bestemt grafikk-biblioteket Pixi.js. Det gjør jobben med WebGL mye enklere, og siden Pixi.js rendrer 2D, så er det perfekt for vårt eksempel der vi kun skal simulere 3D med 2D-bilder. Vi kunne også gjort det med vanilla WebGL, men med å bruke Pixi.js så kan vi gjøre denne teknikken uten å gå for dypt ned i WebGL APIet og shaders.

For å kunne lage denne effekten trenger vi et dybekart eller depth map som føles litt mer naturlig i vår engelske verden. Og det er enkelt forklart et bilde som inneholder informasjonen om avstanden til objektene, sett fra et flatt perspektiv. Med andre ord; dybde i et flatt bilde. Prinsippet er å skille ut noen områder i bilde basert på Z-aksen, altså om det er nærme eller langt ifra.

Tar du et portrettbilde med en iPhone X og åpner bildet i Photoshop, så kan du i Kanaler (Channels) se dybdekartet som mobilen lager.

Mørkere områder kommer nærmest. 📸: Privat
Mørkere områder kommer nærmest. 📸: Privat Vis mer

OBS! Du må åpne HEIC-versjonen av bildet, ikke JPG. HEIC har mulighet for å inneholde flere bilder i samme container.

Som det selvsentrerte mennesket jeg er så skal vi ta utgangspunkt i et bilde av meg selv, og siden det er tatt med et vanlig kamera så må vi lage depth map selv. Og da har man noen valg:

  • Bruke en generator som tar utgangspunkt i originalbildet (dårligst resultat)

  • Tegne opp dybden selv (vanskelig og tidkrevende)

  • Bruke Blender til å lage det for oss (best resultat)

Blender og FaceBuilder

Vi velger å bruke Blender fordi det er veldig mange nyanser av dybde i et ansikt og fordi Blender er gratis. Last ned Blender her.

Tips! En annen mulighet som fungerer på andre typer bilder er å lage et depth map lagbasert i Photoshop, eller lignende programvare. Ta en titt på denne videoen.

Nå skal vi gå gjennom hvordan man laster ned og installerer Blender, KeenTools Core Library og KeenTools FaceBuilder for Blender. Du skal også laste ned en prosjektfil for Blender vi har satt opp. Denne vil normaliserer, blurre og justerer gråtoner for å lage et bedre depth map.

Vi har laget en video slik at det blir enklere for deg å sette opp. Vi viser også hvordan vi kan få et bedre resultat med å bruke Photoshop. Fordi FaceBuild tar ikke høyde for hår, og heldigvis har jeg ennå litt av det. Så vi skal pusse litt på depth map vi eksportere fra Blender for å få et bedre og mer realistisk resultat. Har du ikke tilgang til Photoshop kan du bruke andre programvarer som Gimp.

Bildetekst: Vi bruker macOS, men det skal også fungere på Windows og Linux.

Pixi.js

Nå er vi klar for litt kode og vi skal som sagt bruke Pixi.js og deres innebygde displacement filter. Filteret er egentlig bare en fragment og vertex shader som er skrevet for oss allerede. Se koden her hvis du ønsker å gå mer i dybden for hva Pixi.js gjør for oss.

Siden magien kun skjer med rundt ti linjer kode så tar vi alt i samme slengen og heller tar gjennomgangen i kommentarene:

// PIXI.Application er en enkel måte å fyre opp et canvas som vi kan bruke
// Det blir laget en render (WebGL), ticker (requested animation frame) og en root container (vi kan putte bildene våre i senere)
const pixiApp = new PIXI.Application({ width: 800, height: 800 });

// legger til canvas i DOM
document.body.appendChild(pixiApp.view);

// lager sprites som vi trenger senere direkte fra bildefilene
const imageSprite = PIXI.Sprite.from('https://gmn-demos.s3-eu-west-1.amazonaws.com/assets/robert-bue-v2-hq.jpg');
const depthMapSprite = PIXI.Sprite.from('https://gmn-demos.s3-eu-west-1.amazonaws.com/assets/dept-map-v2-hq.jpg');

// legger spritene til vår stage (root container vi fikk fra PIXI.Application)
pixiApp.stage.addChild(imageSprite, depthMapSprite);

// lager en displacement fra vårt depth map (bruker shaders som nevnt tidligere)
let displacementFilter = new PIXI.filters.DisplacementFilter(depthMapSprite);

// legg til filteret til vår stage (her kan du legge til andre filters også, Pixi.js sine eller dine egne)
pixiApp.stage.filters = [displacementFilter];

Det er alt av kode som skal til for å lage dybden fra vårt depth map på det originale bildet, men for å se effekten når man beveger musen må vi ha et par linjer til:

const mouseMove = function(e) {
	// vi bruker musens posisjon ut fra et senterpunkt på skjermen for å sette x og y skaleringen på filteret vårt
	// så deler vi på 30 for å få litt kontroll på effekten (lek med verdien for å se hva som skjer)
	displacementFilter.scale.x = (window.innerWidth / 2 - e.clientX) / 30;
	displacementFilter.scale.y = (window.innerHeight / 2 - e.clientY) / 30;
};

window.addEventListener('mousemove', mouseMove);

Resultatet gir en interessant effekt som man kan gjenbruke enkelt og kombinere med andre effekter. Håper du synes det var like interessant å følge med, og tweet/send meg om du lager noe kult.

See the Pen PIXI.filters.DisplacementFilter by Robert Bue (@robbue) on CodePen.

Sidenote: Vi kunne også lagt til en tilt-effekt med DeviceOrientationEvent for mobiler, men på iOS må man spør om tillatelse for å bruke gyroskopet og DeviceOrientationEvent er ikke like godt standardisert, så jeg har derfor valgt å ikke inkludere det.