Modul 3.1: Animasjoner

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» / «animation/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.

Frame rate og animasjoner

Frame rate påvirkes av mange faktorer men er spesielt avhengig av at CPU & 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 & tastatur).

Mye av informasjonen i dette notatet er hentet fra (Anyuru, A. 2012).

Metoden requestAnimationFrame()

I WebGL implementeres animasjoner normalt vha. Javascript-metoden requestAnimationFrame(). 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(calllbackmetode) 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: 
export function main() {
// Oppretter et webGLCanvas for WebGL-tegning:
const webGLCanvas = new WebGLCanvas('myCanvas', document.body, 960, 640);
// Hjelpeobjekt som holder på objekter som trengs for rendring:
const renderInfo = {
gl: webGLCanvas.gl,
baseShaderInfo: initBaseShaders(webGLCanvas.gl),
buffers: initBuffers(webGLCanvas.gl),
lastTime: 0,
triangleAnimation: { //Holder på animasjonsinfo:
angle: 0,
rotationsSpeed: 60
}
};
animate(0, renderInfo);
}
function initBaseShaders(gl) {
// Leser shaderkode fra HTML-fila: Standard/enkel shader (posisjon og farge):
let vertexShaderSource = document.getElementById('base-vertex-shader').innerHTML;
let fragmentShaderSource = document.getElementById('base-fragment-shader').innerHTML;

// Initialiserer & kompilerer shader-programmene;
const glslShader = new WebGLShader(gl, vertexShaderSource, fragmentShaderSource);

// Samler all shader-info i ET JS-objekt, som returneres.
return {
program: glslShader.shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(glslShader.shaderProgram, 'aVertexPosition'),
vertexColor: gl.getAttribLocation(glslShader.shaderProgram, 'aVertexColor'),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(glslShader.shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(glslShader.shaderProgram, 'uModelViewMatrix'),
},
};
}

/**
* Oppretter verteksdata for trekanten.
* Et posisjonsbuffer og et fargebuffer.
* MERK: Må være likt antall posisjoner og farger.
*/
function initBuffers(gl) {
const width = 5;
const height = 5;

const positions = new Float32Array([
0.0, height/2, 0.0, // X Y Z
-width/2, -height/2, 0.0, // X Y Z
width/2, -height/2, 0.0 // X Y Z
]);

const colors = new Float32Array([
1, 0.3, 0, 1, //R G B A
1, 0.3, 0, 1, //R G B A
1, 0.3, 0, 1, //R G B A
]);

const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);

const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);

return {
position: positionBuffer,
color: colorBuffer,
vertexCount: positions.length/3
};
}

/**
* Genererer view- og projeksjonsmatrisene.
* Disse utgjør tilsanmmen det virtuelle kameraet.
*/
function initCamera(gl) {
// Kameraposisjon:
const camPosX = 0;
const camPosY = 0;
const camPosZ = 10;

// Kamera ser mot ...
const lookAtX = 0;
const lookAtY = 0;
const lookAtZ = 0;

// Kameraorientering:
const upX = 0;
const upY = 1;
const upZ = 0;

let viewMatrix = new Matrix4();
let projectionMatrix = new Matrix4();

// VIEW-matrisa:
viewMatrix.setLookAt(camPosX, camPosY, camPosZ, lookAtX, lookAtY, lookAtZ, upX, upY, upZ);
// PROJECTION-matrisa (frustum): cuon-utils: Matrix4.prototype.setPerspective = function(fovy, aspect, near, far)
const fieldOfView = 45; // I grader.
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const near = 0.1;
const far = 1000.0;
// PROJEKSJONS-matrisa; Bruker cuon-utils: Matrix4.prototype.setPerspective = function(fovy, aspect, near, far)
projectionMatrix.setPerspective(fieldOfView, aspect, near, far);

return {
viewMatrix: viewMatrix,
projectionMatrix: projectionMatrix
};
}

/**
* Aktiverer position-bufferet.
* Kalles fra draw()
*/
function connectPositionAttribute(gl, baseShaderInfo, positionBuffer) {
const numComponents = 3;
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(
baseShaderInfo.attribLocations.vertexPosition,
numComponents,
type,
normalize,
stride,
offset);
gl.enableVertexAttribArray(baseShaderInfo.attribLocations.vertexPosition);
}

