C/C++ & Visual C++

Kurz DirectX (36.)

┌vodem  |  DatovΘ struktury |  Kurz DirectX  |  Downloads  |  Otßzky a odpov∞di

V dneÜnφ lekci se op∞t vracφm k DirectX a naÜemu projektu. Velkou Φßst Φlßnku v∞nuji dotazu ohledn∞ vytvß°enφ objekt∙ p°es poΦφtßnφ referencφ. Celou tuto problematiku se pokusφm jeÜt∞ jednou vysv∞tlit na jednoduchΘm p°φkladu. V dalÜφ Φßsti se budeme op∞t zab²vat projektem D3DEngine, kter² jsme postupn∞ vytvß°eli v minul²ch lekcφch.

36.1. Vytvß°enφ objekt∙ pomocφ rozhranφ

V projektu D3DEngine jsem zavedl vytvß°enφ objekt∙ p°es jejich rozhranφ, z klienta tedy nikdy nepracujeme s objektem p°φmo, ale v₧dy jen s rozhranφm objektu. Rozhranφ je v podstat∞ taky t°φda (v jin²ch programovacφch jazycφch je pro rozhranφ vyhrazen zcela jin² typ). Tato t°φda se ale vyznaΦuje n∞kolika zvlßÜtnostmi, kterΘ je pot°eba dodr₧et, abychom tuto t°φdu prohlßsit za rozhranφ jinΘ t°φdy.
Asi nejpodstatn∞jÜφm rysem ka₧dΘho rozhranφ je, ₧e neobsahuje ₧ßdnΘ atributy (tedy ΦlenskΘ prom∞nnΘ). Tuto v²sadu majφ pouze t°φdy vlastnφch objekt∙, ale rozhranφ je urΦeno pro p°φstup ke svΘ t°φd∞ a tento p°φstup je umo₧n∞n v²hradn∞ metodami!
Za druhΘ, vÜechny metody jsou Φist∞ virtußlnφ. To znamenß, ₧e nem∙₧ete p°φmo vytvo°it objekt rozhranφ (jakmile mß t°φda alespo≥ jednu Φist∞ virtußlnφ metodu stßvß se z nφ abstraktnφ t°φda, tyto t°φdy jsou urΦeny pro d∞d∞nφ a nelze z nich vytvo°it objekt). Druh² d∙sledek je ten, ₧e vÜechny tyto metody musφte pak v cφlovΘ t°φd∞ objektu implementovat.
Jako poslednφ bod bych uvedl povinnost dvou metod AddRef() a Release(). Tyto dv∞ metody se starajφ o poΦφtßnφ referencφ na objekt. Z tohoto bodu vypl²vß dalÜφ po₧adavek na t°φdu objektu. Tento objekt (a jak²koliv jin², kter² mß rozhranφ) musφ mφt atribut Reference, kter² obsahuje poΦet rozhranφ vytvo°en²ch k danΘmu objektu. Kdykoliv po₧ßdßte o novΘ rozhranφ, tento ΦφtaΦ se inkrementuje. Kdykoliv rozhranφ uvolnφte pomocφ metody Release(), ΦφtaΦ se dekrementuje. Dosßhne-li ΦφtaΦ hodnoty 0, objekt sßm sebe zniΦφ, proto₧e vφ, ₧e o n∞j nikdo nestojφ. Tak₧e klient nikdy p°φmo objekt nevytvß°φ a tφm pßdem ani neuvol≥uje. Musφ ovÜem uvolnit rozhranφ, tφm objektu sd∞lφ, ₧e u₧ o n∞j nemß zßjem.

Tφmto jsem zßrove≥ popsal hrubΘ principy technologie COM (Component object model), ze kterΘho je tento nßÜ model odvozen (ale samoz°ejm∞ je zjednoduÜen pro naÜe pot°eby).

Nynφ si ukß₧eme popsanΘ principy na tom nejjednoduÜφm principu, kter² pak p°evedeme do projektu D3DEngine.
Vytvo°il jsem projekt IntExample (jako interface example). Je to jednoduchß konzolovß aplikace s funkcφ main() (nic slo₧it∞jÜφho toti₧ nepot°ebujeme). Uvedu dv∞ t°φdy CSpaceship a CUnique, u kter²ch si vytvo°φme rozhranφ ISpaceship a IUnique. Na t°φd∞ CUnique p°edvedu princip vytvß°enφ unikßtnφch objekt∙, tak jako mßme n∞kterΘ objekty v Display. Ob∞ t°φdy jsou velmi jednoduchΘ, obsahujφ pßr metod a ka₧dß mß krom∞ atributu Reference takΘ atribut typu int a °et∞zec.

