Přejít k navigační liště

Zdroják » JavaScript » WebGL: Milostný RGB trojúhelník

WebGL: Milostný RGB trojúhelník

Články JavaScript

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…

Nálepky:

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

.

Komentáře

Subscribe
Upozornit na
guest
14 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
caracho

Proto ze nejdou pridavat komentare k uvodnimu dilu, dam to sem.
V souvislosti s uvodem k WebGL by bylo zahodno zminit nasledujici:

http://www.contextis.com/research/blog/webgl-new-dimension-browser-exploitation/
http://blogs.technet.com/b/srd/archive/2011/06/16/webgl-considered-harmful.aspx

Pavel Lang

Bla, bla…
Takže útok přes WebGL může vést k zatuhnutí systému kvůli chybám v driveru grafiky..
No nevím, ale lepší podporovat WebGL a vývojáře donutit díry v driverech zalepit než používat nativní aplikace, které si nikdo nestáhne, nebo pluginy (flash, Java), které si (také) dělají co chtějí.
Raději dám přednost WebGL + nějakému blokování objevených šmejdů.

caracho

Tak to zatuhnuti je lepsi pripad. Daleko zabavnejsi je to cteni cizi videopameti. A predstava ze vyrobci budou zaplatovat vsechny ovladace vsech svych karet na vsech platformach je dost naivni.

Jenda

Zdravím,
ukázka na jsfiddle mi nefunguje. Netušíte, kde může být chyba? Používám Chrome 25, což by měl být podporovaný prohlížeč. Stisknu run a nestane se nic. V konzoli mám chybovou hlášku:
Uncaught TypeError: Cannot read property ‚VERTEX_SHADER‘ of null

Martin Hassman

Zkoušel jste i ukázku z předchozího dílu? Skončila stejnou chybou? http://jsfiddle.net/ondras/qt7sk/

Jenda

Ano obě ukázky nefungují úplně stejně.

Jenda

Je to tak, about:gpu píše:
WebGL: Unavailable. Hardware acceleration unavailable
WebGL multisampling: Unavailable. Hardware acceleration unavailable
Volání getContext(„experimental-webgl“) selže s chybou:
SyntaxError: Unexpected token ILLEGAL
Jenže když se dívám na odkaz z prvního dílu: http://caniuse.com/#feat=webgl tak se tam píše, že chrome 25 by měl WebGL podporovat, moje verze je: 25.0.1323.1 Zkusím teda novější verzi chromu.

Jenda

Tak nevím, nainstaloval jsem si novou verzi: 27.0.1453.93 a výsledek úplně stejný. WebGL nefunguje ani v této verzi. Leda by byl problém operační systém? Protože na stránce: about:gpu mi to hlásí:
Accelerated 2d canvas is unstable in Linux at the moment.
GPU process was unable to boot. Access to GPU disallowed.
Accelerated rasterization has not been enabled or is not supported by the current system.
Tak to mě štve. Chtěl jsem si zkusit něco ve webgl naprogramovat, protože na škole mě OpenGL strašně bavilo, ale asi mám smůlu. Jiný operační systém si kvůli tomu instalovat nebudu.

Jenda

Říká toho spoustu, ale že bych se v tom vyznal říct nemůžu:
http://pastebin.com/qhsygrm7

Jenda

Super. Ta poslední rada pomohla. Díky moc. Jenom dodám, kdyby to po mně ještě někdo hledal na českém systému, tak se to jmenuje: „Přepsat seznam softwarového vykreslování“

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.