Textury na krychli, použití kláves

S využitím kódu z předchozí kapitoly si ukážeme, jak načítat a používat textury. Výsledná krychle bude mít na všech šesti stěnách stejnou texturu. Otexturovat každou stěnu je obdobné a dosažitelné jednoduchou modifikací. Klientská třída Lesson, kterou naleznete v první lekci, je stále stejná.

Jako bonus je implementováno jednoduché řízení rychlostí otáčení kolem všech os, pomocí kurzorových šipek a kláves PageDown, PageUp.

Po spuštění níže uvedeného kódu uvidíte okno, jako je na obrázku.

Krychle s texturou
Krychle s texturou

Třída Renderer

Všechny metody, kromě níže uvedených, zůstávají shodné s metodami z lekce 2. Části kódu, které se nezměnily jsou napsány šedou barvou.

V metodě display() nahradíme téměř celý kód. Místo jehlanu budeme vykreslovat krychli a naučíme se používat texturovací souřadnice. Do metody init() doplníme kód pro zapnutí textur. Nakonec vytvoříme úplně nové metody a jednu privátní třídu pro načítání textur. Do třídy keyPressed() doplníme reakce na stisky potřebných kláves.

Je nutné upozornit, že z důvodu kompatibility musí být rozměry textur mocniny 2 a nejméně 64 a nejvíce 256 pixelů. V případě použití textury o jiných rozměrech, může program příliš vytěžovat systém a počet FPS klesne až na 0 nebo nepůjde vůbec spustit.

Celkově nyní potřebujeme již importovat 14 tříd, pro načítání obrázků, obsluhu OpenGL a podobně. Při programování v Eclipse se doplní automaticky, popřípadě v menu Source je příkaz Organize Imports, který doplní vše potřebné.

Kromě standardních atributů pro GL a GLU vytvoříme dvě konstanty, kde uložíme název souboru s texturou a počet textur, které budeme načítat. Důležité je celočíselné pole textur, ve kterém bude textura uložena. Zbylé atributy jsou pouze pro rotaci krychle v prostoru. Hodnota rotateX je celkový úhel otočení okolo osy X, deltaX je krok, o který se krychle otočí v každém framu, pro osy Y a Z jsou zbylé čtyři proměnné.

private final String textureFile = "lesson03/textures/texture.png";

private final int texturesCount = 1;

 

private float rotateX = 0;

private float rotateY = 0;

private float rotateZ = 0;

 

private float deltaX = 0.05f;

private float deltaY = 0.1f;

private float deltaZ = 0.2f;

 

private int[] texture = new int[texturesCount];

 

private GLU glu = new GLU();

private GL gl;

Krychli je možné vykreslit pomocí dvanácti trojúhelníků podle předchozí kapitoly, ale je jednodušší na začátku vykreslování definovat, že chceme kreslit čtverce. Proto tentokrát použijeme GL_QUADS. Vrcholy se definují stejně jako u trojúhelníku. Ke každému vrcholu se ještě musí uvést texturovací souřadnice. Ty určují, jakým způsobem bude na čtverec textura namapována. Pořadí zadávání bodů a texturovací souřadnice ukazuje obrázek. Před zavoláním glBegin() je nutné nastavit aktuální texturu, se kterou chceme pracovat pomocí metody glBindTexture().

Pořadí bodů, texturovací souřadnice a mapování textury
Pořadí bodů, texturovací souřadnice a mapování textury

public void display(GLAutoDrawable gLDrawable)

{

gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

 

gl.glLoadIdentity();

 

gl.glTranslatef(0f, 0.0f, -5f);

 

gl.glRotatef(rotateX, 1.0f, 0.0f, 0.0f);

gl.glRotatef(rotateY, 0.0f, 1.0f, 0.0f);

gl.glRotatef(rotateZ, 0.0f, 0.0f, 1.0f);

 

gl.glBindTexture(GL.GL_TEXTURE_2D, texture[0]);

 

gl.glBegin(GL.GL_QUADS);

 

gl.glTexCoord2f(0.0f, 0.0f);

gl.glVertex3f(-1.0f, -1.0f, 1.0f);  //bod A

gl.glTexCoord2f(1.0f, 0.0f);

gl.glVertex3f(1.0f, -1.0f, 1.0f);   //bod B

gl.glTexCoord2f(1.0f, 1.0f);

gl.glVertex3f(1.0f, 1.0f, 1.0f);    //bod C

gl.glTexCoord2f(0.0f, 1.0f);

gl.glVertex3f(-1.0f, 1.0f, 1.0f);   //bod D

 

.

.

.

 

gl.glEnd();

 

rotateX += deltaX;

rotateY += deltaY;

rotateZ += deltaZ;

}

Body na krychli
Body na krychli

Do metody init() přidáme volání vlastní metody pro vygenerování textur a pomocí metody glEnable s parametrem GL_TEXTURE_2D je nutné zapnout použití textur. Bez toho by se zobrazila pouze bílá krychle.

public void init(GLAutoDrawable gLDrawable)

{

this.gl = gLDrawable.getGL();

gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f);

gl.glEnable(GL.GL_DEPTH_TEST);

gl.glDepthFunc(GL.GL_LEQUAL);

gl.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST);

gl.glEnable(GL.GL_TEXTURE_2D);

 

generateTextures();

 

gLDrawable.addKeyListener(this);

}

