Modul 1.2: API og språk, Intro. WebGL

I denne sammenheng omtales teknologi som kombinasjon av programmeringsspråk og 3D API. Under er noen aktuelle teknologier for utvikling av 3D datamaskingrafikk:
  • C++ og OpenGL
  • Java og OpenGL (JOGL).
  • C++ og Direct3D
  • C# og Monogame (tidligere XNA)
  • Android, Java og Open GL ES
  • Javascript, WebGL, og OpenGL ES 
  • Javascript og Three.js
Det finnes også andre varianter som for eksempel Microsofts Windows Presentation Foundation som også tilbyr et 3D API. Dette er imidlertid ikke så omfattende som de ovenfor nevnte og er tiltenkt enklere animasjoner og utvikling av 3D brukergrensesnittelementer.

Her kan også kryssplatformverktøy som Xamarin (http://xamarin.com/), Phonegap (http://phonegap.com/) og libGDX (http://libgdx.badlogicgames.com/) nevnes. Alle disse gjør det mulig å utvikle til mobile platformer.

C++ og OpenGL

Bruk av C++ sammen med OpenGL er kanskje den mest utbredte måten å anvende OpenGL på. Det finnes C++ kompilatorer for de fleste operativsystemplattformer. Det samme gjelder OpenGL APIet. En variant er å bruke C++ og Qt-biblioteket fra Trolltech sammen med OpenGL.

Java OpenGL (JOGL)

Dette lar oss bruke Java til å programmere mot OpenGL APIet. JOGL er en referanseimplementasjon av JSR 231 (Java Bindings for OpenGL). Dette er ikke det samme som Java3D som opererer på et høyere abstraksjonsnivå.

C++ og Direct3D

Vi kan selvfølgelig kode direkte mot DirectX APIet vha. unmanaged C++. Dette krever at vi har installert DirectX SDKen. Microsoft Visual Studio er et egnet verktøy for denne type utvikling.

C# og Monogame

MonoGame.net (www.monogame.net) er et åpen kildekode-prosjekt. Monogame er et kryssplattformverktøy som lar oss skrive C#-kode som deretter kan kompileres til aktuell plattform. Monogame støtter per i dag følgende plattformer: iOS, Android, Windows (både OpenGL and DirectX), Mac OS X, Linux, Windows, PlayStation Mobile og OUYA. Monogame er en videreføring av Microsoft XNA som ble "nedlagt" i 2013. 

Avhengig av plattform som det kompileres til brukes OpenGL, OpenGL ES eller DirectX i bunnen.

Android og Open GL ES

Android støtter OpenGL ES som et rent Java eller Kotlin-API. Dersom man behersker Android/Java/Kotlin og samtidig er kjent med WebGL/Javascript vil overgangen til 3D-grafikk på Android-plattformen være enkel.

Introduksjon til WebGL, Javascript og Open GL ES

WebGL

Ved hjelp Javascript og WebGL-APIet kan man utvikle applikasjoner som kan generere og vise 3D-grafikk direkte i nettleseren uten bruk av spesielle plugins e.l. WebGL er basert på OpenGL ES (Khronos Group, OpenGL ES) og brukes sammen med HTML5 og Javascript.

Figuren under viser sammenhengen.



All tegning gjøres på et et HTML5 «canvas» element som tilsvarer et rektangulært område i nettleseren. HTML5 canvas-elementet ble opprinnelig tatt med i HTML-standarden for å kunne tegne 2D grafikk men brukes også til 3D.

Canvas
I 2009 etablerte Khronos-gruppen (www.khronos.org) en arbeidsgruppe for WebGL som nå består av flere nettleserprodusenter som Apple, Google, Mozilla og Opera.

Khronos-gruppen ble etablert i år 2000 og er en «non-profit» organisasjon som utvikler og vedlikeholder standarder for diverse APIer, deriblant OpenGL, OpenGL ES, WebGL, OpenCL m.m.

WebGL versjon 1.0 ble «frosset» i mars 2011 og støtte for WebGL er nå blitt standard i de fleste store nettlesere som Google Chrome, Mozilla Firefox, Microsoft Explorere, Safari og Opera.

Med WebGL har man tilgang til maskinvareakselerert interaktiv 3D-grafikk direkte i nettleseren – dvs. nettleseren utnytter evt. grafikkort i datamaskinen.

Vha. WebGL kan man lage fullverdige 3D spill som kjører direkte i nettleseren. Distribusjonsmodellen blir dermed svært enkel, all kode ligger på en webserver og lastes ned til nettleseren på samme måte som vanlige web-sider. Det er heller ingen «royalties» involvert – WebGL er fritt tilgjengelig.

Siden WebGL er basert på OpenGL ES, som igjen er basert på OpenGL, vil utviklere som tidligere har brukt noen av OpenGL-APIene enkelt kunne ta i bruk WebGL.

WebGL 1.0 eller WebGL 2.0?  

WebGL 1.0 er basert på OpenGL ES 2.0 og benytter shaderspråket GLSL ES versjon 1.0. WebGL 2.0 ble standardisert i 2017 og er basert på OpenGL ES versjon 3.0. WebGL 2.0 støtter derfor, i tillegg til  GLSL ES 1.0, også shaderspråket GLSL ES 3.0.

Dokumentasjon av WebGL APIene ligger her:
WebGL 1.0 APIet gir tilgang til OpenGL ES 2.0 funksjonalitet mens WebGL 2.0 APIet også gir tilgang til OpenGL ES 3.0 spesifikk funksjonalitet. WebGL 1.0 programmer vil dermed også fungere med WebGL 2.0. 

Kodeeksempler knyttet til disse notatene bruker webgl 2.0.

Komme i gang med WebGL

Det er enkelt å komme i gang med WebGL – man trenger i prinsippet kun en teksteditor til å skrive Javascript-kode og en nettleser der man kan teste koden.

Dersom man bruker Firefox til testing bør man også laste ned og installere Firefox-plugin Firebug (https://getfirebug.com/) som gjør det mulig å debugge Javascript-kode direkte i nettleseren. Google Chrome har et slikt verktøy integrert som standard (velg Verktøy | Utviklerverktøy).

Javascript

Javascript ble opprinnelig utviklet av Netscape for å kunne tilføre dynamiske elementer på websider og lastes ned fra webserveren. Javascriptkoden, som gjerne er integrert i html-koden, kjøres lokalt av nettleseren.

Tradisjonelt har Javascript-kode kjørt interpreterende. I nyere versjoner av nettlesere JIT-kompileres gjerne koden før den kjøres. Dette for å øke ytelsen. Javascript støtter også (en form for) objektorientering.

Javascript har en Java-liknende syntaks men er ellers svært forskjellig fra programmeringsspråket Java . Se (Javascript, 2014) og (Javascript tutorial, 2014).

Kort introduksjon til Three.js

Three.js (http://threejs.org/) er et Javascript bibliotek som gjør livet enklere for WebGL-utviklere. Three skjuler flere av de kompliserende elementene som man må forholde seg til i "ren" WebGL, som f.eks. shadere.

Ved hjelp av noen få linjer med kode kan man f.eks. vise en roterende, teksturert og belyst kube. Tilsvarende i ren WebGL krever mange titalls, om ikke hundrevis av kodelinjer.

Før vi sier mer om html5 og canvas går vi rett på et eksempel. Åpner man følgende html-fil i nettleseren vil man se en grønn kube:

Fila demo1.html:

<html>
<head>
<title>En grønn kube</title>
</head>
<body onload="main()">
  <canvas id="webgl" width="600" height="600">
  Denne nettleseren støtter ikke "canvas"-elementet!
  </canvas>
    <script src="../lib/three.js"></script>
    <script src="js/demo1.js"></script>
  </body>
</html>


Fila js/demo1.js:

let scene;
let renderer;
let camera;
let cube;

function main() {
  //Henter referanse til canvaset:
  let mycanvas = document.getElementById('webgl');
  //Lager en scene:
  scene = new THREE.Scene();
  //Oppretter et kamera:
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); 
  //Flytter litt på kamera (står opprinnelig i 0,0,0):
  camera.position.z = 3;
  camera.position.y = 0;
  //Lager et rendererobjekt (og setter størrelse):
  renderer = new THREE.WebGLRenderer({canvas:mycanvas, antialias:true});
  //renderer.setSize(window.innerWidth, window.innerHeight);
  //Definerer geometri og materiale (her kun farge) for en kube:
  let geometry = new THREE.BoxGeometry(1, 1, 1);
  let material = new THREE.MeshBasicMaterial({color : 0x00ff00 });
  //Oppretter et kubemesh vha. geomatri og materiale:
  cube = new THREE.Mesh(geometry, material);
  //Legger kuben til scenen:
  scene.add(cube);
  //Tegner scenen med gitt kamera:
  renderer.render(scene, camera);
}


Html-fila refererer javascriptfila, js/demo1.js, som starter med å hente en referanse til canvas-objektet, dvs. flaten som det skal tegnes på. Videre opprettes et Three scene-objekt. Three bruker en såkalt scenegraf slik at alle elementer, som figurer/objekter, kamera og lyskilder legges til en hierarkisk organisert scenegraf.

Deretter opprettes et kameraobjekt. Her brukes et perspektivkamera. Hva de ulike parametrene betyr vil bli forklart senere. Som standard står kameraet i origo og peker "innover", mot negativ z. Kameraet flyttes litt "utover" slik at det står i [0,0,3] og peker mot origo. Siden kuben er tegnet om origo vil den fanges opp av kameraet og vises på skjermen. Kameraets kan også settes til å peke i andre retninger. Mer om dette senere.

Det må opprettes et "renderer"-objekt som brukes til å rendre (gjengi) hele "scenen". Man kan enkelt legge til flere kuber e.l. i sceneobjektet som også vil bli tegnet når man kaller render() på renderer-objektet.

Selve kuben genereres vha. et geometriobjekt og et materialobjekt. Disse indikerer henholdsvis "geometrien" (form og størrelse) og materialtype, dvs. en kombinasjon av farge, belysning og eventuell tekstur. Her brukes kun farge slik at kuben fremstår som grønn på skjermen.

WebGL: 2 og 3D grafikk direkte i nettleseren

Vi fortsetter her med "ren" WebGL, dvs. uten bruk av Three.js.

HTML5 Canvas-elementet

I Three-eksemplet over brukte vi <canvas> elementet i html-fila. Ved hjelp av <canvas> og Javascript kan man, som vist, generere og vise/gjengi («rendre») både 2D og 3D-grafikk direkte i nettleseren.

Canvas og 2D

Eksemplet under viser hvordan man tegner 2D grafikk på canvas-elementet. HTML-fila ser slik ut:

tegnrektangel/index.html:
<!DOCTYPE html>
<html lang="nb">
<head>
<meta charset="utf-8">
<title>Html5 canvas</title>
<link rel="stylesheet" href="../../base/webgl.css" type="text/css">
</head>

<body>
<script type="module" >
'use strict';
/**
* The purpose of "use strict" is to indicate that the code should be executed in "strict mode".
* With strict mode, you can not, for example, use undeclared variables.
* https://www.w3schools.com/js/js_strict.asp
*/
import {main} from "./tegnRektangel.js";
main();
</script>
</body>
</html>
I .html fila defineres et <script> av type "module". Her importeres en funksjon, main(), fra fila helloRektangel.js. Funksjonen kalles så for å starte programmet. Funksjonen ser slik ut:

tegnrektangel/tegnRektangel.js:
export function main() {
// Oppretter html5-canvas (pakket inn i et div-element):
let divWrapper = document.createElement('div');
let canvasElem = document.createElement('canvas');
document.body.appendChild(divWrapper);
divWrapper.appendChild(canvasElem);
divWrapper.id = 'my2DCanvas';
canvasElem.width = 960;
canvasElem.height = 480;

// RenderingContext for 2D
let gl = canvasElem.getContext('2d');
gl.fillStyle = 'rgba(0, 255, 255)'; //Setter farge.
gl.fillRect(0, 0, 250, 250); //Fylt rektangel.
}
Her opprettes et html canvas element som pakkes inn i et html div-element. Dette legges så inn i document.body slik at det blir en del av html-dokumentet. Her setter vi størrelse på canvaset til 960x480. Vi gir også canvaset en id. Deretter hentes en referanse, gl ("graphics library"), til en 2D kontekst. Vha. denne kan man tegne enkel 2D grafikk vha. fillRect() o.l.

Som vi skal se i neste avsnitt følger vi omtrent samme oppskrift når vi skal tegne 3D grafikk vha. WebGL. Den viktigste forskjellen er at kontekst-objektet hentes ut på følgende måte:

let gl = canvasElem.getContext('webgl2');
Den andre store forskjellen er måten man bruker gl-objektet til å kalle på WebGL/OpenGL ES-metoder/funksjoner for å få generert 3D grafikk. Dette gjøres på en helt annen måte enn hva man gjør i forhold til 2D grafikk.

Canvas og 3D

I denne gjennomgangen skal vi se hvordan man bruker WebGL-kontekst til å tegne på. I det første eksemplet, helloCanvas, setter vi kun bakgrunnsfargen.

hellocanvas/index.html
<!DOCTYPE html>
<html lang="nb">
<head>
<meta charset="utf-8">
<title>Hello WebGL Canvas</title>
<link rel="stylesheet" href="../../base/webgl.css" type="text/css">
</head>

<body>
<div style="top:0px; left:15px; width:100%; text-align:left; color:black;" class="ui">
<h2>Hello WebGL Canvas</h2>
</div>
<script type="module" >
'use strict';
import {main} from "./helloCanvas.js";
main();
</script>
</body>
</html>
I .html fila defineres et <script> av type "module". Her importeres en funksjon, helloCanvas(), fra fila helloCanvas.js. Funksjonen kalles så for å starte programmet. Funksjonen helloCanvas() ser slik ut:

hellocanvas/helloCanvas.js
import {WebGLCanvas} from '../../base/helpers/WebGLCanvas.js';
export function main() {
// Oppretter WebGL-kontekst for 3D/WebGL-tegning:
const canvas = new WebGLCanvas('myCanvas', document.body, 960, 640);
const gl = canvas.gl;
// Klargjør canvas:
gl.clearColor(0.9, 0.2, 0.9, 1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
Ved hjelp av WebGLCanvas-klassen opprettes et WebGLCanvas-objekt. WebGLCanvas-klassen setter gl lik:
this.gl = canvasElem.getContext('webgl2');
I helloCanvas() kan vi så hente ut gl fra canvas-objektet. Videre vil gl bli brukt til å utføre WebGL/OpenGL ES kall for å få generert 3D-grafikk på canvaset. I eksemplet settes kun bakgrunnsfargen til canvaset vha. gl.clearColor(…) og gl.clear(…).

Legg merke til at koden importerer WebGLContext.js. Denne ser slik ut:
/**
* Lager et <div> element som inneholder et <canvas> element.
* <div> elementet gis en id.
*/
export class WebGLCanvas {
constructor(id, parent, width, height) {
let divWrapper = document.createElement('div');
let canvasElem = document.createElement('canvas');
parent.appendChild(divWrapper);
divWrapper.appendChild(canvasElem);
divWrapper.id = id;
canvasElem.width = width;
canvasElem.height = height;
this.gl = canvasElem.getContext('webgl2');
if (!this.gl)
alert('En feil oppsto ved lesing av WebGL-konteksten.');
}
}

WebGL & 3D grafikk

Hvordan fremstiller man 3D objekter på en 2D skjerm? Dette er i korte trekk hva 3D datamaskingrafikk handler om. 3D grafikk kan fremstilles på flere måter vha. WebGL.

Modeller kan tegnes ”manuelt” vha. vertekser og primitiver i et tenkt 3D rom. Foreløpig kan vi tenke på en verteks som et punkt i rommet definert av en x,y og z-verdi mens et primitiv kan være et punkt, en linje eller en trekant som er definert av henholdsvis en, to eller tre vertekser.

Det er også mulig å laste inn modeller fremstilt i andre verktøy, som for eksempel 3D Studio Max, Blender o.l. Slike modeller er på tilsvarende måte konstruert av et (stort) antall vertekser og primitiver som behandles på samme måte som ”manuelle” modeller. Deretter vil verteksene gå gjennom flere transformasjoner, via shaderne og GPUen, før de til slutt ender opp som piksler på skjermen. I første omgang vil «manuelle modeller», dvs. der vi selv definerer vertekser og primitiver, gjennomgås.

Tegne på canvaset – bruk av shader

Shadere må brukes uansett hva vi skal tegne i WebGL. Hva er så en shader? For å kunne svar på dette må man se på hele prosessen, fra vi definerer vertekser til f.eks. en trekant i WebGL/Javascript-programmet til pikslene som utgjør denne trekanten ender opp i skjermbufret og til slutt på canvaset/skjermen.

Hele prosessen omtales som en «graphics/rendering pipeline», se figuren under:
Forenklet graphics/rendering pipeline
Til venstre ser vi Javascript/WebGL-programmet der man spesifiserer verteksene til modellen som skal tegnes.

Dersom vi ser på eksemplet med en enkel trekant består den av tre vertekser. Hver verteks vil bli sent til verteksshaderfunksjonen. Dette er et eget program/funksjon som mottar verteksene og ev. gjør diverse transformasjoner på disse. Legg merke til at det som skjer fra og med verteksshaderen og utover utføres av GPUen. Transformert verteks "videresendes" fra verteksshaderfunksjonen (og sendes videre i pipelinen) ved at gl_Position settes lik (ev. transformert) verteks (aVertexPosition).

Verteksene sammenstilles (slik at de utgjør en trekant) og deretter vil det bli beregnet hvilke piksler/fragmenter (foreløpig sier vi at et fragment tilsvarer en piksel – egentlig er et fragment en «kandidat» til å bli en piksel i skjermbufret, ikke alle fragmenter ender opp som piksler i skjermbufret) som skal til for å få tegnet denne trekanten.

For hver piksel/fragment som inngår i trekanten vil fragmentshaderen bli kjørt. Denne kan manipulere hver enkelt fragment. Fragmentshaderen må gi gl_FragColor en fargeverdiene (RGBA). Denne ender så opp i skjermbufret som til slutt vises på skjermen.

Foreløpig er denne gjennomgangen noe forenklet – det er flere steg som inngår, spesielt mellom fragmentshader og framebuffer, men disse er ikke tatt med her. Her er kun det de viktigste stegene vist.

Tegne et punkt

Vi starter så enkelt som overhodet mulig - det vil si å tegne et enkelt punkt på skjermen. Det nye er her bruk av shadere. Shaderkoden er lagt i html-fila vha. script-tagger av type "x-shader/x-vertex" eller "x-shader/x-fragment". Disse gis også en id. 

Shaderfunksjonene er skrevet i shaderprogrammeringsspråket OpenGL ES Shading Language (Khronos Group, GLSL ES, 2009). Dette er en variant av OpenGL GLSL. Dette språket har sin egen syntaks, datatyper, nøkkelord som attributes og uniforms osv. Vi kommer tilbake til detaljene etter hvert. Vi starter med .html fila:

hellopoint/index.html:
<!DOCTYPE html>
<html lang="nb">
<head>
<meta charset="utf-8">
<title>WebGL Hello Point</title>
<link rel="stylesheet" href="../../base/webgl.css" type="text/css">
</head>

<body>
<div style="top:0px; left:15px; width:100%; text-align:left; color:black;" class="ui">
<h2>WebGL Hello Point</h2>
</div>
<!-- SHADERS -->
<script id="simple-vertex-shader" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;
void main(void) {
gl_Position = vec4(aVertexPosition, 1.0);
gl_PointSize = 50.0;
}
</script>
<script id="simple-fragment-shader" type="x-shader/x-fragment">
void main(void) {
gl_FragColor = vec4(1.0, 0.4, 1.0, 1.0);
}
</script>

<script type="module" >
'use strict';
import {main} from "./helloPoint.js";
main();
</script>
</body>
</html>

hellopoint/hellopoint.js:
'use strict';
import {WebGLContext} from '../../base/helpers/WebGLContext.js';
import {WebGLShader} from '../../base/helpers/WebGLShader.js';

/**
* Et WebGL-program som viser et HTML5-canvas med et punkt.
*/
export function main() {
// Oppretter WebGL-kontekst for 3D/WebGL-tegning:
const canvas = new WebGLCanvas('myCanvas', document.body, 960, 640);
const gl = canvas.gl;
let shaderInfo = initShaders(gl);
draw(gl, shaderInfo);
}

function initShaders(gl) {
// Leser shaderkode fra HTML-fila: Standard/enkel shader (posisjon og farge):
let vertexShaderSource = document.getElementById('simple-vertex-shader').innerHTML;
let fragmentShaderSource = document.getElementById('simple-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'),
},
uniformLocations: {
//.. foreløpig ikke i bruke
},
};
}

function connectPositionAttribute(gl, shaderInfo) {
gl.vertexAttrib3f(shaderInfo.attribLocations.vertexPosition, 0.0, 0.0, 0.0);
}

/**
* Klargjør canvaset.
*/
function clearCanvas(gl) {
gl.clearColor(0.2, 0.9, 0.3, 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);
}

/**
* Tegner!
*/
function draw(gl, shaderInfo) {
clearCanvas(gl);

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

// Kople til/klargjør posisjonsattributtet/parametret:
connectPositionAttribute(gl, shaderInfo);

// Tegn!
gl.drawArrays(gl.POINTS, 0, 1);
}
I html-fila ligger kildekoden til verteks- og fragment-shaderne som programmet bruker for å tegne punktet. Begge shaderfunksjonene består av en main() funksjon.

Javascript-koden bruker WebGL-APIet for å tegne et enkelt punkt på canvaset. Punktet består av en verteks

I main() opprettes et WebGLCanvas-objekt, som inneholder gl-objektet. Dette representerer WebGL-APIet og brukes, som vi ser, "overalt" i programmet til å utføre WebGL-kall. 

Fra main() kalles initShaders(). Denne funksjonen leser vertex- og fragmentshaderne fra html-fila. Ved hjelp av klassen WebGLShaders kompileres shaderkoden. Funksjonen returnerer et objekt som blant annet inneholder det kompilerte shaderProgram-objektet fra WebGLShaders-objektet. 

Deretter kalles draw() som starter med å renske skjermen/canvaset. Deretter aktiveres nevnte  shaderProgram. Ved hjelp av funksjonene connectPositionAttribute() aktiveres aVertexPosition i verteksshaderen. Legg merke til at aVertexPosition er av type vec3 (vektor bestående av 3 elementer, slik [x, y, z]).  vec3 er en GLSL-datatype.

Ved hjelp av funksjonen gl.vertexAttrib3f(…) sendes verteksen [0,0,0] til vertekshaderen og parametret aVertexPosition. I shaderen er denne av type vec3. Vi ser at denne utvides til en vec4 når vi setter gl_Position. Det siste elementet (w) settes her lik 1 (kommer tilbake til hvorfor dette gjøres).

Størrelsen på punktet bestemmes av gl_PointSize som her hardkodes til 50. Denne kunne alternativt blitt sendt inn til shaderen vha. gl.vertexAttrib1f(aVertexPointSize, 50.0).

I siste linje kalles drawArrays(gl.POINTS, 0, 1). Dette trigger verteksshaderen som setter gl_Position = aVertexPosition. Variabelen gl_Position er en spesiell GLSL-variabel og verteksshaderen har som hovedoppgave å gi denne variabelen en verdi. Som regel vil man gjøre diverse transformasjoner (matrisemultiplikasjoner) på aVertexPosition og tilordne resultatet til gl_Position. I eksemplet over utføres ingen transformasjon, gl_Position settes lik vec4(aVertexPosition, 1.0).

Variabelen gl_PointSize er også spesiell og brukes kun i tilfeller der man tegner punkter. Her settes gl_PointSize lik 50 og bestemmer hvor stort punktet blir.

Etter at verteksshaderen har kjørt for alle vertekser (her kun en) vil det automatisk beregnes hvilke piksler som trengs for å tegne punktet. For hver piksel som inngår i punktet kjøres fragmentshaderen. Tenk foreløpig på et fragment som det samme som en piksel. Hovedoppgaven til fragmentshaderen er å returnere fargen til fragmentet. I eksemplet over returneres en «hardkodet» farge, som betyr at alle piksler får samme farge. Det er også mulig å knytte en farge til verteksene slik at fragmentene får farge basert på verteksfargene. Mer om dette etter hvert.

Kjører man koden over i en standard nettleser vil det gi (omtrent) følgende resultat:



Med andre ord; et stort punkt i origo.

Tegne en trekant

En trekant består av tre vertekser og man bruker typisk et verteksarray og et verteksbuffer til dette. Følgende fem steg inngår for å få sendt et verteksarray til shaderen:
  • Opprett et bufferobjekt 
    • gl.createBuffer() 
  • Binde bufret – sette gjeldende buffer 
    • gl.bindBuffer() 
  • Skrive data/verteksarray til bufferet 
    • gl.bufferData() 
  • Kopler shaderparameter til bufferobjektet 
    • gl.vertexAttribPointer() 
  • «Enable» tilordninga (over): 
    • gl.enableVertexAttribArray() 
Følgende kodeeksempel viser dette:

hellotroanglesimple/index.html:

<!DOCTYPE html>
<html lang="nb">
<head>
<meta charset="utf-8">
<title>WebGL Hello Triangle Simple</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 Hello Triangle Simple</h2>
</div>
<!-- SHADERS -->
<script id="simple-vertex-shader" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;
void main(void) {
gl_Position = vec4(aVertexPosition, 1.0);
}
</script>
<script id="simple-fragment-shader" type="x-shader/x-fragment">
uniform lowp vec4 uFragmentColor;
void main(void) {
gl_FragColor = uFragmentColor;
}
</script>
<script type="module" >
'use strict';
import {main} from "./helloTrianglesimple.js";
main();
</script>
</body>

</html>
Vi ser at denne refererer helloTrianglesimple() som ligger i helloTrianglesimple.js:
'use strict';

import {WebGLContext} from '../../base/helpers/WebGLContext.js';
import {WebGLShader} from '../../base/helpers/WebGLShader.js';

/**
* Et WebGL-program som tegner en enkel trekant.
*/
export function helloTrianglesimple() {
// Oppretter WebGL-kontekst for 3D/WebGL-tegning:
const context = new WebGLContext('myCanvas', document.body, 960, 640);
const gl = context.gl;
let shaderInfo = initBaseShaders(gl);
let buffers = initBuffers(gl);
draw(gl, shaderInfo, buffers);
}

function initBaseShaders(gl) {
// Leser shaderkode fra HTML-fila: Standard/enkel shader (posisjon og farge):
let vertexShaderSource = document.getElementById('simple-vertex-shader').innerHTML;
let fragmentShaderSource = document.getElementById('simple-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'),
},
uniformLocations: {
fragmentColor: gl.getUniformLocation(glslShader.shaderProgram, 'uFragmentColor'),
},
};
}

