V minulých dvou dílech jsme se postupně seznámili s XNA a základy 3D. Dnes si naše znalosti prohloubíme a ukážeme si, jak je vlastně takový 3D model reprezentován uvnitř. Vysvětlíme si, co je to vertex buffer a index buffer a pak si v praxi napíšeme třídu Landscape, která bude umět vykreslit obdélníkovou část terénu s horami a vrcholky.
Základem všeho není kruh, ale trojúhelník
V jednom českém seriálu (už si nevzpomínám na název) se tvrdilo, že základem všeho je kruh. Pokud jde o 3D grafiku tak, jak ji známe dnes, rozhodně toto tvrzení neplatí. Jistě jste si v mnoha hrách všimli, že některé objekty, které by měly být kulaté, kulatými rozhodně nejsou (například kola aut ve starších hrách vypadají jako mnohostěny, jsou zkrátka hranaté).
Prakticky ve všech implementacích počítačové 3D grafiky se vychází z toho, že každý trojrozměrný objekt můžeme rozložit na spoustu trojúhelníků. Je jasné, že to nikdy nebude naprosto přesné zobrazení, i kdyby trojúhelníčků bylo strašně moc, ale zkreslení a nepřesnosti nejsou tak velké, takže to nevadí. Když se podíváte na grafiku dnešních her, jistě je jasné, že to funguje, grafika vypadá velmi realisticky a když se objekty rozloží na optimální počet trojúhelníků (aby jich nebylo málo, protože pak to je hranaté, ale nesmí jich být zase moc, aby to stihla grafická karta vykreslit), výsledek vypadá dobře. Grafické karty samozřejmě počítají s tím, že vše se vykresluje pomocí trojúhelníků, jsou přesně pro tento způsob navrženy a zoptimalizovány.
Na tomto obrázku vidíte to, čeho se dnes budeme snažit dosáhnout. Zvrásněný terén rozložíme na mnoho trojúhelníčků a pak necháme grafickou kartu, aby nám je vykreslila. Jednorožec z minulého dílu byl také poskládán z trojúhelníčků, ty se akorát "vybarvily" nějakou texturou, aby to vypadalo realisticky.
Výšková mapa - heightmap
V souvislosti s terénem se používá často zažitý termín heightmap, česky bychom asi řekli výšková mapa. Je to obrázek, který popisuje "nadmořskou" výšku určité části terénu. Výšková mapa vypadá nějak takhle, čím světlejší je barva na daném místě, tím větší je v tomto terénu nadmořská výška:
Všimněte si, že terén zobrazený nahoře odpovídá přesně této výškové mapě - světlá místa jsou vyzvednutá.
Sestavujeme trojúhelníky
Nyní si tedy naši oblast terénu můžeme rozložit na čtvercovou síť, která bude mít stejně sloupců, jako je šířka obrázku v pixelech, a stejně řádků, jako je výška naší mapy. Pak ke každému bodu, kde se protíná vodorovná čára se svislou, můžeme podle světlosti příslušného pixelu zjistit jeho výšku (nemusí to být nadmořská výška, je na nás, jak si ji budeme interpretovat). Pak již není problém určit přesně souřadnice každého bodu mřížky v 3D prostoru.
Výšku nad povrchem máme v souřadnici Y, takže zelené výšky bodů přihodíme mezi původní souřadnice X,Y bodů v mřížce. Tím dostaneme souřadnice X,Y,Z těchto bodů v 3D prostoru. Nyní musíme tyto vrcholy seskupit po trojicích tak, aby nám vznikly požadované trojúhelníky. Jak to uděláme? Použijeme tzv. vertex buffer, což je vlastně seznam vrcholů. Stačí, když stanovíme, že prvky 0, 1 a 2 tvoří první trojúhelník, prvky 3, 4, 5 jsou druhý atd.
Jak tedy nějak systematicky vytvořit seznam trojúhelníků? Tak třeba každý čtverec rozdělíme na 2 trojúhelníky podle obrázku a trojúhelníky do bufferu zapíšeme podle jejich čísel. Jako první vrchol vždy vezmene levý horní a dále postupujeme po směru hodinových ručiček. Naše malá mřížka z obrázku by vypadala nějak takto (na každém řádku je jeden trojúhelník):
[0,3,0], [1,4,0], [1,4,1]
[0,3,0], [1,4,1], [0,4,1]
[1,4,0], [2,5,0], [2,5,1]
[1,4,0], [2,5,1], [1,4,1]
[0,4,1], [1,4,1], [1,5,2]
[0,4,1], [1,5,2], [0,4,2]
[1,4,1], [2,5,1], [2,5,2]
[1,4,1], [2,5,2], [1,5,2]
Má to ale jeden háček - data se nám často opakují. Pokud bychom chtěli změnit výšku jednoho vrcholu, musíme ji změnit na více místech. Vertex buffer bude zbytečně velký, protože každý vrchol je tam několikrát. U vrcholů si nemusíme navíc pamatovat jen jejich pozice, ale třeba navíc jejich barvu či souřadnice mapování textur (o tom později). Celý buffer by tedy mohl zabírat zbytečně moc místa.
Proto v praxi používáme ještě tzv. index buffer. Funguje to jednoduše, vrcholy si nějak očíslujeme a do vertex bufferu uložíme pouze jejich seznam (kde je každý jen jednou). A do index bufferu uložíme trojúhelníky, ale místo celých dat vrcholu tam uložíme jen jeho pořadí ve vertex bufferu.
Index buffer by tedy při pořadí vrcholů ve vertex bufferu na obrázku vypadal nějak takto:
0, 1, 4
0, 4, 3
1, 2, 5
1, 5, 4
3, 4, 7
3, 7, 6
4, 5, 8
4, 8, 7
Já jsem zde sice jak vertex buffer, tak i index buffer vypsal na několik řádků, abychom mohli vidět jednotlivé trojúhelníky, ale v praxi se vše zapíše za sebe, oba dva buffery jsou vlastně jednorozměrná pole.
A na závěr ještě jedna věc - máme mřížku širokou W vrcholů a vysokou H vrcholů. Zajímá nás, kolik bude celkem vrcholů, kolik trojúhelníků a kolik bude položek v index bufferu.
- Počet vrcholů je jasný, pokud je šířka W a výška H, pak vrcholů bude W * H.
- Počet trojúhelníků je o něco zajímavější. Když se podíváme na to jak trojúhelníky tvoříme, tak v každém vrcholu začínají dva trojúhelníky. Výjimkou jsou vrcholy v posledním sloupci a posledním řádku, tam žádné trojúhelníky nezačínají. Vrcholů, ve kterých trojúhelníky začínají, je tedy (W - 1) * (H - 1), a z každého vrcholu máme 2 trojúhelníky. Takže v mřížce W x H je (W - 1) * (H - 1) * 2 trojúhelníků.
- A počet položek v index bufferu je již jednoduchý. Musíme v něm uvést všechny trojúhelníky (víme, kolik jich je, z předchozího bodu) a každý trojúhelník má 3 vrcholy, což jsou 3 položky. Dohromady je tedy (W - 1) * (H - 1) * 6 položek index bufferu.
Pokud jste následující výpočty nepochopili, zkuste si znovu přečíst způsob, jakým trojúhelníky sestavujeme, pořádně si prostudujte uvedené výčty hodnot, které bychom dávali do bufferů, a zkuste si je ukazovat na obrázku. Nebo si to zkuste nakreslit.
Nyní tedy již chápeme princip, jak rozložit terén na trojúhelníky. Pojďme to nyní naprogramovat.
Začínáme
Vytvořte si nový projekt Windows Game z naší šablony a pravým tlačítkem klikněte v podokně Solution Explorer na název projektu a vyberte položku Add / Class podle obrázku.
Třídu pojmenujte Landscape.vb a přidejte do ní tyto deklarace:
Private heights(,) As Single ' výšky terénu v jednotlivých vrcholech
Private verticesCount, indicesCount As Integer ' počty vertexů a indexů
Private width, height As Integer ' šířka a výška oblasti
Private device As GraphicsDevice ' odkaz na objekt GraphicsDevice
Private vb As VertexBuffer, ib As IndexBuffer ' vertex a index buffer
Private effect As BasicEffect ' výchozí efekt
Public Sub New(ByVal graphics As GraphicsDevice)
'schovat si objekt GraphicsDevice
device = graphics
'připravit efekt
effect = New BasicEffect(device, Nothing)
End Sub
heights je dvourozměrné pole, které udává výšky jednotlivých bodů mřížky. Ty načteme z předané heightmapy a podle nich pak určíme 3D souřadnice jednotlivých vertexů. Proměnné verticesCount a indicesCount budou obsahovat počty vertexů a indexů, proměnné width a height budou obsahovat šířku a výšku mřížky.
device reprezentuje objekt GraphicsDevice (grafická karta), který dostane naše třída v konstruktoru a protože jej bude používat často, uloží si jej k sobě, abychom jej nemuseli předávat při každém volání jakékoliv metody. Proměnné vb a ib budou reprezentovat vertex buffer, respektive index buffer a uvnitř proměnné effect bude efekt, který budeme potřebovat k renderování terénu.
Jako malou odbočku trochu angličtiny: vertex = vrchol, množné číslo od vertex je vertices. index = pořadí, množné číslo od index je indices. Proto máme vertex buffer, ale proměnnou verticesCount (počet vrcholů) a indicesCount (počet indexů).
Dále jsme do třídy přidali konstruktor, který uloží odkaz na předaný objekt GraphicsDevice, který budeme potřebovat, a dále nám zinicializuje náš BasicEffect, o tom ale až později.
Načtení výšek z heightmapy
Možná si říkáte, jestli byste tohle nezvládli sami. Já myslím, že ano - znáte namespace System.Drawing (tedy alespoň doufám, pokud ne, naučte se alespoň základy, než se pustíte do 3D). Stačilo by tedy načíst obrázek pomocí metody Image.FromFile, uložit jej do objektu Bitmap a pomocí GetPixel získat barvy jednotlivých pixelů. Má to ovšem dva problémy - zaprvé metoda GetPixel je poměrně pomalá a zmapování velkého obrázku by trvalo dlouho (jsou sice postupy, jak bitmapu "odemknout" a přes Marshal.Copy z ní vytáhnout přímo pole bajtů s hodnotami jednotlivých barevných složek, ale to je jiná kapitola) a zadruhé pokud bychom chtěli spustit naši hru na konzoli XBox 360 (což sice z VB.NET nemůžeme, ale pokud píšete v C#, tak pokud do tohoto jazyka přeložíte kód z těchto článků, bude vám fungovat naprosto stejně), tak by to nefungovalo, protože XBox nemá mimo jiné knihovnu System.Drawing.
Použijeme tedy jiný postup, takový, který umí přímo XNA. Dovnitř naší třídy přidáme metodu LoadContent, které předáme texturu s výškovou mapou a koeficient, kterým určíme, jak moc se má světlost pixelu projevit na výšce terénu. Do třídy Landscape tedy přidejte tuto metodu:
Public Sub LoadTerrain(ByVal tex As Texture2D, ByVal heightScale As Double)
' zjistit rozměry terénu
width = tex.Width : height = tex.Height
ReDim heights(width - 1, height - 1)
' získat jednotlivé pixely textury
Dim b(Width * Height - 1) As Color
tex.GetData(Of Color)(b)
' zjistit výšky v jednotlivých bodech
For y As Integer = 0 To Height - 1
For x As Integer = 0 To Width - 1
With b(x + y * Width)
heights(x, y) = heightScale / 255 * (CDbl(.R) + CDbl(.G) + CDbl(.B)) / 3
End With
Next
Next
' připravit vertex a index buffer
PrepareVertexBuffer()
PrepareIndexBuffer()
End Sub
Nejprve podle šířky a výšky textury nastavíme naše proměnné width a height, které jsme si v této třídě nadeklarovali. Dále pak nastavíme rozměry pole heights na příslušné velikosti - první rozměr bude od 0 do width - 1, druhý bude od 0 do height - 1.
Na dalších dvou řádcích si vytvoříme pole b typu Color od 0 do width * height - 1 a na dalším řádku do tohoto pole nahrajeme metodou GetData po všechny pixely textury po řádcích zleva doprava a odshora dolů.
Dále následují dva cykly v sobě, které nedělají nic jiného, než že nám procházejí po řádcích pole heights. With blok pracuje s výrazem b(x + y * width), což je vlastně pixel v textuře na pozici x, y (pole je ale jednorozměrné, takže musíme pozici pixelu spočítat - pozice na y-tém řádku v textuře je x a před tímto řádkem bylo ještě celých y řádků, kde každý měl width položek). Převod ze souřadnic ve dvojrozměrném poli na index v jednorozměrném poli je patrný z obrázku (jen je nutno říci, že souřadnice musí být číslovány od nuly, což my ale máme !)
Do pole heights(x, y) uložíme výslednou výšku daného bodu, kterou spočítáme takto: (CDbl(.R) + CDbl(.G) + CDbl(.B)) / 3 je aritmetický průměr z hodnot červené, zelené a modré složky našeho pixelu. Musíme použít CDbl, protože operátor + mezi dvěma hodnotami typu Byte předpokládá výsledek také typu Byte a třeba číslo 400 se do bajtu už nevejde, přestože vznikout součtem dvou bajtů může. Proto tedy hodnoty převedeme na Double, sečteme a vydělíme třemi. Vzhledem k tomu, že hodnoty jednotlivých barevných složek jsou v rozsahu od 0 do 255, vydělíme náš spočítaný průměr (který je také z tohoto rozsahu) číslem 255, abychom získali hodnoty od 0 do 1. Ty pak vynásobíme naším parametrem heightScale, který udává výšku nejvyššího možného bodu - pokud tato hodnota bude třeba 5, náš terén bude mít Y souřadnici (výšku) v rozsahu od 0 do 5.
Nakonec naše metoda zavolá metody PrepareVertexBuffer a PrepareIndexBuffer, které nyní napíšeme, takže se nelekejte, že vám Visual Studio vyhazuje chybu.
Vytvoření a naplnění VertexBufferu
Do naší třídy přidejte tuto metodu, která nám připraví VertexBuffer.
Private Sub PrepareVertexBuffer()
' nastavit vertex buffer
verticesCount = width * height
vb = New VertexBuffer(device, GetType(VertexPositionTexture), verticesCount, BufferUsage.WriteOnly)
' vytvořit pole vertexů
Dim verts(verticesCount - 1) As VertexPositionTexture
For y As Integer = 0 To height - 1
For x As Integer = 0 To width - 1
With verts(x + y * width)
.Position = New Vector3(x, heights(x, y), y)
End With
Next
Next
' nahrát vertexy do bufferu
vb.SetData(Of VertexPositionTexture)(verts)
End Sub
V první části nastavíme do proměnné verticesCount počet vertexů a na dalším řádku vytvoříme nový objekt VertexBuffer. Musíme mu předat objekt GraphicsDevice (grafickou kartu) a typ VertexPositionTexture, aby karta věděla, jaké informace budou naše vertexy obsahovat (v tomto případě pozici a mapování textury, to je až pro příští díl). Dále předáváme počet vertexů a nakonec říkáme, že do bufferu budeme jen zapisovat, takže jej XNA může nahrát přímo do paměti grafické karty, aby se s ním rychleji pracovalo.
Ve druhé části musíme připravit data, která do tohoto bufferu "nalijeme". Vytvoříme si tedy jednorozměrné pole všech vertexů (typu VertexPositionTexture; typů vertexů je více, můžeme si dokonce vytvořit vlastní) a v cyklu nastavíme jednotlivým prvkům správnou pozici. Souřadnice x a y, které máme k dispozici uvnitř cyklů, budou souřadnicemi x a z našeho vrcholu (x je v prostoru zleva doprava a z je zezadu dopředu). Jako souřadnici y vertexu dáme výšku, kterou máme již spočítanou v poli heights na pozici x, y. To je vše.
Jako poslední musíme naše pole "natlačit" do bufferu, k čemuž použijeme metodu SetData a ještě jednou jí zopakujeme, jakého typu data jsou (to ne, že by měla třída VertexBuffer sklerózu; je to opět napsáno genericky, typ píšeme, aby kompilátor věděl, jaký datový typ má v parametru očekávat a nemuselo se pořád přetypovávat z obecného typu Object). Tím je náš vertex buffer připraven, to byla ovšem ta jednodušší část.
Vytvoření a naplnění IndexBufferu
Metoda pro přípravu IndexBufferu bude podobná, akorát uvnitř cyklu to bude drobátko hnusnější. Kód této metody je zde:
Private Sub PrepareIndexBuffer()
' nastavit index buffer
indicesCount = (width - 1) * (height - 1) * 6
ib = New IndexBuffer(device, GetType(Integer), indicesCount, BufferUsage.WriteOnly)
' vytvořit pole indexů
Dim indices(indicesCount - 1) As Integer
Dim k As Integer = 0
For y As Integer = 0 To height - 2
For x As Integer = 0 To width - 2
Dim b As Integer = x + y * width
indices(k) = b
indices(k + 1) = b + 1
indices(k + 2) = b + 1 + width
indices(k + 3) = b
indices(k + 4) = b + 1 + width
indices(k + 5) = b + width
k += 6
Next
Next
' nahrát indexy do bufferu
ib.SetData(Of Integer)(indices)
End Sub
Nahoře opět spočítáme počet položek v bufferu a vytvoříme nový IndexBuffer. I tady mu jako minule předáme GraphicsDevice a oznámíme, že má čekat hodnoty typu Integer, že jich bude tolik a tolik a opět do něj budeme jen psát.
Před cyklem si vytvoříme opět jednorozměrné pole indexů potřebné velikosti a uděláme si proměnnou k, která si bude pamatovat, kolik položek již máme v poli indices zapsaných, abychom indexy pořád nemuseli vypočítávat ze souřadnic x a y. Všimněte si, že nám nyní cykly běží od 0 do height - 2, resp. width - 2, při vytváření trojúhelníků vynecháváme ten poslední řádek a sloupec, jak jsme si již vysvětlili výše, protože z vrcholů v posledním sloupci a řádku již žádné trojúhelníky vytvářet nebudeme.
Uvnitř cyklu si v každém kroku uložíme do proměnné b index levého horního vrcholu, kde naše dva trojúhelníky budou začínat. Do pole indices uložíme tedy indexy vrcholů našich dvou trojúhelníků. Pro jistotu zde máte obrázek, v jakém pořadí vrcholy trojúhelníků zadáváme:
Jak vidíme v kódu, první a čtvrtý vrchol je b, tam trojúhelníky začínáme. Druhý vrchol je b + 1, je totiž hned vedle b. Třetí a pátý vrchol je b + 1 + width, je totiž o jednu vedle a o řádek níž (a řádek má width políček). Šestý vrchol ze skupiny je b + width, protože je o jeden řádek níž než b. Jakmile nastavíme příslušné indexy na příslušná místa, zvětšíme k o 6, abychom příští skupinu zapisovali na další políčka a nepřepsali si již spočítané indexy.
Jakmile jsme s touto "opičárnou" hotovi, můžeme stejně jako v předchozí metodě nahrát data do index bufferu a jsme hotovi. Teď přijde již jen vykreslování.
Vykreslení terénu
V konstruktoru třídy Landscape jsme zinicializovali objekt effect typu BasicEffect a já jsem zamlčel, na co jej přesně použijeme. V XNA nemůžeme vykreslovat ve 3D nic, pokud nemáme nastavený žádný efekt. Efektů je víc druhů, nám zatím bude stačit BasicEffect. Ten je mimo jiné zodpovědný za přepočítání souřadnic v 3D prostoru na souřadnice na obrazovce, kromě toho umí mapovat textury a pracovat se světly. Protože jsme ve 3D, musíme samozřejmě mít nachystané naše známé matice world, view a projection. Ty si necháme předat v parametru metody Draw, která se bude starat o vykreslování, a kterou teď napíšeme. Přidejte tedy tento kód do třídy Landscape:
Public Sub Draw(ByVal world As Matrix, ByVal view As Matrix, ByVal proj As Matrix)
' nastavit projekční matice
effect.World = world
effect.View = view
effect.Projection = proj
' vykreslit jen mřížku
device.RenderState.FillMode = FillMode.WireFrame
' použít efekt na vykreslování
effect.Begin(SaveStateMode.None)
For Each pass As EffectPass In effect.CurrentTechnique.Passes ' všechny průchody
pass.Begin()
' nastavit formát vertexu
device.VertexDeclaration = New VertexDeclaration(device, VertexPositionTexture.VertexElements)
' nastavit vertex a index buffer
device.Vertices(0).SetSource(vb, 0, VertexPositionTexture.SizeInBytes)
device.Indices = ib
' vykreslit vertexy
device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, verticesCount, 0, indicesCount / 3)
pass.End()
Next
effect.End()
End Sub
Hned na začátku přiřadíme našemu efektu všechny tři matice, aby se nám terén zobrazil tak, jak má. Dále objektu grafické karty nastavíme RenderState.FillMode na hodnotu FillMode.WireFrame, čímž způsobíme, že se zobrazí jen trojúhelníková mřížka, přesně ta, kterou jste mohli vidět na začátku tohoto článku.
Pak již budeme vykreslovat, takže musíme nejprve připravit efekt. Vykreslovat ve 3D můžeme pouze mezi voláním effect.Begin a effect.End, stejně jako je to při vykreslování ve 2D. Každý efekt může mít více průchodů (náš efekt ne, ale chceme to napsat obecně, takže musíme počítat s tím, že jiný efekt víc průchodů mít může), v každém průchodu musíme vykreslit celou scénu. Vytvoříme tedy For Each cyklus a projdeme každý průchod pass typu EffectPass. Opět můžeme vykreslovat jen mezi voláními pass.Begin a pass.End, takže vykreslování musíme vložit dovnitř.
A nyní čtyři řádky uprostřed bohatě proložené komentáři, které mohou za celé vykreslení terénu. V prvním řádku grafické kartě oznámíme, jak že přesně vypadá náš vertex v bufferu (vytvoříme tedy nový objekt VertexDeclaration a předáme mu pole všech elementů, které v sobě obsahuje typ VertexPositionTexture). Dále musíme grafické kartě říci, který vertex buffer a index buffer má použít. Vertex bufferů může mít GraphicsDevice několik, takže na první z nich zavoláme SetSource a předáme náš vb, sdělíme navíc, že začíná od 0 a velikost jednoho vertexu je rovna hodnotě VertexPositionTexture.SizeInBytes). Nastavení index bufferu je již jednodušší, stačí jej přiřadit do vlastnosti Indices.
Poslední řádek je ten nejdůležitější, provede samotné vykreslení. DrawIndexedPrimitives vyžaduje typ geometrických obrazců (v našem případě TriangleList, seznam trojúhelníků), dále index prvního vertexu ve vertex bufferu a minimální index, pak následuje počet vertexů, index začátku v index bufferu a počet trojúhelníků (což je tedy indicesCount / 3).
A to je celá třída Landscape. Neumí toho zatím moc, pro pořádné použití ve hře ji budeme muset příště naučit spoustu dalších věcí, jako třeba otexturování (a možná i více různými texturami, na každém místě jinou, tedy tzv. multitexturing atd.), zjištění výšky v konkrétním bodě (s neceločíselnými souřadnicemi), protnutí přímky a terénu atd.
Použití třídy Lanscape
Třídu máme hotovou, ale když spustíte projekt, pochopitelně se nic nestane. Nevoláme totiž žádnou z jejích metod. Přepněte se tedy nyní do třídy Game a do deklarací přidejte tyto čtyři řádky (vysvětlovat je již nebudu, 3 znáte z minula a čtvrtý je jasný).
Private land As Landscape ' náš terén
Private world As Matrix = Matrix.Identity
Private view As Matrix = Matrix.CreateLookAt(New Vector3(32, 80, 64), New Vector3(32, 0, 32), Vector3.Up)
Private proj As Matrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, 4 / 3, 1, 1000)
Stáhněte a uložte si tuto výškovou mapu (pravým tlačítkem na ni klikněte a zvolte Uložit obrázek jako). Pak ji přidejte do projektu do složky Content.
Dovnitř metody LoadContent v třídě Game přidejte tyto dva řádky - zinicializujeme náš Landscape a nahrajeme do něj naši výškovou mapu (načtenou jako Texture2D):
land = New Landscape(Me.GraphicsDevice)
land.LoadTerrain(Content.Load(Of Texture2D)("Content\heightmap"), 25)
A samotné vykreslení terénu je již jednoduché, měli byste jej být schopni vymyslet sami. Ale taková potvora nebudu, abych jej sem nenapsal, takže toto přidejte dovnitř metody Draw:
land.Draw(world, view, proj)
A to je vše, v tomto díle video nebude, protože se nám nic nehýbe. Příště se podíváme na textury, aby náš terén vypadal skutečně a ne jako drátěný model.