Ne každý programátor je spřátelen s modelovacím software, jako je 3D Studio MAX nebo Blender. Proto si mnozí programátoři udělají ty svoje modely často raději sami. Nehledě na to, že mnoho modelů potřebujeme v aplikaci generovat za běhu.
Pro modely složené z trojúhelníků byl v grafice zaveden pojem mesh, který budeme používat i my. Původně tento díl tutoriálu vznikl po dlouhém hledání chyby v jednom inventorovském programu, aby ulehčil od podobných problémů všem ostatním. Vše si představíme na dvou modelech ve třech příkladech:
4.1 – Jednoduchá raketa vytvořená „v kódu“ | Použití pouze SoCoordinate3 a SoIndexedTriangleStripSet. |
4.2 – Jednoduchá raketa vytvořená „ze stringu“ | Jiná forma minulého příkladu. |
4.3 – Raketa se vším všudy | Do modelu přidáme normály, materiály a textury. |
U příkladů nám nepůjde ani tak o vizuální kvalitu jako spíše o jednoduchost a snadné pochopení.
Příklad 4.1: Jednoduchá raketa vytvořená „v kódu“
V tomto příkladě použijeme pouze SoCoordinate3 a SoIndexedTriangleStripSet. Více informací o nich je například u skyboxu v předchozích dílech tutoriálu. My se zde soustředíme pouze na konstrukci modelu.
Dalo by se říci, že máme tři možnosti, jak model vytvořit:
- načtení ze souboru (příklad 2.1 ve třetím dílu tutoriálu)
- „ruční“ konstrukce
- konstrukce „ze stringu“
Nejprve si ukážeme ruční konstrukci:
Vytvoříme SoCoordinate3 se vším všudy:
static float vertices[][3] = {
-2, 0,
4,
2, 0,
4,
-2, 1,
2,
2, 1,
2,
2, -1,
2,
-2, -1,
2,
-2, 1,
-3,
2, 1,
-3,
2, -1,
-3,
-2, -1,
-3,
};
static int32_t indices[] = {
2, 0,
3, 1, 4, 0, 5, 2, -1, //predek
6, 2,
7, 3, 8, 4, 9, 5, 6, 2, -1,//trup
6, 7,
9, 8, -1,
//zadek
};
[...]
SoCoordinate3 *coords = new SoCoordinate3;
coords->point.setValues(0, 30, vertices);
root->addChild(coords);
SoIndexedTriangleStripSet *stripSet =
new SoIndexedTriangleStripSet;
stripSet->coordIndex.setValues(
0, 25, indices);
root->addChild(stripSet);
A máme celou scénu postavenou. O rendrování a podobně se nám už postará Open Inventor:
Zdroják tohoto příkladu je ke stažení zde. Po spuštění by se měl objevit následující obrázek:
Příklad 4.2: Jednoduchá raketa vytvořená „ze stringu“
Druhou možností je konstrukce „ze stringu“, což je obdoba načtení ze souboru, ale místo souboru použijeme statický string v paměti. To je velmi elegantní řešení pro malé grafy. (Upozorňuji maniaky na efektivitu kódu, mezi které se rovněž počítám, že není velkého rozdílu mezi dobou provedení předchozího a tohoto příkladu, a svůj čas je lepší trávit u optimalizace jiných částí kódu než u načítání několika uzlů.)
Zde je odpovídající kód, který ve výsledku dělá přesně totéž co minulý příklad:
static char inlineModel[] = "#Inventor V2.1 ascii\n" "" "Separator {" " Coordinate3 {" " point [" " -2 0 4," " 2 0 4," "" " -2 1 2," " 2 1 2," " 2 -1 2," " -2 -1 2," "" " -2 1 -3," " 2 1 -3," " 2 -1 -3," " -2 -1 -3," " ]" " }" " IndexedTriangleStripSet {" " coordIndex [" " 2, 0, 3, 1, 4, 0, 5, 2, -1," " 6, 2, 7, 3, 8, 4, 9, 5, 6, 2, -1," " 6, 7, 9, 8, -1," " ]" " }" "}"; [...]
// Precti scenu ze stringu
SoInput in;
in.setBuffer(inlineModel,
strlen(inlineModel));
SoSeparator *root = SoDB::readAll(&in);
if (root == NULL) exit(-1);
root->ref();
Kód je ke stažení zde.
Jak vidíme, tento kód může být často čitelnější, a je na volbě programátora, který přístup zvolí. Tento přístup se hodí spíše pro malé modely se složitou grafovou strukturou, kdežto předchozí pro modely s mnoha vertexy a velikými statickými poli.
„File formát“ použitý ve stringu je inventorovský a poměrně dobře čitelný. S ním je kompatibilní i VRML1, které je podmnožinou inventorovského formátu. Pro experty jsou zde linky na web3d konsorcium a jejich stránky věnované VRML1 uzlům.
Poznámka ke špatnému osvětlení modelu: Je způsobeno tím, že jsme nespecifikovali normály v našem modelu. Inventor umí normály dopočítat, ale generovací algoritmus, ač nemá chybu, to neudělá vždy podle našich představ. Více informací k tomuto tématu je možno najít v dokumentaci k SoShapeHints::creaseAngle.
Příklad 4.3: Raketa se vším všudy
Tento příklad nás má uvést do plného kontextu rendrování modelů – co vše je možno nastavovat a jak to nastavit. Je třeba upozornit na dva problémy, se kterými se člověk může při této příležitosti potkat a které se jako chyby v kódu velmi špatně hledají. Jsou to:
- špatné indexy odkazující se obyčejně za pole hodnot, např. SoIndexedTriangleStripSet::coordIndex odkazující se až za pole vertexů v SoCoordinate3::point
- špatně nastavený binding, více k tomuto tématu vzápětí
A ještě jedna otázka se občas objevuje: neosvětlený nebo špatně osvětlený model – více informací na konci příkladu.
Podíváme-li se na problematiku rendrování trojúhelníků v OpenGL obecně, tak nám stačí následující třídy:
Tedy souřadnice vertexů, normály, materiály, textura a texturovací souřadnice.
Samotné rendrování obstarávají nejčastěji tyto třídy:
Pro odborníky bych doplnil, že kompletní seznam tříd najdeme v Coin dokumentaci u třídy SoShape, která je rodičovskou třídou pro všechny rendrovací třídy.
Mimo tyto třídy jsou zde ale ještě „bindovací“ třídy, které nám v případě normál říkají, budeme-li je specifikovat pro každý vertex – tedy tři na trojúhelník – nebo jen jedinou normálu pro celý trojúhelník. Normála pro každý vertex znamená, že je normála mezi vrcholy interpolována, s čímž lze dosáhnou například velmi hladkých povrchů bez viditelných hran.
Má-li tedy model 120 trojúhelníků a pro ně 120 normál a nějakým nedopatřením bychom se přepsali a nastavili binding normál na PER_VERTEX, rendrovací funkce by očekávala, že normál je 360 – pro každý trojúhelník tři. To by mělo za důsledek přistup do paměti za alokovanou paměť pro normály a vše končí buď pádem programu, nebo jen špatně vyrendrovaným modelem. Totéž se stane, pokud zaměníme hodnoty PER_VERTEX a PER_VERTEX_INDEXED, což je ještě častější chyba. Vývojáři Coinu se prozatím rozhodli tyto špatné indexy nekontrolovat, protože se jedná o kritické místo, kudy často teče pár milionů vertexů každou vteřinou.
Zde jsou třídy nastavující binding:
Otázku bindingu probíral příklad 3.5 a 3.6 z minulého dílu našeho tutoriálu. Tam jsou uvedeny detailnější informace, popřípadě ještě v Coin dokumentaci k SoMaterialBinding::Binding. My se zde spokojíme s nejčastěji používanými nastaveními.
Jak se dá asi čekat, pro SoTriangleStripSet a SoIndexedTriangleStripSet budeme většinou používat různé bindingy pro jejich normály, materiály a texturovací souřednice. Nejčastější použití jsou tato:
SoTriangle- StripSet |
SoIndexed- TriangleStripSet |
|
bindování vertexů |
jak jdou za sebou | indexovaně |
SoNormal- Binding |
PER_VERTEX, PER_FACE |
PER_VERTEX_INDEXED, PER_FACE_INDEXED |
SoMaterial- Binding |
PER_VERTEX, PER_FACE |
PER_VERTEX_INDEXED, PER_FACE_INDEXED |
SoTexture- Coordinate- Binding |
PER_VERTEX | PER_VERTEX_INDEXED |
Tato tabulka nás samozřejmě neomezuje v tom, míchat bindingy, jak se nám zachce, jen pak musíme ve správném formátu naplnit nódy SoNormal, SoMaterial a SoTextureCoordinate2. Chyby v neodpovídajícím si bindingu, popřípadě chyby v plnění nódů SoNormal, SoMaterial a SoTextureCoordinate2 vedou ke špatnému zobrazení modelu a k pádům aplikace. A protože jsou to občas těžko hledatelné chyby, je vhodné tomu věnovat větší pozornost.
Teď už je čas podívat se na náš vylepšený model. Přibyly normály, materiály a jedna textura vytvářející kabinu. Model snad již nemá cenu komentovat a je uložen v souboru, k jehož načtení použijeme SoDB::readAll() funkci.
Zdroják je k dispozici zde a screenshot následuje:
Další detaily jsou shrnuty v bodech:
-
-1 v indexech
–1 se v poli indexů SoIndexedTriangleStripSet::normalIndex, materialIndex a textureCoordIndex uvádí pouze v režimu PER_VERTEX_INDEXED.
-
defaultní bindování
normály: PER_VERTEX_INDEXED
materiály: OVERALL
textureCoordinates: PER_VERTEX_INDEXED -
nespecifinované indexy
V SoIndexedTriangleStripSet můžeme nechat normalIndex, materialIndex nebo textureCoordIndex prázdné. Pokud tak učiníme a je pro danou položku specifikován binding PER_VERTEX_INDEXED, použijí se indexy z pole coordIndex. Není tedy potřeba plýtvat pamětí tím, že budeme nastavovat stejné indexy pro souřadnice, normály a texturovací souřadnice, což je častá situace.
Občas se stává, že zobrazený model je černý a jde vidět jen, když ho rendrujeme jako wireframe. Nebo, že když vypneme headlight, tedy světlo u kamery, a umístíme do scény jiné světlo, model je stále černý. Zde je několik tipů na možný problém:
- Špatně nastavený SoMaterial a jeho difuzní složka, nebo indexy na materiály, nebo barva a intenzita světla.
- Světlo svítí na model z opačné strany – např. u krajiny světlo svítí zespoda. Zkuste obrátit jeho směr.
- Normály jsou opačně orientované.
Pro experty jsem vyhledal ještě dodatečné informace:
back face culling | odstraňování neviditelných polygonů, nastavuje se skrze nód SoShapeHints |
two side lighting | oboustranné osvětlování polygonů, viz nód SoShapeHints |
různé materiály z obou stran polygonu | více k tomuto tématu Coin dokumentace k SoIndexedShapeSet |
generování normál | Pokud jsme nespecifikovali normály a Inventor je dogenerovává sám, je generovací algoritmus řízen hodnotou SoShapeHints::creaseAngle. Pokud dva sousedící polygony svírají úhel menší jak tato hodnota, považuje se hrana za neviditelnou a normály jsou přes ní interpolovány. Naopak hrany s větším úhlem jsou považovány za ty, které mají být vidět. |
Tímto jsme vyčerpali dnešní téma. Zbývá link na stažení celého článku.
A to je pro dnešek vše.