T°φda CSpaceship:

class CSpaceship : public ISpaceship
{
 
 /* povinny atribut */
  unsigned long m_ulRef;
 
 /* atributy */
  int m_iPosition;

public:
  virtual void Init(int position);
  virtual void Move(int direction);
  virtual void Display();

public:
  /* povinne dve metody rozhrani */
  virtual HRESULT AddRef();
  virtual HRESULT Release();
public:
  CSpaceship(void);
  ~CSpaceship(void);
};

T°φda CUnique:

class CUnique : public IUnique
{
  /* povinny atribut */
  unsigned long m_ulRef;
  /* atributy */
  char *m_szMessage;

public:
  virtual void Start(char * message);
  virtual void Message();

public:
  /* povinne dve metody rozhrani */
  virtual HRESULT AddRef();
  virtual HRESULT Release();

public:
  CUnique(void);
  ~CUnique(void);
};


Nynφ si vÜimn∞te atributu m_ulRef a metod AddRef() a Release(). Ob∞ tyto metody musφ t°φdy implementovat. Zßrove≥ je vid∞t jak spojφme rozhranφ s t°φdou. V tomto p°φpad∞ je to jednoduchΘ, proto₧e ka₧dß t°φda mß pouze jedno rozhranφ, tak₧e staΦφ t°φdu od rozhranφ odvodit. Dostßvßme se tedy k samotn²m rozhranφm, kterΘ jsou samostatn∞ v souboru Interfaces.h:

/*
* Rozhrani tridy CSpaceship
* Tato trida musi byt od odvozena od ISpaceship a tak
* implementuje metody rozhrani
*
* Rozhrani neobsahuje atributy, ale pouze ciste virtualni metody
* tim padem, tyto metody MUSI byt implementovany ve tride CSpaceship
*/

class ISpaceship
{
public:
  /* priklad metody rozhrani */
  virtual void Init(int position) = 0;
  virtual void Move(int direction) = 0;
  virtual void Display() = 0;

public:
  /* povinne dve metody rozhrani */
  virtual HRESULT AddRef() = 0;
  virtual HRESULT Release() = 0;
};

/*
* Druhe ukazkove rozhrani predstavuje rozhrani
* unikatniho objektu. Trida CUnique je odvozena
* od tohoto rozhrani
*/

class IUnique
{
public:
  virtual void Start(char * message) = 0;
  virtual void Message() = 0;

public:
  /* povinne dve metody rozhrani */
  virtual HRESULT AddRef() = 0;
  virtual HRESULT Release() = 0;
};

Rozhranφ obsahujφ pouze Φist∞ virtußlnφ metody (stejnΘ jako jejich t°φdy, to ale samoz°ejm∞ neznamenß, ₧e by t°φdy nemohly obsahovat i jinΘ metody, ale klient bude moci volat pouze metody rozhranφ). Stejn∞ tak nemß smysl, aby rozhranφ m∞lo n∞jakΘ soukromΘ metody. UrΦit∞ vßs te∩ zajφmß jak budou vypadat metody AddRef() a Release(). U₧ jsme se s nimi setkali mnohokrßt, ale pro ·plnost, tady jsou:

HRESULT CSpaceship::AddRef()
{
  return m_ulRef++;
}

HRESULT CSpaceship::Release()
{
  if(m_ulRef > 0)
  {
    m_ulRef--;
  }
  if(m_ulRef == 0)
  {
    delete this;
    printf("Objekt CSpaceship byl znicen.\n");
    return 0;
  }
  else
  {
    return m_ulRef;
  }
}

