#01 - Workspace setup

Vitajte pri seríí videí o tom, ako sa tvoria hry bez použitia herného enginu. Namiesto toho pracujeme priamo s grafickou kartou cez OpenGL a herný engine si vytvoríme sami.

Vitajte pri seríí videí o tom, ako sa tvoria hry bez použitia herného enginu. Namiesto toho budeme pracovať priamo s grafickou kartou cez knižnicu OpenGL a takýto herný engine si sami vytvoríme.

Kód budem písať v Jave s použitím bindingov LWJGL, ale príkazy sú rovnaké vo všetkých jazykoch, čize nie je problém programovať aj v inom jazyku.

Kód budem písať v IntelliJ, ktoré je zadarmo a osobne ho odporúčam, ale je možné použiť aj Eclipse alebo nejaký iný editor kódu.

(radšej teda ten IntelliJ)

Na zostavovanie projektu budem používať Maven, ktorý sa dá veľmi ľahko a rýchlo nakonfiguorvať a odstraňuje mnoho problémov zo sťahovaním knižníc a organizáciou zostavenia. Ak Maven nepoznáte alebo potrebujete krátky refresher toho ako funguje, neváhajte si najprv pozrieť moje videá "Ako na Maven" (odkaz je v popise).

Aby sme aj začali, vytvoríme si nový projekt typu Maven. Vyplníme groupId, artifactId, verziu. Vytvoríme si package a prvú triedu s metódou main, kde náš program začne.

Ďalej budeme potrebovať knižnice LWJGL. Keďže používame Maven, bude to veľmi jednoduché. Otvoríme si stránku LWJGL, kde si Mode nastavíme na Maven (tí ktorí Maven nepoužívajú môžu nechať ZIP a potom si knižnice ručne nastaviť), Preset nastavíme na Minimal OpenGL a skopírujeme tento útržok kódu, ktorý vložíme do pom.xml súboru nášho projektu.

Knižnice máme teraz poriešené a už len nastavíme Run konfigurácie.

Jednu pre zostavenie jar balíčku Mavenom a druhú pre spustenie aplikácie v prostredí IntelliJ (napríklad keď ju budeme chcieť debugovať).

Všetko máme teraz už nastavené a môžeme sa pustiť do vytvorenia okna aplikácie.


# 02 - Vytvorenie okna

V tejto časti napíšeme kód, ktorý otvorí okno v strede monitora, a vytvoríme veľmi jednoduchý game-loop.

Na vytvorenie okna použijeme knižnicu GLFW, ktorá rieši multiplatformové vytváranie okien a OpenGL konextov.

Vytvoríme si novú triedu Window, kde deklarujeme metódy:

  • init(), kde inicializujeme GLFW a vytvoríme okno
  • loop() v ktorej sa bude nachádzať skutočný game loop
  • a ešte cleanup(), kde zmažeme alokovanú pamäť a zatvoríme GLFW.

Okrem toho vytvoríme verejnú metódu run(), v ktorej tieto metódy postupne zavoláme presne v tom poradí ako sme ich vytvorili, a pole window, ktoré bude obsahovať odkaz na okno ktoré vytvoríme v metóde init().

Implementáciu init() začneme zaregistrovaním error callbacku do štandartného výstupu a hneď potom sa pokúsime GLFW inicizalizovať zavolaním funckie glfwInit(), ktorá returne boolean v závislosti od toho, či sa inicializácia podarila alebo nie.

Ak sa inicializácia z akéhokoľvek dôvodu nepodarí, vyvoláme výnimku.

Okno aplikácie by sme chceli vytvoriť v strede monitora, a aby sme predišli tomu, že sa zobrazí a potom sa presunie, musíme definovať, že nechceme aby sa zobrazilo hneď ako ho vytvoríme. Na toto slúžia takzvané window hints.

Aktivujeme defaultné hinty a hint GLFW_VISIBLE nastavíme na hodnotu false.

