Úkolem je implementovat co nejhezčí, nejpraktičtější a nejpohodlnější nástroj na zkoumání průběhu reálných funkcí dvou proměnných. Měli byste zobrazit graf funkce jako plochu y = f(x,z) a umožnit uživateli si ho prohlížet ze všech stran, hrát si s definičním oborem, atd.
Vyhodnocování výrazu: aby mohl uživatel zadávat funkci dvou proměnných, používáme knihovnu NCalc, která je schopná opakovaně vyhodnocovat výraz, dosazovat za proměnné číselné hodnoty, apod. V naší aplikaci budeme označovat dvě volné proměnné ve výrazu x a z, když v 3D prostoru budeme zobrazovat funkci y = y(x,z). Příklad použití knihovny NCalc k vyhodnocení výrazu najdete ve funkci RegenerateGraph() v souboru Graph.cs.
Základem poslouží projekt 113graph z repository
grcis
(GIT).
Je připravena aplikace se základem pro OpenGL vykreslování grafu funkce, čtením
funkce zadávané uživatelem a možností interaktivního ovládání zobrazení.
Pro implementaci vašeho úkolu budete modifikovat předem založené
funkce ve zdrojovém souboru Graph.cs:
Doporučuji dodržet koncepci z pilotní implementace: funkce RegenerateGraph() (jen v případě změny) realokuje nebo jenom naplní příslušné VBO buffery, jeden s atributy vrcholů, druhý obsahuje index-buffer. Funkce RenderScene() pak již žádné funkční hodnoty nepočítá, jen z připravených bufferů vykreslí obrázek.
Projekt používá moderní přístup k programování GPU: data jsou umístěna v bufferech na straně grafické karty (tzv. VBO) a aplikace definuje dva shadery v jazyce GLSL umístěné v externích textových souborech: shaderVertex.glsl a shaderFragment.glsl. Program ve fázi inicializace zdrojové GLSL soubory na disku najde, přeloží, slinkuje a pokračuje dál jedině v případě, že nedošlo k žádné chybě. Moderní GPU by neměly mít problémy s během shaderů (verze GLSL 1.30), pokud byste přesto měli nějaké potíže, podívejte se do logu log.txt v aktuálním adresáři. Kdybyste případné problémy se spouštěním aplikace neuměli sami vyřešit, obraťte se na cvičícího.
V následujících odstavcích je věnována pozornost jen funkcím, které byste mohli potřebovat měnit, případně které doporučujeme k podrobnějšímu studiu pro lepší pochopení principů OpenGL a programování GPU.
Graph.InitOpenGL()
Jednorázová počáteční inicializace systému OpenGL ("Cold start"). Nastavuje základní parametry systému, vytváří obecně používaná data (VBO buffery), inicializuje shadery (viz InitShaderRepository() a SetupShaders()), vytváří procedurální data pro barevnou texturu (GenerateTexture()).
Graph.GenerateTexture()
Textura se v naší aplikaci pro jednoduchost nenačítá z předem připraveného rastrového formátu (to je jinak obvyklý postup v simulátorech a video-hrách), ale vytváříme ji v paměti jako nekomprimované 2D pole pixelů, které následně uploadujeme do GPU.
V této funkci vidíte vytvoření pole dat typu Vector3 – tento typ se někdy používá v realtime grafice pro reprezentaci barvy, jednotlivé barevné složky se ukládají za sebe [R, G, B] do položek [X, Y, Z].
Systémové funkce: GL.GenTexture() deklaruje texturu, její "jméno"
nebo "handle" typu int. Textura zatím není vůbec inicializovaná,
není ani určeno, o jaký formát textury se bude jednat.
GL.TexImage2D() – teprve tato funkce určí formát a velikost textury
a naplní příslušný GPU buffer daty (rastrový obrázek).
GL.TexParameter() – nastavení některých paramettrů, které se při
aplikaci/mapování textury budou používat, zde se jedná o opakovací režim a typ
interpolace dat.
Graph.InitShaderRepository()
Inicializace interního systému shaderů knihovny GrCis (viz OpenglSupport.cs). Tzv. "Shader program" se skládá z jednotlivých "shaders", které jsou propojeny (slinkovány) dohromady. Zde vidíte konfiguraci programu "default" složeného z vertex-shaderu shaderVertex.glsl a fragment-shaderu shaderFragment.glsl. Pokud byste náhodou chtěli experimentovat s vlastními shadery, okopírujte tyto dva zdrojáky pod jinými jmény a v této funkci přidejte svůj nový shader-program. Ten byste pak museli nastavovat jako activeProgram.
Graph.SetupShaders()
Vlastní vyhledání GLSL zdrojových souborů na disku, jejich překlad a slinkování do shader-programů. Nemusíte zde nic měnit, leda že byste potřebovali pracovat s více shader-programy. Funkce vrací false, pokud se nepodařilo v pořádku shadery najít, přeložit nebo slinkovat, v takovém případě aplikace končí, vy si můžete přečíst chybová hlášení v souboru log.txt v aktuálním adresáři.
Graph.RegenerateGraph()
Funkce volaná v aplikaci pokaždé, kdy je potřeba změnit 3D model grafu funkce. Jako vstup dostanete přesně obsah dvou textopvých polí z formuláře: Param: a Expr:. První z nich můžete klasicky využít ke čtení libovolných – vámi definovaných – parametrů, druhý string obsahuje zápis aritmetického výrazu v syntaxi knihovny NCalc. Vaším úkolem je opakovaně volat vyhodnocování aritmetického výrazu, a tak získat 3D souřadnice vrcholů, které budou tvořit trojúhelníkovou síť ("Triangle mesh") grafu funkce.
Pilotní implementace obsahuje pouze kód demonstrující použití class Expression a princip plnění dat VBO bufferů pro GPU – vertex buffer ("zelené pole" ze slajdů) má identifikátor VBOid[0], index buffer ("žluté pole") má identifikátor VBOid[1]. Oba dva VBO buffery již byly založeny, v této funkci je potřaba: zkontrolovat jejich aktuální velikost a případně je přealokovat, a dále je naplnit daty vrcholů a indexy trojúhelníků.
Plnění dat je demonstrováno v sekci "Data for VBO". Používá se
zde přístup do GPU paměti pomocí tzv. mapování paměti: do adresového
prostoru CPU se virtuálně namapuje přímo část paměti GPU (příslušný
buffer) a vy s VBO bufferem můžete pracovat, jako kdyby to bylo pole
v prostředí .NET. Funkce GL.MapBuffer() toto mapování zajistí,
funkcí GL.UnmapBuffer() jej zase musíte na konci plnění dat zrušit.
Poznámka: technicky to v C# děláme v "unsafe" bloku
pomocí ukazatele (pointeru) typu float* (vertex buffer –
všechny atributy vrcholu jsou pro jednoduchost typu float) nebo
uint* (index buffer – máme indexy typu uint).
Formát vrcholu (někdy se nazývá "Vertex layout") je částečně na nás, programátorech, můžeme systému definovat, jaké atributy vrchol obsahuje, v jakém jsou pořadí a jakých jsou typů. Tady si musíme dát pozor – při plnění dat do vertex bufferu (funkce Graph.RegenerateGraph()) musí být v souladu s definicí formátu vrcholu, který se definuje až při vykreslování (funkce Graph.RenderScene() – hledejte tam volání GL.VertexAttribPointer()).
Zpět k naší pilotní implementaci Graph.RegenerateGraph(): my máme ve vrcholu položky (přesně v tomto pořadí!):
Dále se plní index buffer, to už je jednodušší, protože ten buffer je jen prosté jednorozměrné pole uint[]. Opět se plní v unsafe bloku po namapování VBO bufferu a používá se k tomu ukazatel uint* ptr.
Na závěr generování dat grafu funkce sí nezapomeňte nekde zapamatovat, kolik máte celkem trojúhelníků (nebo spíš jejich indexů v IB – zde se k tomu používá proměnná vertices).Pokud byste pracovali na nějaké složitější reprezentaci grafu (například obohacené o souřadnicové osy nebo nějaká měřítka), doporučujeme nechat vlastní graf funkce na začátku VB a IB (tak, jako je to v pilotu) a další data (jejich vrcholy v VB i jejich indexy v IB) dát na konec. V takovém případě byste samozřejmě museli
Graph.RenderScene()
Vlastní vykreslovací funkce – volá se v každém snímku realtimer aplikace, tj. například 60x za sekundu (60fps). Předpokládá se, že již jsou všechna data ke kreslení připravena (Graph.RegenerateGraph()), stejně tak GPU pipeline (ostatní funkce zde již zmíněné). Ještě jednou si zopakujeme, co musí být předem zajištěno:
Velkou částí této funkce je zadávaání dat pro shadery (vertex-shader i fragment-shader), musíme jim poslat transformační matice, parametry pro stínování, případnou texturu, apod. Zde též definujeme konfiguraci atributů v jednotlivých vrcholech ("Vertex layout"). Až úplně na konci najdete vlastní vykreslovací příkaz: GL.DrawPrimitives(). Zde byste případně museli přidávat další kreslicí příkazy, pokud byste chtěli graf funkce o něco obohatit.
Poznámka ke stínování: systém standardně obsahuje podporu pro stínování,
k jeho využití budete potřebovat zadat ve vrcholech jejich normálové vektory
(doporučujeme spočítat povrchové tečné vrcholy plochy grafu pomocí diferencí
nahrazujících parciální derivace ... využití aproximace
"dF(x,y)/dx ~ (F(x+deltax,y) - F(x,y))/deltax").
Stínování lze v aplikaci zapnout pomocí přepínačů "Lighting" a
"Phong".
V poslední části funkce Graph.RenderScene() se vykresluje jednoduchá krychle pomocí archaického systému posílání dat do GPU (viz funkce glVertex()). Nechal jsem to zde jen pro zajímavost, žádné buffery, jen posílání dat vrcholů postupně jeden po druhém, voláním OpenGL funkcí.
Graph.Intersect()
Jen na ukázku – kód, který počítá nejbližší průsečík paprsku s 3D scénou. Paprsek se reprezentuje jako polopřímka s počátečním bodem p0 a směrovým vektorem p1. V aplikaci se tato funkce používá k demonstraci pohledových parametrů kamery: zapněte režim "Debug" a pak stiskněte pravé tlačítko myši. Do 3D světa se přidá paprsek opatřen průsečíkem, pokud byl nalezen. Ve stejném režimu funguje klávesa "F" jako Frustum. 3D doplňky ("dekorace") si pak můžete prohlížet otáčením a škálováním pomocí myši (viz class Trackball).
class RenderingStyle (RenderScene.cs)
Datový objekt obsahující mnoho parametrů definujících režim zobrazení, viz komentáře ve zdrojovém souboru. instance této třídy je předávána mj. při volání funkce Graph.RenderScene(), která podle toho určuje parametry zobrazení.
Form1.Application_Idle() (RenderScene.cs)
Funkce zajišťující dostatečně časté (často pravidelné) překreslvání 3D scény v kontrolce OpenTK.GLControl na formuláři. Aplikace ve Windows nemůže v sobě mít tzv. aplikační smyčku ("event-loop"), tak je potřeba nějak jinak zajistit časté vyvolávání překreslení.
Application_Idle() je funkce, kterou systém volá vždy, když "aplikace nemá co dělat". My uvnitř této funkce překreslujeme 3D obsah ve formuláři a počítáme některé další věci, třeba FPS nebo ošetřujeme, když uživatel ukáže do 3D scény pravým tlačítkem myši (viz výše, režím "Debug").
Za zmínku slouží dvě možné varianty implementace překreslení: buď se jenom
vyvolá tzv. "invalidace" kontrolky glControl1 (a událost
Paint – viz funkce Form1.glControl1_Paint() –
již zajistí překreslení) nebo se přímo v "idle funkci" volá
Form1.Render().
Naše implementace používá implicitně ten první způsob, protože je
pro systém řízený událostmi přirozenější.
Form1.Render()
Kompletní překreslení OpenGL kontrolky glControl1 ve formuláři. Vyvolává se jako reakce na událost Windows Paint. Najdete zde aktualizaci parametrů renderingu z GUI formuláře, inicializaci obrazové paměti ("frame-buffer") a hloubkového bufferu ("depth-buffer"), nastavení kamery z objektu Trackball a nakonec vlastní kreslení ve dvou fázích: kreslení grafu funkce Graph.RenderScene() a případné ladicí doplňky (funkce Form1.Decorate() – viz režim "Debug").
Na konci funkce najdete příkaz glControl1.SwapBuffers();, tj. pokyn zobrazovacímu řetězci ("swap-chain"), že je aktuální snímek hotový a je možné jej prezentovat na obrazovku. Při nejobvyklejším nastavení překreslovacího systému se při prezentaci čeká, až GPU dokreslí na obrazovce snímek a hardwarová výměna (swap) se realizují mezi dvěma anímky. Toto čekání musí být samozřejmě pasivní, aby nezatěžovalo vlákno CPU.
Form1.Decorate()
Dokreslení doplňujících ladicích prvků: paprsek od pozorovatele, zorný jehlan (frustum), apod. Jak je vidět, používá se archaická metoda přenosu dat do GPU.
Form1.UpdateRenderingParams()
Aktualizace parametrů renderingu z textového pole "Param:" formuláře, detaily viz zdrojový kód.
Funkce slouží k inicializaci prvků formuláře a nastavení některých dalších parametrů projektu. Volá se jednou při spuštění aplikace:
V projektu se používá ovladač Trackball, vy si můžete zvolit, které tlačítko myši bude používat Trackball, ta ostatní budete mít k dispozici sami pro případná rozšíření.
Jakákoli zajímavější zobrazení (průhlednost, osvětlení, souřadné osy), analytické funkce v bodě, na který uživatel ukáže (tečné vektory, křivost, tečná rovina), interaktivní změna definičního oboru, načtení definice funkce z textového souboru (config.txt, s možností definice pomocných proměnných?..), cokoli dalšího, co povede k elegantnější a pohodlnější práci s grafem..
Odevzdat do: 28. 2. 2022
Základ: 8 bodů (kreslení symbolicky zadaného grafu funkce s volitelnou jemností
aproximace a interaktivním ovládáním systémem Trackball),
Bonus až 12 bodů za rozšířené funkce (viz výše)
Visual Studio projekt: 113graph
Modifikujte a odevzdejte pouze soubor: Graph.cs
Do funkce InitParams() napište své jméno!
Copyright (C) 2017-2022 J.Pelikán, last change: 2022-02-02 01:03:41 +0100 (Wed, 02 Feb 2022)