/**
* Oppretter verteksbuffer for trekanten.
* Et posisjonsbuffer og et fargebuffer.
* MERK: Må være likt antall posisjoner og farger.
*/
function initBuffers(gl) {
const positions = new Float32Array([
0.7, 0.5, 0, // X Y Z
-0.5, -0.9, 0, // X Y Z
0.5, -0.5, 0, // X Y Z
]);

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);

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

/**
* Aktiverer position-bufferet.
* Kalles fra draw()
*/
function connectPositionAttribute(gl, shaderInfo, 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(
shaderInfo.attribLocations.vertexPosition,
numComponents,
type,
normalize,
stride,
offset);
gl.enableVertexAttribArray(shaderInfo.attribLocations.vertexPosition);
}
function connectColorUniform(gl, shaderInfo) {
let colorRGBA = [1.0, 1.0, 0.0, 1.0];
gl.uniform4f(shaderInfo.uniformLocations.fragmentColor, colorRGBA[0],colorRGBA[1],colorRGBA[2],colorRGBA[3]);
}
/**
* 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);
}

/**
* Tegner!
*/
function draw(gl, shaderInfo, buffers) {
clearCanvas(gl);

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

// Kople posisjon-attributtet til tilhørende buffer:
connectPositionAttribute(gl, shaderInfo, buffers.position);
// Kople til farge uniform:
connectColorUniform(gl, shaderInfo);
// Tegn!
gl.drawArrays(gl.TRIANGLES, 0, buffers.vertexCount);
}
I funksjonen initBuffers() opprettes et VertexBuffer-objekt vha. et Float32Array-objekt, som igjen inneholder de tre verteksene/posisjonene. Bufret må «aktiveres» ved at det bindes til gl.ARRAY_BUFFER før data fra positions-arrayet kopieres til bufret vha. gl.bufferData(…). 