Potom nám už nič nebrániť vytvoriť okno pomocou funkcie glfwCreateWindow(). Prvý argument je šírka, druhý výška okna, tretí je nadpis okna a posledné dva zatiaľ môžeme nechať na 0 a 0. Návratná hodnota je handle vytvoreného okna, ktorý uložíme do premennej window. Budeme ju potrebovať pri volaní ďalších funkcií GLFW.

Skontrolujeme, či sa okno naozaj podarilo vytvoriť, ak nie, vyhodíme výnimku.

Z výšky a šírky okna spravíme konštanty, aby sme ich mohli ďalej používať.

A teraz ešte musíme vypočítať jeho polohu tak, aby bolo v strede monitora. Pomocou funkcií glfwGetVideoMode() a glfwGetPrimaryMonitor() zistíme rozlíšenie monitora, ktoré uložíme do premennej vidMode, a pomocou funkcie glfwSetWindowPos() nastavíme pozíciu vytvoreného okna.

Ešte musíme definovať callback, ktorý sa zavolá, keď užívateľ stlačí Escape, aby sme mohli zatvoriť aplikáciu. Funckia glfwSetKeyCallback() nám práve toto umožnuje. Vo vnútri callbacku skontrolujeme, či je stlačené tlačidlo naozaj Escape. Ak áno, tak zavoláme funkciu glfwSetWindowShouldClose() a tým povieme, že chceme aby sa toto okno zatvorilo.

Už nám neostáva nič iné, iba vytvoriť OpenGL context, zapnúť vsync a zobraziť okno pomocou funkcie glfwShowWindow();


Poďme teraz vytvoriť game-loop. Na začiatku metódy loop() zavoláme GL.createCapabilities() aby sme inicializovali LWJGL.

Ďalej nastavíme clearColor na bielu farbu.

Vytvoríme while loop, ktorý sa bude opakovať dokiaľ nebude vyžiadané zatvorenie okna (to sme vyžiadali v callbacku užívateľského vstupu). Toto skontrolujeme zavolaním funckie glfwWindowShouldClose(). Vo vnútri loopu, vždy:

  1. vyčístíme framebuffer na farbu, ktorú sme vyššie nastavili zavolaním glClear();
    1. GL_COLOR_BUFFER_BIT - vyčistí farebnú časť framebuferru
    2. GL_DEPTH_BUFFER_BIT - vyčistí depth buffer
  2. vymeníme buffery pomocou glfwSwapBuffers();
  3. spracujeme udalosti vstupu zavolaním glfwPollEvents();

V metóde cleanUp(), ktorá sa zavolá tesne predtým ako sa aplikácia skončí najprv uvoľníme všetky callbacky, ktoré sú na našom okne zaregistrované pomocou glfwFreeCallbacks().

Zničíme inštanciu okna zavolaním glfwDestroyWindow(). A na záver ukončíme GLFW a uvoľníme error callback.

.......

Ná záver už len otestujeme, či sa okno zobrazí a či všetko funguje tak ako má. A tadá, funguje.

Ak máte záujem o celý zdrojový kód, môžete si ho stiahnúť s pridanými komentármi z odkazu, ktorý je v popise.


# 03 - VBO a VBO

Co je to VBO, VAO?

Aby grafická karta vedela, čo má kresliť musíme definovať vrcholy objektu - tie nazývame vertexy. Každý vertex má svoju polohu v trojrozmernom priestore a podla toho grafika vie, kde má daný objekt zobraziť. Okrem toho môžu byť ku vertexu pripojené aj iné dáta ako napríklad farba alebo koordináty textúry. Ak chceme napríklad nakresliť trojuholník, budeme tri potrebovať vertexy. Ak ale budeme kresliť niečo zložitejšie, budeme ich potrebovať omnoho viac.

Vertexy môžeme reprezentovať napríklad ako array floatov, pričom každý prvý float bude X-ová súradnica vertexu, každý druhý float bude Y-ová súradnica a každý tretí Z-ová.

Do grafiky vieme takéto dáta dostať pomocou takzvaných Vertex Buffer Object-ov (skrátene VBO).

