WebGL: Texturovat, nemíchat

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áhl 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 poslední části úvodu do WebGL prozkoumáme dvě dosud neprobádané krajiny: texturování (tj. pokrývání trojrozměrných objektů obrazovými daty z externího souboru) a míchání barev (průhlednost). Průvodcem nám bude model proslulého Utahského čajníku; jako první krok tento načteme z externích JSON dat a zobrazíme. Ořízneme veškeré osvětlení a začneme tak s kódem, který obsahuje jen transformace a vykreslení objektu žlutou barvou: http://jsfiddle.net/ondras/3JStb/.

teapot-yellow

Textura

Texturu budeme načítat celkem přímočaře pomocí HTML obrázku. Nejprve se však pojďme podívat, jak je realizováno mapování textury na jednotlivé trojúhelníky modelu: pro každý vrchol potřebujeme dvojici souřadnic, které popisují jeden bod textury. Každému trojúhelníku tělesa tak odpovídá trojúhelník na obrázku s texturou. WebGL pak automaticky provede interpolaci hodnot textury na všechny pixely. Pro nás to znamená nutnost dodat tyto texturové souřadnice; naštěstí je máme k dispozici od autora modelu. Tyto souřadnice jsou vždy dvojice desetinných čísel mezi nulou a jedničkou (textura má tedy v těchto souřadnicích velikost 1×1). Předáme je do vertex shaderu:

attribute vec2 texture;
varying vec2 varyingTexture;

Z pohledu WebGL jde o běžné pole atributů:

var textureLoc = gl.getAttribLocation(program, "texture");
gl.enableVertexAttribArray(textureLoc);

var textureCoordsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data.vertexTextureCoords), gl.STATIC_DRAW);
gl.vertexAttribPointer(textureLoc, 2, gl.FLOAT, false, 0, 0);

Do fragment shaderu pak texturu předáme jako speciální datový typ sampler2D, který předáme do texturovací funkce texture2D:

varying vec2 varyingTexture;
uniform sampler2D sampler;	

void main(void) {
    gl_FragColor = texture2D(sampler, varyingTexture);
}

Pojďme načíst obrázek s texturou. Z bezpečnostních důvodů musí být přenos realizován pomocí CORS, pokud je soubor umístěný na jiné doméně. V případě obrázku použijeme HTML5 atribut crossorigin:

var image = document.createElement("img");
image.crossOrigin = "anonymous";
image.src = "http://bespin.cz/~ondras/webgl/metal.jpg";

Samosebou nemá smysl předávat obrázek do WebGL, dokud nebude plně načten. Jakmile se tak stane (událost load), vytvoříme z obrázku texturu:

image.onload = function() {
    texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
    gl.generateMipmap(gl.TEXTURE_2D);
}

Jaká WebGL volání jsme použili?

  • createTexture pro vytvoření WebGL textury;
  • bindTexture pro označení této textury jako aktivní (stejně jako u bufferů);
  • texImage2D pro nahrání obrazových dat z obrázku do textury. Druhý parametr (nula) říká, že chceme plnou velikost. Další dvě konstanty určují zdrojový a cílový formát obrazových dat; gl.UNSIGNED_BYTE říká, že obrazová data mají jeden bajt na barevný kanál.
  • texParameteri používáme k nastavení toho, jak se budou texturová data při interpolaci zvětšovat (TEXTURE_MAG_FILTER) či zmenšovat (TEXTURE_MIN_FILTER). Pro zvětšení volíme bilineární interpolaci, pro zmenšení techniku mipmappingu. V rámci pokusů je možné oba dva algoritmy nastavit na gl.NEAREST – interpolaci nearest neighbor – a všimnout si, jak vznikají nehezké vizuální artefakty.
  • generateMipmap celkem přímočaře nakonec vygeneruje sadu mipmap.

Naposled musíme ještě texturu předat do fragment shaderu. To pro nás znamená jen spárování uniform hodnoty sampler s texturovací jednotkou (těch je k dispozici celá řada, my použijeme jen tu první – gl.TEXTURE0).

gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);

var samplerLoc = gl.getUniformLocation(program, "sampler");
gl.uniform1i(samplerLoc, 0); // nula odpovídá gl.TEXTURE0

Vidíme nyní čajník pokrytý texturou z obrázku. Zdrojový kód je k dispozici opět na http://jsfiddle.net/ondras/JY8Sp/:

teapot-textured

Míchání barev a průhlednost

Problematiku částečně či plně průhledných barev jsme v tomto seriálu zatím zcela opomíjeli. Je to mimo jiné i proto, že se jedná o překvapivě složitou kapitolu; WebGL (resp. OpenGL) nám v tomto směru vychází vstříc jen lehce.

Při běžném kreslení nový pixel zcela přepíše (překryje) pixel původní, ať už se jedná o barvu pozadí, nebo již vykreslená data. Pokud by ale vykreslovaný pixel nesl nějakou informaci o průhlednosti, bylo by nutné smíchat jeho barvu s barvou, která je již vykreslena. Pojďme to zkusit a celý čajník vykreslit poloprůhledný.

Především to znamená, že v našem případě musíme zahodit testování hloubky (gl.enable(gl.BLEND)): je třeba vykreslit všechny pixely; i ty, co popisují odvrácenou stranu čajníku. Pojďme ve fragment shaderu nadefinovat výslednou barvu s padesátiprocentní průhledností:

gl_FragColor.a = 0.5;

Stále ještě však nevidíme čajník průhledný, protože WebGL ve výchozím nastavení hodnotu alpha ignoruje a vždy vykreslí pixel plnou barvou.

Je tedy nutné zapnout podporu pro míchání (či mísení?) barev:

gl.enable(gl.BLEND);

A zároveň ještě definovat algoritmus (resp. výpočet), který se bude při míchání aplikovat. To se dělá pomocí dvou konstant; tou první se vynásobí barva kresleného pixelu (source), tou druhou barva již vykresleného (destination) a obě se sečtou. V našem případě použijeme toto nastavení:

gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

Definujeme tedy, že vstupní barvu pixelu vynásobíme hodnotou průhlednosti, zatímco existující jejím doplňkem do jedničky. To víceméně odpovídá běžné představě o poloprůhledné barvě (výsledná hodnota je „mix“ popředí a pozadí s váhou určenou průhledností).

Ještě je dobré zmínit, že tímto se informace o průhlednosti zpropaguje i do výsledného canvasu, což nemusí být nutně vhodné: tento je totiž kombinován s barvou pozadí stránky, takže na bílé stránce by byl čajník zabarven do běla. Vypneme proto zcela používání průhlednosti ve výsledném canvasu:

var gl = document.querySelector("canvas").getContext("experimental-webgl", {alpha:false});

Konečně je hotovo: čajník je poloprůhledný. Zdrojový kód ukázky je na adrese http://jsfiddle.net/ondras/3RwZb/.

teapot-alpha

Tímto končí krátký úvodní seriál o WebGL. Všechny ukázky z tohoto dílu jsou k dispozici pro stažení v archivu.

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

Přehled komentářů

Jan Prachař
Ondřej Žára Re:
Petr Bečka
Ondřej Žára Re:
Zdroj: https://www.zdrojak.cz/?p=8909