I draw() koples aVertexPosition til data i position-bufret vha. vertexAttribPointer(…) før bufret til slutt «enables». Funksjonen vertexAttribPointer() forteller WebGL at data (posisjoner eller farger) skal hentes fra bufret som sist ble bundet vha. bindBuffer(…). 

Funksjonsprototypen til vertexAtttribPointer() ser slik ut:

    gl.vertexAttribPointer(
      location,
      numComponents,
      typeOfData,
      normalizeFlag,
      strideToNextPieceOfData,
      offsetIntoBuffer);

Første parameter indikerer hvilken parameter i verteksshaderen som skal motta data, andre parameter angir antall float-verdier per verteks (for posisjon er denne 3 og for farger er den typisk 4). Det tredje parametret angir datatype som brukes (her gl.FLOAT). Nest siste parameter, strideToNextPieceOfData, er aktuell når man knytter andre verdier, som f.eks. farge eller normalvektor, til verteksene. Det siste parametret angir hvor i bufret første verteks ligger (her indeks 0).

Deretter sendes det inn en fargeverdi, uFragmentColor. Her indikerer prefikset "u" at dette er en såkalt «uniform» parameter. I dette tilfellet betyr det at alle fragmenter får samme farge. I fragmentshaderen ser vi at gl_FragColor settes lik uFragmentColor.

