Modul 3.1: Animasjoner

Animasjon

Introduksjon

Animasjon av en modell betyr i prinsippet å vise en sekvens av statiske bilder for å lage en kontinuerlig bevegelse eller endring av form og/eller størrelse. For hvert bilde flytter eller endres formen til modellen slik at den tilsynelatende flytter seg eller endrer form/størrelse.

I WebGL animeres modeller ved hjelp av en animasjonsløkke, på engelsk kalt animation loop eller game loop. Animasjon oppnås ved å tegne skjermbildet n ganger per sekund der n f.eks. er 60. Man sier da at skjermoppdateringsraten eller «frame rate» er 60 som betyr at skjermen tegnes hvert 1/60 = 0,0167 sekund.

For enkle animasjoner trenger ikke skjermbildet oppdatere seg så ofte – et av de første 3D first person shooter spill, 3D Monster Maze, hadde en frame rate på 6 og var allikevel ansett som en suksess. I dag vil 3D-spill med en frame rate på mellom 30 og 60 anses som akseptabel, men kan også være høyere.

Frame rate og animasjoner

Frame rate påvirkes av mange faktorer men er spesielt avhengig av at CPU og GPU klarer å generere og gjengi (rendre) grafikken raskt nok. Hvis ikke vil frame raten gå ned – som i verste fall gir utslag som «hakkede» bevegelser og lite responsivt brukergrensesnitt. Også faktorer som nettverkstrafikk, brukerinput m.m. vil kunne påvirke frame rate.

Animasjonen oppnås f.eks. ved at modellen flyttes en lite stykke for hvert skjermbilde / frame. På denne måten kan man få modeller/figurer til å bevege seg i alle retninger f.eks. basert på brukerinput (mus og tastatur).

Metoden requestAnimationFrame()

I WebGL implementeres animasjoner normalt vha. Javascript-metoden requestAnimationFrame() [27]. Dette er en Javascript-metode som alltid vil forsøke å opprettholde en optimal frame rate. Den sørger også for å sette animasjonen i pausetilstand dersom brukeren navigerer til en annen tab/side i nettleseren o.l.

Metoden requestAnimationFrame(callbackmetode) brukes ved å oppgi navnet på en «callbackmetode», som nettleseren skal kalle på, som argument. Dersom man f.eks. kaller callbackmetoden animate() vil man initiere et kall på denne slik:

window.requestAnimationFrame(animate); 

Metoden benyttes f.eks. som vist i animate() funksjonen i eksemplet under:

Legg merke til at animate() funksjonen mottar argumentet currentTime. Denne variabelen inneholder antall millisekunder siden oppstart av programmet, og øker dermed kontinuerlig. Denne kan brukes til å gjøre animasjoner uavhengig av frame rate.

Eksempelkoden over animerer en trekant. Trekanten roteres vha. en rotasjonsmatrise som igjen genereres vha. en vinkel som endres for hver frame.

Kompensere for varierende frame rate

Siden nettlesere vil oppnå ulik frame rate, i ulike sammenhenger, er det viktig å ta hensyn til dette når man animerer modeller. Hvis ikke vil modellene flytte/rotere med ulike hastigheter. Normalt ønsker man ikke dette. I et bilspill ønsker man at bilen beveger seg like fort (og langt) selv om frame raten varierer.

I eksemplet over ble perioden mellom kall på animate() brukt til å sørge for en jevn rotasjon (tilsvarende kan brukes ved translasjon/forflytning) som vil være uavhengig av frame rate. Ved første gangs kall på animate() settes currentTime til 0. Dette betyr igjen at første kall på animate() vil sette renderInfo.animation.angle lik 0 og dermed ingen rotasjon. Ved påfølgende kall vil currentTime være antall millisekunder siden oppstart og renderInfo.animation.angle vil få en verdi.

Vha. variabelen renderInfo.animation.rotationSpeed bestemmer man hvor mange grader trekanten skal rotere per sekund. Variabelen elapsed settes lik antall sekunder siden forrige kall på animate(). Verdien på denne vil variere avhengig av hvor lang animate() og draw() metodene bruker.

Elapsed deles på 1000 slik at vi kan operere med sekunder. Deretter settes lastTime lik currentTime slik at elapsed kan beregnes på nytt neste gang draw() kjøres.

Variabelen renderInfo.animation.angle akkumuleres slik: rotationSpeed * elapsed. Dersom f.eks. elapsed blir beregnet til 10ms (0.01) hundre ganger på rad vil angle øke med 60 grader på et sekund (merk imidlertid at elapsed i praksis vil variere).

I draw() settes modellmatrisa lik en rotasjonsmatrise beregnet vha. angle.

Beregne og overvåke antall frames per sekund (FPS)

Ved å følge med på applikasjonens FPS kan man umiddelbart fange opp «dårlig» kode som sørger for fall i FPS. Dersom man som utvikler ser at FPS plutselig faller dramatisk er det verdt å stoppe opp og se over den siste kodebiten som ble lagt til.

Jo flere frames per sekund applikasjonen kjører med jo finere/jevnere vil animasjonen være. FPS (frames per seconds) tilpasser seg skjermens oppdateringsfrekvens. Denne er ofte 60 eller høyere. Dataskjermer kjører f.eks. med en oppdateringsfrekvens på 60 Hz og nettlesere vil forsøke å holde FPS lik skjermens oppdateringsfrekvens for å unngå unødvendig "tegnearbeid". Dagens skjermer opererer ofte med høyere frekvenser enn 60 Hz, f.eks. 75, 120, 144, 240 Hz eller mer.

