Vytváříme Hello World pro WebGL

Když jsem se před časem poprvé ponořil do světa WebGL, začínal jsem na zelené louce. Kdybych chtěl mít rychle nějaký výstup, jistě bych sáhnul po hotovém řešení, poskytujícím přímo graf scény (například vynikající three.js). Já chtěl ale vědět, jak a proč ty věci fungují; každou funkci si vyzkoušet a pochopit její účel. Své poznatky budu sepisovat, kdyby se náhodou někomu hodily…

Seriál: Začínáme s WebGL (5 dílů)

  1. Vytváříme Hello World pro WebGL 15.5.2013
  2. WebGL: Milostný RGB trojúhelník 22.5.2013
  3. WebGL: Šrouby a matice 29.5.2013
  4. WebGL: Darth Shader 5.6.2013
  5. WebGL: Texturovat, nemíchat 12.6.2013

WebGL asi není třeba dlouze představovat – jde o JavaScriptové API pro akcelerované vykreslování grafiky do HTML canvasu; rozhraní je navrženo tak, aby bylo architektonicky identické s OpenGL ES 2.0. Pokud tedy máte s OpenGL nějakou zkušenost (já neměl žádnou), bude pro vás tento článek možná zbytečný a nezáživný.

Co vývojáře po prvních pár experimentech s WebGL nejvíce zaujme?

  • Podpora prohlížečů je dobrá, ale může se lišit v závislosti na konkrétním hardwaru či operačním systému.
  • WebGL je kromobyčejně ukecané, v porovnání s typickým JS API. To je daň za ohromnou flexibilitu a výkon.
  • WebGL se mizerně ladí; zpravidla je lepší postupovat po malých krůčcích a dokázat případné problémy redukovat a izolovat.
  • Na WebGL i OpenGL není skoro nic trojrozměrné; většinu 3D věcí si človek musí zařídit sám.

Během tohoto úvodního článku zkusíme vytvořit nejmenší funkční ukázku WebGL: nakreslíme jeden barevný bod. Něco takového je v tradičním 2D canvasu otázkou tří řádků kódu (viz článek Canvas – říkejme tomu plocha na kreslení); řešení ve WebGL je o poznání složitější a okouknout ho můžeme na této adrese: http://jsfiddle.net/ondras/qt7sk/.

Pojďme se nejprve podívat, co všechno WebGL potřebuje, aby mohlo něco vykreslit:

  1. Souřadnice prvků, které chceme vykreslovat. Můžou být jedno- až čtyřrozměrné; v našem případě to bude jeden dvourozměrný bod.
  2. Vertex shader; program, jehož úkolem je zejména přepočítat naše vlastní souřadice do tzv. clipspace, souřadného systému pro vykreslování v canvasu.
  3. Fragment shader (někdy též méně korektně nazývaný pixel shader); program, jehož úkolem je spočítat barvu jednotlivých bodů všech vykreslovaných objektů.

Abychom mohli s WebGL cokoliv dělat, musíme nejprve z HTML canvasu získat kontext:

var gl = document.querySelector("canvas").getContext("experimental-webgl");

Všechna následná volání budou metody objektu gl. Název „experimental-webgl“ je dočasný (i když jej používají všechny současné prohlížeče) a bude v budoucnu nahrazen plnohodnotným „webgl“.

Shadery

Vertex i fragment shader jsou programy v jazyce, který byl velmi originálně nazván GLSL (GL Shading Language). Tyto programy jsou vykonávány přímo na GPU (přesněji: ovladač grafické karty je kompiluje do kódu, který je vykonáván na GPU), a proto jsou extra rychlé. Pro nás to bohužel znamená nutnost naučit se krom WebGL API ještě další jazyk, ale naštěstí to není nic složitého. Jeho syntaxe je podobná C a pro náš testovací program budou oba shadery triviální. Zdrojový kód shaderů se ve WebGL předává jako řetězec; můžeme ho proto uložit do JS stringu, do externího souboru (XHR) nebo do uzlu <script> ve stránce. Pro naše účely bude bohatě stačit předání v JS řetězci. Ve WebGL musíme shader nejprve vytvořit, pak mu předat zdrojový kód a pak ho zkompilovat. Jak bude vypadat ten náš?

attribute vec2 pos;

void main(void) {
    gl_Position = vec4(pos, 0.0, 1.0);
    gl_PointSize = 5.0;
}

Jeho vstupem je proměnná (dvourozměrné pole) pos; hlavním úkolem vertex shaderu je naplnit pro každý vstupní bod vestavěnou proměnnou gl_Position, která odpovídá výslednému umístění bodu. Zajímavé je, že gl_Position je čtyřrozměrné pole; proto k našemu dvourozměrnému doplníme nulu (na ose Z; tato hodnota nás vůbec nezajímá) a jedničku. Jednička na konci je důležitá: gl_Position je tzv. homogenní souřadnice, která u bodů v prostoru potřebuje poslední hodnotu právě jedna. (Proč? Více o tomto v některé z dalších kapitol, kde si povíme o tom, jak přesně probíhá výpočet pozice v canvasu.) Jako bonus ještě nastavíme vestavěnou hodnotu gl_PointSize, aby náš bod vypadal větší než jeden pixel.