Proto₧e jsou u obou t°φd toto₧nΘ, nebudu zde uvßd∞t dvakrßt tent²₧ k≤d a zam∞°φm se na d∙le₧it∞jÜφ v∞ci. O tom, jak metody pracujφ jsem se zmφnil ji₧ v ·vodu, zde to mßte prakticky. Metoda AddRef() vracφ inkrementovan² poΦet referencφ na objekt (nezapome≥te prom∞nnou m_ulRef nulovat v konstruktoru t°φdy). Metoda Release() je o n∞co mßlo slo₧it∞jÜφ. Pokud je poΦet referencφ v∞tÜφ ne₧ 0, znamenß to, ₧e klient uvol≥uje rozhranφ a proto je t°eba ΦφtaΦ snφ₧it. Pokud existuje dalÜφ rozhranφ na tent²₧ objekt, funkce vrßtφ snφ₧en² poΦet referencφ, ale objekt z∙stane v pam∞ti (stßle je ₧ßdßn). Naopak v p°φpad∞, ₧e poΦet referencφ klesne na nulovou hodnotu, znamenß to, ₧e o objekt u₧ nikdo nestojφ a proto se sßm zniΦφ.

Poznßmka: Typ HRESULT je op∞t p°evzat z COMu, ale je to prost² unsigned long.

Mßme t°φdy, mßme rozhranφ, co dßl? Nynφ pot°ebujeme n∞jakou funkci (opravdu funkci, nikoliv metodu), pomocφ kterΘ zφskßme po₧adovanΘ rozhranφ! Je to asi nejd∙le₧it∞jÜφ v∞c celΘho modelu. V projektu D3DEngine se tyto funkce jmenovaly t°eba CreateDisplayObject(). Zde nenφ ₧ßdnΘ Display, proto uvedenou funkci nazvu prost∞ CreateObject(), ale bude fungovat ·pln∞ stejn∞. Deklarace a definice tΘto funkce je v souborech Manager.h a Manager.cpp:

/*
* Kazde rozhrani ma sve unikatni ID, kterym pak
* funkci CreateObject() rekneme, o ktere rozhrani stojime.
*/

enum EXIID
{
  EXIID_ISpaceship,
  EXIID_IUnique
};

/*
* Pomoci teto funkce pristupujeme ke tride CSpaceship
* pres rozhrani ISpaceship. HRESULT = DWORD = unsigned long (prevzato z COMu)
*/

HRESULT CreateObject(EXIID InterfaceID, void ** ppv);

Typ EXIID identifikuje rozhranφ, kterΘ zde mßme - tedy pouze dv∞. Funkce CreateObject() mß dva parametry, prvnφm urΦφme, o kterΘ rozhranφ mßme zßjem, p°es druh² se pak p°edßvß ukazatel rozhranφ. Podφvejme se na implementaci funkce:

/*
* Zde musi byt vlozeny vsechny tridy ktere maji interface
*/

#include "spaceship.h"
#include "unique.h"

/*
* Implementace funkce CreateObject()
*/

HRESULT CreateObject(EXIID InterfaceID, void ** ppv)
{
  /* ukazatele unikatnich objektu se v prubehu programu nemeni */
  static IUnique * g_theUnique = NULL;
  if(!ppv)
  {
    return -1;
  }
  /* vrati spravny ukazatel podle ID rozhrani */
  switch(InterfaceID) {
    case EXIID_IUnique:
      if(!g_theUnique)
      {
        *ppv = g_theUnique = new CUnique;
      }
      else {
        *ppv = g_theUnique;
      }
      ((IUnique*)(*ppv))->AddRef();
      break;
    case EXIID_ISpaceship:
      *ppv = new CSpaceship;
      ((CSpaceship*)(*ppv))->AddRef();
      break;
    default:
      return -2;
  }
  return 0;
}

Za prvΘ je t°eba vlo₧it hlaviΦkovΘ soubory vÜech t°φd, ke kter²m mßme rozhranφ. Na prvnφm °ßdku funkce jsou ukazatele na unikßtnφ objekty (samoz°ejm∞ na jejich rozhranφ). Tento ukazatel se p°i prvnφm volßnφ vytvo°φ, ale p°i dalÜφch volßnφ se vracφ po°ßd prvnφ (tφm je zp∙sobena jeho unikßtnost). Hlavnφ Φßst funkce je p°φkaz switch, kde se podle ID rozhranφ rozhodujeme, kter² objekt vytvo°φme. Proto₧e t°φdy jsou odvozeny od sv²ch rozhranφ m∙₧eme napsat toto:

IUnique unq = new CUnique;

Odborn∞ se tato technika naz²vß polymorfismus, ale zßrove≥ pat°φ mezi zßkladnφ techniky objektovΘho programovßnφ, Φili to tu rozebφrat nebudu. NicmΘn∞ tφmto zp∙sobφme, ₧e aΦkoliv unq je typu IUnique, budeme volat metody naÜφ t°φdy CUnique. Na konci ka₧dΘ v∞tve nesmφme zapomenout zavolat metodu AddRef().