Ona ich potom pomocou takzvaných "shaderov" spracuje a výsledkom bude render, ktorý vidíme na obrazovke.

Keďže ale vertexové dáta môžu mať ľubovoľnú štruktúru, musíme grafickej karte povedať, ako je náš vertex buffer štruktúrovaný. Teda ako v ňom nájde pozíciu, farbu a textúrové koordináty vertexov.

To vieme spraviť pomocou metódy glVertexAttribPointer(). Ale aby sme nemuseli túto metódu volať každý krát, použijeme takzvaný Vertex Array Object (skrátene VAO). To je objekt, ktorý predstavuje akýsi mapping medzi Vertex Buffer Objekami a štruktúrov attribútov v shaderoch.

Majme napríklad shader, ktorý prijíma ako atribúty pozíciu vertexu, jeho farbu a textúrové koordináty a takýto buffer objekt. VAO si potom pamätá definícu toho, akým spôsobom sa majú z Vertex Buffer Objectu vytiahnúť jednotlivé atribúty a použiť v shader programoch.

Poďme sa teda pozrieť na implementáciu.


Je dôležité vedieť, že každý OpenGL objekt, ktorý vytvoríme musíme aj explicitne zmazať. Tieto objakty sa nevytvárajú v java heape, takže nám s ich odstránením garbage collector nepomôže. Preto si vytvoríme interface Disposable, ktorý nám povie, že daný Java objekt sa dá a máme ho explicitne zmazať.


Vytvoríme teda classu pre VBO BufferObject. Každý OpenGL objekt má aj svoj identifikátor, ktorý má formu integeru, ten si potrebujeme uložiť lebo ho budeme neskôr potrebovať. V konštruktore toto id inicializujeme tak, že zavoláme funkciu glGenBuffers(), ktorá v OpenGL nový buffer object vytvorí a vráti nám jeho ID.

Hneď implementuje aj metódu dispose(), v ktorej vytvorený buffer object z OpenGL zmažeme zavolaním funkcie glDeleteBuffers(), pričom jediným argumentom bude id buffer objectu, ktorý chceme zmazať.

Aby sme mohli náš buffer object použiť v rendrovaní, musíme ho najprv bind-núť. <<<<co to je bindovanie?>>>> Na to si vytvoríme metódu bind() v ktorej zavoláme glBindBuffer() pričom prvý argument bude konštanta GL_ARRAY_BUFFER (ktorá hovorí, že sa jedná o Vertex Buffer) a druhý bude naše id.

Ešte potrebujeme spôsob akým do buffer objectu nahráme vertex dáta. A na to si vytvoríme metódu upload(), ktorá bude prijímať float[], a integer, ktorý reprezentuje hint, akým spôsobom budeme tento buffer používať. V tele metódy zavoláme funkciu glBufferData() pomocou ktorej nahráme do grafickej karty dáta. Prvý argument bude znova GL_ARRAY_BUFFER (lebo stále pracujeme s vertex bufferom), druhý bude array floatov a tretí hint, ktorý sme dostali ako argument.

Implementáciu buffer objectu pre naše VBO máme nateraz hotovú a môžeme začať implementovať triedu pre VAO.


Postupovať budeme veľmi podobne. Vytvoríme triedu s názvom VAO, pridáme premennú id, v ktorej bude id vytvoreného Vertex Array Objektu. V konštruktore toto id inicializujeme zavolaním glGenVertexArrays().

Znovu musíme implementovať aj metódu dispose(), kde tento-krát zavoláme glDeleteVertexArrays() s id-čkom, ktoré sme získali v konštruktore.

Rovnako ako Buffer Objecty aj Vertex Array Objecty musíme pred použitím bind-núť, takže analogicky vytvoríme metódu bind() kde zavoláme glBindVertexArray() pričom jediný argument bude id tohoto VAO.

