WebGL: Milostný RGB trojúhelník

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

V předchozí části jsme se seznámili se základními koncepty práce s WebGL, napsali triviální shadery a vykreslili jeden či více bodů. Přišel čas ozkoušet další geometrické primitivum, totiž čáru. Zároveň zkusíme zapracovat na obarvení. Výsledný kód dnešní ukázky je opět online k dispozici, na adrese http://jsfiddle.net/ondras/gf5c6/.

Naše shadery budou časem komplikovanější, proto je přestaneme ukládat jako JS řetězce a odložíme si je do HTML:

<script id="vs" type="x-shader/x-vertex">
...
</script>

<script id="fs" type="x-shader/x-fragment">
...
</script>

Načtení obsahu těchto „skriptů“ je triviální:

var str = document.querySelector("#vs").textContent;
var str = document.querySelector("#fs").textContent;

Tím dokážeme hezky oddělit shadery od zbylé logiky, psané v JavaScriptu.

Interpolace mezi shadery

Zatímco vertex shader je vykonáván pro každý vrchol zadané geometrie, fragment shader bude spuštěn pro každý vyrenderovaný pixel. Toho můžeme využít a předat hodnotu z vertex shaderu do fragment shaderu; taková hodnota se ve fragment shaderu objeví změněná (interpolovaná) tak, aby odpovídala vzdálenosti právě zpracovávaného pixelu od jednotlivých vrcholů geometrie.

Zkusíme proto vykreslit trojúhelník, jehož vrcholy obarvíme červeně, zeleně a modře. Tuto barevnou hodnotu předáme z vertex shaderu do fragment shaderu; kreslením pixelů na hranách tak vytvoříme barevný přechod. Pro tento účel definujme v shaderech nový typ parametru, tzv. varying: automaticky interpolovaná hodnota, do které přiřazujeme ve vertex shaderu a ze které čteme ve fragment shaderu.

attribute vec2 pos;
attribute vec3 color;
varying vec3 varyingColor;

void main(void) {
  gl_Position = vec4(pos, 0.0, 1.0);
  varyingColor = color;
}
precision mediump float;
varying vec3 varyingColor;

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

Ve fragment shaderu je nyní již také nutné určit, s jakou přesností mají být výpočty (interpolace) prováděny – proto řádek precision mediump float;.

Vstupní atribut vertex shaderu color bude naplněn stejně, jako již existující pos: barvy pro každý vrchol zadáme v JavaScriptu. Vytvoříme si pro tento účel nový buffer:

var colorLoc = gl.getAttribLocation(program, "color");
gl.enableVertexAttribArray(colorLoc);
var colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
var colors = [
  1.0, 0.0, 0.0,
  0.0, 1.0, 0.0,
  0.0, 0.0, 1.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
gl.vertexAttribPointer(colorLoc, 3, gl.FLOAT, false, 0, 0);

V posledním řádku říkáme, že hodnoty z tohoto bufferu budou k dispozici v atributu color vždy po trojicích floatů (čtvrtou složku, alpha, doplňujeme jako jedničku ve fragment shaderu). Souřadnice vrcholů definujeme stejně jako v minulé kapitole:

var posLoc = gl.getAttribLocation(program, "pos");
gl.enableVertexAttribArray(posLoc);
var vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
var vertices = [
   0.5, 0.5,
  -0.5, 0.5,
   0.0, -0.5
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);

Jedná se tedy o dvojice hodnot (vrcholy trojúhelníku). Nyní bychom již mohli začít kreslit, jenže naším cílem jsou tři čáry, tj. celkem šest dvojic souřadnic. Všimněme si, že počáteční a koncové body čar se shodují (jedna začíná tam, kde druhá končí), takže by mohlo být nadbytečné body ve vertexBufferu duplikovat. Použijeme proto techniku indexovaných prvků.

Indexy

V minulé kapitole jsme samotné vykreslení provedli voláním metody drawArrays, která opakovaně volá vertex shader pro všechny nachystané hodnoty vstupních atributů. Alternativou je použít metodu drawElements, jejíž vstupem je tzv. ELEMENT_ARRAY_BUFFER – pole obsahující indexy do bufferu s definicí vrcholů. Tím lze snadno vyřešit potíž s opakováním vrcholů: každý vrchol definujeme jen jednou, ale v bufferu s indexy jej zmíníme několikrát. Pro tento účel si nachystáme oddělený indexový buffer:

var indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);

A naplníme jej daty, v našem případě šesti indexy (každý odkazuje na jednu dvojici souřadnic ve vertexBufferu). Tyto indexy předáme jako typované pole Uint8Array, tj. bajtové hodnoty. Pokud bychom potřebovali větší indexy, použili bychom Uint16Array.

var indices = [0, 1, 1, 2, 2, 0];
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indices), gl.STATIC_DRAW);

Pro vykreslení použijeme již zmiňovanou metodu drawElements, která má lehce odlišnou signaturu:

gl.drawElements(gl.LINES, 6, gl.UNSIGNED_BYTE, 0);

Druhý parametr je počet hodnot v index bufferu, které chceme zpracovat; třetí parametr popisuje velikost jednoho indexu; poslední parametr je offset (kreslíme od prvního prvku). Ještě nám uteklo volání gl.lineWidth(3), kterým nastavujeme šířku renderované čáry.