/**
* Aktiverer color-bufferet.
* Kalles fra draw()
*/
function connectColorAttribute(gl, baseShaderInfo, colorBuffer) {
const numComponents = 4;
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.vertexAttribPointer(
baseShaderInfo.attribLocations.vertexColor,
numComponents,
type,
normalize,
stride,
offset);
gl.enableVertexAttribArray(baseShaderInfo.attribLocations.vertexColor);
}

/**
* Klargjør canvaset.
* Kalles fra draw()
*/
function clearCanvas(gl) {
gl.clearColor(0.9, 0.9, 0.9, 1); // Clear screen farge.
gl.clearDepth(1.0);
gl.enable(gl.DEPTH_TEST); // Enable "depth testing".
gl.depthFunc(gl.LEQUAL); // Nære objekter dekker fjerne objekter.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}

/**
* Animasjonsløkke.
*/
function animate(currentTime, renderInfo) {
// Sørger for at animate kalles på nytt, for animasjon (60fps):
window.requestAnimationFrame((currentTime) => {
animate(currentTime, renderInfo);
});
//window.requestAnimationFrame(animate);
console.log(currentTime);

if (currentTime == undefined)
currentTime = 0; //Udefinert første gang.

//Tar høyde for varierende frame rate:
let elapsed = getElapsed(currentTime, renderInfo);

renderInfo.triangleAnimation.angle = renderInfo.triangleAnimation.angle + (renderInfo.triangleAnimation.rotationsSpeed * elapsed);
renderInfo.triangleAnimation.angle %= 360; // "Rull rundt" dersom angle >= 360 grader.
renderInfo.triangleAnimation.lastTime = currentTime; // Setter lastTime til currentTime.

draw(currentTime, renderInfo);
}

/**
* Beregner forløpt tid siden siste kall.
* @param currentTime
* @param renderInfo
*/
function getElapsed(currentTime, renderInfo) {
let elapsed = 0.0;
if (renderInfo.lastTime !== 0.0) // Først gang er lastTime = 0.0.
elapsed = (currentTime - renderInfo.lastTime)/1000; // Deler på 1000 for å operere med sekunder.
renderInfo.lastTime = currentTime; // Setter lastTime til currentTime.
return elapsed;
}

/**
* Tegner!
*/
function draw(currentTime, renderInfo) {
let gl = renderInfo.gl;
let baseShaderInfo = renderInfo.baseShaderInfo;
let buffers = renderInfo.buffers;
clearCanvas(gl);

// Aktiver shader:
gl.useProgram(baseShaderInfo.program);

// Kople posisjon og farge-attributtene til tilhørende buffer:
connectPositionAttribute(gl, baseShaderInfo, buffers.position);
connectColorAttribute(gl, baseShaderInfo, buffers.color);

// Lag viewmodel-matris::
let modelMatrix = new Matrix4();
//modelMatrix.setIdentity();
modelMatrix.setRotate(renderInfo.triangleAnimation.angle, 0, 0, 1);

let cameraMatrixes = initCamera(gl); //<== NB!
let modelviewMatrix = new Matrix4(cameraMatrixes.viewMatrix.multiply(modelMatrix)); // NB! rekkefølge!

// Send matrisene til shaderen:
gl.uniformMatrix4fv(baseShaderInfo.uniformLocations.modelViewMatrix, false, modelviewMatrix.elements);
gl.uniformMatrix4fv(baseShaderInfo.uniformLocations.projectionMatrix, false, cameraMatrixes.projectionMatrix.elements);

// Tegn!
gl.drawArrays(gl.TRIANGLES, 0, buffers.vertexCount);
}
Her ser vi at animate() kalles første gang i staren av programmet, dvs. fra funksjonen main(). Som vi ser starter animate() med å initiere et nytt kall på animate() vha. requestAnimationFrame(). Herfra kalles så draw() funksjonen. Legg merke til at vi samler gl, baseShaderInfo, buffers og animasjonsinfo i et et eget objekt, renderInfo. Dette sendes til animate() og videre til draw() som bruker informasjonen i objektet til å tegne korrekt.  

Legg også merke til at animate(…) funksjonen blant annet. 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.

Legg merke til at vi ikke tar hensyn til frame rate i dette eksemplet. Dersom nettleseren, av ulike grunner, opplever redusert frame rate vil også trekanten rotere tregere.

Kompensere for varierende frame rate

Siden nettlesere vi oppnår ulik frame rate i ulike sammenhenger er det viktig å ta hensyn til dette når man animerer modeller. Hvis ikke vil modellene bevege seg 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å draw() 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() vil currentTime være 0. Dette betyr igjen at første kall på animate() vil sette renderInfo.triangleAnimation.angle lik 0 og dermed ingen rotasjon. Ved neste gangs kall vil currentTime være antall millisekunder siden oppstart og renderInfo.triangleAnimation.angle vil få en verdi.

Vha. variabelen renderInfo.triangleAnimation.rotationSpeed bestemmer man hvor mange grader trekanten skal rotere per sekund. Variabelen elapsed settes lik antall millisekunder siden forrige kall på draw(). Verdien på denne vil variere avhengig av hvor lang tid draw() metoden 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.triangleAnimation.angle akkumuleres vha. (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, som i utgangspunktet ikke har noe med FPS å gjøre, og nettlesere vil forsøke å holde FPS lik denne verdien for å unngå blafring ("flickering") på skjermen. Nyere skjermer opererer ofte med høyrere frekvenser, f.eks. 120 Hz.

Det er i teorien enkelt å beregne FPS-verdien. Dette gjøres ved å beregne hvor lang tid det tar å «rendre» en frame. Dette gjøres igjen ved å avlese tiden i starten av draw() metoden og huske denne. Ved neste gangs kall på draw() avleser man tiden på nytt og trekker fra forrige avleste tid. Man har da tiden det tar å tegne en frame. Vi kan nå beregne FPS ved å telle antall frames som tegnes i løpet av et sekund.

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 samme eksempel som over utvidet med fps-bererning og visning (uthevet): 
/**
* Et WebGL-program som tegner en enkel trekant.
* Beregner og viser FPS.
* Bruker ikke klasser, kun funksjoner.
*/
export function main() {
// Oppretter et webGLCanvas for WebGL-tegning:
const webGLCanvas = new WebGLCanvas('myCanvas', document.body, 960, 640);
// Hjelpeobjekt som holder på objekter som trengs for rendring:
const renderInfo = {
gl: webGLCanvas.gl,
baseShaderInfo: initBaseShaders(webGLCanvas.gl),
trianlgeBuffers: initTrianlgeBuffers(webGLCanvas.gl),
animationInfo: { //Holder på animasjonsinfo:
angle: 0,
rotationsSpeed: 60,
},
currentlyPressedKeys: [],
lastTime: 0, // Holder på tidspunkt for forrige frame.
fpsInfo: { // Holder på fps-info:
frameCount: 0,
lastTimeStamp: 0
}

};
animate( 0, renderInfo);
}

function initBaseShaders(gl) {
// Leser shaderkode fra HTML-fila: Standard/enkel shader (posisjon og farge):
let vertexShaderSource = document.getElementById('base-vertex-shader').innerHTML;
let fragmentShaderSource = document.getElementById('base-fragment-shader').innerHTML;

// Initialiserer & kompilerer shader-programmene;
const glslShader = new WebGLShader(gl, vertexShaderSource, fragmentShaderSource);

// Samler all shader-info i ET JS-objekt, som returneres.
return {
program: glslShader.shaderProgram,
attribLocations: {
vertexPosition: gl.getAttribLocation(glslShader.shaderProgram, 'aVertexPosition'),
vertexColor: gl.getAttribLocation(glslShader.shaderProgram, 'aVertexColor'),
},
uniformLocations: {
projectionMatrix: gl.getUniformLocation(glslShader.shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(glslShader.shaderProgram, 'uModelViewMatrix'),
},
};
}

/**
* Oppretter verteksbuffer for trekanten.
* Et posisjonsbuffer og et fargebuffer.
* MERK: Må være likt antall posisjoner og farger.
*/
function initTrianlgeBuffers(gl) {
const width = 5;
const height = 5;

const positions = new Float32Array([
0.0, height/2, 0.0, // X Y Z
-width/2, -height/2, 0.0, // X Y Z
width/2, -height/2, 0.0 // X Y Z
]);

const colors = new Float32Array([
1, 0.3, 0, 1, //R G B A
1, 0.3, 0, 1, //R G B A
1, 0.3, 0, 1, //R G B A
]);

const positionBuffer = gl.createBuffer();
gl.
bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.
bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
gl.
bindBuffer(gl.ARRAY_BUFFER, null);

const colorBuffer = gl.createBuffer();
gl.
bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.
bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
gl.
bindBuffer(gl.ARRAY_BUFFER, null);

return {
position: positionBuffer,
color: colorBuffer,
vertexCount: positions.length/3
};
}

/**
* Genererer view- og projeksjonsmatrisene.
* Disse utgjør tilsanmmen det virtuelle kameraet.
*/
function initCamera(gl) {
// Kameraposisjon:
const camPosX = 0;
const camPosY = 0;
const camPosZ = 10;

// Kamera ser mot ...
const lookAtX = 0;
const lookAtY = 0;
const lookAtZ = 0;

// Kameraorientering:
const upX = 0;
const upY = 1;
const upZ = 0;

let viewMatrix = new Matrix4();
let projectionMatrix = new Matrix4();

// VIEW-matrisa:
viewMatrix.setLookAt(camPosX, camPosY, camPosZ, lookAtX, lookAtY, lookAtZ, upX, upY, upZ);
// PROJECTION-matrisa (frustum): cuon-utils: Matrix4.prototype.setPerspective = function(fovy, aspect, near, far)
const fieldOfView = 45; // I grader.
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const near = 0.1;
const far = 1000.0;
// PROJEKSJONS-matrisa; Bruker cuon-utils: Matrix4.prototype.setPerspective = function(fovy, aspect, near, far)
projectionMatrix.setPerspective(fieldOfView, aspect, near, far);

return {
viewMatrix: viewMatrix,
projectionMatrix: projectionMatrix
};
}

/**
* Aktiverer position-bufferet.
* Kalles fra draw()
*/
function connectPositionAttribute(gl, baseShaderInfo, positionBuffer) {
const numComponents = 3;
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.
bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.
vertexAttribPointer(
baseShaderInfo.
attribLocations.vertexPosition,
numComponents,
type,
normalize,
stride,
offset);
gl.
enableVertexAttribArray(baseShaderInfo.attribLocations.vertexPosition);
}

/**
* Aktiverer color-bufferet.
* Kalles fra draw()
*/
function connectColorAttribute(gl, baseShaderInfo, colorBuffer) {
const numComponents = 4;
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
gl.
bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.
vertexAttribPointer(
baseShaderInfo.
attribLocations.vertexColor,
numComponents,
type,
normalize,
stride,
offset);
gl.
enableVertexAttribArray(baseShaderInfo.attribLocations.vertexColor);
}

/**
* Klargjør canvaset.
* Kalles fra draw()
*/
function clearCanvas(gl) {
gl.
clearColor(0.9, 0.9, 0.9, 1); // Clear screen farge.
gl.clearDepth(1.0);
gl.
enable(gl.DEPTH_TEST); // Enable "depth testing".
gl.depthFunc(gl.LEQUAL); // Nære objekter dekker fjerne objekter.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}

/**
* Animasjonsløkke.
*/
function animate(currentTime, renderInfo) {
window.requestAnimationFrame((currentTime) => {
animate(currentTime, renderInfo);
});
// Finner tid siden siste kall på draw().
let elapsed = getElapsed(currentTime, renderInfo);

renderInfo.
animationInfo.angle = renderInfo.animationInfo.angle + (renderInfo.animationInfo.rotationsSpeed * elapsed);
renderInfo.
animationInfo.angle %= 360;

draw(currentTime, renderInfo);

calculateFps(currentTime, renderInfo.fpsInfo);
}

/**
* Beregner forløpt tid siden siste kall.
* @param currentTime
* @param renderInfo
*/
function getElapsed(currentTime, renderInfo) {
let elapsed = 0.0;
if (renderInfo.lastTime !== 0.0) // Først gang er lastTime = 0.0.
elapsed = (currentTime - renderInfo.lastTime)/1000; // Deler på 1000 for å operere med sekunder.
renderInfo.lastTime = currentTime; // Setter lastTime til currentTime.
return elapsed;
}

/**
* Beregner og viser FPS.
* @param currentTime
* @param renderInfo
*/
function calculateFps(currentTime, fpsInfo) {
if (!currentTime) currentTime = 0;
// Sjekker om ET sekund har forløpt...
if (currentTime - fpsInfo.lastTimeStamp >= 1000) {
// Viser FPS i .html ("fps" er definert i .html fila):
document.getElementById('fps').innerHTML = fpsInfo.frameCount;
// Nullstiller fps-teller:
fpsInfo.frameCount = 0;
//Brukes for å finne ut om det har gått 1 sekund - i så fall beregnes FPS på nytt.
fpsInfo.lastTimeStamp = currentTime;
}
// Øker antall frames per sekund:
fpsInfo.frameCount++;
}


/**
* Tegner!
*/
function draw(currentTime, renderInfo) {
clearCanvas(renderInfo.gl);

// Aktiver shader:
renderInfo.gl.useProgram(renderInfo.baseShaderInfo.program);

// Kople posisjon og farge-attributtene til tilhørende buffer:
connectPositionAttribute(renderInfo.gl, renderInfo.baseShaderInfo, renderInfo.trianlgeBuffers.position);
connectColorAttribute(renderInfo.gl, renderInfo.baseShaderInfo, renderInfo.trianlgeBuffers.color);

// Lag viewmodel-matris::
let modelMatrix = new Matrix4();
modelMatrix.setIdentity();
modelMatrix.rotate(renderInfo.animationInfo.angle, 0, 0, 1);

let cameraMatrixes = initCamera(renderInfo.gl); //<== NB!
let modelviewMatrix = new Matrix4(cameraMatrixes.viewMatrix.multiply(modelMatrix)); // NB! rekkefølge!

// Send matrisene til shaderen:
renderInfo.gl.uniformMatrix4fv(renderInfo.baseShaderInfo.uniformLocations.modelViewMatrix, false, modelviewMatrix.elements);
renderInfo.
gl.uniformMatrix4fv(renderInfo.baseShaderInfo.uniformLocations.projectionMatrix, false, cameraMatrixes.projectionMatrix.elements);

// Tegn!
renderInfo.gl.drawArrays(renderInfo.gl.TRIANGLES, 0, renderInfo.trianlgeBuffers.vertexCount);
}
I .html fila er fps-elementet definert slik:
<!DOCTYPE html>
<
html lang="nb">
<
head>
<
meta charset="utf-8">
<
title>WebGL Triangle Animation FPS</title>
<
link rel="stylesheet" href="../../base/webgl.css" type="text/css">
<
script src="../../base/lib/cuon-matrix.js"></script>
<
script src="../../base/lib/gl-matrix.js"></script>
</
head>

<
body>
<
div style="top:0px; left:15px; width:100%; text-align:left; color:black;" class="ui">
<
h2>WebGL Triangle Animation FPS</h2>
FPS: <span id="fps">--</span><br>
</
div>
    ...
Her brukes et eget objekt, fpsInfo, for å holde på verdiene som brukes til beregning av FPS. Via <div> elementet navngitt som «fps» vil beregnet FPS vises i nettleseren. Verdien på denne oppdateres hvert sekund.

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».

DOM

DOM står for Document Object Model og brukes til å strukturere og representerere 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:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Tester DOM API</title>
</head>
<body onload="main()">
<div id="minuri">
Base-URI: <span id="uri">--</span>
</div>
<script type="text/javascript">
function main() {
  var uri = document.baseURI;
  document.getElementById("uri").innerHTML = uri;
}
</script>
</body>
</html>

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 (DOM, W3Schools.com) 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 (Anyuru, A. , 2012) og/eller (JavaScript HTML DOM Events) for mer informasjon om dette.

Tastaturhendelser

Man kan fange opp, og håndtere, følgende hendelser når bruker trykker en tastaturknapp:
  • keydown
  • keyup
  • keypress
Disse inntrer i samme rekkefølgen som angitt her. Hendelsene keydown og keyup er knyttet til fysiske knapper på tastaturet. Hendelsen keypress er knyttet til et tegn / «character».

Man legger til en «listener» slik:
  • document.addEventListener('keydown', handleKeyDown, false);
Tilhørende eventmetode kan se slik ut:

function handleKeyDown(event) {
  console.log("Keydown, keyCode=%d, charCode=%d", event.keyCode, event.charCode);
}

Legg merke til at keyCode representerer fysisk knapp på tastaturet (dersom A trykkes er denne 65, samme enten det er stor eller liten A trykkes) mens keyChar representerer tegnsettkode til tegnet som trykkes (vil være ulik for liten og stor A).

NB! Dette fungerer ulikt i ulike nettlesere. Se (Anyuru, A. 2012) for mer info.

Håndtere flere samtidige knappetrykk

Dette er spesielt aktuelt for spill – det er som kjekt å kunne gi gass og samtidig svinge i et bilspill. Se (Anyuru, A. 2012) for mer info. 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.

Musehendelser

Flere hendelser kan fanges opp men man kommer langt med følgende tre hendelser.
  • mousemove
  • mousedown
  • mouseup
Mousemove inntrer når man beveger pekeren over «klientområdet» i nettleseren. Her følger det med parametre som indikerer x,y-posisjonen til pekeren. Mousedown og mouseup inntrer når en av museknappene enten trykkes ned eller slippes. Hvilken knapp som trykkes/slippes følger som parameter.

Ingen kommentarer:

Legg inn en kommentar