Ďalej vytvoríme metódu attribPointer(), ktorá bude definovať formát jednotlivých vertex attribútov s argumentami:

  • (int) location - index atribútu
  • (int) size - veľkosť atribútu v množstve "jednotiek". vec3 znamená 3. môže byť z rozsahu 1-4.
  • (int) type - jeden z GL_FLOAT, GL_DOUBLE, GL_INT, GL_UNSIGNED_BYTE
  • (boolean) normalized - či majú byť dáta normalizované -> nie
  • (int) stride - počet bajtov medzi jednotlivými vertex atribútami (pre vec3, ktoré sú hneď za sebou to bude 3 * Float.BYTES)
  • (int) offset - offest v bajtoch od začiatku vertex dát

Okrem toho vytvoríme metódu enableAttrib() a argumentom location, ktorou povieme, že chceme použiť atribút s indexom location.

Implementáciu VAO máme hotovú a môžeme sa pustiť do implementácie shaderov.


# 04 - Shadery

Po tom ako nahráme vertexové dáta do grafickej karty pomocou VBO ich potrebujeme nejako spracovať a vytvoriť z nich render, ktorý sa zobrazí na obrazovke.

Na toto slúžia špeciálne programy, ktoré bežia na grafickej karte a nazývajú sa "shadery". Píšu sa v jazyku GLSL, ktorý sa nápadne podobá na céčko.

Pre každý jeden vertex spustí grafická karta vertex shader, ktorý zvyčajne premiestni jednotlivé objekty na scénu a postará sa o efekt perspektívy, ale používa sa napríklad aj na animovanie postáv a iné úlohy ako je napr. simulácia fyziky particlov. Výstupom vertex shaderu sú transformované (premiestnené) vertexy.

Tie potom grafická karta spojí do primitívnych tvarov (zvyčajne trojuholníkov) a tie potom zrasterizuje. To znamená, že kaźdý trojuholník rozdelí na konečný počet bodov - ich počet zaisí od rozlíšenia a nazývame ich fragmenty.

Pre každý jeden fragment sa potom spustí fragment shader - ten vypočíta farbu daného fragmentu z textúry a polohy a vzdialenosti svetelných zdrojov.

Každý jeden shader sa musí pred použitím skompilovať a to dosiahneme zavolaním funkcie glCompileShader(). Následne sa skompilované shadery attachnú k tkz. Programu pomocou funkcie glAttachShader() a potom sa zlinkujú zavolním funkcie glLinkProgram().


Poďme si teda celý proces zrhnúť.

Pri načítaní:

  1. vytvoríme si Vertex Buffer Object
  2. nahráme do neho dáta
  3. špecifikujeme ich formát cez Vertex Array Object a attrib. pointery
  4. skompilujeme vertex a fragment shader
  5. linkneme ich do programu

Pri rendrovaní:

  1. Bindneme VAO
  2. Useneme Program
  3. Vyvoláme draw call

Teraz sa môžeme vrhnúť na implementáciu.


Začneme tým, že si vytvoríme triedu pre Shader objekt, ktorá bude samozrejme implementovať interface Disposable. Štandardne bude obsahovať aj integer id, ktorý v konštruktore inicializujeme tento krát volaním glCreateShader(). Iné je však to, že glCreateShader() prijíma aj jeden argument a to typ shaderu, ktorý chceme vytvoriť, tento argument si definujeme ako argument konštruktoru.

Shader objekt zničíme zavolaním glDeleteShaders() v metóde dispose().

Následne si implementujeme metódu source() pre nahranie zdrojového kódu shaderu do grafiky. Zdroják budeme mať vo forme stringu, ktorý iba passneme do funkcie glShaderSource(), kde prvý arugment je id shader objektu a druhý string obsahujúci zdroják shaderu.

Keďže shadery sa musia pred použitím skompilovať, vytvoríme si metódu compile(). V jej tele sa pokúsime shader skompilovať zavolaním glCompileShader(), jediným argumentom je zase len id shader objektu.