I draw() funksjonen renskes skjermen og drawArrays() kalles med gl.TRIANGLES som første parameter. Det andre parametret antyder hvor i bufret første verteks skal hentes fra (her indeks 0) mens det siste parametret indikerer hvor mange vertekser som skal tegnes. Det første parametret, gl.TRIANGLES, antyder hvordan verteksene skal sammenstilles. For hver verteks i som ligger i verteksbufret vil fragmentshaderen kjøre. Aktuell verteks vil komme som innparameter via aVertexPosition. Videre ser vi at verteksshaderen setter gl_Position lik aVertexPosition.

Mer om bindBuffer()

Anta at vi har definert følgende vertekstabell:

let positions = [-50.0, 50.0, 0.0,
-50.0,-50.0, 0.0,
50.0,-50.0, 0.0,
50.0, 50.0, 0.0];

var myBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, myBuffer);

Etter dette vil myBuffer være gjeldende buffer. Dette betyr at påfølgende bufferoperasjoner vil utføres vha. dette, inntil det "avbindes" eller at et annet buffer settes som gjeldende vha. et nytt bindBuffer() kall.

Det første parametret til bindBuffer() kan ha følgende to verdier:
  • gl.ARRAY_BUFFER 
  • gl.ELEMENT_ARRAY_BUFFER. 