Nynφ u₧ zb²vß cel² systΘm vyzkouÜet na krßtkΘm progrßmku ve funkci main():

IUnique *theONE, *theSecond;

CreateObject(EXIID_IUnique, (void**) &theONE);
printf("\nPrvni rozhrani IUnique: 0x%08p\n", theONE);
if(theONE)
{
  theONE->Start("Toto je unikatni objekt.");
  theONE->Message();

  CreateObject(EXIID_IUnique, (void**) &theSecond);
  printf("Nove rozhrani IUnique: 0x%08p\nJe videt, ze ukazatele jsou stejne.\n", theSecond);

  printf("Druhe rozhrani vypisuje zpravu:\n");
  theSecond->Message();

  printf(" - Zprava je porad stejna.\n\n");

  theONE->Release();
  theSecond->Release();
}

V prvnφ Φßsti otestujeme unikßtnost t°φdy CUnique. Vytvo°φme prvnφ rozhranφ a nechßme si vypsat jeho adresu. Nastavφme n∞jak² °et∞zec a nechßme ho vypsat (zatφm pouze prvnφm rozhranφm). Nynφ vytvo°φme druhΘ rozhranφ stejnΘho typu a op∞t nechßme vypsat na obrazovku! Ukazatele jsou stejnΘ! Zavolßme metodu Message() druhΘho rozhranφ a "neΦekan∞" se vypφÜe zprßva prvnφho rozhranφ, tak₧e opravdu pracujeme po°ßd se stejn²m objektem! Nakonec ob∞ rozhranφ uvolnφme, Φφm₧ zruÜφme i objekt CUnique.

Nynφ se podφvejme na test druhΘ rozhranφ ISpaceship:

ISpaceship *ship1, *ship2;
CreateObject(EXIID_ISpaceship, (void**) &ship1);
CreateObject(EXIID_ISpaceship, (void**) &ship2);
if(ship1 && ship2)
{
  printf("\nPrvni rozhrani ISpaceship: 0x%08p\n", ship1);
  printf("Druhe rozhrani ISpaceship: 0x%08p\n", ship2);
  printf(" - Je videt, ze ukazatele se lisi = DVA RUZNE OBJEKTY!\n");

  ship1->Init(5);
  printf("\nPozice prvni rakety:\n");
  ship1->Display();

  ship2->Init(3);
  printf("Pozice druhe rakety:\n");
  ship2->Display();

  ship1->Move(11);
  printf("\nPrvni raketa se pohnula o 11 a to nema zadny vliv na druhou raketu.\n");
  ship1->Display();
  ship2->Display();

  printf("\n");
  ship1->Release();
  ship2->Release();
}

Rovnou vytvo°φme dva objekty s rozhranφm ISpaceship a op∞t si nechßme vypsat ukazatele na n∞. Nynφ jsou r∙znΘ! Pokud nastavφme pozici prvnφ rakety na 5 a druhΘ na 3, dv∞ raketu budou mφt opravdu r∙znΘ pozice. Nynφ se prvnφ p°esune, ale pozice druhΘ rakety je stßle stejnß! Opravdu jsme tedy vytvo°ili dva r∙znΘ objekty. Nakonec uvolnφme ob∞ rozhranφ a tφm i objekty v pam∞ti.

Doufßm, ₧e Vßm toto malΘ vysv∞tlenφ staΦilo. SystΘm v naÜem velkΘ projektu je na puntφk stejn², jen je vÜe ve v∞tÜφm m∞°φtku.

36.2. Reorganizace projektu D3DEngine

ProblΘm naÜeho projektu tkvφ v tom, ₧e t°φda CTerrain je souΦßst projektu Tester. Proto vytvo°φme dalÜφ knihovnu Engine, kterou vlo₧φme mezi Tester a Display. Engine bude vyu₧φvat Display a Input a nakonec Tester bude testovat vÜechny uvedenΘ knihovny. Tento ·kon se zdß na prvnφ pohled jednoduch², ale v novΘ knihovn∞ musφme vytvo°it Manager objekt∙ a hlavn∞ musφme vytvo°it rozhranφ t°φdy CTerrain.