FPS-verdien kan beregnes ved å måle hvor lang tid det tar å «rendre» hver frame og telle hvor mange frames programmet klarer å rendre per sekund.

Dersom skjermfrekvensen f.eks. er 60 Hz vil nettleseren forsøke å opprettholde 60 fps. Dersom det tar mindre tid enn 1/60 sekunder å tegne en frame, vil requestAnimationFrame vente til det har gått 1/60 sekunder før ny frame tegnes, og FPS vil bli 60. Dersom det tar mer tid enn 1/60 vil FPS bli mindre enn 60.

Vi må da ha en variabel som holder på antall frames og en som holder rede på tiden. Etter at et sekund er gått viser man antall frames som ble tegnet i løpet dette sekundet. Deretter nullstilles variablene og tellinga starter på nytt.

Koden under viser utdrag fra koden over der fps-beregning og visning utføres:

triangleAnimationFPS.js

Her brukes verdien fra getElapsed() til å akkumulere fpsInfo.elapsedTotal. Fps-verdien, i koden kalt fpsInfo.frameCount, inkrementeres med 1 i hver runde av animate(). Når det har gått et sekund, dvs. at fpsInfo.elapsedTotal er større enn eller lik 1.0, oppdateres html-feltet 'fps' med en ny fps-verdi, slik: fpsInfo.frameCount / fpsInfo.elapsedTotal. Verdien på fpsInfo.elapsedTotal vil være 1.0 eller litt mer og tallet som vises vil omentrentlig lik fpsInfo.frameCount. Deretter nullstilles både fpsInfo.frameCount og fpsInfo.elapsedTotal, for å gjøre klar til telling av antall frames for neste sekund.

Under er komplett html-fil for eksemplet over vist. Legg merke til hvordan fps-elementet er definert:

index.html

Brukerinput og DOM

Brukerinput og hendelseshåndtering er kontrollert av Javascript og er ikke en del av WebGL. Nettleseren genererer "hendelser" (events) som kan fanges opp av vår Javascript/WebGL-applikasjon. Hendelser kan f.eks. være brukerinput som f.eks. «keydown», «keyup», «mouseup», «mousedown» m.fl. I tillegg kan man fange opp og håndtere hendelser som «ferdig å laste en fil», «mistet kontekst» og «kontekst gjenopprettet».

Document Object Model (DOM)

DOM brukes til å strukturere og representere web-dokumenter. Ved hjelp av DOM-APIet har man tilgang til innholdet i web-dokumenter. APIet kan f.eks. brukes fra Javascript til å aksessere elementer i HTML, XHTML og XML-dokumenter.

Det er nettleseren sitt ansvar å tolke (parse) HTML fila og generere DOM-treet for dokumentet.

I et slikt dokument er alt en node. Selve dokumentet er en node, alle html-tagger og attributter er noder, teksten innafor html-elementene er også noder. Alle noder organiseres i en trestruktur som kalles et «DOM tre». Rotnoden i treet er document-noden.

Via rotnoden document kan man f.eks. finne URIen til dokumentet vha. document.baseURI. I Javascript kan dette f.eks. gjøres slik:

let baseUri = document.baseURI; 

Html/Javascript-koden under viser et komplett eksempel på dette:

Man kan f.eks. hente en referanse til «canvaset» vha. følgende kodelinje:

let canvas = document.getElementById("myCanvas"); 

Her brukes også «rotnoden» til å hente et canvas som er en del av HTML-siden som dette Javascriptet er en del av.

Se [7] for mer informasjon om dette.

Enkel hendelseshåndtering

Vi har allerede sett hvordan events trigger start av main() metoden i eksemplet over. Se kap. 6 i [2] og/eller [14] for mer informasjon om dette.

Tastatur- og musehendelser

Man kan eksemplevis fange opp, og håndtere, følgende hendelser når brukeren trykker en tastaturknapp:

  • keydown
  • keyup

Man legger til en event handler -funksjoner slik:

/**
* Knytter tastatur-events til eventfunksjoner.
*/
function initKeyPress(currentlyPressedKeys) {
    document.addEventListener(
        'keyup',
        (event) => {
            currentlyPressedKeys[event.code] = false;
            console.log("Code: " + event.code);
            console.log("Key: " + event.key);
            },
        false
    );
    document.addEventListener(
        'keydown',
        (event) => {currentlyPressedKeys[event.code] = true;},
        false);
}

Merk at initKeyPress() kalles, og kjøres kun en gang, f.eks. fra main-funksjonen. Eventfunksjonene, dvs. hvilken kode som kjører når bruker trykker ned og slipper en tastaturknapp, ligger klammeparentesene { og } bak (event) => .

Legg merke til at event.code har verdien "KeyA" mens event.key har verdien "a" dersom "a"-knappen trykkes ned. Trykker man Shift+a blir event.key lik "A" (stor a).

Håndtere flere samtidige knappetrykk

Dette er spesielt aktuelt for spill - man bør jo kunne gi gass og samtidig svinge i et bilspill. I korte trekk: bruk keydown og keyup-eventmetoder, ta vare på tastetrykk i en liste/array. For hver frame kalles en egen metode som håndterer alle tastetrykk. Se kodeeksempler.

Musehendelser

Det er også mulig å fange opp musehendelser, f.eks. "mouseup". Dette gjøres på samme måte som for "keyup"/"keydown" vist over.

Ingen kommentarer:

Legg inn en kommentar