V minulém díle tohoto seriálu jsme se seznámili s XNA frameworkem a ukázali si, jak založit nový projekt. Předvedli jsme si použití Content Pipeline a vysvětlili jsme si strukturu třídy Game a také to, k čemu jsou jednotlivé její metody. Dnes se naučíme pracovat s 3D modely, které nám dodá grafik, a hlavně si ukážeme, jak v XNA takový model načíst a vykreslit na obrazovku.
Kde sehnat 3D model?
Pokud se podíváte na stránky XNA Creators Club (http://creators.xna.com), což je oficiální web technologie XNA, najdete tam v jedné sekci odkaz na Turbo Squid. Na tomto webu najdete velké množství 3D modelů pro použití v XNA, některé jsou zdarma, jiné si musíte koupit. Je zde poměrně velký výběr, abyste viděli hned modely, které jsou zdarma, doporučuji použít řazení podle ceny. Vybírejte pouze modely, které jsou k dispozici ve formátu fbx. XNA sice umí otevřít i formát x, ale to už zase nepodporuje naše šablona pro Content Pipeline ve VB.NET. V některém z příštích dílů to do této šablony přidám, nyní si musíme vystačit s formátem fbx.
Pokud máte svého grafika, je třeba, aby vám model vyexportoval právě ve formátu fbx. Prakticky každý software pro vytváření modelů dnes formát fbx podporuje, anebo existuje nějaký plugin, který tuto podporu přidá. Je třeba také vzít v úvahu, že formát fbx nemusí podporovat všechny funkce, které umí daný grafický software, takže některé efekty, které grafik do modelu přidá, se pak ve hře nemusí zobrazit. Záleží, který software grafik používá.
Starter Kits
Na oficiálním webu XNA Creators Club, kde mimo jiné najdete spoustu článků, si můžete také stáhnout ukázkové příklady (Starter Kity). Jsou to již hotové hry naprogramované v XNA, u kterých máte dostupné zdrojové kódy, takže se můžete podívat, jak co funguje. Doporučuji například Racing Game Starter Kit, je tam k vidění téměř vše, co by se vám u her mohlo hodit. Neděste se toho čísla 33 fps (snímků za sekundu), je to snímané na notebooku na maximální rozlišení. Když jsem v nastavení vypnul postprocessing obrazu (který způsobuje to rozmazání; je to tam schválně, ve velké rychlosti to vypadá dobře), měl jsem asi 120 fps. XNA s výkonem opravdu nemá problémy.
Stáhneme si model a jdeme programovat
Abychom měli všichni stejný model, který vykreslíme, použijeme tohoto jednorožce. Je k dispozici zdarma (pro stažení se jen musíte zaregistrovat) a ve formátu fbx. Stažený archiv rozbalte, abyste mohli pak do Visual Studia model přidat.
Vytvořte si nový projekt Windows Game podle předchozího dílu a do složky Content přidejte rozbalený soubor unicorn.fbx. Výhodou formátu fbx je mimo jiné i to, že všechny textury se dají zabalit spolu s modelem, takže nemáte jeden model a k tomu dvacet obrázků s texturami, jako třeba u formátu x.
Pro reprezentaci modelu se používá datový typ Model, do deklarací někam nahoru přidejte (ne dovnitř metody!) tento řádek:
Private unicorn As Model ' model jednorožce
Dále již ze včerejška víme, že model musíme do této proměnné načíst. Dovnitř metody LoadContent tedy vložte tento kód:
unicorn = Content.Load(Of Model)("Content\unicorn")
Spolu s modelem se takto již načtou všechny textury, takže budeme mít připraveno naprosto vše. Než se ale vrhneme na samotné vykreslení, musím nastínit alespoň trochu teorie týkající se transformací. Nehodlám zde prezentovat přesné definice, jednak to neumím a druhak bych tím asi napíchal více škody než užitku. Zkusím jen prakticky vysvětlit, co náš kód dělá.
Matice a vektory
Při vykreslování ve 2D prostoru je naše situace jednoduchá. Pozici čehokoliv určíme jedním dvousložkovým vektorem (Vector2), což je vlastně dvojice souřadnic.
Základním bodem ve 2D prostoru je počátek, který má souřadnice [0, 0]. V praxi o bývá horní levý roh monitoru. Pokud chceme v XNA vyjádřit pozici nějakého bodu ve 2D prostoru, např. pozici bodu [3, 2], reprezentujeme ji pomocí dvousložkového vektoru. Vektor je červená šipka na obrázku - udává směr a vzdálenost. Souřadnice vektoru (3, 2) jsou pak rozdílem souřadnic koncového a počátečního bodu. Koncový bod je [3, 2], počáteční je [0, 0], a tím pádem tedy souřadnice vektoru jsou (3 - 0, 2 - 0), což je rovno (3, 2). Všimněte si, že souřadnice vektoru se píší do kulatých závorek, kdežto souřadnice bodů píšeme do závorek hranatých.
Pokud jsme tedy v minulém dílu chtěli vykreslit míček na pozici [50, 43], pak jsme metodě SpriteBatch.Draw předali jako pozici obrázku hodnotu New Vector2(50, 43). Předali jsme jí vlastně vektor vedoucí z počátku do bodu, který chceme zobrazit.
3D
Problém 3D prostoru je ten, že se jej snažíme vykreslovat na monitor, který je dvojrozměrný. Nikdo asi nečeká, že se nám na monitor vejde celý 3D prostor, ale jen jeho malá část. To nešlo koneckonců ani ve 2D, monitor má také jen konečné rozměry. Abychom mohli vykreslit něco ve 3D prostoru a bylo to pro nás použitelné, musíme znát alespoň 2 údaje:
- místo, kde stojíme, místo, kam se díváme, a natočení kamery
- způsob promítání prostoru na monitor
První bod se dá shrnout jako vlastnosti a pozice kamery a druhému bodu obecně říkáme projekce. Každý z těchto bodů můžeme jednoznačně popsat tzv. maticí (tabulka několika čísel). Pro pokročilejší práci se hodí vědět, jak taková matice vypadá přesně, ale zatím to potřebovat nebudeme.
Matice výhledu (view matrix)
XNA má samozřejmě nástroje pro sestavení takové matice, takže my je můžeme jen použít. Matice výhledu nám říká, odkud se díváme, kam se díváme, a musíme také říci, kde je vrchní strana kamery (kdybychom chtěli programovat hru s akrobatickým létáním, potřebovali bychom občas mít kameru "vzhůru nohama"). Do deklarací nahoru tedy přidejte tento řádek:
Private view As Matrix = Matrix.CreateLookAt(New Vector3(0, 0.5, 2), Vector3.Zero, Vector3.Up)
Nadeklarovali jsme proměnnou view typu Matrix (matice) a přiřadili jsme jí výsledek volání Matrix.CreateLookAt. Tato statická metoda vyžaduje 3 parametry - prvním je pozice, kde se nacházíme (souřadnice ve 3D prostoru, takže musíme použít Vector3), druhým je pozice, kam se díváme (model umístíme do počátku, takže můžeme použít konstantu Vector3.Zero, nebo bychom samozřejmě mohli napsat New Vector3(0, 0, 0)), a vrchní strana kamery má mířit normálně nahoru, proto jsme předali hodnotu Vector3.Up.
Matice projekce (projection matrix)
Projekcí je mnoho různých druhů, ze školy jistě znáte volné rovnoběžné promítání, pokud jste měli deskriptivní geometrii, určitě budete znát i Mongeovo promítání. Ve hrách se ovšem uplatňují jiné typy projekcí:
Na tomto obrázku vidíte roh ulice nahoře v izometrii a dole v perspektivě. Vidíte, že izometrie zachovává rovnoběžnost hran - všechny ve skutečnosti rovnoběžné hrany jsou rovnoběžné i na obrázku. Naproti tomu v perspektivě je zachována jen rovnoběžnost svislých hran, vodorovné hrany se sbíhají k sobě. Izometrii používají některé strategické hry (jeden příklad za všechny je má oblíbená hra SimCity), perspektiva se používá prakticky ve všech 3D hrách. Existuje ještě mnoho dalších metod, jak zobrazit 3D prostor na 2D plochu, ale nám bude stačit perspektiva, protože tak věci vidíme ve skutečnosti.
Perspektiva se dá pro naše herní potřeby poměrně snadno nadefinovat tzv. promítacím kuželem. Tento kužel udává část prostoru, kterou vidíme.
Promítací kužel je kužel, který vidí kamera či oko. Protože předměty, které jsou těsně před kamerou, by vypadaly příliš zkresleně, vykreslujeme jen předměty za přední ořezovou plochou (near clip plane). Naopak příliš vzdálené předměty nevykreslujeme, protože se stejně vejdou jen do několika pixelů na hranici rozlišení monitoru, slabší počítače to akorát zdržuje a nestíhají vykreslovat. Vykreslujeme tedy jen to, co je před zadní ořezovou plochou (far clip plane).
V praxi tento promítací kužel definujeme pomocí zorného prostorového úhlu ve steradiánech (co to je vědět nemusíte, běžně používáme úhel pí / 4), dále potřebujeme vědět poměr stran obrazovky (v našem případě 4 / 3) a pak vzdálenost přední a zadní ořezové plochy od kamery. Do deklarací tedy přidejte tento řádek, ve kterém vytváříme matici projekce:
Private proj As Matrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, 4 / 3, 1, 100)
Globální matice (world matrix)
Často ve hrách používáme i tzv. globální matici. Hodí se například v případě, že chcete simulovat různé efekty vztahující se na všechny objekty ve scéně. My si tuto matici nadeklarujeme také, ale zatím ji nebudeme využívat. Přiřadíme jí hodnotu Matrix.Identity, což je jednotková matice, která má tu vlastnost, že nedělá vůbec nic. Do deklarací tedy přidejte toto:
Private world As Matrix = Matrix.Identity
K čemu všechny tyhle opičárny s maticemi?
Možná se ptáte, proč se tyto všechny věci zapisují pomocí matic. Odpověď je prostá - je to velmi efektivní a poměrně rychlé na výpočty. Matice se dají mezi sebou násobit (vektor se dá považovat také za matici). Pokud to zjednoduším, tak když máme souřadnice nějakého bodu v 3D prostoru a postupně je vynásobíme globální maticí, pak maticí pohledu a nakonec maticí projekce, dostaneme přímo souřadnice tohoto bodu na monitoru a jeho vzdálenost "za monitorem". No a pak už není těžké vykreslit pro každý pixel na monitoru jen ten bod, který je od monitoru nejblíže (ty ostatní jsou za ním, takže nejsou vidět). Grafické karty jsou navrženy speciálně tak, aby uměly matice násobit velmi rychle, takže celý výpočet je sice časově náročný, ale není to nic, na co by karta nebyla připravena.
Transformace
Matice můžeme použít také k provádění transformací. Mezi nejběžnější transformace patří posunutí (translation), otočení (rotation) a změna měřítka (scale).
Matici posunutí nám vytvoří metoda Matrix.CreateTranslation, stačí jí jen předat vektor, o který chceme vše posunout. Pokud máme například bod [0, 0, 4] a chceme celou scénu posunout o vektor (1, 1, 1), souřadnice vektoru a bodu se přičtou, takže výsledný bod bude na souřadnicích [1, 1, 5].
Matici rotace nám vytvoří metody Matrix.CreateRotationX, Matrix.CreateRotationY a Matrix.CreateRotationZ. Musíme jim předat úhel v radiánech, o který chceme otáčet, přičemž otočení probíhá podle osy X, Y, nebo Z, podle toho, kterou metodu použijeme. Pokud budeme chtít vykreslit model na nějakou pozici a otočený o 90 stupňů, musíme nejdřív model umístit do počátku, pak jej otočit podle příslušné osy, a posunout až potom. Kdybychom nejdřív posunuli a pak otočili, dostal by se nám model na úplně jiné místo, protože se otáčí ne kolem své vlastní osy, ale kolem osy celé scény.
Matici změny měřítka si můžeme vytvořit pomocí metody Matrix.CreateScale, kde jako parametr předáme příslušný koeficient (větší než 1 zvětšuje, menší než 1 zmenšuje).
Pokud chceme nějak tyto transformace zkombinovat, jednoduše vynásobíme jejich matice. Pokud mám připravenou matici r s otočením a matici p s posunutím na požadované místo, kombinace těchto transformací je matice r * p. Je třeba ale dát pozor na pořadí matic, násobení matic totiž není komutativní! Je to přesně to, o čem jsem psal v odstavci o maticích otočení. Matice násobíme v pořadí, v jakém provádíme příslušné transformace, v tomto případě nejprve otáčíme, pak posouváme.
Ještě zbývá zmínit, že osa X vede zleva doprava, osa Y zdola nahoru a osy Z zepředu dozadu. Vše je názorně vidět z tohoto obrázku:
A jak tedy vykreslit model?
Pokud čekáte, že vykreslení modelu bude stejně jako všechno ostatní, co jsme v XNA zatím viděli, na jeden řádek, trochu vás zklamu. Možná jen částečně, pokud si do projektové šablony přidáte do třídy Game proceduru RenderModel, tak to na jeden řádek bude. Nyní zde uvede kód procedury, která model vyrenderuje, a v zápětí se podrobně podíváme na to, jak přesně funguje.
Public Sub RenderModel(ByVal m As Model, ByVal position As Vector3, ByVal rotation As Double)
' Vykreslit celý model
Dim transforms(m.Bones.Count - 1) As Matrix
m.CopyAbsoluteBoneTransformsTo(transforms)
For Each mesh As ModelMesh In m.Meshes ' projít všechny části modelu
For Each eff As BasicEffect In mesh.Effects ' všem efektům nastavit pozice
' nastavit všechny 3 matice
eff.World = world * Matrix.CreateRotationY(rotation) * transforms(mesh.ParentBone.Index) * Matrix.CreateTranslation(position)
eff.View = view
eff.Projection = proj
eff.EnableDefaultLighting() ' vypnout standardní osvětlení
Next
mesh.Draw() ' vykreslit část modelu
Next
End Sub
Máme tedy metodu RenderModel, která dostane jako parametry model, který vykreslujeme, jeho pozici a otočení podle svislé osy Y. Každý model může mít (a většinou má) více částí, každá z nich používá jiný materiál (barvu, texturu) a má jinou pozici (každá část může mít svůj vlastní počátek). My někdy nemusíme chtít vykreslovat celý model (například když budeme mít model postavy, můžeme chtít jednotlivé části těla posouvat na různá místa a vykreslovat je zvlášť atd.). Proto i my v této proceduře musíme renderovat model po částech.
Uděláme si tedy pole transforms a zavoláme na modelu metodu CopyAbsoluteBoneTransformsTo, které toto pole předáme. Pozice každé části modelu totiž závisí na pozici jednotlivých částí kostry modelu (model postavy má svou kostru a když hýbeme touto kostrou, tak se pohybují i pozice jednotlivých částí modelu). Pak spustíme cyklus For Each, který mám projde všechny části modelu (objekty ModelMesh z kolekce Meshes našeho modelu).
Nyní pracujeme s každou částí modelu. Na každou tuto část se může vztahovat několik efektů (efekty pracují obecně se vzhledem modelu, mohou určovat například barvu, texturu, světlo atd.). Aby vše správně fungovalo, musíme všem efektům nastavit správně matice world, view a projection, takže uvnitř cyklu procházejícího všechny objekty BasicEffect každé části modelu nastavujeme tyto matice. Přiřazení matice view a projection je jasné, zajímavější je matice world. Vezmeme naši globální matici, kterou jsme si nadeklarovali nahoře (má hodnotu Matrix.Identity). Model chceme otočit podle osy Y a parametru rotation naší metody, takže ji vynásobíme maticí otočení podle osy Y. Dále musíme vynásobit maticí, která nese pozici kosti, která s danou částí modelu hýbe (v našem modelu tohle sice nepoužíváme, ale chceme mít metodu univerzální), aby se nám všechny části napozicovaly podle dané kosti. A protože celý model chceme ještě posunout na příslušnou pozici, takže to celé vynásobíme maticí posunutí na hodnotu parametru position. A úplně na závěr zavoláme eff.EnableDefaultLightning, čímž zapneme na model výchozí osvětlení, aby to vše vypadalo lépe.
Jakmile máme všechny efekty nastaveny, vykreslíme celou část modelu zavoláním metody Draw.
Když to shrnu, vezmeme všechny části modelu, projdeme všechny jejich efekty, nastavíme jim správně pozice z ohledem na pozice příslušných kostí kostry modelu a pak všechny části vykreslíme.
Dokončení
Aby se nám model vykreslil, musíme samozřejmě zavolat proceduru RenderModel. Přidejte tedy dovnitř metody Draw tento řádek:
RenderModel(unicorn, Vector3.Zero, gameTime.TotalGameTime.TotalMilliseconds * 0.001)
Jak jste si jistě všimnuli, model vykreslujeme na pozici počátku a jako rotaci předáváme počet milisekund od spuštění aplikace vynásobený čímsi, aby pohyb byl rozumně rychlý. Jednorožec se nám tedy bude otáčet plynule podle osy Y, jak ukazuje následující video:
To je pro dnešek vše, v příštím díle se podíváme na to, jak vytvořit jednoduchý terén.