Dersom bufret inneholder verteksdata brukes ARRAY_BUFFER mens ELEMENT_ARRAY_BUFFER brukes dersom det inneholder indeksdata.

En figur/modell kan bestå av mange trekanter der flere av trekantene kan dele samme verteks. I stedet for å lagre verteksdata flere ganger (i et verteksbuffer) lagres kun indeksen, i et indeksbuffer (mer om dette senere).

Deretter brukes typisk bufferData() for å fylle bufret med verteksdata:
  • gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
Etter dette bør man kople fra bufret vha.:
  • gl.bindBuffer(gl.ARRAY_BUFFER, null);
Før man kan kalle på drawArrays() må man kople shaderparametre med til aktuelle bufferobjekter.

Kople shaderparametre til bufferobjekter

Etter at bufferobjektet er opprettet må dette koples til shaderparametre / attributter. Eksempler på shaderattributt er a_Position definert i verteksshaderen. Et verteksshaderattributt er assosiert med et, og kun et, bufferobjekt som vist i figuren under.
Shaderattributt koplet til bufferobjekt
Vi kopler shaderparametre til bufferobjektet slik:
  • Binde til aktuelt bufferobjekt. 
  • Setter et shaderattributtet til å peke på bufret, dvs. aktiverte buffer. 
  • Til slutt må attributtet gjøres aktivt («enable»). 