Nejprve tedy vlo₧φme nov² projekt Engine! Vytvo°te Windows DLL. Do vytvo°enΘho adresß°e Engine zkopφrujte soubory z vedlejÜφho projektu (t°eba Input): Common.h, Manager.h, Manager.cpp a Interfaces.h. Nakonec jeÜt∞ p°ekopφrujte soubory Terrain.h a Terrain.cpp z projektu Tester.

VÜechny uvedenΘ soubory do projektu Engine p°idejte t°eba p°es kontextovΘ menu (polo₧ka Add Existing Item). Nynφ budeme soubory postupn∞ upravovat.

Common.h:

/*
* Include this header into project where you want to use Engine objects
*/

#ifndef ENGINE_API
#define ENGINE_API __declspec( dllimport )
#endif // ENGINE_API

#include "interfaces.h"
#include "manager.h"
#include "terrain.h"

Zde je t°eba nadeklarovat novou hodnoty ENGINE_API pro import resp. export funkcφ a rozhranφ z knihovny. Tento soubor pak budeme vklßdat do vÜech soubor∙, kde budeme chtφt novou knihovnu pou₧φt.

Manager.h:

enum ENGIID
{
  ENGIID_ITerrain
};

// Call This function to get desired interface
ENGINE_API HRESULT CreateEngineObject(ENGIID InterfaceID, void ** ppv);


V tomto souboru nadefinujeme typ ENGIID (Engine Interface ID) a p°idßme jeden prvek ENGIID_ITerrain, kter² bude identifikovat rozhranφ t°φdy CTerrain. Dßle zde bude deklarace funkce pro vytvß°enφ objekt∙ CreateEngineObject().

Manager.cpp:

#include "StdAfx.h"
#define ENGINE_API __declspec(dllexport)

#include "Interfaces.h"

#include "../Common/Common.h"
#include "../Display/Common.h"
#include "../Input/Common.h"

#include "Manager.h"
#include "Terrain.h"

// Call This function to get desired interface
ENGINE_API HRESULT CreateEngineObject(ENGIID InterfaceID, void ** ppv)
{
  // unique objects
  static ITerrain * g_theTerrain = NULL;
  if(!ppv)
  {
    return ERROR_INVALID_PARAMETER;
  }
  // get pointer according IID
  switch(InterfaceID) {
    case ENGIID_ITerrain:
      if(!g_theTerrain)
      {
        *ppv = g_theTerrain = new CTerrain;
      }
      else {
        *ppv = g_theTerrain;
      }
      ((ITerrain*)(*ppv))->AddRef();
      break;
    default:
      return E_NOINTERFACE;
  }
  return S_OK;
}

V tomto souboru bude implementace funkce CreateEngineObject(). Zatφm mßme pouze jedno rozhranφ ITerrain (za chvilku ho nadefinujeme) a objekt skr²vajφcφ se za tφmto rozhranφm bude navφc unikßtnφ. Jak tato funkce pracuje jsem probral o pßr °ßdk∙ v²Üe.

Interfaces.h:

enum TERRAIN_METHOD
{
  TM_HEIGHTMAP = 0,
  TM_CIRCLE
};

class ENGINE_API ITerrain
{
public:
  virtual HRESULT GenerateTerrain(int x, int y, TERRAIN_METHOD tmMethod = TM_CIRCLE) = 0;
  virtual HRESULT GenerateTerrainFromFile(LPCSTR szFile) = 0;
  virtual HRESULT DestroyTerrain() = 0;
  virtual int GetTitlesX() = 0;
  virtual int GetTitlesY() = 0;
  virtual float GetTerrainHeignt(int x, int y) = 0;
  virtual void IncreaseLODLevel() = 0;
  virtual void DecreaseLODLevel() = 0;

  virtual void SetFlag(DWORD dwFlag) = 0;
  virtual void UnsetFlag(DWORD dwFlag) = 0;
  virtual void InvertFlag(DWORD dwFlag) = 0;
  virtual void ResetFlags() = 0;

  virtual HRESULT Render() = 0;
  virtual HRESULT Restore() = 0;

public:
  // interface functions
  virtual HRESULT AddRef() = 0;
  virtual HRESULT Release() = 0;
};