A můžeme jít na další shader:

void main(void) {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

Fragment shader je ještě jednodušší: do vestavěné proměnné gl_FragColor akorát přiřadí červenou. Barvy se zapisují též jako čtyřrozměrné vektory; čtvrtá složka je průhlednost (1 = žádná průhlednost). Všechny složky jsou hodnoty mezi nulou a jedničkou.

Následně z obou shaderů vytvoříme tzv. OpenGL program a nastavíme jej jako aktivní. To proto, že v praxi budeme chtít mít k dispozici programů/shaderů celou řadu:

var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);

Dále si „sáhneme“ na vstupní hodnotu vertex shaderu (pos) a řekneme, že její hodnotu budeme definovat pomocí JS pole:

var posLoc = gl.getAttribLocation(program, "pos");
gl.enableVertexAttribArray(posLoc);

Většina příprav je za námi, jde se renderovat! Vlastně ne, ještě jsme nikam nezadali naši geometrii, respektive souřadnice našeho jediného bodu. Pro tento účel se musíme seznámit s buffery.

Buffery

Veškerá data, předávaná do GPU, jsou uložena v tzv. bufferech – kusech paměti, rychle přístupných z grafické karty. Práce s buffery může vypadat podivně, protože připomíná stavový automat: nejprve nastavíme buffer jako aktivní (bindBuffer) a tím říkáme, že všechny následné operace budou prováděny právě nad ním. Žádné předávání bufferů jako parametrů.

var posBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0.5, 0.5]), gl.STATIC_DRAW);

Konstantou gl.ARRAY_BUFFER specifikujeme, že chceme pracovat s obecným polem. Ještě je tu varianta gl.ELEMENT_ARRAY_BUFFER, o které si povíme někdy příště. Voláním bufferData nahrajeme zadané JS pole (musí to být typované pole, v našem případě Float32Array) do paměti grafické karty. Třetí parametr (gl.STATIC_DRAW) říká, jak máme v plánu tato data využívat a měnit: v našem případě říkáme, že je nechceme měnit vůbec.

gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);

Tímto voláním provážeme právě aktivní buffer (ten, kde jsou souřadnice našeho bodu) s vstupem vertex shaderu. Dalšími parametry říkáme, že se z bufferu bude číst po dvou hodnotách, že to jsou destinná čísla, že je nechceme normalizovat, že nechceme žádné hodnoty přeskakovat a že budeme číst od začátku bufferu. Uff!

gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);

Konečně jsme něco namalovali! Voláním clearColor nastavujeme barvu, kterou se má canvas vymazat (černou). Voláním clear plátno vymažeme a vposled dáme pokyn k vykreslení. Naši geometrii chceme vykreslovat jako prosté body (gl.POINTS), začneme na prvním a vykreslíme jeden. Vidíme červený čtvereček, hurá!

Celý zdrojový kód z tohoto článku

Ve spustitelné podobě ho najdete na http://jsfiddle.net/ondras/qt7sk/, kde s ním můžete dále experimentovat (třeba při řešení domácího úkolu).

var gl = document.querySelector("canvas").getContext("experimental-webgl");

var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, 'attribute vec2 pos; void main(void) { gl_Position = vec4(pos, 0.0, 1.0); gl_PointSize = 5.0;}');
gl.compileShader(vertexShader);

var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, 'void main(void) {gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);}');
gl.compileShader(fragmentShader);

var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);

var posLoc = gl.getAttribLocation(program, "pos");
gl.enableVertexAttribArray(posLoc);

var posBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0.5, 0.5]), gl.STATIC_DRAW);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);

gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);

Domácí úkol

Na konec této kapitoly můžeme zvážit nějaká drobná vylepšení za domácí úkol:

  • Změnit barvu bodu? Jasně, stačí upravit fragment shader.
  • Změnit pozici bodu? Jasně, stačí upravit pole souřadic, předávané v metodě bufferData.
  • Vykreslit více bodů? Jasně, přidat další souřadnice do pole bodů a zvýšit jejich počet v metodě drawArrays.
  • Vykreslit body jako kolečka? Pokud se čtverečky nelíbí, bude to obtížnější. Takovou úpravu bychom provedli ve fragment shaderu (který je vykonáván pro každý vykreslovaný pixel). Přidali bychom test hodnoty gl_PointCoord, která nabývá hodnot mezi nulou a jedničkou a určuje vzdálenost od středu vykreslovaného vrcholu. Pokud by vzdálenost od středu byla vyšší než zadaný poloměr, bod bychom vykreslili jinou barvou (např. průhlednou).

Autor pracuje ve společnosti Seznam na všem, co alespoň trochu souvisí s JavaScriptem. Ve volném čase se mimo jiné zabývá věcmi, které alespoň trochu souvisí s JavaScriptem. O obojím občas tweetuje jako @0ndras.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 3

Přehled komentářů

Jan Prachař
nn domaca uloha
Ondra B Chyba v prohlížeči
Zdroj: https://www.zdrojak.cz/?p=8124