V minulých třech dílech jsme se ponořili do základů XNA frameworku, vysvětlili si základní pojmy, naučili jsme se pracovat s 2D a 3D grafikou a v neposlední řadě jsme provedli již poměrně složitější rozložení terénu na trojúhelníky a jeho vykreslení. Dnes se naučíme pracovat s texturami a ukážeme si, jak namapovat na náš terén nějakou texturu a jak terén nasvítit tak, aby vypadal dobře.
Načtení textury a příprava pro renderování
Poněkud netradičně začneme programovat hned. Stáhněte si tuto texturu trávy a přidejte ji do projektu.
Dále otevřete třídu Landscape a nahoru do deklarací přidejte vlastnost Texture, která bude vracet a nastavovat aktuální texturu terénu:
Private _texture As Texture2D ' textura terénu
Public Property Texture() As Texture2D
Get
Return _texture
End Get
Set(ByVal value As Texture2D)
_texture = value
End Set
End Property
Mohli bychom udělat místo vlastnosti public proměnnou, ale zvykněte si na cokoliv, co má být viditelné ven, používat Property. Nikdy nevíte, jestli za týden nebudete potřebovat při nastavení hodnoty této proměnné spustit nějakou proceduru nebo provést nějaký kód. Když máte vlastnost, jde to hladce zařídit, stačí přidat příslušný kód do sekce Set, resp. Get, pokud chceme před vrácením hodnoty něco ošetřit.
Aby se textura aplikovala při vykreslování, do procedury Draw přidejte nahoru za nastavení matic tyto dva řádky (ještě před cyklus):
' zapnout texturování
effect.Texture = Texture
effect.TextureEnabled = True
Tím jsme nastavili efektu, který terén vykresluje, naši texturu a řekli jsme mu, že ji má použít. Dále smažte z této metody tento řádek, nebudeme již chtít kreslit jen mřížku, ale celou plochu terénu:
device.RenderState.FillMode = FillMode.WireFrame
A dovnitř metody LoadAllContent uvnitř třídy Game přidejte tento řádek (měli byste sami jednoznačně rozhodnout, jestli před inicializaci terénu nebo až za ní):
land.Texture = Content.Load(Of Texture2D)("Content\grass")
Mapování textur
V minulém díle jsme pomocí složité opičárny sestavili terén a nyní bychom jej chtěli pokrýt texturou trávy tak, aby dobře vypadal. A k tomu se pojí termín mapování, je to vlastně stanovení, jak se má texturou terén potáhnout. Když například vytapetujete stěnu v obýváku, vzor se opakuje. A podobně je to s texturou - pokud bychom náš obrázek trávy roztáhli na celou plochu terénu, při pohledu zblízka bychom viděli tohle, což moc hezky nevypadá:
Budeme tedy chtít, aby se naše textura několikrát zopakovala. Samozřejmě musíme mít texturu, která na sebe navazuje, spousta textur při opakování vytváří znatelné hranice, které jsou nežádoucí:
Naše textura trávy ale při opakování navazuje a vypadá pěkně, proto ji můžeme na celou plochu terénu použít 8 x 8 krát. Samozřejmě textury nemusíme mapovat rovnoměrně, můžeme je někde roztáhnout více a jinde zase "smrsknout", to už je na nás. Samotné mapování určíme pomocí souřadnic mapování textur (obvykle se pro ně používají písmena U a V). Jak to funguje je vidět na obrázku:
Na tomto obbrázku jsem označil vrcholy a vypsal k nim jejich souřadnice [U; V], které definují namapování textury. Pokud tedy na šířce terénu chceme texturu "vtěsnat" 8x, musíme souřadnice vrcholů U rovnoměrně osadit hodnotami od 0 do 8. Je nutno ještě zdůraznit, že každý vrchol musí mít souřadnice mapování nastavené. Je jasné, že většina vrcholů nebude mít tyto souřadnice celočíselné.
Otevřete si tedy metodu PrepareVertexBuffer a dovnitř cyklů hned pod řádek, kde nastavujeme pozici, vložte navíc řádek:
.TextureCoordinate = New Vector2(x / width * 8, y / height * 8)
Hodnoty proměnných x a y jsou v rozmezí od 0 do width, resp. height. Pokud tedy x vydělíme hodnotou width, dostaneme výsledek v rozsahu od 0 do 1, což by se nám hodilo v případě, že chceme texturu roztáhnout na celou plochu terénu. Když tuto hodnotu ještě vynásobíme osmi, bude v rozsahu od 0 do 8, což přesně potřebujeme. x skáče rovnoměrně, takže i tyto výsledné souřadnice budou rozmístěny rovnoměrně. To samé platí i pro souřadnice y. Pokud změníte ve třídě Game vytváření matice view podle následující ukázky, uvidíte terén s pěkně vypadajícím mapováním textury.
Private view As Matrix = Matrix.CreateLookAt(New Vector3(32, 12, 16), New Vector3(32, 8, 32), Vector3.Up)
Textura terénu je již lepší, ale stále ještě naší scéně mnoho chybí. Je to zcela jistě osvětlení, chtělo by to ztmavit některé stěny a některé zase rozjasnit, aby bylo na terénu něco vidět.
Normály
Abychom mohli používat světla, musíme každému vrcholu přidat ještě normálu. Normála plochy je vektor, který vede kolmo na tuto rovinu. Můžete si to představit jednoduše jako když na desku stolu postavíte kolmo tužku - tužka udává směr normály desky stolu. Pokud světlo dopadá na nějakou plochu, je potřeba znát normálu, protože pomocí ní můžeme spočítat, kolik světla na stůl dopadá. Pokud světlo na stůl dopadá šikmo, je osvícen méně, pokud na něj dopadá kolmo, je osvícen nejvíce, a pokud náhodou na stůl světlo svítí z druhé strany (zespoda), není horní deska osvětlená tímto světlem vůbec. Pro představu je zde jednoduchý obrázek:
Normála je tlustá modrá šipka, červené šipky udávají směr světla. Vidíme, že když světlo dopadá šikmo, plocha je středně tmavá, pokud světlo dopadá kolmo, plocha je nejsvětlejší, a pokud ukazuje normála na druhou stranu, plocha není osvícená a je na ní stín. Výhodou normály je, že jednoznačně určuje směr natočení plochy.
V XNA nemají normálu plochy, ale vrcholy (informace o plochách jako takových nemáme). Vzhledem k tomu, že vrchol leží vždy na rozhraní několika ploch, musíme podle okolních vrcholů (které určují směr naklonení plochy) dopočítat normálu. Toto počítání normály ve vrcholu není matematicky přesné, ale nám bude prozatím stačit. Jak na to ukazuje následující obrázek:
Chceme určit směr normály prostředního vrcholu. Vezmeme si sousední vrcholy napravo a nalevo a spojíme je (zajímá nás jen vektor této spojnice), dále si vezmeme sousední vrcholy vpředu a vzadu a provedeme s nimi to samé (zjistíme též vektor jejich spojnice). Tyto dva vektory nám určují jednu velkou plochu, jejíž normála nám poslouží jako hledaný výsledek. Je to jen přibližné řešení, ale je velmi jednoduché a dává poměrně dobré výsledky.
Jak zjistit vektory spojnic? Vcelku jednoduše - vektor z bodu A do bodu B zjistíme tak, že odečteme souřadnice bodu A od souřadnic bodu B (zvlášť X, Y a Z). První vektor má souřadnici X rovnou dvěma (vrcholy jsou od sebe vzdálené 1 jednotku), Y souřadnici rovnou rozdílu výšek vrcholu vpravu a vlevo a Z souřadnici 0, protože oba dva sousední vrcholy ji mají stejnou. Obdobně zjistíme vektor druhé spojnice - X bude 0, Y rozdíl výšek a Z bude-2.
Pokud známe dva vektory a chceme k nim vypočítat třetí, který je kolmý na oba z nich, můžeme použít tzv. vektorový součin (možná znáte ze střední školy). Záleží na pořadí, v jakém vektory vynásobíme (existuje něco jako pravidlo utahování šroubu, které pořadí určí, ale tím vás nebudu zatěžovat), pokud dáme pořadí opačné, bude vektor ukazovat přesně na druhou stranu, tedy dolů (kolmost na oba dva výchozí se samozřejmě zachová). Samotný vektorový součin už za nás udělá XNA, má na to totiž funkci Vector3.Cross. Vrcholům, které jsou na kraji oblasti a chyběl by jim nějaký soused, nastavíme normálu na hodnotu Vector3.Up, což je vektor, který míří kolmo nahoru.
Přidání informací o normále do vertexů
Pro informace o vrcholu používáme datový typ VertexPositionTexture, který se nám ale nyní nehodí. Vzhledem k tomu, že je tento typ uveden na více místech, je nejrychlejší provést hromadné nahrazení. Vstupte tedy do třídy Landscape a stiskněte Ctrl-H. Vyplňte dialog podle obrázku a klikněte na tlačítko ReplaceAll.
Nyní vstupte opět do metody PrepareVertexBuffer a za přiřazení souřadnic mapování textur přidejte ještě tento blok, který spočítá normálu vnitřním bodům a krajním ji nastaví přímo nahoru:
If x > 0 And x < width - 1 And y > 0 And y < height - 1 Then
Dim v1 As New Vector3(2, heights(x + 1, y) - heights(x - 1, y), 0)
Dim v2 As New Vector3(0, heights(x, y + 1) - heights(x, y - 1), 2)
.Normal = Vector3.Cross(v2, v1)
.Normal.Normalize()
Else
.Normal = Vector3.Up
End If
Podmínka se tedy splní, pokud nejsme na kraji mapy. V takovém případě spočítáme vektory v1 a v2, což jsou vektory spojnic sousedů. Normála našeho vertexu pak bude rovna Vector3.Cross(v2, v1), což je vektorový součin našich vektorů. Nakonec na normálu zavoláme ještě funkci Normalize, aby se vektor zkrátil na délku 1 (směr zůstane zachován), protože s ním pak může XNA rychleji pracovat.
Světlo
Nyní v naší scéně zapneme světlo. Najděte metodu Draw v třídě Landscape a přidejte do ní za nastavení matic a textury (před aplikování efektu) tento kód. Hned si vysvětlíme, co co znamená:
' osvětlení scény
effect.LightingEnabled = True
effect.DirectionalLight0.Enabled = True
effect.DirectionalLight0.Direction = New Vector3(15, -7, -18) ' směr světla
effect.DirectionalLight0.DiffuseColor = New Vector3(1, 1, 0.7) ' barva světla
effect.DirectionalLight0.SpecularColor = New Vector3(0.5, 0.5, 0.32) ' nasvícené plochy
effect.AmbientLightColor = New Vector3(0.7, 0.7, 0.7) ' stín (tam, kam nejde světlo)
Nejprve na efektu zapneme vlastnost LightningEnabled, aby zapnul podporu světel. Vidíme, že můžeme použít 3 různá directional lights (směrová světla). Budeme pracovat jen s prvním z nich - effect.DirectionalLight0. Zapneme jej nastavením vlastnosti Enabled na True. Dále mu nastavíme směr (svazek paprsků tohoto světla se nerozšiřuje, jako třeba světlo lampičky; je vhodné jej použít na světlo hodně vzdáleného zdroje, např. Slunce). DiffuseColor je barva tohoto světla (skládáme jí pomocí složek RGB spektra, akorát místo celočíselných hodnot od 0 do 255 dáváme desetinné hodnoty od 0 do 1), v tomto případě tedy skoro bílá, trošku nažloutlá. SpecularColor je barva nejnasvícenějších ploch, těch, kam světlo dopadá nejkolměji. Chceme, aby nasvícené plochy byly trochu ozářené, takže barvu nastavíme trochu střídměji (asi tak na polovinu).
Nakonec musíme celému efektu nastavit barvu AmbientLightColor. Standardně je nastavena na černou a jedná se o barvu globálního světla, které se použije, pokud na plochu nedopadá světlo žádné. Ve skutečném světě i ve stínu, kam sluneční paprsky nedopadají, není úplná tma, je tam jen šero. Proto my tuto barvu nastavíme na světle šedou, aby nám plochy hor odvrácené od Slunce nezčernaly moc, to by nevypadalo hezky. Kdyžtak si s hodnotami světel zkuste trochu pohrát, tak nejlépe zjistíte, co která dělá.
A to by bylo pro dnešek vše, výsledek vypadá takto:
Nalevo vidíme velmi světlé plochy, tam se projevilo SpecularLight, máme tam také spoustu zastíněných ploch, kam sluneční paprsky nedopadají.
V příštím díle se naučíme pracovat s klávesnicí a myší, do naší hry přidáme nám již dobře známého jednorožce, na kterém si trochu zajezdíme.