Som allerede vist over aktiveres aktuelt buffer slik:
  • gl.bindBuffer(gl.ARRAY_BUFFER, myBuffer);
Ved hjelp av WebGL-funksjonen vertexAttribPointer() koples shaderattributtet (f.eks. a_Position) til verteksbufret:

let floatsPerVertex = 3;
let posAttrib = gl.getAttribLocation(gl.program, 'a_Position');
gl.vertexAttribPointer(posAttrib, floatsPerVertex, gl.FLOAT, 
 false, 0, 0);

Funksjonen gl.getAttribLocation() returnerer shaderparametrets posisjon (i form av en integer) i shaderen.

Parametrene til vertexAttribPointer(index, size, type, norm, stride, offset) har følgende betydning:
  • Index (her posAttrib) er en indeks som refererer til attributtet a_Position definert i verteksshaderen. Dette får sin verdi vha. gl.getAttribLocation() som vist. 
  • Size (her floatsPerVertex = 3) indikerer antall floats per verteks. 
  • Type (her FLOAT) indikerer datatypen til verdiene i bufret. 
  • Norm (her false). Vi bruker stort sett alltid false her. 
  • Stride (her 0). Denne indikerer at verteksverdiene følger direkte etter hverandre. Dersom bufret også inneholder fargeverdier i tillegg til koordinatverdier vil dette parametret indikere «avstanden» mellom koordinatverdiene til hver enkelt verteks. 
  • Offset (her 0). Indikerer startindeksen til hvor i bufret data skal hentes fra. 