Grafická karta nám v prípade, že sa shader nepodarilo skompilovať poskytne informácie o tom, kde je problém formou takzvaného "info logu". Ten získame pomocou funkcie glGetShaderInfoLog(), ktorá nám vráti string a ak nie je prázdny, tak ho vypíšeme do konzoly (Toto nám pomôže počas vývoja).

Následne ešte musíme skontrolovať, či sa shader naozaj úspešne skompiloval (pretože grafická karta môže do info logu zapísať aj warningy, ktoré neznamenajú hneď že kompilácia bola neúspešná) tak, že získame parameter GL_COMPILE_STATUS zavolaním funckie glGetShaderi(). Ak bola kompilácia neúspešná, toto volanie nám vráti false a vtedy vyvoláme výnimku.

Základnú implementáciu triedy Shader máme a môžeme začať implementovať triedu Program, ktorá jednotlivé Shader objekty (napr. vertex shader, fragment shader) spája.


Znovu si implentujeme interface Disposable, vytvoríme inštančnú premennú id, ktorú v konštruktore triedy Program inicializujeme tentokrát zavolaním glCreateProgram(). V metóde dispose() tentokrát použijeme glDeteProgram().

Triede pridáme metódu attach(), ktorá bude prijímať jeden argument a to Shader objekt, ktorý chceme k tomuto Programu attachnúť. Okrem toho, že shader attachneme k programu, si vytvoríme aj List<Shader>, do ktorého shader v tejto metóde pridáme. Následne zavoláme glAttachShader() s idčkom programu a idčkom shaderu. Getter na idčko shaderu ešte nemáme, takže ho vytvoríme teraz.

Podobne ako sme pri Shaderi vytvorili metódu compile() teraz vytvoríme v Programe metódu link() ktorá nám Program s attachnutými shadrami zlinkuje a pripraví na použitie. Prvý príkaz bude glLinkProgram() s idčkom tohoto programu.

Následne získame info log linkovania programu pomocou glGetProgramInfoLog(), skontrolujeme či je string prázdny, ak nie je, tak ho vypíšeme do konzoly.

Musíme ešte skontrolovať, či sa linkovanie naozaj podarilo a to tak, že získame parameter GL_LINK_STATUS volaním glGetProgrami() s idčkom programu a týmto parametrom. Ak nám volanie vráti false, vyvoláme výnimku.

Ak sa dostaneme sem, tak sme Program určite úspešne zlinkovali a teraz môžeme všetky attachnuté Shadre (ktoré získame iterovaním list shaderov, ktorý sme si predtým naplnili) z Programu detachnút a následne ich zničiť zavolaním dispose().

List ešte clearneme, aby sme nezanechali hard referencie. A metóda linkovnaia je kompletná.

Implementujeme už iba metódu use(), ktorú budeme volať vždy keď budeme chcieť tento program použiť na rendrovanie. V jej tele iba zavoláme funkciu glUseProgram() s idečkom tohoto programu.


Teraz už máme pripravené všetky komponenty potrebné k rendrovaniu a môžeme sa do neho pustiť a konečne aj niečo nakresliť.


# 05 - Farebný trojuhloník (atrib. interpolation)

V predchádzajúcich častiach sme si vytvorili všetky potrebné triedy k rendrovaniu. Pozrime sa ešte raz na to, ako budeme postupovať:

Pri načítaní:

  1. vytvoríme si Vertex Buffer Object
  2. nahráme do neho dáta
  3. špecifikujeme ich formát cez Vertex Array Object a attrib. pointery
  4. skompilujeme vertex a fragment shader
  5. linkneme ich do programu

Pri rendrovaní:

  1. Bindneme VAO
  2. Useneme Program
  3. Vyvoláme draw call

V triede Window si vytvoríme novú metódu load(), ktorú zavoláme jeden krát z metódy loop() ešte pred tým ako vstúpime do skutočného loopu, a v nej budeme inicializovať všetky premenné.


#06 - Textúrovaný trojuholník


# 07 - MVP, Matrices, Uniforms


# 08 - FPS Kamera


# 09 - OBJ Importer /

results matching ""

    No results matching ""