A je tu poslednφ p°idan² soubor (tedy zcela nov² soubor). Zde nadefinujeme rozhranφITerrain nßle₧ejφcφ t°φd∞ CTerrain. Vlo₧φme do n∞j vÜechny metody CTerrain, kterΘ jsou ve°ejnΘ a p°idßme dv∞ metody AddRef() a Release(), kterΘ pat°φ do ka₧dΘho rozhranφ. VÜechny metody budou Φist∞ virtußlnφ.

Na ·pln² zßv∞r musφme jeÜt∞ trochu upravit t°φdu CTerrain v souborech Terrain.h a Terrain.cpp. Za prvΘ je t°eba odvodit CTerrain od ITerrain:

class CTerrain : public ITerrain
{
  DWORD m_dwFlags;
  DWORD m_dwRef;

  ...

  ...

public:
  // interface functions
  virtual HRESULT AddRef();
  virtual HRESULT Release();

  CTerrain(void);
  ~CTerrain(void);
};

P°idßme atribut m_dwRef (ΦφtaΦ na poΦφtßnφ referencφ) a dv∞ metody rozhranφ. Do souboru Terrain.cpp p°idßme implementaci metod AddRef() a Release():

HRESULT CTerrain::AddRef()
{
  return m_dwRef++;
}

HRESULT CTerrain::Release()
{
  if(m_dwRef > 0)
  {
    m_dwRef--;
  }
  if(m_dwRef == 0)
  {
    delete this;
    TRACE("CTerrain object destroyed.");
    return 0;
  }
  else
  {
    return m_dwRef;
  }
}

Nezapome≥te v konstruktoru vynulovat atribut m_dwRef.

Nynφ upravme projekt Tester (soubor Tester2.cpp). Vlo₧φme nov² hlaviΦkov² soubor:

#include "../engine/common.h"

Z prom∞nnΘ g_theTerrain musφme ud∞lat ukazatel na ITerrain:

ITerrain *g_theTerrain = NULL;

Dßle vytvo°φme objekt CTerrain s rozhranφm ITerrain:

if(S_OK == (dwRet = CreateEngineObject(ENGIID_ITerrain, (void**) &g_theTerrain)))
{
  // read terrain method from ini file
  TERRAIN_METHOD iTerrainMethod = (TERRAIN_METHOD)cmnReadSetupInt(_S_TERRAIN, _S_METHOD);
  if(iTerrainMethod == TM_HEIGHTMAP)
  {
    if(cmnReadSetupString(_S_TERRAIN, _S_HEIGHTMAP, szHeightMap, MAX_PATH) < 1)
    {
      strcpy(szHeightMap, "default.bmp");
    }
    g_theTerrain->GenerateTerrainFromFile(szHeightMap);
  }
  else
  {
    g_theTerrain->GenerateTerrain(256, 256, iTerrainMethod);
  }
}
else
{
  TRACE("Cannot create Terrain object.");
  Exit();
  return dwRet;
}

Ve zbytku souboru staΦφ opravit g_theTerrain s teΦkou (.) na g_theTerrain se Üipkou (->). Aby Üel projekt zkompilovat, musφte jeÜt∞ upravit nastavenφ projektu Engine.

V²stupnφ soubor (Output file) musφ b²t zm∞n∞n na ../Release/Engine.dll, aby se v²slednß dll knihovna vytvo°ila v adresß°i Release celΘho projektu D3DEngine.

Jako dalÜφ knihovny (Additional dependencies), kterΘ projekt bude pot°ebovat musφte nastavit tyto dv∞: ../common/common.lib D3dx8.lib. Nezapome≥te ob∞ nastavenφ ud∞lat i pro Debug. Nynφ zm∞≥te nastavenφ zßvislostφ v projektu D3DEngine tak, aby projekt Engine byl zßvisl² na Display a Input, a projekt Tester na novΘm projektu Engine. Teprve te∩ odstra≥te soubory Terrain.h a Terrain.cpp z projektu Tester.

Upraven² projekt si samoz°ejm∞ m∙₧ete stßhnout v sekci Downloads pod nßzvem D3DEngine.

36.3 Zßv∞r

Tak a je tu konec dneÜnφ lekce. Doufßm, ₧e jsem Vßm op∞t trochu pomohl. P°φÜt∞ se budeme zab²vat Φßsticov²mi systΘmy (particle systems), pomocφ kter²ch se vytvß°φ t°eba mraky nebo kou°.

T∞Üφm se p°φÜt∞ nashledanou.

 

Ond°ej BuriÜin a Ji°φ Formßnek