V metodě generateTextures() nejprve zavoláme metodu glGenTextures(), které předáme jako parametr, kolik textur chceme generovat, proměnnou, do které se budou ukládat textury a offset textury. Pomocí GL_TEXTURE_2D definujeme, že jde o 2D texturu a pomocí glBindTexture() řekneme, se kterou texturou chceme pracovat. Pak načteme a vytvoříme texturu (viz níže). Poslední dva řádky definují, jaké filtrování se má použít na texturu, pokud je menší (MIN), respektive větší (MAG) než skutečný obrázek, který načítáme ze souboru. Filtr nemusíme použít žádný (GL_NONE), ale textura není na pohled hezká z blízka, protože přes celou obrazovku se zobrazí jen několik málo velkých pixelů z původního obrázku. Další možné filtry jsou GL_NEAREST, GL_LINEAR. Chceme-li použít mipmapping, využíváme kombinací předchozích dvou filtrů, například GL_NEAREST_MIPMAP_NEAREST nebo GL_LINEAR_MIPMAP_NEAREST a podobně. Filtr GL_LINEAR je náročnější na výkon hardware, ale výsledná textura vypadá hezky z blízka i z dálky. Při zvětšení jsou pixely rozmazány, takže nejsou vidět ostré hrany mezi pixely.

private void generateTextures()

{

gl.glGenTextures(texturesCount, texture, 0);

 

gl.glBindTexture(GL.GL_TEXTURE_2D, texture[0]);

 

Texture newTexture = null;

 

try

{

newTexture = readTexture(textureFile);

}

catch (IOException e)

{

e.printStackTrace();

throw new RuntimeException(e);

}

 

makeRGBTexture(gl, glu, newTexture, GL.GL_TEXTURE_2D);

 

gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);

gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);

}

Metody readTexture(), readImage() a getResourceAsStream() obstarávají správné nalezení souboru s texturou buď v JAR archívu nebo na disku a načtený obrázek předají metodě readPixels(), která zprostředkuje načtení obrázku po jednotlivých pixelech.

private Texture readTexture(String filename) throws IOException

{

BufferedImage bufferedImage = readImage(filename);

return readPixels(bufferedImage);

}

 

private BufferedImage readImage(String resourceName) throws IOException

{

return ImageIO.read(getResourceAsStream(resourceName));

}

 

public InputStream getResourceAsStream(final String filename) throws IOException

{

InputStream stream = ClassLoader.getSystemResourceAsStream(filename);

 

if (stream == null)

{

return new FileInputStream(filename);

}

else

{

return stream;

}

}

Metoda readPixels() není z pohledu OpenGL zajímavá. Slouží ke čtení jednotlivých pixelů z obrázku, který je uložen jako BufferedImage. Obrázek postupně prochází po jednotlivých pixelech a do ByteBuffer ukládá hodnoty jednotlivých složek RGB pro každou barvu. Vrací objekt Texture.

private Texture readPixels(BufferedImage img)

{

int bytesPerPixel = 3;

int[] packedPixels = new int[img.getWidth() * img.getHeight()];

 

PixelGrabber pixelgrabber = new PixelGrabber(img, 0, 0, img.getWidth(), img.getHeight(), packedPixels, 0, img.getWidth());

 

try

{

pixelgrabber.grabPixels();

}

catch (InterruptedException e)

{

throw new RuntimeException();

}

 

ByteBuffer unpackedPixels = BufferUtil.newByteBuffer(packedPixels.length * bytesPerPixel);

 

for (int row = img.getHeight() - 1; row >= 0; row--)

{

for (int col = 0; col < img.getWidth(); col++)

{

int packedPixel = packedPixels[row * img.getWidth() + col];

 

unpackedPixels.put((byte) ((packedPixel >> 16) & 0xFF));

unpackedPixels.put((byte) ((packedPixel >> 8) & 0xFF));

unpackedPixels.put((byte) ((packedPixel >> 0) & 0xFF));

}

}

 

unpackedPixels.flip();

 

return new Texture(unpackedPixels, img.getWidth(), img.getHeight());

}

Metoda makeRGBTexture() vytváří texturu ve formátu, který se používá při přiřazení textury na objekt.

private void makeRGBTexture(GL gl, GLU glu, Texture img, int target)

{

gl.glTexImage2D(target, 0, GL.GL_RGB, img.getWidth(), img.getHeight(), 0, GL.GL_RGB, GL.GL_UNSIGNED_BYTE, img.getPixels());

}

Privátní třída Texture, která uchovává informace o vygenerované textuře do doby, než se textura skutečně vytvoří pomocí glTexImage2D() v metodě makeRGBTexture(). Obsahuje výšku, šířku a jednotlivé pixely obrázku.

private class Texture

{

private ByteBuffer pixels;

private int width;

private int height;

 

public Texture(ByteBuffer pixels, int width, int height)

{

this.height = height;

this.pixels = pixels;

this.width = width;

}

 

public int getHeight()

{

return height;

}

 

public ByteBuffer getPixels()

{

return pixels;

}

 

public int getWidth()

{

return width;

}

}

Po doplnění reakcí na stisky kláves bude možné regulovat rychlost otáčení krychle podle libovolné osy.

public void keyPressed(KeyEvent e)

{

if (e.getKeyCode() == KeyEvent.VK_ESCAPE)

System.exit(0);

 

if (e.getKeyCode() == KeyEvent.VK_LEFT)

deltaX -= 0.05;

 

if (e.getKeyCode() == KeyEvent.VK_RIGHT)

deltaX += 0.05;

 

if (e.getKeyCode() == KeyEvent.VK_DOWN)

deltaY -= 0.05;

 

if (e.getKeyCode() == KeyEvent.VK_UP)

deltaY += 0.05;

 

if (e.getKeyCode() == KeyEvent.VK_PAGE_DOWN)

deltaZ -= 0.05;

 

if (e.getKeyCode() == KeyEvent.VK_PAGE_UP)

deltaZ += 0.05;

}