OpenGL - textury
V této kapitole se naučíte zobrazovat obrázky. Přesněji řečeno, naučíte se, jak použit obrázek jako texturu trojúhelníka nebo krychle.
Použití textury
Použití textury je v zásadě jednoduché. Jen zase musíte udělat asi bambilión úkonů. Stručně sepsáno, jde o následující kroky:
- Vytvoříte buffer pro každou texturu, kterou chcete použít.
- Do bufferů nahrajete obrázek ve formátu, kterému OpenGL rozumí.
- Definujete si souřadnice na textuře, které odpovídají vrcholům trojúhelníků, které chcete otexturovat.
- Souřadnice textury (texture coordinate) nahrajete do bufferu pro attribute proměnnou z GLSL shaderu, obdobně, jako jakákoliv jiná vertex data.
-
V fragment shaderu si vytvoříte uniform proměnnou typu
sampler2D
(pro každou texturu). Tyto proměnné budou obsahovat ID bufferu textury. -
V fragment shaderu použijete interní funkci
texture(id,textureCoord)
pro získání barvy fragmentu z textury. S touto barvou si pak můžete dělat co chcete.
Nahrání obrázku do bufferu
Začněme dvěma prvními body. Potřebujete vytvořit buffer pro texturu a nějaký obrázek v běžném formátu (JPG, PNG, GIF …) do bufferu nahrát ve formátu vhodném pro OpenGL.
Tyto dva kroky udělám ve funkci initBuffers()
. Formát vhodný
pro OpenGL je nekomprimovaný formát. Interní strukturu vám popisovat nebudu.
Ke konverzi běžných obrázků použiji knihovnu stb_image.h.
Knihovna stb_image.h zvládne nahrát běžné formáty obrázků a zkonvertovat je do potřebného formátu pro OpenGL. Není ale úplně dokonalá. Občas se stane, že si s nějakým formátem úplně nerozumí a výsledek je takový divný, šedivý, pruhovaný obrázek, který vypadá jak z rozladěné televize. V takovém případě mi pomohlo obrázek otevřít v gimpu nebo v malování z Windows, uložit ho (bez jakékoliv změny) a najednou vše fungovalo jak má.
Další problém s touto knihovnou je, že při jejím překladu překladač
hlásí velké množství varování. A protože se jedná jen o hlavičkový
soubor, tak se tato varování vypisují při každém překladu zdrojového
souboru, který tento hlavičkový soubor includuje. Z tohoto důvodu jsem
vytvořil soubory stb-image.h a stb-image.c.
S novou verzí stb_image.h už se žádné varování neukazuje, ale soubory stb-image.h a stb-image.c jsem už ve zdrojácích ponechal.
Soubor stb-image.h vypadá takto:
#ifdef STB_IMAGE_STATIC
#define STBIDEF static
#else
#define STBIDEF extern
#endif
typedef unsigned char stbi_uc;
STBIDEF stbi_uc *sx_stbi_load (char const *filename, int *x, int *y, int *comp, int req_comp);
STBIDEF void sx_stbi_image_free (void *retval_from_stbi_load);
První řádek definuje makro, které se má definovat, podle dokumentace k stb_image.h. Nevím proč, bez řečí dokumentaci poslouchám. (Má se includovat v jednom souboru, aby se definovala jakási implementace.)
Další definovaná makra tu jsou kvůli kompatiblitě s gcc a Visual Studiem.
Následuje deklarace funkcí, které jsem definoval v stb-image.c:
#include "stb_image.h"
STBIDEF stbi_uc *sx_stbi_load (char const *filename, int *x, int *y, int *comp, int req_comp)
{
return stbi_load (filename, x, y, comp, req_comp);
}
STBIDEF void sx_stbi_image_free (void *retval_from_stbi_load) {
stbi_image_free (retval_from_stbi_load);
}
První funkce volá stbi_load()
z stb_image.h, která je zodpovědná
na načtení a zkonvertování obrázku do formátu vhodného pro OpenGL. Vrací
ukazatel na alokovanou paměť, kde je zkonvertovaný obrázek uložen. Druhá funkce
se stará o uvolnění této alokované paměti.
První argument funkce je cesta v souborovém systému k obrázku. Do druhého a třetího argumentu funkce uloží šířku a výšku obrázku. Do čtvrtého argumentu uloží počet složek barvy, ze které se skládá jeden pixel. PNG a GIF obrázky mívají uložen alfa kanál (průhlednost), takže mají obvykle 4 složky (Red, Green, Blue, Alpha). JPEG průhlednost neukládá, takže má většinou 3 složky (RGB). (Ale může jich mít i méně.)
Tak to sme si udělali odbočku ke knihovně stbi_load.h a teď zpátky
k našemu příkladu, k funkci initBuffer()
.
- {
- glBindTexture(GL_TEXTURE_2D, TEX[0]);
- {
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
- }
- else {
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
- }
- sx_stbi_image_free(image);
- }
- glBindTexture(GL_TEXTURE_2D, 0);
Na začátku se generují buffery pro (jeden) VAO a (dva) BO, jak už znáte z
předchozích kapitol. Navíc se generuje buffer pro texturu pomocí
funkce glGenTextures()
. Globální proměnná TEX
je definována jako GLuint TEX[1];
(prozatím si vystačíme
s jednou texturou). To je tedy krok 1.
Následuje krok 2. Nejdříve se načte obrázek ze souboru do paměti, pak se
pomocí funkce glTexImage2D()
nahraje do bufferu grafické karty
a nakonec se uvolní již nepotřebná paměť.
Buffery fungují, jako obvykle, jako stavové automaty. Nejdříve se tedy musí
přibindovat voláním glBindTexture()
.
OpenGL podporuje mnoho druhů textur. Textura GL_TEXTURE_2D se používá pro
2D obrázek. Později v této kapitole vám ještě představím
GL_TEXTURE_CUBE_MAP.
Jaké další existují můžete vidět v nápovědě k
funkci glTexImage2D().
Než se dostanu k popisu této funkce, musím zmínit
glTexParameteri()
. Tato funkce se používá k nastavování
všelijakých vlastností zrovna přibindované textury. Můžete mít přibindováno
více textur různých druhů (ale vždy jen jednu texturu pro jeden druh),
proto má jako první argument typ textury, na kterou se má nastavovavná
vlastnost aplikovat. V příkladu se nastavuje funkce, která se má použít pri
zmenšování (GL_TEXTURE_MIN_FILTER), resp. zvětšování obrázku
(GL_TEXTURE_MAG_FILTER). V obou případech nastavuji funkci (resp.
identifikátor GL funkce) na GL_LINEAR. Další možná funkce je např.
GL_NEAREST. Jak se funce liší, najdete v dokumentaci (rozdíly jsou hlavně v
kvalitě vs rychlosti). Nebo si je vyzkoušejte a uvidíte jak
zvětšná/zmenšená textura vypadá. Jen vězte, že bez tohoto nastavení se vám
textura nezobrazí.
A teď k argumentům funkce glTexImage2D()
.
První argument určuje, do bufferu jakého typu textury se obrázek nahraje. Jak už jsem psal, můžete mít přibindováno více bufferů pro různé typy textur, ale pro daný typ textury vždy jen jeden buffer. Takže určením typu textury jednoznačně určíte, do jakého bufferu se data nahrají.
Druhý argument se používá pro nahrávání tzv. mipmap. Zmenšování obrázku je výpočetně náročná operace a také zapříčiňuje ztrátu kvality. OpenGL můžete trochu ulehčit vygenerováním tzv. mipmap. OpenGL si předgeneruje obrázek v různých velikostech, takže pak při vykreslení textury použije nejbližší předgenerovanou mipmpapu a zmenšení/zvětšení už není tak náročné.
Zavoláním funkce glTexImage2D()
s druhým argumentem 0
nahrajete „defaultní“ obrázek, ze kterého se počítají všechny
další velikosti. Zavolání glTexImage2D()
s druhým argumentem n
> 0 vám umožňuje nahrát n-tou mipmapu. To vám umožňuje vylepšit kvalitu
zmenšené textury, nebo při zmenšení použít úplně jinou texturu :-).
Třetí argument určuje interní formát, ve kterém bude textura uložena. Když necháme stranou některé exotické možnosti, tak nám zbývá GL_RGB nebo GL_RGBA. Pokud vás průhlednost nezajímá, respektive nahráváte obrázek, který průhlednost neobsahuje, pak použijte GL_RGB, které interně zabírá méně místa.
Techničtější informace o interním formátu naleznete na www.opengl.org/wiki/Image_Format.
Další parametry popisují formát nahrávaného obrázku. Nejdříve se určuje šířka a výška obrázku, další argument, border, musí být vždy 0 (to je nějaký historický nesmysl). Další argument říká, jak jsou definované barvy pixelu (nejpoužívanější jsou zase GL_RGB nebo GL_RGBA) a poslední argument je odkaz na vlastní data obrázku.
Pokud změníte interní formát obrázku z G_RGBA na G_RGB, přijdete o průhlednost (místo průhledných oblastí se vám zobrazí nejspíše černá barva). Pokud ale změníte formát pixelů nahrávaného obrázku, tak se vám obrázek nahraje blbě a uvidíte „efekt rozladěné televize“. Vyzkoušejte za domácí úkol :-).
Definování souřadnic textury
V druhé části funkce initBuffers()
se nahrávají vertex data
– vrcholy trojúhelníků a souřadnice textury odpovídající
jednotlivým vrcholům. To už znáte z předchozích kapitol.
- glBindVertexArray(VAO[0]);
- {
- //vertices
- glBindBuffer(GL_ARRAY_BUFFER, BO[0]);
- glEnableVertexAttribArray(0);
- //textures
- glBindBuffer(GL_ARRAY_BUFFER, BO[2]);
- glEnableVertexAttribArray(2);
- }
- glBindVertexArray(0); // Unbind VAO
- }
Vertex data jsem zase definoval v souboru vertex-data.h. Vrcholy trojúhelníků takto:
#define VERTEXT_DATA_H
#include <GLFW/glfw3.h>
GLfloat vertices[] = {
// right triangle
1.25, 1.0, 0.0, // Vrchol A
1.25, -1.0, 0.0, // B
-0.75, -1.0, 0.0, // C
// left triangle
-1.0, 1.0, 0.0, // D
1.0, 1.0, 0.0, // E
-1.0, -1.0, 0.0 // F
};
Trojúhelníky jsem od sebe malinko odsadil, aby byly lépe rozeznatelné. Pro lepší představu jsem vám tentorkát nakreslil i obrázek:
Vertex data – vertices
Souřadnice textury mají počátek [0,0] v levém dolním rohu. Pravý horní roh má souřadnice [1,1]. Viz následující obrázek.
Vertex data – TexCoord
Souřadnice textury, kde vrcholy obrázku odpovídají vrcholům trojúhelníků, vypadají takto:
// right triangle
1.0, 1.0,
0.0, 1.0,
0.0, 0.0,
// left triangle
1.0, 0.0,
1.0, 1.0,
0.0, 0.0
};
#endif
Výsledné natočení obrázku je dáno nejen tím, jak definujete souřadnice textury, ale i tím, jak jsou definovány vrcholy trojúhelníků. Je rozdíl v tom, jestli jsou definované po směru nebo proti směru hodinových ručiček, protože v jednom případě se díváte na zadní stranu a v druhém na přední stranu trojúhelníka. Konec konců, v programu můžete scénu myší otáčet a tím si nastavit polohu trojúhelníka – pozici textury – interaktivně. Zkrátka, občas je to docela alchimie nastavit pozici textury tak, jak chcete.
Definice shaderů
Začněme vertex shaderem:
layout (location = 0) in vec3 vertexPosition;
layout (location = 2) in vec2 textureCoord;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
out vec4 vColor;
out vec2 texCoord;
void main(void) {
vec4 mvPosition = uMVMatrix * vec4(vertexPosition, 1.0);
gl_Position = uPMatrix * mvPosition;
vColor = vec4(0.0, 0.0, 1.0, 1.0);
texCoord = vec2(textureCoord.x, textureCoord.y);
}
V shaderu můžete vidět nějaké pohrobky z kapitoly o osvětlení, těch
si nemusíte všímat. Důležitá je jen definice textureCoord,
kam se budou sypat souřadnice textury, dále out vec2 texCoord
,
což je proměnná, kterou se budou souřadnice předávat do fragment
shaderu. A na konec poslední řádek, kde se souřadnice předávají
do fragment shaderu skrze texCoord
.
Souřadnice jsem mohl předat jednoduše takto: texCoord =
textureCoord;
. Proč jsem tak neučinil, to se dozvíte v zápětí.
A teď, o trochu zajímavější, fragment shader:
uniform vec3 uAmbientColor;
uniform sampler2D texture1;
in vec4 vColor;
in vec2 texCoord;
void main(void) {
vec3 vLightIntenzity = uAmbientColor;
vec4 c = vec4(vColor.rgb * vLightIntenzity, vColor.a);
if (uAmbientColor.r + uAmbientColor.g + uAmbientColor.b > 0.0) {
gl_FragColor = mix(texture(texture1, texCoord), c, 0.5);
}
else {
gl_FragColor = texture(texture1, texCoord);
}
}
Uniform proměnná texture1 obsahuje index odkazující na naší texturu. Jak je to ale možné, když jsem nikde tuto proměnnou v main.c nenastavoval?
Pokud uniform nenastavíte, defaultně je 0. Ve chvíli, kdy texturu
přibindujete (voláním funkce glBindTexute()
), tak se textura
spojí s tzv. texture unit. Defaultní texture unit je
GL_TEXTURE0, které odpovídá index 0.
Podrobněji se k tomuto tématu vrátím v další části.
Získání barvy z textury je pak jen otázkou zavolání funkce
texture()
, tak jak vidíte v příkladu.
Funkce texture()
je přetížená funkce, což znamená,
že si dokáže poradit s různými typy textur. Ve starších verzích
OpenGL a ve WebGL tato funkce neexistuje a musíte použít funkci
dle typu textury. Např. pro naší GL_TEXTURE_2D je to
texture2D()
, pro cubemap, popisovanou níže,
je to textureCube()
atp.
Jako takový bombónek na konec najdete v fragment shaderu kus kódu,
který, v případě že uAmbientColor není černá barva,
smíchá barvu s barvou textury pomocí funkce mix()
.
Tato funkce smíchá své dva první argumenty v poměru daném třetím
argumentem (v příkladu 50:50). Poměr může být zadán číslem od 0.0 do 1.0.
Výsledek můžete vidět zde:
Animace textury. V dalších záložkách můžete vidět WebGL použité pro tuto animaci.
attribute vec3 vertexPosition;
attribute vec2 textureCoord;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
varying vec4 vColor;
varying vec2 texCoord;
void main(void) {
vec4 mvPosition = uMVMatrix * vec4(vertexPosition, 1.0);
gl_Position = uPMatrix * mvPosition;
vColor = vec4(0.0, 0.0, 1.0, 1.0);
texCoord = vec2(textureCoord.x, textureCoord.y);
}
</script>
<script id="shader-fs-textura" type="x-shader/x-fragment">
precision mediump float;
uniform vec3 uAmbientColor;
uniform sampler2D texture1;
varying vec4 vColor;
varying vec2 texCoord;
void main(void) {
vec3 vLightIntenzity = uAmbientColor;
vec4 c = vec4(vColor.rgb * vLightIntenzity, vColor.a);
if (uAmbientColor.r + uAmbientColor.g + uAmbientColor.b > 0.0) {
gl_FragColor = mix(texture2D(texture1, texCoord), c, 0.5);
}
else {
gl_FragColor = texture2D(texture1, texCoord);
}
}
</script>
// right triangle
1.25, 1.0, 0.0,
1.25, -1.0, 0.0,
-0.75, -1.0, 0.0,
// left triangle
-1.0, 1.0, 0.0,
1.0, 1.0, 0.0,
-1.0, -1.0, 0.0
];
var texCoord = [
// right triangle
1.0, 1.0,
0.0, 1.0,
0.0, 0.0,
// left triangle
1.0, 0.0,
1.0, 1.0,
0.0, 0.0
];
var buffers = {};
var useColor = {};
useColor.ambient = 0;
function initVariables(gl, shaderProgram) {
variables.vertexPositionLoc = gl.getAttribLocation(shaderProgram, "vertexPosition");
variables.textureCoordLoc = gl.getAttribLocation(shaderProgram, "textureCoord");
gl.enableVertexAttribArray(variables.vertexPositionLoc);
gl.enableVertexAttribArray(variables.textureCoordLoc);
variables.uMVMatrixLoc = gl.getUniformLocation(shaderProgram, "uMVMatrix");
variables.uPMatrixLoc = gl.getUniformLocation(shaderProgram, "uPMatrix");
variables.uAmbientColorLoc = gl.getUniformLocation(shaderProgram, "uAmbientColor");
variables.texture1 = gl.getUniformLocation(shaderProgram, "texture1");
}
buffers.positionBuffer = gl.createBuffer();
buffers.texCoordBuffer = gl.createBuffer();
buffers.texture1 = gl.createTexture();
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoord), gl.STATIC_DRAW);
var logoImage = new Image();
logoImage.onload = function() {
gl.bindTexture(gl.TEXTURE_2D, buffers.texture1);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, logoImage);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.bindTexture(gl.TEXTURE_2D, null);
};
logoImage.src = '/rc/sally/images/c/opengl/logo.gif';
}
var lastX, lastY;
var canvas1 = document.getElementById('textura-canvas');
var lastDownTarget;
$(document).on('mousedown', function(event) {
lastDownTarget = event.target;
lastX = event.pageX; lastY = event.pageY;
});
$(document).on('mousemove', function(event) {
if(lastDownTarget != canvas1)
return;
var buttonDown = event.buttons == null ?event.wich : event.buttons;
if(!buttonDown) return;
x = event.pageY - lastY;
y = event.pageX - lastX;
lastX = event.pageX;
lastY = event.pageY;
var xAngle = WebGLUtils.degToRad(x/5.0);
var yAngle = WebGLUtils.degToRad(y/5.0);
updateAngles(xAngle, yAngle);
event.preventDefault();
return false;
});
});
mat4.identity(rotationMatrix);
function updateAngles(xAngle, yAngle) {
var newRotationMatrix = mat4.create();
mat4.identity(newRotationMatrix);
mat4.rotate(newRotationMatrix, newRotationMatrix, xAngle, [1, 0, 0]);
mat4.rotate(newRotationMatrix, newRotationMatrix, yAngle, [0, 1, 0]);
mat4.multiply(rotationMatrix, newRotationMatrix, rotationMatrix);
}
function drawScene(gl, canvas) {
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(1, 0,945, 0,8925, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
var uPMatrix = mat4.create();
var uMVMatrix = mat4.create();
mat4.identity(uPMatrix);
mat4.identity(uMVMatrix);
/********************************************/
mat4.perspective(uPMatrix, WebGLUtils.degToRad(45), canvas.width / canvas.height, 1.0, 100.0);
mat4.translate(uMVMatrix, uMVMatrix, [0.0, 0.0, -10.0]);
mat4.multiply(uMVMatrix, uMVMatrix, rotationMatrix);
/********************************************/
gl.uniformMatrix4fv(variables.uMVMatrixLoc, false, uMVMatrix);
gl.uniformMatrix4fv(variables.uPMatrixLoc, false, uPMatrix);
if (useColor.ambient) {
gl.uniform3f(variables.uAmbientColorLoc, 0.4, 0.4, 0.4);
}
else {
gl.uniform3f(variables.uAmbientColorLoc, 0.0, 0.0, 0.0);
}
// vykresli logo
gl.bindTexture(gl.TEXTURE_2D, buffers.texture1);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.positionBuffer);
gl.vertexAttribPointer(variables.vertexPositionLoc, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.texCoordBuffer);
gl.vertexAttribPointer(variables.textureCoordLoc, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, vertices.length/3);
}
function animate() {
}
drawScene(gl, canvas);
animate();
requestAnimFrame(function() { tick(gl, canvas);});
}
function webGLStart() {
var canvas = document.getElementById("textura");
var gl = WebGLUtils.setupWebGL(canvas,{ antialias: true });
if(!gl) return;
var shaderProgram = WebGLUtils.initShaders(gl, "shader-vs-textura","shader-fs-textura");
gl.useProgram(shaderProgram);
initVariables(gl,shaderProgram);
initBuffers(gl);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
tick(gl, canvas);
}
webGLStart();
Pozorný čtenář si jistě všiml, že je obrázek hlavou dolů. To je dáno tím, že data pro obrázek jsou nahraná od horního levého rohu a ne od dolního, jak se zadávají souřadnice. Otoční obrázku je jen otázkou změny y-ové souřadnice v vertex shaderu:
texCoord = vec2(textureCoord.x, -textureCoord.y);
//...
Můžete také použít funkci z stb_image.h, kterou stačí zavolat před načtením obrázku:
To už vám nechám za domácí úkol.
Více textur
V dalším příkladu se vám pokusím vysvětlit použití texture units. V příkladu budou použity dvě textury (v jednom shader programu), takže se použití texture units nedá vyhnout.
První obrázek je smějící se kuci.gif, druhý obrázek rozzuřený kuci2.gif.
V příkladu zopakuji obrázek na textuře 4x (v ose x i y). A navíc se rozzuřený „Kučí“ bude zobrazovat jen pokud bude myš nad oknem.
Jde tedy celkem o 3 změny, jejichž výsledek si můžete prohlédnout zde:
attribute vec3 vertexPosition;
attribute vec2 textureCoord;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
varying vec2 texCoord;
varying vec4 position;
void main(void) {
vec4 mvPosition = uMVMatrix * vec4(vertexPosition, 1.0);
gl_Position = uPMatrix * mvPosition;
position = gl_Position;
texCoord = vec2(textureCoord.x, textureCoord.y);
}
</script>
<script id="shader-fs-textury" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D texture1;
uniform sampler2D texture2;
uniform bool mouseIn;
varying vec2 texCoord;
varying vec4 position;
void main(void) {
if(!mouseIn) {
gl_FragColor = texture2D(texture1, texCoord);
return;
}
if(texCoord.x > 1.0 && texCoord.x < 3.0 && texCoord.y > 1.0 && texCoord.y < 3.0) {
gl_FragColor = texture2D(texture2, texCoord);
}
else {
gl_FragColor = texture2D(texture1, texCoord);
}
}
</script>
// right triangle
1.0, 1.0, 0.0,
1.0, -1.0, 0.0,
-1.0, -1.0, 0.0,
// left triangle
-1.0, 1.0, 0.0,
1.0, 1.0, 0.0,
-1.0, -1.0, 0.0
];
var texCoord = [
// right triangle
4.0, 0.0,
4.0, 4.0,
0.0, 4.0,
// left triangle
0.0, 0.0,
4.0, 0.0,
0.0, 4.0
];
var buffers = {};
function initVariables(gl, shaderProgram) {
variables.vertexPositionLoc = gl.getAttribLocation(shaderProgram, "vertexPosition");
variables.textureCoordLoc = gl.getAttribLocation(shaderProgram, "textureCoord");
gl.enableVertexAttribArray(variables.vertexPositionLoc);
gl.enableVertexAttribArray(variables.textureCoordLoc);
variables.uMVMatrixLoc = gl.getUniformLocation(shaderProgram, "uMVMatrix");
variables.uPMatrixLoc = gl.getUniformLocation(shaderProgram, "uPMatrix");
variables.mouseIn = gl.getUniformLocation(shaderProgram, "mouseIn");
variables.texture1 = gl.getUniformLocation(shaderProgram, "texture1");
variables.texture2 = gl.getUniformLocation(shaderProgram, "texture2");
}
buffers.positionBuffer = gl.createBuffer();
buffers.texCoordBuffer = gl.createBuffer();
buffers.texture1 = gl.createTexture();
buffers.texture2 = gl.createTexture();
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoord), gl.STATIC_DRAW);
var kuciImage = new Image();
kuciImage.onload = function() {
gl.bindTexture(gl.TEXTURE_2D, buffers.texture1);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, kuciImage);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
};
kuciImage.src = '/rc/sally/images/c/opengl/kuci.gif';
var kuci2Image = new Image();
kuci2Image.onload = function() {
gl.bindTexture(gl.TEXTURE_2D, buffers.texture2);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, kuci2Image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
};
kuci2Image.src = '/rc/sally/images/c/opengl/kuci2.gif';
}
$(function() {
var lastX, lastY;
var canvas1 = document.getElementById('textury-canvas');
var lastDownTarget;
$(document).on('mousedown', function(event) {
lastDownTarget = event.target;
lastX = event.pageX; lastY = event.pageY;
});
$(document).on('mousemove', function(event) {
if(lastDownTarget != canvas1)
return;
var buttonDown = event.buttons == null ?event.wich : event.buttons;
if(!buttonDown) return;
x = event.pageY - lastY;
y = event.pageX - lastX;
lastX = event.pageX;
lastY = event.pageY;
var xAngle = WebGLUtils.degToRad(x/5.0);
var yAngle = WebGLUtils.degToRad(y/5.0);
updateAngles(xAngle, yAngle);
event.preventDefault();
return false;
});
$(canvas1).on('mouseenter', function(event) { mouseIn = 1; });
$(canvas1).on('mouseleave', function(event) { mouseIn = 0; });
});
mat4.identity(rotationMatrix);
function updateAngles(xAngle, yAngle) {
var newRotationMatrix = mat4.create();
mat4.identity(newRotationMatrix);
mat4.rotate(newRotationMatrix, newRotationMatrix, xAngle, [1, 0, 0]);
mat4.rotate(newRotationMatrix, newRotationMatrix, yAngle, [0, 1, 0]);
mat4.multiply(rotationMatrix, newRotationMatrix, rotationMatrix);
}
function drawScene(gl, canvas) {
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(1, 0,945, 0,8925, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
var uPMatrix = mat4.create();
var uMVMatrix = mat4.create();
mat4.identity(uPMatrix);
mat4.identity(uMVMatrix);
/********************************************/
mat4.perspective(uPMatrix, WebGLUtils.degToRad(45), canvas.width / canvas.height, 1.0, 100.0);
mat4.translate(uMVMatrix, uMVMatrix, [0.0, 0.0, -2.5]);
mat4.multiply(uMVMatrix, uMVMatrix, rotationMatrix);
/********************************************/
gl.uniformMatrix4fv(variables.uMVMatrixLoc, false, uMVMatrix);
gl.uniformMatrix4fv(variables.uPMatrixLoc, false, uPMatrix);
gl.uniform1i(variables.texture1, 0);
gl.uniform1i(variables.texture2, 1);
gl.uniform1i(variables.mouseIn, mouseIn);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, buffers.texture1);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, buffers.texture2);
// vykresli
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.positionBuffer);
gl.vertexAttribPointer(variables.vertexPositionLoc, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.texCoordBuffer);
gl.vertexAttribPointer(variables.textureCoordLoc, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, vertices.length/3);
}
function animate() {
}
drawScene(gl, canvas);
animate();
requestAnimFrame(function() { tick(gl, canvas);});
}
function webGLStart() {
var canvas = document.getElementById("textura", { antialias: true });
var gl = WebGLUtils.setupWebGL(canvas);
if(!gl) return;
var shaderProgram = WebGLUtils.initShaders(gl, "shader-vs-textury","shader-fs-textury");
gl.useProgram(shaderProgram);
initVariables(gl,shaderProgram);
initBuffers(gl);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
tick(gl, canvas);
}
webGLStart();
Texture units
Pokud chcete použít více textur v jednom shaderu, musíte každou z nich
připojit k jiné texture unit. To se dělá tak, že se nejdříve příslušná
texutre unit aktivuje (pomocí glActiveTexture()
) a pak se
zavolá glBindTexture()
.
Funkce glActiveTexture()
má jako argument id texture unit.
První je GL_TEXTURE0, druhá GL_TEXTURE1 atd. Máte zaručeno, že existují
alespoň dvě texture units.
Následující kus kódu určuje, kolik jich máte k dispozici:
glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &max_combined_texture_image_units);
printf("Max texture units = %i\n", max_combined_texture_image_units);
Texture units můžete indexovat i jako GL_TEXTURE0 + i (kde i < max_combined_texture_image_units).
Prakticky to tedy znamená, že, oproti předchozímu příkladu, se ve
funkci initBuffers()
vytvoří dva TEX buffery,
do každého se nahraje jeden obrázek (kuci.gif a kuci2.gif) a pak se zavolá
následující:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, TEX[0]);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, TEX[1]);
//...
V rámci optimalizace můžete zavolat glActiveTexture()
už před bindováním textur při jejich nahrávání. První volání s
GL_TEXTURE0 není úplně nutné, protože tato texture unit je zapnutá
defaultně, ale je to dobrý zvyk, jak se vyhnout chybám v případě, že v
nějaké jiné části kódu se už glActiveTexture()
někde volalo.
Ve fragment shaderu se vytvoří potřebné uniform proměnné:
uniform sampler2D texture1;
uniform sampler2D texture2;
//...
A ve funkci draw()
(nebo kde se vám to hodí),
se uniform proměnné inicializují:
glUniform1i(variables.texture1, 0);
glUniform1i(variables.texture2, 1);
//...
Ač to hodně svádí k tomu, abyste místo čísel 0 a 1 použili konstanty GL_TEXTURE0 a GL_TEXTURE1, tak tyto konstanty rozhodně nejsou definovány jako 0 a 1. Je to možná šokující, ale je to tak.
A to je vše, co je potřeba pro použití více textur v jednom shader programu.
Opakování textury
Pokud určíte souřadnice textury větší než 1.0, například 4.0, pak textura
vyplní na trojúhelníku vše, co odpovídá prostoru 0.0 až 1.0 (první čtvrtině).
Čím se vyplní zbytek, prostor (1.0 až 4.0)? To se nastavuje pomccí
parametru GL_TEXTURE_WRAP_S
(pro směr x) a
GL_TEXTURE_WRAP_T
(pro směr y).
Parametry se nastavují funkcí glTexParameteri()
a mohou nabývat
jedné z následujících hodnot: GL_REPEAT
,
GL_MIRRORED_REPEAT
, GL_CLAMP_TO_EDGE
a
GL_CLAMP_TO_BORDER
.
V příkladu chci texturu opakovat, takže souřadnice struktury vynásobím čtyřmi a někde po přibindování textury zavolám následující kód:
glBindTexture(GL_TEXTURE_2D, TEX[0]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
//...
glBindTexture(GL_TEXTURE_2D, TEX[1]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
//...
- GL_REPEAT
- říká, že se má obrázek opakovat
- GL_MIRRORED_REPEAT
- říká, že se má obrázek opakovat a každý druhý zrcadlově otočit.
- GL_CLAMP_TO_EDGE
- říká, že se má okraj obrázku protáhnout, jako duha, do konce vyplňované plochy.
- GL_CLAMP_TO_BORDER
- Zbytek plochy se vyplní border barvou.
Pokud použijete poslední možnost, musíte nastavit border barvu. Třeba nějak takto:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
//...
Obrázky Kučího, které jsem použil dříve, mají na okraji jen průhledné pozadí. Proto by se na nich GL_CLAMP_TO_EDGE moc hezky neukázalo. Přidal jsem tedy barevné pozadí:
Události myši
Druhá textura se zobrazí jen při najetí myši nad okno. Událostem vstupu/výstupu myši z okna se říká enter / leave events.
Ve funkci main()
se zaregistruje funkce pro obsluhu těchto
událostí:
Funkci cursor_enter_callback()
jsem definoval v souboru events.h,
spolu s globální proměnnou mouseIn, která udržuje informaci o tom,
zda je myš nad oknem nebo ne:
Tuto globální proměnnou pak předám pomocí uniform proměnné do fragment
shaderu (ve funkci draw()
):
glUniform1i(variables.mouseIn, mouseIn);
//...
A to je vše (vytvoření a inicializaci variables.mouseIn zvládnete sami). Toto je výsledný fragment shader, který používá dvě textury:
uniform sampler2D texture1;
uniform sampler2D texture2;
uniform bool mouseIn;
in vec2 texCoord;
in vec4 position;
void main(void) {
if(!mouseIn) {
gl_FragColor = texture(texture1, texCoord);
return;
}
if(texCoord.x > 1.0 && texCoord.x < 3.0 && texCoord.y > 1.0 && texCoord.y < 3.0) {
gl_FragColor = texture(texture2, texCoord);
}
else {
gl_FragColor = texture(texture1, texCoord);
}
}
Souřadnice textury byly definované od 0.0 do 4.0. Podmínka v shaderu zajistí, že se druhá textura použije uprostřed otexturovaného čtverce (pokud je mouseIn true), tj v rozsahu od 1.0 do 3.0.
Za domácí úkol můžete přepsat podmínku s pomocí position, aby se druhá textura používala uprostřed okna (bez ohledu na natočení čtverce).
Podmínce v shaderu s mouseIn se dá vyhnout. Stačí ve funkci
draw()
nahrát do variables.texture2 ten správný index
dle mouseIn proměnné. To bude výhodné, pokud bude myš většinu
času nad oknem. Jinak je to nevýhodné, protože se bude častěji provádět
složitější podmínka s texCoord …
Skybox
Doposud všechny zobrazované objekty plavaly ve vzduchoprázdnu. Skybox je jedna z možností, jak simulovat prostor.
Na obrázku je příklad jednoho takového skyboxu. Jedná se o šest čtverců, které se namapují na krychli, uvnitř které se budou zobrazovat další objekty.
Přiklad vychází z prvního příkladu této kapitoly. Navíc jen definuje vertex data pro krychli a souřadnice textury.
Jednotlivé díly textury jsou v skyboxu rozdělené do tří řádků a čtyř sloupců. Takže souřadnice textur pro jednotivé strany krychle vypadají nějak takto:
//back
0.25, 1.0/3,
.5, 1.0/3,
0.25, 2.0/3,
.5, 1.0/3,
.5, 2.0/3,
0.25, 2.0/3,
//left
0.0, 1.0/3,
.25, 1.0/3,
0.0, 2.0/3,
0.25, 1.0/3,
.25, 2.0/3,
0.0, 2.0/3,
//...
};
Shadery vypadají stejně jako v prvním příkladu. Shader program se jen zavolá 2x. Jednou pro krychli se skyboxem a podruhé pro čtverec s logem.
Rozměry krychle jsem udělal od -1000.0 do 10000.0, takže se do krychle leccos vejde :-). Aby se krychle zobrazila, musil jsem zvětšit zorné pole perspektivy:
mat4x4_perspective(uPMatrix, degToRad(90.0), ((GLfloat)width) / height, 1.0, 2000.0);
...
V tomto příkladu nebyla potřeba žádná nová dovednost. Ale je to hezká ukázka toho, že můžete použít jednu texturu pro otexturování více povrchů (a ještě nám na textuře zbylo volné (čeré) místo).
attribute vec3 vertexPosition;
attribute vec2 textureCoord;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
varying vec4 vColor;
varying vec2 texCoord;
void main(void) {
vec4 mvPosition = uMVMatrix * vec4(vertexPosition, 1.0);
gl_Position = uPMatrix * mvPosition;
vColor = vec4(0.0, 0.0, 1.0, 1.0);
texCoord = vec2(textureCoord.x, -textureCoord.y);
}
</script>
<script id="shader-fs-skybox" type="x-shader/x-fragment">
precision mediump float;
uniform vec3 uAmbientColor;
uniform sampler2D texture1;
varying vec4 vColor;
varying vec2 texCoord;
void main(void) {
vec3 vLightIntenzity = uAmbientColor;
vec4 c = vec4(vColor.rgb * vLightIntenzity, vColor.a);
if (uAmbientColor.r + uAmbientColor.g + uAmbientColor.b > 0.0) {
gl_FragColor = mix(texture2D(texture1, texCoord), c, 0.5);
}
else {
gl_FragColor = texture2D(texture1, texCoord);
}
}
</script>
// right triangle
1.0, 1.0, 0.0,
1.0, -1.0, 0.0,
-1.0, -1.0, 0.0,
// left triangle
-1.0, 1.0, 0.0,
1.0, 1.0, 0.0,
-1.0, -1.0, 0.0
];
var texCoord = [
// right triangle
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// left triangle
0.0, 0.0,
1.0, 0.0,
0.0, 1.0
];
var boxVertices = [
// back
-1000.0, -1000.0, -1000.0,
1000.0, -1000.0, -1000.0,
-1000.0, 1000.0, -1000.0,
1000.0, -1000.0, -1000.0,
1000.0, 1000.0, -1000.0,
-1000.0, 1000.0, -1000.0,
// left
-1000.0, -1000.0, 1000.0,
-1000.0, -1000.0, -1000.0,
-1000.0, 1000.0, 1000.0,
-1000.0, -1000.0, -1000.0,
-1000.0, 1000.0, -1000.0,
-1000.0, 1000.0, 1000.0,
// right
1000.0, -1000.0, 1000.0,
1000.0, -1000.0, -1000.0,
1000.0, 1000.0, 1000.0,
1000.0, -1000.0, -1000.0,
1000.0, 1000.0, -1000.0,
1000.0, 1000.0, 1000.0,
// front
-1000.0, -1000.0, 1000.0,
1000.0, -1000.0, 1000.0,
-1000.0, 1000.0, 1000.0,
1000.0, -1000.0, 1000.0,
1000.0, 1000.0, 1000.0,
-1000.0, 1000.0, 1000.0,
// bottom
-1000.0, -1000.0, 1000.0,
-1000.0, -1000.0, -1000.0,
1000.0, -1000.0, 1000.0,
1000.0, -1000.0, 1000.0,
-1000.0, -1000.0, -1000.0,
1000.0, -1000.0, -1000.0,
// top
-1000.0, 1000.0, 1000.0,
-1000.0, 1000.0, -1000.0,
1000.0, 1000.0, 1000.0,
-1000.0, 1000.0, -1000.0,
1000.0, 1000.0, 1000.0,
1000.0, 1000.0, -1000.0
];
var boxTexCoord = [
//back
0.25, 1.0/3,
.5, 1.0/3,
0.25, 2.0/3,
.5, 1.0/3,
.5, 2.0/3,
0.25, 2.0/3,
//left
0.0, 1.0/3,
.25, 1.0/3,
0.0, 2.0/3,
0.25, 1.0/3,
.25, 2.0/3,
0.0, 2.0/3,
//right
0.75, 1.0/3,
.5, 1.0/3,
0.75, 2.0/3,
0.5, 1.0/3,
.5, 2.0/3,
0.75, 2.0/3,
//front
1.0, 1.0/3,
.75, 1.0/3,
1.0, 2.0/3,
0.75, 1.0/3,
.75, 2.0/3,
1.0, 2.0/3,
// bottom
0.5, 1.0/3,
0.5, 0.0,
0.25, 1.0/3,
0.25, 1.0/3,
0.5, 0.0,
0.25, 0.0,
// top
0.25, 1.0,
0.25, 2.0/3,
0.5, 1.0,
0.25, 2.0/3,
0.5, 1.0,
0.5, 2.0/3
];
var buffers = {};
var useColor = {};
useColor.ambient = 0;
function initVariables(gl, shaderProgram) {
variables.vertexPositionLoc = gl.getAttribLocation(shaderProgram, "vertexPosition");
variables.textureCoordLoc = gl.getAttribLocation(shaderProgram, "textureCoord");
gl.enableVertexAttribArray(variables.vertexPositionLoc);
gl.enableVertexAttribArray(variables.textureCoordLoc);
variables.uMVMatrixLoc = gl.getUniformLocation(shaderProgram, "uMVMatrix");
variables.uPMatrixLoc = gl.getUniformLocation(shaderProgram, "uPMatrix");
variables.uAmbientColorLoc = gl.getUniformLocation(shaderProgram, "uAmbientColor");
variables.texture1 = gl.getUniformLocation(shaderProgram, "texture1");
}
//logo
buffers.positionBuffer = gl.createBuffer();
buffers.texCoordBuffer = gl.createBuffer();
buffers.texture1 = gl.createTexture();
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoord), gl.STATIC_DRAW);
var logoImage = new Image();
logoImage.onload = function() {
gl.bindTexture(gl.TEXTURE_2D, buffers.texture1);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, logoImage);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.bindTexture(gl.TEXTURE_2D, null);
};
logoImage.src = '/rc/sally/images/c/opengl/logo.gif';
//skybox
buffers.sPositionBuffer = gl.createBuffer();
buffers.sTexCoordBuffer = gl.createBuffer();
buffers.sTexture1 = gl.createTexture();
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.sPositionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(boxVertices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.sTexCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(boxTexCoord), gl.STATIC_DRAW);
var sboxImage = new Image();
sboxImage.onload = function() {
gl.bindTexture(gl.TEXTURE_2D, buffers.sTexture1);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, sboxImage);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.bindTexture(gl.TEXTURE_2D, null);
};
sboxImage.src = '/rc/sally/images/c/opengl/miramar_large.jpg';
}
var lastX, lastY;
var canvas1 = document.getElementById('skybox-canvas');
var lastDownTarget;
$(document).on('mousedown', function(event) {
lastDownTarget = event.target;
lastX = event.pageX; lastY = event.pageY;
});
$(document).on('mousemove', function(event) {
if(lastDownTarget != canvas1)
return;
var buttonDown = event.buttons == null ?event.wich : event.buttons;
if(!buttonDown) return;
x = event.pageY - lastY;
y = event.pageX - lastX;
lastX = event.pageX;
lastY = event.pageY;
var xAngle = WebGLUtils.degToRad(x/5.0);
var yAngle = WebGLUtils.degToRad(y/5.0);
updateAngles(xAngle, yAngle);
event.preventDefault();
return false;
});
});
mat4.identity(rotationMatrix);
function updateAngles(xAngle, yAngle) {
var newRotationMatrix = mat4.create();
mat4.identity(newRotationMatrix);
mat4.rotate(newRotationMatrix, newRotationMatrix, xAngle, [1, 0, 0]);
mat4.rotate(newRotationMatrix, newRotationMatrix, yAngle, [0, 1, 0]);
mat4.multiply(rotationMatrix, newRotationMatrix, rotationMatrix);
}
function drawScene(gl, canvas) {
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(1.0, 1.0, 1.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
var uPMatrix = mat4.create();
var uMVMatrix = mat4.create();
mat4.identity(uPMatrix);
mat4.identity(uMVMatrix);
/********************************************/
mat4.perspective(uPMatrix, WebGLUtils.degToRad(45), canvas.width / canvas.height, 1.0, 2000.0);
mat4.translate(uMVMatrix, uMVMatrix, [0.0, 0.0, -10.0]);
mat4.multiply(uMVMatrix, uMVMatrix, rotationMatrix);
/********************************************/
gl.uniformMatrix4fv(variables.uMVMatrixLoc, false, uMVMatrix);
gl.uniformMatrix4fv(variables.uPMatrixLoc, false, uPMatrix);
if (useColor.ambient) {
gl.uniform3f(variables.uAmbientColorLoc, 0.4, 0.4, 0.4);
}
else {
gl.uniform3f(variables.uAmbientColorLoc, 0.0, 0.0, 0.0);
}
// vykresli skybox
gl.bindTexture(gl.TEXTURE_2D, buffers.sTexture1);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.sPositionBuffer);
gl.vertexAttribPointer(variables.vertexPositionLoc, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.sTexCoordBuffer);
gl.vertexAttribPointer(variables.textureCoordLoc, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, boxVertices.length/3);
// vykresli logo
gl.bindTexture(gl.TEXTURE_2D, buffers.texture1);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.positionBuffer);
gl.vertexAttribPointer(variables.vertexPositionLoc, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.texCoordBuffer);
gl.vertexAttribPointer(variables.textureCoordLoc, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, vertices.length/3);
}
function animate() {
}
drawScene(gl, canvas);
animate();
requestAnimFrame(function() { tick(gl, canvas);});
}
function webGLStart() {
var canvas = document.getElementById("textura");
var gl = WebGLUtils.setupWebGL(canvas,{ antialias: true });
if(!gl) return;
var shaderProgram = WebGLUtils.initShaders(gl, "shader-vs-skybox","shader-fs-skybox");
gl.useProgram(shaderProgram);
initVariables(gl,shaderProgram);
initBuffers(gl);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
tick(gl, canvas);
}
webGLStart();
Toto není jediný způsob, jak vytvořit iluzi prostoru. Můžete si vystačit i s jednou plochou, na kterou vykreslíte nějaké pozadí, a kterou budete otáčet vždy tak, aby čelila pohledu kamery. Nebo můžete vytvořit z mnoha trojúhleníků velkou kopuli, na kterou vykreslíte příslušnou texturu (vytvoření vhodné textury pro takovou polokouli není ale zrovna snadné).
Cubemap
Cubemap (GL_TEXTURE_CUBE_MAP) je další z několika druhů textury.
Používá se, jak název napovídá, k otexturování krychle. Tedy k něčemu podobnému, jako jsem popisoval u skyboxu. Jen se data nahrávají trochu jinak a i souřadnice textury se určují jinak.
Souřadnice textury se určují pomocí vektorů, viz obrázek níže. Jde o vektor, jehož počátek je uprostřed pomyslné krychle. Definováním vektoru (pomocí tří souřadnic) určíte směr. Místo, kde se vektor protne se stěnou krychle, určuje výslednou barvu.
Cubemap
Nejpřirozenější je asi vykreslovat cubemap na krychli, ale nikde není dáno, že tento druh textury nemůžete použít na cokoliv jiného.
Souřadnice cubemap textury jsou tedy trojrozměrné. Pro cubemap texturu se musí každá strana nahrávat zvlášť (viz ukázka zdrojového kódu dále), takže je potřeba skybox rozřezat na 6 samostatných obrázků.
Pro inicializování cubemap můžete použít následující funkci:
- /*------------------------------------------------*/
- /* 08textury/cubemap/load-cubemap.h */
- #include <GLFW/glfw3.h>
- {
- GLuint i;
- glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
- {
- image = sx_stbi_load(faces[i], &width, &height, &n, 0);
- glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
- }
- else {
- glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
- }
- sx_stbi_image_free(image);
- }
- glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
- glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
- return textureID;
- }
Funkce dostane jako první argument ID texture bufferu a jako druhý argument pole s názvy 6-ti souborů.
Funkce dělá v zásadě jen to, že přibinduje buffer s typem textury GL_TEXTURE_CUBE_MAP, nastavuje některé, vám již známé, parametry textury a dále nahraje obsah šesti obrázků do tzv. texture targets, které se adresují pomocí konstant z následující tabulky:
GL_TEXTURE_CUBE_MAP_POSITIVE_X | = GL_TEXTURE_CUBE_MAP_POSITIVE_X + 0 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_X | = GL_TEXTURE_CUBE_MAP_POSITIVE_X + 1 |
GL_TEXTURE_CUBE_MAP_POSITIVE_Y | = GL_TEXTURE_CUBE_MAP_POSITIVE_X + 2 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y | = GL_TEXTURE_CUBE_MAP_POSITIVE_X + 3 |
GL_TEXTURE_CUBE_MAP_POSITIVE_Z | = GL_TEXTURE_CUBE_MAP_POSITIVE_X + 4 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z | = GL_TEXTURE_CUBE_MAP_POSITIVE_X + 5 |
Tím se inicializuje všech šest stran krychle. Funkci můžete zavolat např. takto:
- //...
- "miramar_large/right.jpg",
- "miramar_large/left.jpg",
- "miramar_large/top.jpg",
- "miramar_large/bottom.jpg",
- "miramar_large/back.jpg",
- "miramar_large/front.jpg",
- };
- glActiveTexture(GL_TEXTURE0);
- load_cubemap(TEX[0], faces);
- //...
- }
Vertex data pro krychli mohou vypadat nějak takto:
- GLfloat boxVertices[] = {
- -1000.0f, 1000.0f, -1000.0f,
- -1000.0f, -1000.0f, -1000.0f,
- 1000.0f, -1000.0f, -1000.0f,
- 1000.0f, -1000.0f, -1000.0f,
- 1000.0f, 1000.0f, -1000.0f,
- -1000.0f, 1000.0f, -1000.0f,
- //...
- };
- GLfloat boxTexCoord[] = {
- -1.0f, 1.0f, -1.0f,
- -1.0f, -1.0f, -1.0f,
- 1.0f, -1.0f, -1.0f,
- 1.0f, -1.0f, -1.0f,
- 1.0f, 1.0f, -1.0f,
- -1.0f, 1.0f, -1.0f,
- //...
- }
Dále je potřeba upravit vertex shader, protože souřadnice textury už nejsou 2D vektory, ale 3D.
Kvůli úpravě shaderů nezobrazuji v příkladu čtverec s logem, protože bych musel použít dva shader programy a tím příklad zbytečně kompikoval. (V přechozím příkladě se skyboxem jsem mohl použít stejné shadery na vykreslení skyboxu i loga.)
layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec3 textureCoord;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
out vec3 texCoord;
void main(void) {
vec4 mvPosition = uMVMatrix * vec4(vertexPosition, 1.0);
texCoord = textureCoord;
gl_Position = uPMatrix * mvPosition;
}
Fragment shader se také musí upravit kvůli 3D souřadnicím textury, ale hlavně uniform proměnná pro texturu je nyní typu sampleCube.
uniform bool useAmbient;
uniform vec3 uAmbientColor;
uniform samplerCube texture1;
in vec3 texCoord;
void main(void) {
if (useAmbient) {
vec4 vColor = vec4(0.0, 0.0, 1.0, 1.0);
vec4 c = vec4(vColor.rgb * uAmbientColor, vColor.a);
gl_FragColor = mix(texture(texture1, texCoord), c, 0.5);
}
else {
gl_FragColor = texture(texture1, texCoord);
}
}
Ve WebGL musíte místo funkce texture()
použít
funkci textureCube()
.
A zde je výsledek:
attribute vec3 vertexPosition;
attribute vec3 textureCoord;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
varying vec3 texCoord;
void main(void) {
vec4 mvPosition = uMVMatrix * vec4(vertexPosition, 1.0);
texCoord = textureCoord;
gl_Position = uPMatrix * mvPosition;
}
</script>
<script id="shader-fs-cubemap" type="x-shader/x-fragment">
precision mediump float;
uniform bool useAmbient;
uniform vec3 uAmbientColor;
uniform samplerCube texture1;
varying vec3 texCoord;
void main(void) {
if (useAmbient) {
vec4 vColor = vec4(0.0, 0.0, 1.0, 1.0);
vec4 c = vec4(vColor.rgb * uAmbientColor, vColor.a);
gl_FragColor = mix(textureCube(texture1, texCoord), c, 0.5);
}
else {
gl_FragColor = textureCube(texture1, texCoord);
}
}
</script>
// Positions
-1000.0, 1000.0, -1000.0,
-1000.0, -1000.0, -1000.0,
1000.0, -1000.0, -1000.0,
1000.0, -1000.0, -1000.0,
1000.0, 1000.0, -1000.0,
-1000.0, 1000.0, -1000.0,
-1000.0, -1000.0, 1000.0,
-1000.0, -1000.0, -1000.0,
-1000.0, 1000.0, -1000.0,
-1000.0, 1000.0, -1000.0,
-1000.0, 1000.0, 1000.0,
-1000.0, -1000.0, 1000.0,
1000.0, -1000.0, -1000.0,
1000.0, -1000.0, 1000.0,
1000.0, 1000.0, 1000.0,
1000.0, 1000.0, 1000.0,
1000.0, 1000.0, -1000.0,
1000.0, -1000.0, -1000.0,
-1000.0, -1000.0, 1000.0,
-1000.0, 1000.0, 1000.0,
1000.0, 1000.0, 1000.0,
1000.0, 1000.0, 1000.0,
1000.0, -1000.0, 1000.0,
-1000.0, -1000.0, 1000.0,
-1000.0, 1000.0, -1000.0,
1000.0, 1000.0, -1000.0,
1000.0, 1000.0, 1000.0,
1000.0, 1000.0, 1000.0,
-1000.0, 1000.0, 1000.0,
-1000.0, 1000.0, -1000.0,
-1000.0, -1000.0, -1000.0,
-1000.0, -1000.0, 1000.0,
1000.0, -1000.0, -1000.0,
1000.0, -1000.0, -1000.0,
-1000.0, -1000.0, 1000.0,
1000.0, -1000.0, 1000.0
];
var boxTexCoord = [
// Positions
-1.0, 1.0, -1.0,
-1.0, -1.0, -1.0,
1.0, -1.0, -1.0,
1.0, -1.0, -1.0,
1.0, 1.0, -1.0,
-1.0, 1.0, -1.0,
-1.0, -1.0, 1.0,
-1.0, -1.0, -1.0,
-1.0, 1.0, -1.0,
-1.0, 1.0, -1.0,
-1.0, 1.0, 1.0,
-1.0, -1.0, 1.0,
1.0, -1.0, -1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, -1.0,
1.0, -1.0, -1.0,
-1.0, -1.0, 1.0,
-1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
1.0, -1.0, 1.0,
-1.0, -1.0, 1.0,
-1.0, 1.0, -1.0,
1.0, 1.0, -1.0,
1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, 1.0, -1.0,
-1.0, -1.0, -1.0,
-1.0, -1.0, 1.0,
1.0, -1.0, -1.0,
1.0, -1.0, -1.0,
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0
];
var buffers = {};
var useColor = {};
useColor.ambient = 0;
function initVariables(gl, shaderProgram) {
variables.vertexPositionLoc = gl.getAttribLocation(shaderProgram, "vertexPosition");
variables.textureCoordLoc = gl.getAttribLocation(shaderProgram, "textureCoord");
gl.enableVertexAttribArray(variables.vertexPositionLoc);
gl.enableVertexAttribArray(variables.textureCoordLoc);
variables.uMVMatrixLoc = gl.getUniformLocation(shaderProgram, "uMVMatrix");
variables.uPMatrixLoc = gl.getUniformLocation(shaderProgram, "uPMatrix");
variables.uAmbientColorLoc = gl.getUniformLocation(shaderProgram, "uAmbientColor");
variables.texture1 = gl.getUniformLocation(shaderProgram, "texture1");
variables.useAmbientLoc = gl.getUniformLocation(shaderProgram, "useAmbient");
}
//logo
buffers.positionBuffer = gl.createBuffer();
buffers.texCoordBuffer = gl.createBuffer();
buffers.texture1 = gl.createTexture();
var faces = [
"miramar_large/right.jpg",
"miramar_large/left.jpg",
"miramar_large/top.jpg",
"miramar_large/bottom.jpg",
"miramar_large/back.jpg",
"miramar_large/front.jpg"
];
load_cubemap(gl, buffers.texture1, faces);
//cubemap
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(boxVertices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(boxTexCoord), gl.STATIC_DRAW);
}
var lastX, lastY;
var canvas1 = document.getElementById('cubemap-canvas');
var lastDownTarget;
$(document).on('mousedown', function(event) {
lastDownTarget = event.target;
lastX = event.pageX; lastY = event.pageY;
});
$(document).on('mousemove', function(event) {
if(lastDownTarget != canvas1)
return;
var buttonDown = event.buttons == null ?event.wich : event.buttons;
if(!buttonDown) return;
x = event.pageY - lastY;
y = event.pageX - lastX;
lastX = event.pageX;
lastY = event.pageY;
var xAngle = WebGLUtils.degToRad(x/5.0);
var yAngle = WebGLUtils.degToRad(y/5.0);
updateAngles(xAngle, yAngle);
event.preventDefault();
return false;
});
});
mat4.identity(rotationMatrix);
function updateAngles(xAngle, yAngle) {
var newRotationMatrix = mat4.create();
mat4.identity(newRotationMatrix);
mat4.rotate(newRotationMatrix, newRotationMatrix, xAngle, [1, 0, 0]);
mat4.rotate(newRotationMatrix, newRotationMatrix, yAngle, [0, 1, 0]);
mat4.multiply(rotationMatrix, newRotationMatrix, rotationMatrix);
var x = '';
for(i = 0; i < rotationMatrix.length; i++)
x += rotationMatrix[i]+',';
if(!(i+1 % 4)) x+="<br />";
$('#x').html(x);
}
function drawScene(gl, canvas) {
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(1.0, 1.0, 1.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
var uPMatrix = mat4.create();
var uMVMatrix = mat4.create();
mat4.identity(uPMatrix);
mat4.identity(uMVMatrix);
/********************************************/
mat4.perspective(uPMatrix, WebGLUtils.degToRad(90), canvas.width / canvas.height, 1.0, 2000.0);
//mat4.translate(uMVMatrix, uMVMatrix, [0.0, 0.0, -5.0]);
//mat4.scale(uMVMatrix, uMVMatrix, [0.002, 0.002, 0.002]);
mat4.multiply(uMVMatrix, uMVMatrix, rotationMatrix);
/********************************************/
gl.uniformMatrix4fv(variables.uMVMatrixLoc, false, uMVMatrix);
gl.uniformMatrix4fv(variables.uPMatrixLoc, false, uPMatrix);
gl.uniform3f(variables.uAmbientColorLoc, 0.4, 0.4, 0.4);
gl.uniform1i(variables.useAmbientLoc, useColor.ambient);
// vykresli cubemap
gl.bindTexture(gl.TEXTURE_CUBE_MAP, buffers.texture1);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.positionBuffer);
gl.vertexAttribPointer(variables.vertexPositionLoc, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.texCoordBuffer);
gl.vertexAttribPointer(variables.textureCoordLoc, 3, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, boxVertices.length/3);
}
function animate() {
}
drawScene(gl, canvas);
animate();
requestAnimFrame(function() { tick(gl, canvas);});
}
function webGLStart() {
var canvas = document.getElementById("textura");
var gl = WebGLUtils.setupWebGL(canvas, { antialias: true });
if(!gl) return;
var shaderProgram = WebGLUtils.initShaders(gl, "shader-vs-cubemap","shader-fs-cubemap");
gl.useProgram(shaderProgram);
initVariables(gl,shaderProgram);
initBuffers(gl);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
tick(gl, canvas);
}
webGLStart();
Poznámka na závěr: ve zdrojových kódech k tomuto příkladu jsem vám nechal zakomentované tyto dvě řádky:
mat4x4_scale_aniso(uMVMatrix, uMVMatrix, 0.002, 0.002, 0.002);
Pokud je odkomentujete, krychle se zmenší natolik, že se nani nebudete dívat zevnitř, ale z venku.
Fullscreen
Abyste si příklad s cubemap vychutnali, spouštím program ve fullscrennu. K tomu mi slouží následující kus zdrojového kódu:
GLFWmonitor* monitor = glfwGetPrimaryMonitor();
const GLFWvidmode* mode = glfwGetVideoMode(monitor);
glfwWindowHint(GLFW_RED_BITS, mode->redBits);
glfwWindowHint(GLFW_GREEN_BITS, mode->greenBits);
glfwWindowHint(GLFW_BLUE_BITS, mode->blueBits);
glfwWindowHint(GLFW_REFRESH_RATE, mode->refreshRate);
window = glfwCreateWindow(mode->width, mode->height, "08 - cubemap", monitor, NULL);
V kódu nejdříve zjistím informace o aktuálním nastavení monitoru
a pak je použiji pro nastavení vytvářeného nového okna. Díky čtvrtému
argumentu funkce glfwCreateWindow()
se okno rozplácne přes
celý monitor (fullscreen). (Počítač může být připojen k více monitorům,
funkce glfwGetPrimaryMonitor()
vrací odkaz na ten, který
považuje systém za primární.)
Závěr
Použití textur přináší do OpenGL nový stupeň zábavy :-). Představil jsem vám zde dva základní typy textur a některé jejich vlastnosti.
Na internetu můžete najít spousty cubemap textur (stačí vygooglit cubemap nebo skybox).
Ve zdrojových kódech ke stažení najdete v příkladu s cubemap stažené obrázky ze stránky www.custommapmakers.org/skyboxes.php. Jsou ve formátu TGA. Jedná se o nekomprimované obrázky, takže jejich načtení je mnohem, mnohem rychlejší než třeba načítání JPG, PNG nebo GIF. Vyzkoušejte, uvidíte :-).