Úloha 113: Graf funkce v OpenGL

Ú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.

Graph

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áklad

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:

  • RegenerateGraph(string par, string expr) – volá se vždy po stisku tlačítka "Regenerate" nebo po změnách polí "Param" nebo "Expr". V prvním textovém parametru dostává pole parametrů (např. s definovaným definičním oborem, jemností aproximace, ..) a v druhém uživatelem zadanou funkci (lze používat nezávislé proměnné x,y nebo x,z)
  • RenderScene(IDynamicCamera cam, RenderingStyle style, ref long primitiveCounter) – volá se vždy při nutnosti překreslení scény (funkce) do OpenGL widgetu. Doplňte dle libosti pro rozšířené zobrazení (souřadné osy, tečny, tečné roviny, ..)
  • Intersect() – volá se při ukazování myší na model grafu. V parametrech jsou počáteční bod paprsku ve 3D a směrový vektor paprsku. Funkce by měla spočítat nejbližší průsečík paprsku s grafem (v parametrickém tvaru), volitelně může např. nastavit "Status" na formuláři..
  • Mouse*() – obsluha událostí od myši
  • KeyHandle() – obsluha klávesnice (framework již konzumuje klávesy 'f', 'o' a 'r')

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.

Shadery

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.

Další podrobnosti kódu souvisejícího s OpenGL a GLSL

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í!):

  • texturové souřadnice [s, t] – nepovinné
  • barva vrcholu [R, G, B] – nepovinná, já ji zde používám
  • normálový vektor [Nx, Ny, Nz] – nepovinný
  • souřadnice vrcholu [x, y, z] – povinná!
Podívejte se do prvního unsafe bloku, pomocí ukazatele float* ptr se tam postupně naplní barvy a souřadnice čtyř vrcholů s indexy 0 až 3.

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

  • zapamatovat si rozsahy dat v bufferech, například: na kterém indexu začínají data souřadnicových os, jak jsou ta rozšiřující data v bufferu organizována...
  • adekvátně upravit kreslicí kód ve funkci Graph.RenderScene() – hledejte sekci s vlastním kreslicím příkazem GL.DrawElements().
Pokud se do nějakého rozšíření pustíte, pravděpodobně pro vás nebude těžké doplnit těch nekolik analogických kousků kódu.

 

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:

  • inicializace systému OpenGL a OpenTK, včetně kontrolky openTK.GLControl, která v našem formuláři realizuje OpenGL výstup.
  • inicializace potřebných bufferů, textur a shaderů, viz výše.
  • naplnění VBO bufferů daty pro 3D graf funkce, viz Graph.RegenerateGraph().
  • interaktivní otáčení grafu zajišťuje pomocí myši class Trackball (viz). Tento objekt poskytuje transformační matice pro vykreslování, tzv. "model-view matrix" (první dvě matice ze slavného slajdu) i "projection matrix" (třetí matice z téhož slajdu).

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.

Graph.InitParams()

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:

  • param – počáteční nastavení pole parametrů (minimálně nastavení definičního oboru grafu domain)
  • tooltip – čárkami oddělený seznam parametrů akceptovaných v Param:. U více řádek lze použít oddělovač '\r'.
  • expr – počáteční symbolický zápis aritmetického výrazu (viz NCalc)
  • name – vaše jméno
  • trackballButton – které tlačítko myši má používat Trackball?

Prohlížení grafu

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í.

Bonusy

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..

Termín

Odevzdat do: 28. 2. 2022

Body

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)

Projekt

Visual Studio projekt: 113graph

Zdrojový soubor

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)