Rekapitulace

Pojďme se nyní podívat, jak dohromady fungují jednotlivé buffery, jejich délky a indexy:

  • Kreslíme čáry (gl.LINES) metodou drawElements, chceme vykreslit šest hodnot z indexového bufferu.
  • Čára je definována dvěma body, vykreslíme proto tři čáry.
  • Naše indexy odkazují na tři různé záznamy ve vertexBufferu, každý z nich odpovídá jednomu volání vertex shaderu. Jeho atribut pos je z bufferu plněn po dvojicích floatů.

Zejména je dobré si uvědomit, že z pohledu WebGL existují jen tři vrcholy v naší geometrii (i když vykreslujeme čáry mezi šesti body), tj. při tomto řešení není možné, aby dvě čáry měly různou barvu u společného vrcholu. V jiných úlohách by toto mohla být komplikace (např. pokud bychom kreslili kostku, jejíž stěny by měly mít různé barvy a společné vrcholy).

Dodatek o souřadných systémech

Na závěr této kapitoly se pojďme podívat na jednotlivé souřadné systémy, které ve WebGL používáme.

  1. Naše geometrie a její vrcholy jsou zadávány v obecných prostorových souřadnicích, na které není kladeno žádné omezení. Jedná se o desetinná čísla, kladná i záporná.
  2. Tato obecná geometrie je ve vertex shaderu transformována do souřadnic v tzv. clipspace, prostoru omezeném hodnotami 1 a -1 na všech osách. Tento prostor již koresponduje s plochou canvasu: osa X roste doprava, osa Y nahoru, osa Z směrem z obrazovky k uživateli. Hodnoty, které po výstupu z vertex shaderu přetečou rozpětí -1 až 1 jsou zahozeny a nebudou vykresleny.
  3. Poslední krok před vykreslením je převod do NDC (Normalized Device Coordinates): hodnoty X a Y v clipspace se vydělí čtvrtou složkou vektoru a vynásobí polovinou pixelové velikosti canvasu. Tím získáváme body na obrazovce monitoru.

Experimentálně můžeme zkusit ve vertex shaderu vrátit tyto hodnoty a pozorovat umístění v canvasu:

  • gl_Position = vec4(0.0, 0.0, 0.0, 1.0) je bod [0, 0, 0, 1] v clipspace a vykreslí se uprostřed canvasu,
  • gl_Position = vec4(0.5, 0.5, 0.0, 1.0) je bod [0.5, 0.5, 0, 1] v clipspace a vykreslí se uprostřed pravého horního kvadrantu,
  • gl_Position = vec4(0.5, 0.5, 0.0, 2.0) je stejný jako bod [0.25, 0.25, 0, 1] a bude vykreslen mezi dvěma předchozími.

Používání čtvrté složky a ignorování souřadnice Z nás zatím nemusí pálit; podíváme se jim na zoubek v některém z příštích dílů. Připomínám, že zdrojový kód k tomuto článku je na adrese http://jsfiddle.net/ondras/gf5c6/.

<canvas></canvas>

<script id="vs" type="x-shader/x-vertex">
attribute vec2 pos;
attribute vec3 color;
varying vec3 varyingColor;

void main(void) {
 gl_Position = vec4(pos, 0.0, 1.0);
 varyingColor = color;
}
</script>

<script id="fs" type="x-shader/x-fragment">
precision mediump float;
varying vec3 varyingColor;

void main(void) {
 gl_FragColor = vec4(varyingColor, 1.0);
}
</script>
var gl = document.querySelector("canvas").getContext("experimental-webgl");

var str = document.querySelector("#vs").textContent;
var vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, str);
gl.compileShader(vs);

var str = document.querySelector("#fs").textContent;
var fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, str);
gl.compileShader(fs);

var program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
gl.useProgram(program);

var colorLoc = gl.getAttribLocation(program, "color");
gl.enableVertexAttribArray(colorLoc);
var colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
var colors = [
  1.0, 0.0, 0.0,
  0.0, 1.0, 0.0,
  0.0, 0.0, 1.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
gl.vertexAttribPointer(colorLoc, 3, gl.FLOAT, false, 0, 0);

var posLoc = gl.getAttribLocation(program, "pos");
gl.enableVertexAttribArray(posLoc);
var vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
var vertices = [
  0.5,  0.5,
 -0.5,  0.5,
  0.0, -0.5
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);

var indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
var indices = [0, 1, 1, 2, 2, 0];
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indices), gl.STATIC_DRAW);

gl.lineWidth(3);
gl.drawElements(gl.LINES, 6, gl.UNSIGNED_BYTE, 0);

.

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: 14

Přehled komentářů

caracho Komentar k prvnimu dilu
langpavel "Security"
caracho
Ondřej Žára Re: Komentar k prvnimu dilu
Jenda Nefunkční ukázka
Martin Hassman Re: Nefunkční ukázka
Jenda Re: Nefunkční ukázka
Ondřej Žára Re: Nefunkční ukázka
Jenda Re: Nefunkční ukázka
Jenda Re: Nefunkční ukázka
Ondřej Žára Re: Nefunkční ukázka
Jenda Re: Nefunkční ukázka
Ondřej Žára Re:
Jenda Re:
Zdroj: https://www.zdrojak.cz/?p=8140