Deretter må «attributtpekeren», posAttrib, gjøres aktiv, slik:
  • gl.enableVertexAttribArray(posAttrib);
Verteksshaderen vil nå hente posisjonsdata fra assosiert verteksbuffer som vist i figuren under:



I dette tilfellet vil shaderen hente tre verdier i slengen pga. at verdien på andre parameter til vertexAttribPointer() er satt lik 3.

Se tilgjengelige kodeeksempler.

5 kommentarer:

  1. Nyttig guide. Hvis du er ute etter egnede mobilapplikasjonsutviklere, kan Cleveroad hjelpe deg med dette.

    SvarSlett
  2. Hei, du har denne setningen: "Funksjonen helloPoint() oppretter et WebGLCanvas-objekt og henter, ved. hjelp av dette, gl-objektet. Dette brukes til å utføre WebGL-kall." Tror du mener Funksjonen main() i helloPoint modulen. Det finnes ingen funksjon helloPoint() i hele git prosjektet.

    SvarSlett

  3. "I neste steg vil verteks- og fragmentshaderne lastes og kompileres i initShaders(…) funksjonen. Legg merke til bruk av klassen WebGLShader." - initBaseShaders(...)

    SvarSlett
  4. Takk for tilbakemelding. Teksten er justert.

    SvarSlett
  5. i eksempelkoden til threejs kan man istedetfor den lokale threejs bruke https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js dersom man vil enkelt teste koden.

    SvarSlett